Limited reader macros
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
-lib hscript
|
||||
-cp src
|
||||
-D analyzer-optimize
|
@@ -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:
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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()) {
|
||||
|
12
src/test/cases/ReaderMacroTestCase.hx
Normal file
12
src/test/cases/ReaderMacroTestCase.hx
Normal 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());
|
||||
}
|
||||
}
|
8
src/test/cases/ReaderMacroTestCase.kiss
Normal file
8
src/test/cases/ReaderMacroTestCase.kiss
Normal 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
|
||||
)
|
||||
|
Reference in New Issue
Block a user