969 lines
41 KiB
Haxe
969 lines
41 KiB
Haxe
package kiss;
|
|
|
|
import haxe.macro.Expr;
|
|
import haxe.macro.Context;
|
|
import kiss.Reader;
|
|
import kiss.ReaderExp;
|
|
import kiss.Kiss;
|
|
import kiss.CompileError;
|
|
import kiss.CompilerTools;
|
|
import uuid.Uuid;
|
|
import hscript.Parser;
|
|
import haxe.EnumTools;
|
|
|
|
using kiss.Kiss;
|
|
using kiss.Prelude;
|
|
using kiss.Reader;
|
|
using kiss.Helpers;
|
|
using StringTools;
|
|
using tink.MacroApi;
|
|
|
|
// Macros generate new Kiss reader expressions from the arguments of their call expression.
|
|
typedef MacroFunction = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> Null<ReaderExp>;
|
|
|
|
class Macros {
|
|
public static function builtins() {
|
|
var macros:Map<String, MacroFunction> = [];
|
|
|
|
function renameAndDeprecate(oldName:String, newName:String) {
|
|
var form = macros[oldName];
|
|
macros[oldName] = (wholeExp, args, k) -> {
|
|
CompileError.warnFromExp(wholeExp, '$oldName has been renamed to $newName and deprecated');
|
|
form(wholeExp, args, k);
|
|
}
|
|
macros[newName] = form;
|
|
}
|
|
|
|
macros["load"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 1, '(load "[file]")');
|
|
switch (args[0].def) {
|
|
case StrExp(otherKissFile):
|
|
return Kiss.load(otherKissFile, k);
|
|
default:
|
|
throw CompileError.fromExp(args[0], "only argument to load should be a string literal of a .kiss file path");
|
|
}
|
|
};
|
|
|
|
macros["loadFrom"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, 2, '(loadFrom "[haxelib name]" "[file]")');
|
|
|
|
var libPath = switch (args[0].def) {
|
|
case StrExp(libName):
|
|
Helpers.libPath(libName);
|
|
default:
|
|
throw CompileError.fromExp(args[0], "first argument to loadFrom should be a string literal of a haxe library's name");
|
|
};
|
|
switch (args[1].def) {
|
|
case StrExp(otherKissFile):
|
|
Kiss.load(otherKissFile, k, libPath);
|
|
default:
|
|
throw CompileError.fromExp(args[1], "second argument to loadFrom should be a string literal of a .kiss file path");
|
|
}
|
|
null;
|
|
};
|
|
|
|
function destructiveVersion(op:String, assignOp:String) {
|
|
macros[assignOp] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, null, '($assignOp [var] [v1] [values...])');
|
|
var b = wholeExp.expBuilder();
|
|
b.call(
|
|
b.symbol("set"), [
|
|
exps[0],
|
|
b.call(
|
|
b.symbol(op),
|
|
exps)
|
|
]);
|
|
};
|
|
}
|
|
|
|
destructiveVersion("%", "%=");
|
|
destructiveVersion("^", "^=");
|
|
destructiveVersion("+", "+=");
|
|
destructiveVersion("-", "-=");
|
|
destructiveVersion("*", "*=");
|
|
destructiveVersion("/", "/=");
|
|
|
|
// These shouldn't be ident aliases because they are common variable names
|
|
var opAliases = [
|
|
"min" => "Prelude.min",
|
|
"max" => "Prelude.max"
|
|
];
|
|
|
|
macros["apply"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, 2, '(apply [func] [argList])');
|
|
var b = wholeExp.expBuilder();
|
|
|
|
var callOn = switch (exps[0].def) {
|
|
case FieldExp(field, exp):
|
|
exp;
|
|
default:
|
|
b.symbol("null");
|
|
};
|
|
var func = switch (exps[0].def) {
|
|
case Symbol(func) if (opAliases.exists(func)):
|
|
b.symbol(opAliases[func]);
|
|
default:
|
|
exps[0];
|
|
};
|
|
var args = exps[1];
|
|
b.call(
|
|
b.symbol("Reflect.callMethod"), [
|
|
callOn, func, args
|
|
]);
|
|
};
|
|
|
|
macros["range"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(1, 3, '(range [?min] [max] [?step])');
|
|
var b = wholeExp.expBuilder();
|
|
var min = if (exps.length > 1) exps[0] else b.symbol("0");
|
|
var max = if (exps.length > 1) exps[1] else exps[0];
|
|
var step = if (exps.length > 2) exps[2] else b.symbol("1");
|
|
b.callSymbol("Prelude.range", [min, max, step]);
|
|
};
|
|
|
|
// Most conditional compilation macros are based on this macro:
|
|
macros["#if"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, 3, '(#if [cond] [then] [?else])');
|
|
|
|
var b = wholeExp.expBuilder();
|
|
var conditionExp = exps.shift();
|
|
var thenExp = exps.shift();
|
|
var elseExp = if (exps.length > 0) exps.shift(); else b.none();
|
|
|
|
var parser = new Parser();
|
|
var conditionInterp = new KissInterp(true);
|
|
var conditionStr = Reader.toString(conditionExp.def);
|
|
for (flag => value in Context.getDefines()) {
|
|
// Don't overwrite types that are put in all KissInterps, i.e. the kiss namespace
|
|
if (!conditionInterp.variables.exists(flag)) {
|
|
conditionInterp.variables.set(flag, value);
|
|
}
|
|
}
|
|
try {
|
|
var hscriptStr = Prelude.convertToHScript(conditionStr);
|
|
#if test
|
|
Prelude.print("#if condition hscript: " + hscriptStr);
|
|
#end
|
|
var conditionHScript = parser.parseString(hscriptStr);
|
|
return if (Prelude.truthy(conditionInterp.execute(conditionHScript))) {
|
|
#if test
|
|
Prelude.print("using thenExp");
|
|
#end
|
|
thenExp;
|
|
} else {
|
|
#if test
|
|
Prelude.print("using elseExp");
|
|
#end
|
|
elseExp;
|
|
}
|
|
} catch (e) {
|
|
throw CompileError.fromExp(conditionExp, 'condition for #if threw error $e');
|
|
}
|
|
};
|
|
|
|
// But not this one:
|
|
macros["#case"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, null, '(#case [expression] [cases...] [optional: (otherwise [default])])');
|
|
var b = wholeExp.expBuilder();
|
|
|
|
var caseVar = exps.shift();
|
|
var matchPatterns = [];
|
|
var matchBodies = [];
|
|
var matchBodySymbols = [];
|
|
var caseArgs = [caseVar];
|
|
for (exp in exps) {
|
|
switch (exp.def) {
|
|
case CallExp(pattern, bodyExps):
|
|
matchPatterns.push(pattern);
|
|
matchBodies.push(b.begin(bodyExps));
|
|
var gensym = b.symbol();
|
|
matchBodySymbols.push(gensym);
|
|
caseArgs.push(b.call(pattern, [gensym]));
|
|
default:
|
|
throw CompileError.fromExp(exp, "invalid pattern expression for #case");
|
|
}
|
|
}
|
|
|
|
var caseExp = b.callSymbol("case", caseArgs);
|
|
|
|
var parser = new Parser();
|
|
var caseInterp = new KissInterp();
|
|
var caseStr = Reader.toString(caseExp.def);
|
|
for (matchBodySymbol in matchBodySymbols) {
|
|
caseInterp.variables.set(Prelude.symbolNameValue(matchBodySymbol), matchBodies.shift());
|
|
}
|
|
for (flag => value in Context.getDefines()) {
|
|
if (flag != "kiss")
|
|
caseInterp.variables.set(flag, value);
|
|
}
|
|
try {
|
|
var hscriptStr = Prelude.convertToHScript(caseStr);
|
|
#if test
|
|
Prelude.print("#case hscript: " + hscriptStr);
|
|
#end
|
|
var caseHScript = parser.parseString(hscriptStr);
|
|
return caseInterp.execute(caseHScript);
|
|
} catch (e) {
|
|
throw CompileError.fromExp(caseExp, '#case evaluation threw error $e');
|
|
}
|
|
}
|
|
|
|
function bodyIf(formName:String, underlyingIf:String, negated:Bool, wholeExp:ReaderExp, args:Array<ReaderExp>, k) {
|
|
wholeExp.checkNumArgs(2, null, '($formName [condition] [body...])');
|
|
var b = wholeExp.expBuilder();
|
|
var condition = if (negated) {
|
|
b.call(
|
|
b.symbol("not"), [
|
|
args[0]
|
|
]);
|
|
} else {
|
|
args[0];
|
|
}
|
|
return b.call(b.symbol(underlyingIf), [
|
|
condition,
|
|
b.begin(args.slice(1))
|
|
]);
|
|
}
|
|
macros["when"] = bodyIf.bind("when", "if", false);
|
|
macros["unless"] = bodyIf.bind("unless", "if", true);
|
|
macros["#when"] = bodyIf.bind("#when", "#if", false);
|
|
macros["#unless"] = bodyIf.bind("#unless", "#if", true);
|
|
|
|
macros["cond"] = cond.bind("cond", "if");
|
|
macros["#cond"] = cond.bind("#cond", "#if");
|
|
|
|
// (or... ) uses (cond... ) under the hood
|
|
macros["or"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, null, "(or [v1] [v2] [values...])");
|
|
var b = wholeExp.expBuilder();
|
|
|
|
var uniqueVarSymbol = b.symbol();
|
|
|
|
b.begin([
|
|
b.call(b.symbol("localVar"), [
|
|
b.meta("mut", b.typed("Dynamic", uniqueVarSymbol)),
|
|
b.symbol("null")
|
|
]),
|
|
b.call(b.symbol("cond"), [
|
|
for (arg in args) {
|
|
b.call(
|
|
b.call(b.symbol("set"), [
|
|
uniqueVarSymbol,
|
|
arg
|
|
]), [
|
|
uniqueVarSymbol
|
|
]);
|
|
}
|
|
])
|
|
]);
|
|
};
|
|
|
|
// (and... uses (cond... ) and (not ...) under the hood)
|
|
macros["and"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k) -> {
|
|
wholeExp.checkNumArgs(2, null, "(and [v1] [v2] [values...])");
|
|
var b = wholeExp.expBuilder();
|
|
|
|
var uniqueVarSymbol = b.symbol();
|
|
|
|
var condCases = [
|
|
for (arg in args) {
|
|
b.call(
|
|
b.call(
|
|
b.symbol("not"), [
|
|
b.call(
|
|
b.symbol("set"), [uniqueVarSymbol, arg])
|
|
]), [
|
|
b.symbol("null")
|
|
]);
|
|
}
|
|
];
|
|
condCases.push(b.call(b.symbol("true"), [uniqueVarSymbol]));
|
|
|
|
b.begin([
|
|
b.call(
|
|
b.symbol("localVar"), [
|
|
b.meta("mut", b.typed("Dynamic", uniqueVarSymbol)),
|
|
b.symbol("null")
|
|
]),
|
|
b.call(
|
|
b.symbol("cond"),
|
|
condCases)
|
|
]);
|
|
};
|
|
|
|
function arraySet(wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
|
var b = wholeExp.expBuilder();
|
|
return b.call(
|
|
b.symbol("set"), [
|
|
b.call(b.symbol("nth"), [exps[0], exps[1]]),
|
|
exps[2]
|
|
]);
|
|
}
|
|
macros["setNth"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(3, 3, "(setNth [list] [index] [value])");
|
|
arraySet(wholeExp, exps, k);
|
|
};
|
|
macros["dictSet"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(3, 3, "(dictSet [dict] [key] [value])");
|
|
arraySet(wholeExp, exps, k);
|
|
};
|
|
|
|
macros["assert"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 2, "(assert [expression] [message])");
|
|
var b = wholeExp.expBuilder();
|
|
var expression = exps[0];
|
|
var basicMessage = 'Assertion ${expression.def.toString()} failed';
|
|
var messageExp = if (exps.length > 1) {
|
|
b.callSymbol("+", [b.str(basicMessage + ": "), exps[1]]);
|
|
} else {
|
|
b.str(basicMessage);
|
|
};
|
|
b.callSymbol("unless", [
|
|
expression,
|
|
b.callSymbol("throw", [messageExp])
|
|
]);
|
|
};
|
|
|
|
function stringsThatMatch(exp:ReaderExp, formName:String) {
|
|
return switch (exp.def) {
|
|
case StrExp(s):
|
|
[s];
|
|
case ListExp(strings):
|
|
[
|
|
for (s in strings)
|
|
switch (s.def) {
|
|
case StrExp(s):
|
|
s;
|
|
default:
|
|
throw CompileError.fromExp(s, 'initiator list of $formName must only contain strings');
|
|
}
|
|
];
|
|
default:
|
|
throw CompileError.fromExp(exp, 'first argument to $formName should be a String or list of strings');
|
|
};
|
|
}
|
|
|
|
macros["defmacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(3, null, '(defMacro [name] [[args...]] [body...])');
|
|
|
|
var name = switch (exps[0].def) {
|
|
case Symbol(name): name;
|
|
default: throw CompileError.fromExp(exps[0], "macro name should be a symbol");
|
|
};
|
|
|
|
var argList = switch (exps[1].def) {
|
|
case ListExp(macroArgs): macroArgs;
|
|
case CallExp(_, _):
|
|
throw CompileError.fromExp(exps[1], 'expected a macro argument list. Change the parens () to brackets []');
|
|
default:
|
|
throw CompileError.fromExp(exps[1], 'expected a macro argument list');
|
|
};
|
|
|
|
// This is similar to &opt and &rest processing done by Helpers.makeFunction()
|
|
// but combining them would probably make things less readable and harder
|
|
// to maintain, because defmacro makes an actual function, not a function definition
|
|
var minArgs = 0;
|
|
var maxArgs = 0;
|
|
// Once the &opt meta appears, all following arguments are optional until &rest
|
|
var optIndex = -1;
|
|
// Once the &rest or &body meta appears, no other arguments can be declared
|
|
var restIndex = -1;
|
|
var requireRest = false;
|
|
var argNames = [];
|
|
|
|
var macroCallForm = '($name';
|
|
|
|
for (arg in argList) {
|
|
if (restIndex != -1) {
|
|
throw CompileError.fromExp(arg, "macros cannot declare arguments after a &rest or &body argument");
|
|
}
|
|
switch (arg.def) {
|
|
case Symbol(name):
|
|
argNames.push(name);
|
|
if (optIndex == -1) {
|
|
++minArgs;
|
|
macroCallForm += ' [$name]';
|
|
} else {
|
|
macroCallForm += ' [?$name]';
|
|
}
|
|
++maxArgs;
|
|
case MetaExp("opt", {pos: _, def: Symbol(name)}):
|
|
argNames.push(name);
|
|
macroCallForm += ' [?$name]';
|
|
optIndex = maxArgs;
|
|
++maxArgs;
|
|
case MetaExp("rest", {pos: _, def: Symbol(name)}):
|
|
if (name == "body") {
|
|
CompileError.warnFromExp(arg, "Consider using &body instead of &rest when writing macros with bodies.");
|
|
}
|
|
argNames.push(name);
|
|
macroCallForm += ' [$name...]';
|
|
restIndex = maxArgs;
|
|
maxArgs = null;
|
|
case MetaExp("body", {pos: _, def: Symbol(name)}):
|
|
argNames.push(name);
|
|
macroCallForm += ' [$name...]';
|
|
restIndex = maxArgs;
|
|
requireRest = true;
|
|
maxArgs = null;
|
|
default:
|
|
throw CompileError.fromExp(arg, "macro argument should be an untyped symbol or a symbol annotated with &opt or &rest");
|
|
}
|
|
}
|
|
|
|
macroCallForm += ')';
|
|
if (optIndex == -1)
|
|
optIndex = minArgs;
|
|
if (restIndex == -1)
|
|
restIndex = optIndex;
|
|
|
|
macros[name] = (wholeExp:ReaderExp, innerExps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(minArgs, maxArgs, macroCallForm);
|
|
var b = wholeExp.expBuilder();
|
|
var innerArgNames = argNames.copy();
|
|
|
|
var args:Map<String, Dynamic> = [];
|
|
for (idx in 0...optIndex) {
|
|
args[innerArgNames.shift()] = innerExps[idx];
|
|
}
|
|
for (idx in optIndex...restIndex) {
|
|
args[innerArgNames.shift()] = if (exps.length > idx) innerExps[idx] else null;
|
|
}
|
|
if (innerArgNames.length > 0) {
|
|
var restArgs = innerExps.slice(restIndex);
|
|
if (requireRest && restArgs.length == 0) {
|
|
throw CompileError.fromExp(wholeExp, 'Macro $name requires one or more expression for &body');
|
|
}
|
|
args[innerArgNames.shift()] = restArgs;
|
|
}
|
|
|
|
// Return the macro expansion:
|
|
return Helpers.runAtCompileTime(b.callSymbol("begin", exps.slice(2)), k, args);
|
|
};
|
|
|
|
null;
|
|
};
|
|
renameAndDeprecate("defmacro", "defMacro");
|
|
|
|
macros["undefmacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 1, '(undefMacro [name])');
|
|
|
|
var name = switch (exps[0].def) {
|
|
case Symbol(name): name;
|
|
default: throw CompileError.fromExp(exps[0], "macro name should be a symbol");
|
|
};
|
|
|
|
k.macros.remove(name);
|
|
null;
|
|
};
|
|
renameAndDeprecate("undefmacro", "undefMacro");
|
|
|
|
macros["defreadermacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(3, null, '(defReaderMacro [optional &start] ["[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, "defReaderMacro");
|
|
default:
|
|
stringsThatMatch(exps[0], "defReaderMacro");
|
|
};
|
|
for (s in strings) {
|
|
switch (exps[1].def) {
|
|
case ListExp([{pos: _, def: Symbol(streamArgName)}]):
|
|
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());
|
|
Helpers.runAtCompileTime(body, k, [streamArgName => stream]).def;
|
|
};
|
|
case CallExp(_, []):
|
|
throw CompileError.fromExp(exps[1], 'expected an argument list. Change the parens () to brackets []');
|
|
default:
|
|
throw CompileError.fromExp(exps[1], 'second argument to defreadermacro should be [steamArgName]');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
renameAndDeprecate("defreadermacro", "defReaderMacro");
|
|
|
|
macros["undefreadermacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 1, '(undefReaderMacro [optional &start] ["[startingString]" or [startingStrings...]])');
|
|
// reader macros undeclared in the form (undefReaderMacro &start ...) will be removed from the table
|
|
// for reader macros that must be at the beginning of lines
|
|
// 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,
|
|
// this macro will undefine all of them
|
|
var strings = switch (exps[0].def) {
|
|
case MetaExp("start", stringsExp):
|
|
table = k.startOfLineReadTable;
|
|
stringsThatMatch(stringsExp, "undefReaderMacro");
|
|
default:
|
|
stringsThatMatch(exps[0], "undefReaderMacro");
|
|
};
|
|
for (s in strings) {
|
|
table.remove(s);
|
|
}
|
|
return null;
|
|
};
|
|
renameAndDeprecate("undefreadermacro", "undefReaderMacro");
|
|
|
|
// Having this floating out here is sketchy, but should work out fine because the variable is always re-set
|
|
// through the next function before being used in defalias or undefalias
|
|
var aliasMap:Map<String, ReaderExpDef> = null;
|
|
|
|
function getAliasName(k:KissState, nameExpWithMeta:ReaderExp, formName:String):String {
|
|
var error = CompileError.fromExp(nameExpWithMeta, 'first argument to $formName should be &call [alias] or &ident [alias]');
|
|
var nameExp = switch (nameExpWithMeta.def) {
|
|
case MetaExp("call", nameExp):
|
|
aliasMap = k.callAliases;
|
|
nameExp;
|
|
case MetaExp("ident", nameExp):
|
|
aliasMap = k.identAliases;
|
|
nameExp;
|
|
default:
|
|
throw error;
|
|
};
|
|
return switch (nameExp.def) {
|
|
case Symbol(whenItsThis):
|
|
whenItsThis;
|
|
default:
|
|
throw error;
|
|
};
|
|
}
|
|
|
|
macros["defalias"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, 2, "(defAlias [[&call or &ident] whenItsThis] [makeItThis])");
|
|
var name = getAliasName(k, exps[0], "defAlias");
|
|
|
|
aliasMap[name] = exps[1].def;
|
|
return null;
|
|
};
|
|
renameAndDeprecate("defalias", "defAlias");
|
|
|
|
macros["undefalias"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 1, "(undefAlias [[&call or &ident] alias])");
|
|
var name = getAliasName(k, exps[0], "undefAlias");
|
|
|
|
aliasMap.remove(name);
|
|
return null;
|
|
};
|
|
renameAndDeprecate("undefalias", "undefAlias");
|
|
|
|
// Macros that null-check and extract patterns from enums (inspired by Rust)
|
|
function ifLet(wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
|
wholeExp.checkNumArgs(2, 3, "(ifLet [[enum bindings...]] [thenExp] [?elseExp])");
|
|
var b = wholeExp.expBuilder();
|
|
|
|
var thenExp = exps[1];
|
|
var elseExp = if (exps.length > 2) {
|
|
exps[2];
|
|
} else {
|
|
b.symbol("null");
|
|
};
|
|
|
|
var bindingList = exps[0].bindingList("ifLet");
|
|
var firstPattern = bindingList.shift();
|
|
var firstValue = bindingList.shift();
|
|
var firstValueSymbol = b.symbol();
|
|
|
|
return b.callSymbol("let", [
|
|
b.list([firstValueSymbol, firstValue]),
|
|
b.callSymbol("if", [
|
|
firstValueSymbol,
|
|
b.call(
|
|
b.symbol("case"), [
|
|
firstValueSymbol,
|
|
b.call(
|
|
firstPattern, [
|
|
if (bindingList.length == 0) {
|
|
exps[1];
|
|
} else {
|
|
ifLet(wholeExp, [
|
|
b.list(bindingList)
|
|
].concat(exps.slice(1)), k);
|
|
}
|
|
]),
|
|
b.call(
|
|
b.symbol("otherwise"), [
|
|
elseExp
|
|
])
|
|
]),
|
|
elseExp
|
|
])
|
|
]);
|
|
}
|
|
|
|
macros["ifLet"] = ifLet;
|
|
|
|
macros["whenLet"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, null, "(whenLet [[enum bindings...]] [body...])");
|
|
var b = wholeExp.expBuilder();
|
|
b.callSymbol("ifLet", [
|
|
exps[0],
|
|
b.begin(exps.slice(1)),
|
|
b.symbol("null")
|
|
]);
|
|
};
|
|
|
|
macros["unlessLet"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, null, "(unlessLet [[enum bindings...]] [body...])");
|
|
var b = wholeExp.expBuilder();
|
|
b.callSymbol("ifLet", [
|
|
exps[0],
|
|
b.symbol("null"),
|
|
b.begin(exps.slice(1))
|
|
]);
|
|
};
|
|
|
|
// TODO test this
|
|
function awaitLet(wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
|
wholeExp.checkNumArgs(2, null, "(awaitLet [[promise bindings...]] [body...])");
|
|
var bindingList = exps[0].bindingList("awaitLet");
|
|
var firstName = bindingList.shift();
|
|
var firstValue = bindingList.shift();
|
|
var b = wholeExp.expBuilder();
|
|
|
|
return b.call(b.field("then", firstValue), [
|
|
b.call(b.symbol("lambda"), [
|
|
b.list([firstName]),
|
|
if (bindingList.length == 0) {
|
|
b.call(b.symbol("begin"), exps.slice(1));
|
|
} else {
|
|
awaitLet(wholeExp, [b.list(bindingList)].concat(exps.slice(1)), k);
|
|
}
|
|
]),
|
|
// Handle rejections:
|
|
b.call(b.symbol("lambda"), [
|
|
b.list([b.symbol("reason")]),
|
|
b.call(b.symbol("throw"), [
|
|
// TODO generalize CompileError to KissError which will also handle runtime errors
|
|
// with the same source position format
|
|
b.str("rejected promise")
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
macros["awaitLet"] = awaitLet;
|
|
|
|
// TODO test defNew
|
|
macros["defnew"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, null, "(defNew [[args...]] [[optional property bindings...]] [optional body...]");
|
|
|
|
var args = exps.shift();
|
|
var bindingList = [];
|
|
|
|
if (exps.length != 0) {
|
|
switch (exps[0].def) {
|
|
case ListExp(_):
|
|
bindingList = exps.shift().bindingList("defNew", true);
|
|
default:
|
|
}
|
|
}
|
|
var bindingPairs = Prelude.groups(bindingList, 2);
|
|
|
|
var propertyDefs = [for (bindingPair in bindingPairs) {
|
|
var b = bindingPair[0].expBuilder();
|
|
b.call(b.symbol("prop"), [bindingPair[0]]);
|
|
}];
|
|
var propertySetExps = [for (bindingPair in bindingPairs) {
|
|
var b = bindingPair[1].expBuilder();
|
|
b.call(b.symbol("set"), [b.symbol(Helpers.varName("a prop property binding", bindingPair[0])), bindingPair[1]]);
|
|
}];
|
|
|
|
var argList = [];
|
|
// &prop in the argument list defines a property supplied directly as an argument
|
|
for (arg in Helpers.argList(args, "defNew")) {
|
|
var b = arg.expBuilder();
|
|
switch (arg.def) {
|
|
case MetaExp("prop", propExp):
|
|
argList.push(propExp);
|
|
propertyDefs.push(
|
|
b.call(b.symbol("prop"), [propExp]));
|
|
// TODO allow &prop &mut or &mut &prop
|
|
switch (propExp.def) {
|
|
case TypedExp(_, {pos: _, def: Symbol(name)}):
|
|
propertySetExps.push(
|
|
b.call(b.symbol("set"), [b.field(name, b.symbol("this")), b.symbol(name)]));
|
|
default:
|
|
throw CompileError.fromExp(arg, "invalid use of &prop in defNew");
|
|
}
|
|
default:
|
|
argList.push(arg);
|
|
}
|
|
}
|
|
|
|
var b = wholeExp.expBuilder();
|
|
|
|
return b.begin(propertyDefs.concat([
|
|
b.call(b.symbol("method"), [
|
|
b.symbol("new"),
|
|
b.list(argList)
|
|
].concat(propertySetExps).concat(exps))
|
|
]));
|
|
};
|
|
renameAndDeprecate("defnew", "defNew");
|
|
|
|
macros["collect"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, 1, "(collect [iterator or iterable])");
|
|
var b = wholeExp.expBuilder();
|
|
b.call(b.symbol("for"), [b.symbol("elem"), exps[0], b.symbol("elem")]);
|
|
};
|
|
|
|
function once(macroName:String, wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
|
wholeExp.checkNumArgs(1, null, '($macroName [body...])');
|
|
var b = wholeExp.expBuilder();
|
|
var flag = b.symbol();
|
|
// define the field:
|
|
k.convert(b.call(b.symbol(macroName), [b.meta("mut", flag), b.symbol("true")]));
|
|
return b.call(b.symbol("when"), [flag, b.call(b.symbol("set"), [flag, b.symbol("false")])].concat(exps));
|
|
}
|
|
|
|
macros["once"] = once.bind("var");
|
|
macros["oncePerInstance"] = once.bind("prop");
|
|
|
|
macros["defMacroVar"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, 2, "(defMacroVar <name> <value>)");
|
|
|
|
var name = exps[0].symbolNameValue();
|
|
|
|
k.macroVars[name] = Helpers.runAtCompileTimeDynamic(exps[1], k);
|
|
|
|
return null;
|
|
};
|
|
|
|
macros["defMacroFunction"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(3, null, "(defMacroFunction <name> [<args>] <body...>)");
|
|
var b = wholeExp.expBuilder();
|
|
var name = exps[0].symbolNameValue();
|
|
var lambdaExp = b.callSymbol("lambda", [exps[1]].concat(exps.slice(2)));
|
|
|
|
k.macroVars[name] = Helpers.runAtCompileTimeDynamic(lambdaExp, k);
|
|
|
|
return null;
|
|
};
|
|
|
|
// Replace "try" with this in a try-catch statement to let all exceptions throw
|
|
// their original call stacks. This is more convenient for debugging than trying to
|
|
// comment out the "try" and its catches, and re-balance parens
|
|
macros["letThrow"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(1, null, "(letThrow [thing] [catches...])");
|
|
exps[0];
|
|
};
|
|
|
|
// The wildest code in Kiss to date
|
|
// TODO test exprCase!!
|
|
macros["exprCase"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(2, null, "(exprCase [expr] [pattern callExps...])");
|
|
var toMatch = exps.shift();
|
|
|
|
var b = wholeExp.expBuilder();
|
|
var functionKey = Uuid.v4();
|
|
|
|
exprCaseFunctions[functionKey] = (toMatchValue:ReaderExp) -> {
|
|
for (patternExp in exps) {
|
|
switch (patternExp.def) {
|
|
case CallExp(pattern, body):
|
|
if (matchExpr(pattern, toMatchValue)) {
|
|
return b.begin(body);
|
|
}
|
|
default:
|
|
throw CompileError.fromExp(patternExp, "bad exprCase pattern expression");
|
|
}
|
|
}
|
|
|
|
throw CompileError.fromExp(wholeExp, 'expression ${toMatch.def.toString()} matches no pattern in exprCase');
|
|
};
|
|
|
|
return b.call(b.symbol("Macros.exprCase"), [b.str(functionKey), toMatch, b.symbol("k")]);
|
|
};
|
|
|
|
// Maybe the NEW wildest code in Kiss?
|
|
macros["#extern"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
|
wholeExp.checkNumArgs(4, null, "(#extern <BodyType> <lang> <?compileArgs object> [<typed bindings...>] <body...>)");
|
|
|
|
var bodyType = exps.shift();
|
|
var langExp = exps.shift();
|
|
var originalLang = langExp.symbolNameValue();
|
|
// make the lang argument forgiving, because many will assume it can match the compiler defines and command-line arguments of Haxe
|
|
var lang = switch (originalLang) {
|
|
case "python" | "py": "Python";
|
|
case "js" | "javascript": "JavaScript";
|
|
default: originalLang;
|
|
};
|
|
|
|
var allowedLangs = EnumTools.getConstructors(CompileLang);
|
|
if (allowedLangs.indexOf(lang) == -1) {
|
|
throw CompileError.fromExp(langExp, 'unsupported lang for #extern: $originalLang should be one of $allowedLangs');
|
|
}
|
|
var langArg = EnumTools.createByName(CompileLang, lang);
|
|
|
|
var compileArgsExp = null;
|
|
var bindingListExp = null;
|
|
var nextArg = exps.shift();
|
|
switch (nextArg.def) {
|
|
case CallExp({pos: _, def: Symbol("object")}, _):
|
|
compileArgsExp = nextArg;
|
|
nextArg = exps.shift();
|
|
case ListExp(_):
|
|
// Let the next switch handle the binding list
|
|
default:
|
|
throw CompileError.fromExp(nextArg, "second argument to #extern can either be a CompileArgs object or a list of typed bindings");
|
|
}
|
|
switch (nextArg.def) {
|
|
case ListExp(_):
|
|
bindingListExp = nextArg;
|
|
default:
|
|
throw CompileError.fromExp(nextArg, "#extern requires a list of typed bindings");
|
|
}
|
|
|
|
var compileArgs:CompilationArgs = if (compileArgsExp != null) {
|
|
Helpers.runAtCompileTimeDynamic(compileArgsExp, k);
|
|
} else {
|
|
{};
|
|
}
|
|
|
|
var b = wholeExp.expBuilder();
|
|
|
|
// TODO generate tink_json writers and parsers for this
|
|
var bindingList = bindingListExp.bindingList("#extern", true);
|
|
|
|
var idx = 0;
|
|
var stringifyExpList = [];
|
|
var parseBindingList = [];
|
|
while (idx < bindingList.length) {
|
|
var type = "";
|
|
var untypedName = switch (bindingList[idx].def) {
|
|
case TypedExp(_type, symbol = {pos: _, def: Symbol(name)}):
|
|
type = _type;
|
|
symbol;
|
|
default: throw CompileError.fromExp(bindingList[idx], "name in #extern binding list must be a typed symbol");
|
|
};
|
|
switch (bindingList[idx + 1].def) {
|
|
// _ in the value position of the #extern binding list will reuse the name as the value
|
|
case Symbol("_"):
|
|
bindingList[idx + 1] = untypedName;
|
|
default:
|
|
}
|
|
|
|
stringifyExpList.push(b.the(b.symbol("String"), b.callSymbol("tink.Json.stringify", [b.the(b.symbol(type), bindingList[idx + 1])])));
|
|
parseBindingList.push(bindingList[idx]);
|
|
parseBindingList.push(b.callSymbol("tink.Json.parse", [b.callField("shift", b.symbol("__args__"), [])]));
|
|
idx += 2;
|
|
}
|
|
|
|
var externExps = [
|
|
b.let([b.symbol("__args__"), b.callSymbol("Sys.args", [])], [
|
|
b.callSymbol("set", [
|
|
b.symbol("Prelude.printStr"),
|
|
b.symbol("Prelude._externPrintStr")
|
|
]),
|
|
b.callSymbol("Prelude._printStr", [
|
|
b.callSymbol("tink.Json.stringify", [
|
|
b.the(bodyType, if (bindingList.length > 0) {
|
|
b.let(parseBindingList, exps);
|
|
} else {
|
|
b.begin(exps);
|
|
})
|
|
])
|
|
]),
|
|
b.callSymbol("Sys.exit", [b.symbol("0")])
|
|
])
|
|
];
|
|
b.the(
|
|
bodyType,
|
|
b.callSymbol("tink.Json.parse", [
|
|
b.call(b.raw(CompilerTools.compileToScript(externExps, langArg, compileArgs).toString()), [b.list(stringifyExpList)])
|
|
]));
|
|
};
|
|
|
|
return macros;
|
|
}
|
|
|
|
static var exprCaseFunctions:Map<String, ReaderExp->ReaderExp> = [];
|
|
|
|
public static function exprCase(id:String, toMatchValue:ReaderExp, k:KissState):ReaderExp {
|
|
return Helpers.runAtCompileTime(exprCaseFunctions[id](toMatchValue), k);
|
|
}
|
|
|
|
static function matchExpr(pattern:ReaderExp, instance:ReaderExp):Bool {
|
|
switch (pattern.def) {
|
|
case Symbol("_"):
|
|
return true;
|
|
case CallExp({pos: _, def: Symbol("exprOr")}, altPatterns):
|
|
for (altPattern in altPatterns) {
|
|
if (matchExpr(altPattern, instance))
|
|
return true;
|
|
}
|
|
return false;
|
|
case Symbol(patternSymbol):
|
|
return switch (instance.def) {
|
|
case Symbol(instanceSymbol) if (patternSymbol == instanceSymbol):
|
|
true;
|
|
default:
|
|
false;
|
|
};
|
|
case ListExp(patternExps):
|
|
switch (instance.def) {
|
|
case ListExp(instanceExps) if (patternExps.length == instanceExps.length):
|
|
for (idx in 0...patternExps.length) {
|
|
if (!matchExpr(patternExps[idx], instanceExps[idx]))
|
|
return false;
|
|
}
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
case CallExp(patternFuncExp, patternExps):
|
|
switch (instance.def) {
|
|
case CallExp(instanceFuncExp, instanceExps) if (patternExps.length == instanceExps.length):
|
|
if (!matchExpr(patternFuncExp, instanceFuncExp))
|
|
return false;
|
|
for (idx in 0...patternExps.length) {
|
|
if (!matchExpr(patternExps[idx], instanceExps[idx]))
|
|
return false;
|
|
}
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
// I don't think I'll ever want to match specific string literals, raw haxe, field expressions,
|
|
// key-value expressions, quasiquotes, unquotes, or UnquoteLists. This function can be expanded
|
|
// later if those features are ever needed.
|
|
default:
|
|
throw CompileError.fromExp(pattern, "unsupported pattern for exprCase");
|
|
}
|
|
}
|
|
|
|
// cond expands telescopically into a nested if expression
|
|
static function cond(formName:String, underlyingIf:String, wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
|
wholeExp.checkNumArgs(1, null, '($formName [cases...])');
|
|
var b = wholeExp.expBuilder();
|
|
return switch (exps[0].def) {
|
|
case CallExp(condition, body):
|
|
b.call(b.symbol(underlyingIf), [
|
|
condition,
|
|
b.begin(body),
|
|
if (exps.length > 1) {
|
|
cond(formName, underlyingIf, b.callSymbol(formName, exps.slice(1)), exps.slice(1), k);
|
|
} else {
|
|
b.symbol("null");
|
|
}
|
|
]);
|
|
default:
|
|
throw CompileError.fromExp(exps[0], 'top-level expression of (cond... ) must be a call list starting with a condition expression');
|
|
};
|
|
}
|
|
}
|