Limited reader macros
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
|
-lib hscript
|
||||||
-cp src
|
-cp src
|
||||||
-D analyzer-optimize
|
-D analyzer-optimize
|
@@ -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:
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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()) {
|
||||||
|
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