Rough implementation of list-eater pattern matching. Close #1

This commit is contained in:
2021-07-22 15:58:21 -06:00
parent 19b452309b
commit 7739c6fd28
6 changed files with 133 additions and 7 deletions

View File

@@ -179,17 +179,53 @@ class Helpers {
} }
} }
public static function makeSwitchCase(caseExp:ReaderExp, k:KissState, ?guard:Expr):Case { // The name of this function is confusing--it actually makes a Haxe `case` expression, not a switch-case expression
public static function makeSwitchCase(caseExp:ReaderExp, k:KissState):Case {
var guard:Expr = null; var guard:Expr = null;
var restExpIndex = -1;
var restExpName = "";
var expNames = [];
var listVarSymbol = null;
function makeSwitchPattern(patternExp:ReaderExp):Array<Expr> { function makeSwitchPattern(patternExp:ReaderExp):Array<Expr> {
return switch (patternExp.def) { return switch (patternExp.def) {
case CallExp({pos: _, def: Symbol("when")}, whenExps): case CallExp({pos: _, def: Symbol("when")}, whenExps):
patternExp.checkNumArgs(2, 2, "(when [guard] [pattern])"); patternExp.checkNumArgs(2, 2, "(when [guard] [pattern])");
if (guard != null) if (guard != null)
throw CompileError.fromExp(caseExp, "case expression can only have one `when` guard"); throw CompileError.fromExp(caseExp, "case pattern can only have one `when` guard");
guard = macro Prelude.truthy(${k.convert(whenExps[0])}); guard = macro Prelude.truthy(${k.convert(whenExps[0])});
makeSwitchPattern(whenExps[1]); makeSwitchPattern(whenExps[1]);
case ListEatingExp(exps) if (exps.length == 0):
throw CompileError.fromExp(patternExp, "list-eating pattern should not be empty");
case ListEatingExp(exps):
for (idx in 0...exps.length) {
var exp = exps[idx];
switch (exp.def) {
case Symbol(_):
expNames.push(exp);
case ListRestExp(name):
if (restExpIndex > -1) {
throw CompileError.fromExp(patternExp, "list-eating pattern cannot have multiple ... or ...[restVar] expressions");
}
restExpIndex = idx;
restExpName = name;
default:
throw CompileError.fromExp(exp, "list-eating pattern can only contain symbols, ..., or ...[restVar]");
}
}
if (restExpIndex == -1) {
throw CompileError.fromExp(patternExp, "list-eating pattern is missing ... or ...[restVar]");
}
if (expNames.length == 0) {
throw CompileError.fromExp(patternExp, "list-eating pattern must match at least one single element");
}
var b = patternExp.expBuilder();
listVarSymbol = b.symbol();
guard = k.convert(b.callSymbol(">", [b.field("length", listVarSymbol), b.raw(Std.string(expNames.length))]));
makeSwitchPattern(listVarSymbol);
default: default:
[k.forCaseParsing().convert(patternExp)]; [k.forCaseParsing().convert(patternExp)];
} }
@@ -197,9 +233,40 @@ class Helpers {
return switch (caseExp.def) { return switch (caseExp.def) {
case CallExp(patternExp, caseBodyExps): case CallExp(patternExp, caseBodyExps):
var pattern = makeSwitchPattern(patternExp);
var b = caseExp.expBuilder();
var body = if (restExpIndex == -1) {
k.convert(b.begin(caseBodyExps));
} else {
var letBindings = [];
for (idx in 0...restExpIndex) {
letBindings.push(expNames.shift());
letBindings.push(b.callSymbol("nth", [listVarSymbol, b.raw(Std.string(idx))]));
}
if (restExpName == "") {
restExpName = "_";
}
letBindings.push(b.symbol(restExpName));
var sliceArgs = [b.raw(Std.string(restExpIndex))];
if (expNames.length > 0) {
sliceArgs.push(b.callSymbol("-", [b.field("length", listVarSymbol), b.raw(Std.string(expNames.length))]));
}
letBindings.push(b.call(b.field("slice", listVarSymbol), sliceArgs));
while (expNames.length > 0) {
var idx = b.callSymbol("-", [b.field("length", listVarSymbol), b.raw(Std.string(expNames.length))]);
letBindings.push(expNames.shift());
letBindings.push(b.callSymbol("nth", [listVarSymbol, idx]));
}
var letExp = b.callSymbol("let", [b.list(letBindings)].concat(caseBodyExps));
k.convert(letExp);
};
// These prints for debugging need to be wrapped in comments because they'll get picked up by convertToHScript()
// Prelude.print('/* $pattern */');
// Prelude.print('/* $body */');
// Prelude.print('/* $guard */');
{ {
values: makeSwitchPattern(patternExp), values: pattern,
expr: k.convert(CallExp(Symbol("begin").withPosOf(caseExp), caseBodyExps).withPosOf(caseExp)), expr: body,
guard: guard guard: guard
}; };
default: default:

View File

@@ -29,8 +29,10 @@ class Reader {
readTable["("] = (stream, k) -> CallExp(assertRead(stream, k), readExpArray(stream, ")", k)); readTable["("] = (stream, k) -> CallExp(assertRead(stream, k), readExpArray(stream, ")", k));
readTable["["] = (stream, k) -> ListExp(readExpArray(stream, "]", k)); readTable["["] = (stream, k) -> ListExp(readExpArray(stream, "]", k));
readTable["[::"] = (stream, k) -> ListEatingExp(readExpArray(stream, "]", k));
readTable["..."] = (stream, k) -> ListRestExp(nextToken(stream, "name for list-eating rest exp", true));
// Provides a nice syntactic sugar for (if... {[then block]} {[else block]}), // Provides a nice syntactic sugar for (if... {[then block]} {[else block]}),
// and also handles string interpolation cases like "${}more" // and also handles string interpolation cases like "${exp}moreString"
readTable["{"] = (stream:Stream, k) -> CallExp(Symbol("begin").withPos(stream.position()), readExpArray(stream, "}", k)); readTable["{"] = (stream:Stream, k) -> CallExp(Symbol("begin").withPos(stream.position()), readExpArray(stream, "}", k));
readTable['"'] = readString; readTable['"'] = readString;
@@ -133,9 +135,9 @@ class Reader {
public static final terminators = [")", "]", "}", '"', "/*", "\n", " "]; public static final terminators = [")", "]", "}", '"', "/*", "\n", " "];
public static function nextToken(stream:Stream, expect:String) { public static function nextToken(stream:Stream, expect:String, allowEmptyString = false) {
switch (stream.takeUntilOneOf(terminators, true)) { switch (stream.takeUntilOneOf(terminators, true)) {
case Some(tok) if (tok.length > 0): case Some(tok) if (tok.length > 0 || allowEmptyString):
return tok; return tok;
default: default:
stream.error('Expected $expect'); stream.error('Expected $expect');
@@ -380,6 +382,17 @@ class Reader {
',${exp.def.toString()}'; ',${exp.def.toString()}';
case UnquoteList(exp): case UnquoteList(exp):
',@${exp.def.toString()}'; ',@${exp.def.toString()}';
case ListEatingExp(exps):
var str = '[::';
str += [
for (exp in exps) {
exp.def.toString();
}
].join(" ");
str += ']';
str;
case ListRestExp(name):
'...${name}';
} }
} }
} }

View File

@@ -20,4 +20,6 @@ enum ReaderExpDef {
Quasiquote(exp:ReaderExp); // `[exp] Quasiquote(exp:ReaderExp); // `[exp]
Unquote(exp:ReaderExp); // ,[exp] Unquote(exp:ReaderExp); // ,[exp]
UnquoteList(exp:ReaderExp); // ,@[exp] UnquoteList(exp:ReaderExp); // ,@[exp]
ListEatingExp(exps:Array<ReaderExp>); // [::exp exp ...exps exp]
ListRestExp(name:String); // ...exps or ...
} }

View File

@@ -348,6 +348,7 @@
(otherwise (Assert.fail))) (otherwise (Assert.fail)))
(case 5 (case 5
((when false (or 5 6)) (Assert.fail)) ((when false (or 5 6)) (Assert.fail))
((when true (or 7 8 9)) (Assert.fail))
(otherwise (Assert.pass))) (otherwise (Assert.pass)))
// In Haxe, // In Haxe,
// `switch (Some(true)) { case Some(true | false): "a"; default: "b"; }` // `switch (Some(true)) { case Some(true | false): "a"; default: "b"; }`

View File

@@ -0,0 +1,12 @@
package test.cases;
import utest.Assert;
import utest.Test;
import kiss.Prelude;
@:build(kiss.Kiss.build())
class ListEatingTestCase extends Test {
function testListEating() {
_testListEating();
}
}

View File

@@ -0,0 +1,31 @@
(defun _testListEating []
// TODO document that list-eating only works on explictly Array-typed variables
(let [:Array<Int> l [1 2 3 4]]
(case l
([::a b ... c d]
(Assert.fail))
([a b c d]
(Assert.pass))
(otherwise
(Assert.fail)))
(case l
([::a ...rest]
(Assert.equals 1 a)
(Assert.equals (.toString [2 3 4]) (.toString rest)))
(otherwise
(Assert.fail)))
(case l
([::a ...rest b]
(Assert.equals 1 a)
(Assert.equals (.toString [2 3]) (.toString rest))
(Assert.equals 4 b))
(otherwise
(Assert.fail)))
(case l
([::...rest last]
(Assert.equals (.toString [1 2 3]) (.toString rest))
(Assert.equals 4 last))
(otherwise
(Assert.fail)))))