Start of line reader macros
This commit is contained in:
@@ -60,7 +60,7 @@ class EmbeddedScript {
|
||||
classFields.push(field);
|
||||
}
|
||||
|
||||
Reader.readAndProcess(new Stream(scriptFile), k.readTable, (nextExp) -> {
|
||||
Reader.readAndProcess(new Stream(scriptFile), k, (nextExp) -> {
|
||||
var field = Kiss.readerExpToField(nextExp, k, false);
|
||||
if (field != null) {
|
||||
classFields.push(field);
|
||||
|
@@ -157,7 +157,7 @@ class Helpers {
|
||||
public static function defAlias(k:KissState, whenItsThis:String, makeItThisInstead:ReaderExpDef) {
|
||||
// The alias has to be followed by a terminator to count!
|
||||
for (terminator in Reader.terminators) {
|
||||
k.readTable[whenItsThis + terminator] = (s:Stream) -> {
|
||||
k.readTable[whenItsThis + terminator] = (s:Stream, k) -> {
|
||||
s.putBackString(terminator);
|
||||
makeItThisInstead;
|
||||
}
|
||||
@@ -207,8 +207,8 @@ class Helpers {
|
||||
var parser = new Parser();
|
||||
if (interps.length == 0) {
|
||||
var interp = new Interp();
|
||||
interp.variables.set("read", Reader.assertRead.bind(_, k.readTable));
|
||||
interp.variables.set("readExpArray", Reader.readExpArray.bind(_, _, k.readTable));
|
||||
interp.variables.set("read", Reader.assertRead.bind(_, k));
|
||||
interp.variables.set("readExpArray", Reader.readExpArray.bind(_, _, k));
|
||||
interp.variables.set("ReaderExp", ReaderExpDef);
|
||||
interp.variables.set("nextToken", Reader.nextToken.bind(_, "a token"));
|
||||
interp.variables.set("kiss", {
|
||||
|
@@ -20,7 +20,8 @@ typedef ExprConversion = (ReaderExp) -> Expr;
|
||||
|
||||
typedef KissState = {
|
||||
className:String,
|
||||
readTable:Map<String, ReadFunction>,
|
||||
readTable:ReadTable,
|
||||
startOfLineReadTable:ReadTable,
|
||||
fieldForms:Map<String, FieldFormFunction>,
|
||||
specialForms:Map<String, SpecialFormFunction>,
|
||||
macros:Map<String, MacroFunction>,
|
||||
@@ -35,6 +36,7 @@ class Kiss {
|
||||
var k = {
|
||||
className: className,
|
||||
readTable: Reader.builtins(),
|
||||
startOfLineReadTable: new ReadTable(),
|
||||
fieldForms: FieldForms.builtins(),
|
||||
specialForms: SpecialForms.builtins(),
|
||||
macros: Macros.builtins(),
|
||||
@@ -70,7 +72,7 @@ class Kiss {
|
||||
if (k == null)
|
||||
k = defaultKissState();
|
||||
|
||||
Reader.readAndProcess(stream, k.readTable, (nextExp) -> {
|
||||
Reader.readAndProcess(stream, k, (nextExp) -> {
|
||||
#if test
|
||||
Sys.println(nextExp.def.toString());
|
||||
#end
|
||||
|
@@ -178,12 +178,8 @@ class Macros {
|
||||
]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
macros["defreadermacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, null, '(defreadermacro ["[startingString]" or [startingStrings...]] [[streamArgName]] [body...])');
|
||||
|
||||
// reader macros can define a list of strings that will trigger the macro. When there are multiple,
|
||||
// the macro will put back the initiating string into the stream so you can check which one it was
|
||||
var stringsThatMatch = switch (exps[0].def) {
|
||||
function stringsThatMatch(exp:ReaderExp) {
|
||||
return switch (exp.def) {
|
||||
case StrExp(s):
|
||||
[s];
|
||||
case ListExp(strings):
|
||||
@@ -197,14 +193,31 @@ class Macros {
|
||||
}
|
||||
];
|
||||
default:
|
||||
throw CompileError.fromExp(exps[0], 'first argument to defreadermacro should be a String or list of strings');
|
||||
throw CompileError.fromExp(exp, 'first argument to defreadermacro should be a String or list of strings');
|
||||
};
|
||||
}
|
||||
|
||||
for (s in stringsThatMatch) {
|
||||
macros["defreadermacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, null, '(defreadermacro ["[startingString]" or [startingStrings...]] [[streamArgName]] [body...])');
|
||||
|
||||
// reader macros declared in the form (defreadermacro &start ...) will only be applied
|
||||
// at the beginning of lines
|
||||
var table = k.readTable;
|
||||
|
||||
// reader macros can define a list of strings that will trigger the macro. When there are multiple,
|
||||
// the macro will put back the initiating string into the stream so you can check which one it was
|
||||
var strings = switch (exps[0].def) {
|
||||
case MetaExp("start", stringsExp):
|
||||
table = k.startOfLineReadTable;
|
||||
stringsThatMatch(stringsExp);
|
||||
default:
|
||||
stringsThatMatch(exps[0]);
|
||||
};
|
||||
for (s in strings) {
|
||||
switch (exps[1].def) {
|
||||
case ListExp([{pos: _, def: Symbol(streamArgName)}]):
|
||||
k.readTable[s] = (stream) -> {
|
||||
if (stringsThatMatch.length > 1) {
|
||||
table[s] = (stream, k) -> {
|
||||
if (strings.length > 1) {
|
||||
stream.putBackString(s);
|
||||
}
|
||||
var body = CallExp(Symbol("begin").withPos(stream.position()), exps.slice(2)).withPos(stream.position());
|
||||
|
@@ -2,6 +2,7 @@ package kiss;
|
||||
|
||||
import haxe.ds.Option;
|
||||
import kiss.Stream;
|
||||
import kiss.Kiss;
|
||||
|
||||
using kiss.Reader;
|
||||
|
||||
@@ -35,58 +36,58 @@ enum ReaderExpDef {
|
||||
UnquoteList(exp:ReaderExp); // ,@[exp]
|
||||
}
|
||||
|
||||
typedef ReadFunction = (Stream) -> Null<ReaderExpDef>;
|
||||
typedef ReadFunction = (Stream, KissState) -> Null<ReaderExpDef>;
|
||||
typedef ReadTable = Map<String, ReadFunction>;
|
||||
|
||||
class Reader {
|
||||
// The built-in readtable
|
||||
public static function builtins() {
|
||||
var readTable:Map<String, ReadFunction> = [];
|
||||
var readTable:ReadTable = [];
|
||||
|
||||
readTable["("] = (stream) -> CallExp(assertRead(stream, readTable), readExpArray(stream, ")", readTable));
|
||||
readTable["["] = (stream) -> ListExp(readExpArray(stream, "]", readTable));
|
||||
readTable["\""] = (stream) -> StrExp(stream.expect("closing \"", () -> stream.takeUntilAndDrop("\"")));
|
||||
readTable["/*"] = (stream) -> {
|
||||
stream.dropUntil("*/");
|
||||
stream.dropString("*/");
|
||||
readTable["("] = (stream, k) -> CallExp(assertRead(stream, k), readExpArray(stream, ")", k));
|
||||
readTable["["] = (stream, k) -> ListExp(readExpArray(stream, "]", k));
|
||||
readTable["\""] = (stream, k) -> StrExp(stream.expect("closing \"", () -> stream.takeUntilAndDrop("\"")));
|
||||
readTable["/*"] = (stream, k) -> {
|
||||
stream.takeUntilAndDrop("*/");
|
||||
null;
|
||||
};
|
||||
readTable["//"] = (stream) -> {
|
||||
stream.dropUntil("\n");
|
||||
readTable["//"] = (stream, k) -> {
|
||||
stream.takeUntilAndDrop("\n");
|
||||
null;
|
||||
};
|
||||
readTable["#|"] = (stream) -> RawHaxe(stream.expect("closing |#", () -> stream.takeUntilAndDrop("|#")));
|
||||
readTable["#|"] = (stream, k) -> RawHaxe(stream.expect("closing |#", () -> stream.takeUntilAndDrop("|#")));
|
||||
// For defmacrofuns, unquoting with , is syntactic sugar for calling a Quote (Void->T)
|
||||
|
||||
readTable[":"] = (stream) -> TypedExp(nextToken(stream, "a type path"), assertRead(stream, readTable));
|
||||
readTable[":"] = (stream, k) -> TypedExp(nextToken(stream, "a type path"), assertRead(stream, k));
|
||||
|
||||
readTable["&"] = (stream) -> MetaExp(nextToken(stream, "a meta symbol like mut, optional, rest"), assertRead(stream, readTable));
|
||||
readTable["&"] = (stream, k) -> MetaExp(nextToken(stream, "a meta symbol like mut, optional, rest"), assertRead(stream, k));
|
||||
|
||||
readTable["!"] = (stream:Stream) -> CallExp(Symbol("not").withPos(stream.position()), [assertRead(stream, readTable)]);
|
||||
readTable["!"] = (stream:Stream, k) -> CallExp(Symbol("not").withPos(stream.position()), [assertRead(stream, k)]);
|
||||
|
||||
// Helpful for defining predicates to pass to Haxe functions:
|
||||
readTable["?"] = (stream:Stream) -> CallExp(Symbol("Prelude.truthy").withPos(stream.position()), [assertRead(stream, readTable)]);
|
||||
readTable["?"] = (stream:Stream, k) -> CallExp(Symbol("Prelude.truthy").withPos(stream.position()), [assertRead(stream, k)]);
|
||||
|
||||
// Lets you dot-access a function result without binding it to a name
|
||||
readTable["."] = (stream:Stream) -> FieldExp(nextToken(stream, "a field name"), assertRead(stream, readTable));
|
||||
readTable["."] = (stream, k) -> FieldExp(nextToken(stream, "a field name"), assertRead(stream, k));
|
||||
|
||||
// Lets you construct key-value pairs for map literals or for-loops
|
||||
readTable["=>"] = (stream:Stream) -> KeyValueExp(assertRead(stream, readTable), assertRead(stream, readTable));
|
||||
readTable["=>"] = (stream, k) -> KeyValueExp(assertRead(stream, k), assertRead(stream, k));
|
||||
|
||||
readTable[")"] = (stream:Stream) -> {
|
||||
readTable[")"] = (stream, k) -> {
|
||||
stream.putBackString(")");
|
||||
throw new UnmatchedBracketSignal(")", stream.position());
|
||||
};
|
||||
readTable["]"] = (stream:Stream) -> {
|
||||
readTable["]"] = (stream, k) -> {
|
||||
stream.putBackString("]");
|
||||
throw new UnmatchedBracketSignal("]", stream.position());
|
||||
};
|
||||
|
||||
readTable["`"] = (stream) -> Quasiquote(assertRead(stream, readTable));
|
||||
readTable[","] = (stream) -> Unquote(assertRead(stream, readTable));
|
||||
readTable[",@"] = (stream) -> UnquoteList(assertRead(stream, readTable));
|
||||
readTable["`"] = (stream, k) -> Quasiquote(assertRead(stream, k));
|
||||
readTable[","] = (stream, k) -> Unquote(assertRead(stream, k));
|
||||
readTable[",@"] = (stream, k) -> UnquoteList(assertRead(stream, k));
|
||||
|
||||
// Because macro keys are sorted by length and peekChars(0) returns "", this will be used as the default reader macro:
|
||||
readTable[""] = (stream) -> Symbol(nextToken(stream, "a symbol name"));
|
||||
readTable[""] = (stream, k) -> Symbol(nextToken(stream, "a symbol name"));
|
||||
|
||||
return readTable;
|
||||
}
|
||||
@@ -101,9 +102,9 @@ class Reader {
|
||||
return tok;
|
||||
}
|
||||
|
||||
public static function assertRead(stream:Stream, readTable:Map<String, ReadFunction>):ReaderExp {
|
||||
public static function assertRead(stream:Stream, k:KissState):ReaderExp {
|
||||
var position = stream.position();
|
||||
return switch (read(stream, readTable)) {
|
||||
return switch (read(stream, k)) {
|
||||
case Some(exp):
|
||||
exp;
|
||||
case None:
|
||||
@@ -111,40 +112,54 @@ class Reader {
|
||||
};
|
||||
}
|
||||
|
||||
public static function read(stream:Stream, readTable:Map<String, ReadFunction>):Option<ReaderExp> {
|
||||
static function chooseReadFunction(stream:Stream, readTable:ReadTable):Null<ReadFunction> {
|
||||
var readTableKeys = [for (key in readTable.keys()) key];
|
||||
readTableKeys.sort((a, b) -> b.length - a.length);
|
||||
|
||||
for (key in readTableKeys) {
|
||||
switch (stream.peekChars(key.length)) {
|
||||
case Some(ky) if (ky == key):
|
||||
stream.dropString(key);
|
||||
return readTable[key];
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function read(stream:Stream, k:KissState):Option<ReaderExp> {
|
||||
var readTable = k.readTable;
|
||||
stream.dropWhitespace();
|
||||
|
||||
if (stream.isEmpty())
|
||||
return None;
|
||||
|
||||
var position = stream.position();
|
||||
var readTableKeys = [for (key in readTable.keys()) key];
|
||||
readTableKeys.sort((a, b) -> b.length - a.length);
|
||||
|
||||
for (key in readTableKeys) {
|
||||
switch (stream.peekChars(key.length)) {
|
||||
case Some(k) if (k == key):
|
||||
stream.dropString(key);
|
||||
var expOrNull = readTable[key](stream);
|
||||
return if (expOrNull != null) {
|
||||
Some(expOrNull.withPos(position));
|
||||
} else {
|
||||
read(stream, readTable);
|
||||
}
|
||||
default:
|
||||
}
|
||||
var readFunction = null;
|
||||
if (stream.startOfLine)
|
||||
readFunction = chooseReadFunction(stream, k.startOfLineReadTable);
|
||||
if (readFunction == null)
|
||||
readFunction = chooseReadFunction(stream, k.readTable);
|
||||
if (readFunction == null)
|
||||
throw 'No macro to read next expression';
|
||||
|
||||
var expOrNull = readFunction(stream, k);
|
||||
return if (expOrNull != null) {
|
||||
Some(expOrNull.withPos(position));
|
||||
} else {
|
||||
read(stream, k);
|
||||
}
|
||||
|
||||
throw 'No macro to read next expression';
|
||||
}
|
||||
|
||||
public static function readExpArray(stream:Stream, end:String, readTable:Map<String, ReadFunction>):Array<ReaderExp> {
|
||||
public static function readExpArray(stream:Stream, end:String, k:KissState):Array<ReaderExp> {
|
||||
var array = [];
|
||||
while (!stream.startsWith(end)) {
|
||||
stream.dropWhitespace();
|
||||
if (!stream.startsWith(end)) {
|
||||
try {
|
||||
array.push(assertRead(stream, readTable));
|
||||
array.push(assertRead(stream, k));
|
||||
} catch (s:UnmatchedBracketSignal) {
|
||||
if (s.type == end)
|
||||
break;
|
||||
@@ -160,13 +175,13 @@ class Reader {
|
||||
/**
|
||||
Read all the expressions in the given stream, processing them one by one while reading.
|
||||
**/
|
||||
public static function readAndProcess(stream:Stream, readTable:Map<String, ReadFunction>, process:(ReaderExp) -> Void) {
|
||||
public static function readAndProcess(stream:Stream, k:KissState, process:(ReaderExp) -> Void) {
|
||||
while (true) {
|
||||
stream.dropWhitespace();
|
||||
if (stream.isEmpty())
|
||||
break;
|
||||
var position = stream.position();
|
||||
var nextExp = Reader.read(stream, readTable);
|
||||
var nextExp = Reader.read(stream, k);
|
||||
// The last expression might be a comment, in which case None will be returned
|
||||
switch (nextExp) {
|
||||
case Some(nextExp):
|
||||
|
@@ -23,6 +23,8 @@ class Stream {
|
||||
|
||||
var absolutePerNewline = 1;
|
||||
|
||||
public var startOfLine(default, null) = true;
|
||||
|
||||
public function new(file:String) {
|
||||
// Banish ye Windows line-endings
|
||||
content = File.getContent(file);
|
||||
@@ -82,9 +84,14 @@ class Stream {
|
||||
line += 1;
|
||||
lineLengths.push(column);
|
||||
column = 1;
|
||||
startOfLine = true;
|
||||
case c if (c.trim() == ""):
|
||||
absoluteChar += 1;
|
||||
column += 1;
|
||||
default:
|
||||
absoluteChar += 1;
|
||||
column += 1;
|
||||
startOfLine = false;
|
||||
}
|
||||
}
|
||||
content = content.substr(count);
|
||||
|
@@ -10,6 +10,10 @@ class ReaderMacroTestCase extends Test {
|
||||
Assert.equals("String that takes the rest of the line", ReaderMacroTestCase.myLine());
|
||||
}
|
||||
|
||||
function testReadNot() {
|
||||
Assert.isTrue(ReaderMacroTestCase.myBool());
|
||||
}
|
||||
|
||||
function testDefAlias() {
|
||||
Assert.equals(9, ReaderMacroTestCase.mySum);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
(defreadermacro "!" [stream]
|
||||
(defreadermacro &start "!" [stream]
|
||||
(let [line (stream.expect "a string line" (lambda [] (stream.takeLine)))]
|
||||
(ReaderExp.StrExp line)))
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
!String that takes the rest of the line
|
||||
)
|
||||
|
||||
(defun myBool []
|
||||
(begin !false))
|
||||
|
||||
(defalias pluppers +)
|
||||
(defalias fluffers 5)
|
||||
(defalias buffers 4)
|
||||
|
Reference in New Issue
Block a user