Limited reader macros

This commit is contained in:
2020-11-21 10:37:50 -07:00
parent 8c2bbc4b1d
commit ec5082a05c
7 changed files with 76 additions and 7 deletions

View File

@@ -1,2 +1,3 @@
-lib hscript
-cp src
-D analyzer-optimize

View File

@@ -51,7 +51,9 @@ class Kiss {
// The last expression might be a comment, in which case None will be returned
switch (nextExp) {
case Some(nextExp):
classFields.push(readerExpToField(nextExp, position, k));
var field = readerExpToField(nextExp, position, k);
if (field != null)
classFields.push(field);
case None:
stream.dropWhitespace(); // If there was a comment, drop whitespace that comes after
}
@@ -60,15 +62,16 @@ class Kiss {
return classFields;
}
static function readerExpToField(exp:ReaderExp, position:String, k:KissState):Field {
static function readerExpToField(exp:ReaderExp, position:String, k:KissState):Null<Field> {
var fieldForms = k.fieldForms;
// Macros at top-level are allowed if they expand into a fieldform, or don't become an expression, like defmacro
// Macros at top-level are allowed if they expand into a fieldform, or null like defreadermacro
var macros = k.macros;
return switch (exp) {
case CallExp(Symbol(mac), args) if (macros.exists(mac)):
readerExpToField(macros[mac](args, k), position, k);
var expandedExp = macros[mac](args, k);
if (expandedExp != null) readerExpToField(macros[mac](args, k), position, k) else null;
case CallExp(Symbol(formName), args) if (fieldForms.exists(formName)):
fieldForms[formName](position, args, readerExpToHaxeExpr.bind(_, k));
default:

View File

@@ -2,13 +2,15 @@ package kiss;
import haxe.macro.Expr;
import haxe.macro.Context;
import hscript.Parser;
import hscript.Interp;
import kiss.Reader;
import kiss.Kiss;
using kiss.Helpers;
// Macros generate new Kiss reader expressions from the arguments of their call expression.
typedef MacroFunction = (Array<ReaderExp>, KissState) -> ReaderExp;
typedef MacroFunction = (Array<ReaderExp>, KissState) -> Null<ReaderExp>;
class Macros {
public static function builtins() {
@@ -70,6 +72,37 @@ class Macros {
CallExp(Symbol("defun"), exps);
}
// For now, reader macros only support a one-expression body implemented in #|raw haxe|#
macros["defreadermacro"] = (exps:Array<ReaderExp>, k:KissState) -> {
if (exps.length != 3) {
throw 'wrong number of expressions for defreadermacro: $exps should be String, [streamArgName], RawHaxe';
}
switch (exps[0]) {
case StrExp(s):
switch (exps[1]) {
case ListExp([Symbol(streamArgName)]):
switch (exps[2]) {
case RawHaxe(code):
k.readTable[s] = (stream) -> {
var parser = new Parser();
var interp = new Interp();
interp.variables.set("ReaderExp", ReaderExp);
interp.variables.set(streamArgName, stream);
interp.execute(parser.parseString(code));
};
default:
throw 'third argument to defreadermacro should be #|raw haxe|#, not ${exps[2]}';
}
default:
throw 'second argument to defreadermacro should be [steamArgName], not ${exps[1]}';
}
default:
throw 'first argument to defreadermacro should be a String, not ${exps[0]}';
}
return null;
};
return macros;
}

View File

@@ -82,9 +82,10 @@ class Reader {
public static function readExpArray(stream:Stream, end:String, readTable:Map<String, ReadFunction>):Array<ReaderExp> {
var array = [];
while (stream.expect('$end to terminate list', () -> stream.peekChars(end.length)) != end) {
while (!stream.startsWith(end)) {
stream.dropWhitespace();
array.push(assertRead(stream, readTable));
if (!stream.startsWith(end))
array.push(assertRead(stream, readTable));
}
stream.dropString(end);
return array;

View File

@@ -38,6 +38,13 @@ class Stream {
return '$file:$line:$column';
}
public function startsWith(s:String) {
return switch (peekChars(s.length)) {
case Some(s1) if (s == s1): true;
default: false;
};
}
/** Every drop call should end up calling dropChars() or the position tracker will be wrong. **/
private function dropChars(count:Int) {
for (idx in 0...count) {
@@ -96,6 +103,10 @@ class Stream {
return Some(toReturn);
}
public function takeLine():Option<String> {
return takeUntilAndDrop("\n");
}
public function expect(whatToExpect:String, f:Void->Option<String>):String {
var position = position();
switch (f()) {

View File

@@ -0,0 +1,12 @@
package test.cases;
import utest.Test;
import utest.Assert;
import kiss.Prelude;
@:build(kiss.Kiss.build("src/test/cases/ReaderMacroTestCase.kiss"))
class ReaderMacroTestCase extends Test {
function testReadBang() {
Assert.equals("String that takes the rest of the line", ReaderMacroTestCase.myLine());
}
}

View File

@@ -0,0 +1,8 @@
(defreadermacro "!" [stream] #|ReaderExp.StrExp(stream.expect("a string line", function () stream.takeLine()))|#)
(defreadermacro )
(defun myLine []
!String that takes the rest of the line
)