Files
kiss/src/kiss/Helpers.hx

633 lines
26 KiB
Haxe

package kiss;
import haxe.macro.Expr;
import haxe.macro.Context;
import haxe.macro.PositionTools;
import hscript.Parser;
import hscript.Interp;
import kiss.Reader;
import kiss.ReaderExp;
import kiss.CompileError;
import kiss.Kiss;
import kiss.SpecialForms;
import kiss.Prelude;
import kiss.cloner.Cloner;
import uuid.Uuid;
import sys.io.Process;
using uuid.Uuid;
using tink.MacroApi;
using kiss.Reader;
using kiss.Helpers;
using kiss.Kiss;
using StringTools;
using haxe.macro.ExprTools;
/**
* Compile-time helper functions for Kiss. Don't import or reference these at runtime.
*/
class Helpers {
public static function macroPos(exp:ReaderExp) {
var kissPos = exp.pos;
return PositionTools.make({
min: kissPos.absoluteChar,
max: kissPos.absoluteChar,
file: kissPos.file
});
}
public static function withMacroPosOf(e:ExprDef, exp:ReaderExp):Expr {
return {
pos: macroPos(exp),
expr: e
};
}
static function startsWithUpperCase(s:String) {
return s.charAt(0) == s.charAt(0).toUpperCase();
}
public static function parseTypePath(path:String, ?from:ReaderExp):TypePath {
return switch (parseComplexType(path, from)) {
case TPath(path):
path;
default:
var errorMessage = 'Haxe could not parse a type path from $path';
if (from == null) {
throw errorMessage;
} else {
throw CompileError.fromExp(from, errorMessage);
}
};
}
public static function parseComplexType(path:String, ?from:ReaderExp):ComplexType {
// Trick Haxe into parsing it for us:
var typeCheckStr = '(thing : $path)';
var errorMessage = 'Haxe could not parse a complex type from `$path` in `${typeCheckStr}`';
function throwError() {
if (from == null) {
throw errorMessage;
} else {
throw CompileError.fromExp(from, errorMessage);
};
}
try {
var typeCheckExpr = Context.parse(typeCheckStr, Context.currentPos());
return switch (typeCheckExpr.expr) {
case EParenthesis({pos: _, expr: ECheckType(_, complexType)}):
complexType;
default:
throwError();
return null;
};
} catch (err) {
throwError();
return null;
}
}
public static function explicitType(nameExp:ReaderExp):ComplexType {
return switch (nameExp.def) {
case MetaExp(_, innerExp):
explicitType(innerExp);
case TypedExp(type, _):
Helpers.parseComplexType(type, nameExp);
default: null;
};
}
public static function varName(formName:String, nameExp:ReaderExp, nameType = "variable") {
return switch (nameExp.def) {
case Symbol(name):
name;
case MetaExp(_, nameExp) | TypedExp(_, nameExp):
varName(formName, nameExp);
default:
throw CompileError.fromExp(nameExp, 'The first argument to $formName should be a $nameType name, :Typed $nameType name, and/or &meta $nameType name.');
};
}
// TODO generic type parameter declarations
public static function makeFunction(?name:ReaderExp, returnsValue:Bool, argList:ReaderExp, body:List<ReaderExp>, k:KissState, formName:String):Function {
var funcName = if (name != null) {
varName(formName, name, "function");
} else {
"";
};
var numArgs = 0;
// Once the &opt meta appears, all following arguments are optional until &rest
var opt = false;
// Once the &rest meta appears, no other arguments can be declared
var rest = false;
var restProcessed = false;
function makeFuncArg(funcArg:ReaderExp):FunctionArg {
if (restProcessed) {
throw CompileError.fromExp(funcArg, "cannot declare more arguments after a &rest argument");
}
return switch (funcArg.def) {
case MetaExp("rest", innerFuncArg):
if (funcName == "") {
throw CompileError.fromExp(funcArg, "lambda does not support &rest arguments");
}
// rest arguments define a Kiss special form with the function's name that wraps
// the rest args in a list when calling it from Kiss
k.specialForms[funcName] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
var realCallArgs = args.slice(0, numArgs);
var restArgs = args.slice(numArgs);
realCallArgs.push(ListExp(restArgs).withPosOf(wholeExp));
ECall(k.convert(Symbol(funcName).withPosOf(wholeExp)), realCallArgs.map(k.convert)).withMacroPosOf(wholeExp);
};
opt = true;
rest = true;
makeFuncArg(innerFuncArg);
case MetaExp("opt", innerFuncArg):
opt = true;
makeFuncArg(innerFuncArg);
default:
if (rest) {
restProcessed = true;
} else {
++numArgs;
}
{
// These could use varName() and explicitType() but so far there are no &meta annotations for function arguments
name: switch (funcArg.def) {
case Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)}):
name;
default:
throw CompileError.fromExp(funcArg, 'function argument should be a symbol or typed symbol');
},
type: switch (funcArg.def) {
case TypedExp(type, _):
Helpers.parseComplexType(type, funcArg);
default: null;
},
opt: opt
};
};
}
var expr = if (body.length == 0) {
EReturn(null).withMacroPosOf(if (name != null) name else argList);
} else {
var block = k.convert(CallExp(Symbol("begin").withPos(body[0].pos), body).withPos(body[0].pos));
if (returnsValue) {
EReturn(block).withMacroPosOf(body[-1]);
} else {
block;
};
}
// To make function args immutable by default, we would use (let...) instead of (begin...)
// to make the body expression.
// But setting null arguments to default values is so common, and arguments are not settable references,
// so function args are not immutable.
return {
ret: if (name != null) Helpers.explicitType(name) else null,
args: switch (argList.def) {
case ListExp(funcArgs):
funcArgs.map(makeFuncArg);
case CallExp(_, _):
throw CompileError.fromExp(argList, 'expected an argument list. Change the parens () to brackets []');
default:
throw CompileError.fromExp(argList, 'expected an argument list');
},
expr: expr
}
}
// 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 restExpIndex = -1;
var restExpName = "";
var expNames = [];
var listVarSymbol = null;
function makeSwitchPattern(patternExp:ReaderExp):Array<Expr> {
return switch (patternExp.def) {
case CallExp({pos: _, def: Symbol("when")}, whenExps):
patternExp.checkNumArgs(2, 2, "(when [guard] [pattern])");
if (guard != null)
throw CompileError.fromExp(caseExp, "case pattern can only have one `when` guard");
guard = macro Prelude.truthy(${k.convert(whenExps[0])});
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:
[k.forCaseParsing().convert(patternExp)];
}
}
return switch (caseExp.def) {
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: pattern,
expr: body,
guard: guard
};
default:
throw CompileError.fromExp(caseExp, "case expressions for (case...) must take the form ([pattern] [body...])");
}
}
/**
Throw a CompileError if the given expression has the wrong number of arguments
**/
public static function checkNumArgs(wholeExp:ReaderExp, min:Null<Int>, max:Null<Int>, ?expectedForm:String) {
if (expectedForm == null) {
expectedForm = if (max == min) {
'$min arguments';
} else if (max == null) {
'at least $min arguments';
} else if (min == null) {
'no more than $max arguments';
} else if (min == null && max == null) {
throw 'checkNumArgs() needs a min or a max';
} else {
'between $min and $max arguments';
};
}
var args = switch (wholeExp.def) {
case CallExp(_, args): args;
default: throw CompileError.fromExp(wholeExp, "Can only check number of args in a CallExp");
};
if (min != null && args.length < min) {
throw CompileError.fromExp(wholeExp, 'Not enough arguments. Expected $expectedForm');
} else if (max != null && args.length > max) {
throw CompileError.fromExp(wholeExp, 'Too many arguments. Expected $expectedForm');
}
}
public static function removeTypeAnnotations(exp:ReaderExp):ReaderExp {
var def = switch (exp.def) {
case Symbol(_) | StrExp(_) | RawHaxe(_) | Quasiquote(_):
exp.def;
case CallExp(func, callArgs):
CallExp(removeTypeAnnotations(func), callArgs.map(removeTypeAnnotations));
case ListExp(elements):
ListExp(elements.map(removeTypeAnnotations));
case TypedExp(type, innerExp):
innerExp.def;
case MetaExp(meta, innerExp):
MetaExp(meta, removeTypeAnnotations(innerExp));
case FieldExp(field, innerExp):
FieldExp(field, removeTypeAnnotations(innerExp));
case KeyValueExp(keyExp, valueExp):
KeyValueExp(removeTypeAnnotations(keyExp), removeTypeAnnotations(valueExp));
default:
throw CompileError.fromExp(exp, 'cannot remove type annotations');
};
return def.withPosOf(exp);
}
// hscript.Interp is very finicky about some edge cases.
// This function handles them
private static function mapForInterp(expr:Expr):Expr {
return expr.map(subExp -> {
switch (subExp.expr) {
case ETry(e, catches):
catches = [for (c in catches) {
// hscript.Parser expects :Dynamic after the catch varname
{
type: Helpers.parseComplexType("Dynamic"),
name: c.name,
expr: c.expr
};
}];
{
pos: subExp.pos,
expr: ETry(e, catches)
};
default: subExp;
}
});
}
static var parser = new Parser();
static function compileTimeHScript(exp:ReaderExp, k:KissState) {
var hscriptExp = mapForInterp(k.forMacroEval().convert(exp));
var code = hscriptExp.toString(); // tink_macro to the rescue
#if macrotest
Prelude.print("Compile-time hscript: " + code);
#end
// Need parser external to the KissInterp to wrap parsing in an informative try-catch
var parsed = try {
parser.parseString(code);
} catch (e) {
throw CompileError.fromExp(exp, 'macro-time hscript parsing failed with $e:\n$code');
};
return parsed;
}
public static function runAtCompileTimeDynamic(exp:ReaderExp, k:KissState, ?args:Map<String, Dynamic>):Dynamic {
var parsed = compileTimeHScript(exp, k);
var interp = new KissInterp();
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("printExp", printExp);
interp.variables.set("kiss", {
ReaderExp: {
ReaderExpDef: ReaderExpDef
}
});
interp.variables.set("Macros", Macros);
for (name => value in k.macroVars) {
interp.variables.set(name, value);
}
function innerRunAtCompileTimeDynamic(innerExp:ReaderExp) {
// in case macroVars have changed
for (name => value in k.macroVars) {
interp.variables.set(name, value);
}
var locals = interp.getLocals();
interp.setLocals(new Cloner().clone(locals));
var value = interp.publicExprReturn(compileTimeHScript(innerExp, k));
interp.setLocals(locals);
if (value == null) {
throw CompileError.fromExp(exp, "compile-time evaluation returned null");
}
return value;
}
function innerRunAtCompileTime(exp:ReaderExp) {
return compileTimeValueToReaderExp(innerRunAtCompileTimeDynamic(exp), exp);
}
interp.variables.set("eval", innerRunAtCompileTimeDynamic);
interp.variables.set("Helpers", {
evalUnquotes: evalUnquotes.bind(_, innerRunAtCompileTime)
});
if (args != null) {
for (arg => value in args) {
interp.variables.set(arg, value);
}
}
var value:Dynamic = interp.execute(parsed);
if (value == null) {
throw CompileError.fromExp(exp, "compile-time evaluation returned null");
}
return value;
}
public static function runAtCompileTime(exp:ReaderExp, k:KissState, ?args:Map<String, Dynamic>):ReaderExp {
var value = runAtCompileTimeDynamic(exp, k, args);
var expResult = compileTimeValueToReaderExp(value, exp);
#if macrotest
Prelude.print('Compile-time value: ${Reader.toString(expResult.def)}');
#end
return expResult;
}
// The value could be either a ReaderExp, ReaderExpDef, Array of ReaderExp/ReaderExpDefs, or something else entirely,
// but it needs to be a ReaderExp for evalUnquotes()
static function compileTimeValueToReaderExp(e:Dynamic, source:ReaderExp):ReaderExp {
// TODO if it's a string, return a StrExp. That way, symbolNameValue() won't be required
// TODO if it's a number, return a Symbol(number.toString()).
return if (Std.isOfType(e, Array)) {
var arr:Array<Dynamic> = e;
var listExps = arr.map(compileTimeValueToReaderExp.bind(_, source));
ListExp(listExps).withPosOf(source);
} else if (e.def == null) {
(e : ReaderExpDef).withPosOf(source);
} else {
(e : ReaderExp);
}
}
public static function printExp(e:Dynamic, label = "") {
var toPrint = label;
if (label.length > 0) {
toPrint += ": ";
}
var expDef = if (e.def != null) e.def else e;
toPrint += Reader.toString(expDef);
Prelude.printStr(toPrint);
return e;
}
static function evalUnquoteLists(l:Array<ReaderExp>, innerRunAtCompileTime:(ReaderExp)->Dynamic):Array<ReaderExp> {
var idx = 0;
while (idx < l.length) {
switch (l[idx].def) {
case UnquoteList(exp):
l.splice(idx, 1);
var listToInsert:Dynamic = innerRunAtCompileTime(exp);
// listToInsert could be either an array (from &rest) or a ListExp (from [list syntax])
var newElements:Array<ReaderExp> = if (Std.isOfType(listToInsert, Array)) {
listToInsert;
} else {
switch (listToInsert.def) {
case ListExp(elements):
elements;
default:
throw CompileError.fromExp(listToInsert, ",@ can only be used with lists");
};
};
for (el in newElements) {
l.insert(idx++, el);
}
default:
idx++;
}
}
return l;
}
public static function evalUnquotes(exp:ReaderExp, innerRunAtCompileTime:(ReaderExp)->Dynamic):ReaderExp {
var recurse = evalUnquotes.bind(_, innerRunAtCompileTime);
var def = switch (exp.def) {
case Symbol(_) | StrExp(_) | RawHaxe(_):
exp.def;
case CallExp(func, callArgs):
CallExp(recurse(func), evalUnquoteLists(callArgs, innerRunAtCompileTime).map(recurse));
case ListExp(elements):
ListExp(evalUnquoteLists(elements, innerRunAtCompileTime).map(recurse));
case TypedExp(type, innerExp):
TypedExp(type, recurse(innerExp));
case FieldExp(field, innerExp):
FieldExp(field, recurse(innerExp));
case KeyValueExp(keyExp, valueExp):
KeyValueExp(recurse(keyExp), recurse(valueExp));
case Unquote(innerExp):
var unquoteValue:Dynamic = innerRunAtCompileTime(innerExp);
if (unquoteValue == null) {
throw CompileError.fromExp(innerExp, "unquote evaluated to null");
} else if (Std.isOfType(unquoteValue, ReaderExpDef)) {
unquoteValue;
} else if (Reflect.getProperty(unquoteValue, "def") != null) {
unquoteValue.def;
} else {
throw CompileError.fromExp(exp, "unquote didn't evaluate to a ReaderExp or ReaderExpDef");
};
case MetaExp(meta, innerExp):
MetaExp(meta, recurse(innerExp));
default:
throw CompileError.fromExp(exp, 'unquote evaluation not implemented');
};
return def.withPosOf(exp);
}
// Return convenient functions for succinctly making new ReaderExps that link back to an original exp's
// position in source code
public static function expBuilder(posRef:ReaderExp) {
function _symbol(?name:String) {
return Prelude.symbol(name).withPosOf(posRef);
}
function call(func:ReaderExp, args:Array<ReaderExp>) {
return CallExp(func, args).withPosOf(posRef);
}
function callSymbol(symbol:String, args:Array<ReaderExp>) {
return call(_symbol(symbol), args);
}
function field(f:String, exp:ReaderExp) {
return FieldExp(f, exp).withPosOf(posRef);
}
function list(exps:Array<ReaderExp>) {
return ListExp(exps).withPosOf(posRef);
}
function objectWith (bindings:Array<ReaderExp>, captures:Array<ReaderExp>) {
return callSymbol("objectWith", [list(bindings)].concat(captures));
}
function str(s:String) {
return StrExp(s).withPosOf(posRef);
}
function raw(code:String) {
return RawHaxe(code).withPosOf(posRef);
}
function int(v:Int) {
return _symbol(Std.string(v));
}
function float(v:Float) {
return _symbol(Std.string(v));
}
return {
call: call,
callSymbol: callSymbol,
callField: (fieldName:String, callOn:ReaderExp, args:Array<ReaderExp>) -> call(field(fieldName, callOn), args),
print: (arg:ReaderExp) -> CallExp(Symbol("print").withPosOf(posRef), [arg]).withPosOf(posRef),
the: (type:ReaderExp, value:ReaderExp) -> callSymbol("the", [type, value]),
list: list,
str: str,
symbol: _symbol,
int: int,
float: float,
raw: raw,
typed: (path:String, exp:ReaderExp) -> TypedExp(path, exp).withPosOf(posRef),
meta: (m:String, exp:ReaderExp) -> MetaExp(m, exp).withPosOf(posRef),
field: field,
keyValue: (key:ReaderExp, value:ReaderExp) -> KeyValueExp(key, value).withPosOf(posRef),
begin: (exps:Array<ReaderExp>) -> callSymbol("begin", exps),
let: (bindings:Array<ReaderExp>, body:Array<ReaderExp>) -> callSymbol("let", [list(bindings)].concat(body)),
objectWith: objectWith,
throwCompileError: (reason:String) -> {
callSymbol("throw", [
callSymbol("CompileError.fromExpStr", [
// pos
objectWith([
_symbol("file"), str(posRef.pos.file),
_symbol("line"), int(posRef.pos.line),
_symbol("column"), int(posRef.pos.column),
_symbol("absoluteChar"), int(posRef.pos.absoluteChar),
], []),
// expStr
str(Reader.toString(posRef.def)),
str(reason)
])
]);
},
none: () -> None.withPosOf(posRef)
};
}
public static function argList(exp:ReaderExp, forThis:String):Array<ReaderExp> {
return switch (exp.def) {
case ListExp(argExps):
argExps;
default:
throw CompileError.fromExp(exp, '$forThis arg list should be a list expression');
};
}
public static function bindingList(exp:ReaderExp, forThis:String, allowEmpty = false):Array<ReaderExp> {
return switch (exp.def) {
case ListExp(bindingExps) if ((allowEmpty || bindingExps.length > 0) && bindingExps.length % 2 == 0):
bindingExps;
default:
throw CompileError.fromExp(exp, '$forThis bindings should be a list expression with an even number of sub expressions (at least 2)');
};
}
}