Start of line reader macros

This commit is contained in:
2020-12-13 13:42:19 -07:00
parent 9600766b22
commit 04e6ffe3f0
8 changed files with 107 additions and 63 deletions

View File

@@ -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);

View File

@@ -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", {

View File

@@ -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

View File

@@ -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());

View File

@@ -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):

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)