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 -cp src
-D analyzer-optimize -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 // The last expression might be a comment, in which case None will be returned
switch (nextExp) { switch (nextExp) {
case Some(nextExp): case Some(nextExp):
classFields.push(readerExpToField(nextExp, position, k)); var field = readerExpToField(nextExp, position, k);
if (field != null)
classFields.push(field);
case None: case None:
stream.dropWhitespace(); // If there was a comment, drop whitespace that comes after stream.dropWhitespace(); // If there was a comment, drop whitespace that comes after
} }
@@ -60,15 +62,16 @@ class Kiss {
return classFields; 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; 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; var macros = k.macros;
return switch (exp) { return switch (exp) {
case CallExp(Symbol(mac), args) if (macros.exists(mac)): 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)): case CallExp(Symbol(formName), args) if (fieldForms.exists(formName)):
fieldForms[formName](position, args, readerExpToHaxeExpr.bind(_, k)); fieldForms[formName](position, args, readerExpToHaxeExpr.bind(_, k));
default: default:

View File

@@ -2,13 +2,15 @@ package kiss;
import haxe.macro.Expr; import haxe.macro.Expr;
import haxe.macro.Context; import haxe.macro.Context;
import hscript.Parser;
import hscript.Interp;
import kiss.Reader; import kiss.Reader;
import kiss.Kiss; import kiss.Kiss;
using kiss.Helpers; using kiss.Helpers;
// Macros generate new Kiss reader expressions from the arguments of their call expression. // 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 { class Macros {
public static function builtins() { public static function builtins() {
@@ -70,6 +72,37 @@ class Macros {
CallExp(Symbol("defun"), exps); 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; return macros;
} }

View File

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

View File

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