Rough implementation of list-eater pattern matching. Close #1
This commit is contained in:
@@ -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:
|
||||||
|
@@ -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}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 ...
|
||||||
}
|
}
|
||||||
|
@@ -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"; }`
|
||||||
|
12
src/test/cases/ListEatingTestCase.hx
Normal file
12
src/test/cases/ListEatingTestCase.hx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
31
src/test/cases/ListEatingTestCase.kiss
Normal file
31
src/test/cases/ListEatingTestCase.kiss
Normal 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)))))
|
Reference in New Issue
Block a user