Files
kiss/src/kiss/Kiss.hx
2021-08-12 23:09:43 -06:00

384 lines
15 KiB
Haxe

package kiss;
#if macro
import haxe.Exception;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.io.Path;
import kiss.Stream;
import kiss.Reader;
import kiss.ReaderExp;
import kiss.FieldForms;
import kiss.SpecialForms;
import kiss.Macros;
import kiss.CompileError;
import kiss.cloner.Cloner;
using kiss.Helpers;
using kiss.Reader;
using tink.MacroApi;
using haxe.io.Path;
typedef ExprConversion = (ReaderExp) -> Expr;
typedef KissState = {
className:String,
readTable:ReadTable,
startOfLineReadTable:ReadTable,
fieldForms:Map<String, FieldFormFunction>,
specialForms:Map<String, SpecialFormFunction>,
macros:Map<String, MacroFunction>,
wrapListExps:Bool,
loadedFiles:Map<String, Null<ReaderExp>>,
callAliases:Map<String, ReaderExpDef>,
identAliases:Map<String, ReaderExpDef>,
fieldList:Array<Field>,
// TODO This map was originally created to track whether the programmer wrote their own main function, but could also
// be used to allow macros to edit fields that were already defined (for instance, to decorate a function or add something
// to the constructor body)
fieldDict:Map<String, Field>,
loadingDirectory:String,
hscript:Bool,
macroVars:Map<String, Dynamic>
};
class Kiss {
public static function defaultKissState():KissState {
var className = Context.getLocalClass().get().name;
var k = {
className: className,
readTable: Reader.builtins(),
startOfLineReadTable: new ReadTable(),
fieldForms: FieldForms.builtins(),
specialForms: SpecialForms.builtins(),
macros: Macros.builtins(),
wrapListExps: true,
loadedFiles: new Map<String, ReaderExp>(),
// Helpful built-in aliases
// These ones might conflict with a programmer's variable names, so they only apply in call expressions:
callAliases: [
// TODO some of these probably won't conflict, and could be passed as functions for a number of reasons
"print" => Symbol("Prelude.print"),
"sort" => Symbol("Prelude.sort"),
"groups" => Symbol("Prelude.groups"),
"pairs" => Symbol("Prelude.pairs"), // TODO test pairs
"reversed" => Symbol("Prelude.reversed"), // TODO test reversed
"memoize" => Symbol("Prelude.memoize"), // TODO test memoize
"symbolName" => Symbol("Prelude.symbolName"),
"symbolNameValue" => Symbol("Prelude.symbolNameValue"),
"symbol" => Symbol("Prelude.symbol"),
"expList" => Symbol("Prelude.expList"),
"map" => Symbol("Lambda.map"),
"filter" => Symbol("Prelude.filter"),
"flatten" => Symbol("Lambda.flatten"),
"has" => Symbol("Lambda.has"),
"count" => Symbol("Lambda.count"),
"enumerate" => Symbol("Prelude.enumerate"),
"assertProcess" => Symbol("Prelude.assertProcess"),
"random" => Symbol("Std.random"),
"walkDirectory" => Symbol("Prelude.walkDirectory"),
"getTarget" => Symbol("Prelude.getTarget"),
// These work with (apply) because they are added as "opAliases" in Macros.kiss:
"min" => Symbol("Prelude.min"),
"max" => Symbol("Prelude.max"),
],
identAliases: [
// These ones won't conflict with variables and might commonly be used with (apply)
"+" => Symbol("Prelude.add"),
"-" => Symbol("Prelude.subtract"),
"*" => Symbol("Prelude.multiply"),
"/" => Symbol("Prelude.divide"),
"%" => Symbol("Prelude.mod"),
"^" => Symbol("Prelude.pow"),
">" => Symbol("Prelude.greaterThan"),
">=" => Symbol("Prelude.greaterEqual"),
"<" => Symbol("Prelude.lessThan"),
"<=" => Symbol("Prelude.lesserEqual"),
"=" => Symbol("Prelude.areEqual"),
// These ones *probably* won't conflict with variables and might be passed as functions
"chooseRandom" => Symbol("Prelude.chooseRandom"),
// These ones *probably* won't conflict with variables and might commonly be used with (apply) because they are variadic
"concat" => Symbol("Prelude.concat"),
"zipKeep" => Symbol("Prelude.zipKeep"),
"zipDrop" => Symbol("Prelude.zipDrop"),
"zipThrow" => Symbol("Prelude.zipThrow"),
"joinPath" => Symbol("Prelude.joinPath"),
],
fieldList: [],
fieldDict: new Map(),
loadingDirectory: "",
hscript: false,
macroVars: new Map()
};
return k;
}
static function _try<T>(operation:() -> T):Null<T> {
#if !macrotest
try {
#end
return operation();
#if !macrotest
} catch (err:StreamError) {
Sys.stderr().writeString(err + "\n");
Sys.exit(1);
return null;
} catch (err:CompileError) {
Sys.stderr().writeString(err + "\n");
Sys.exit(1);
return null;
} catch (err:UnmatchedBracketSignal) {
Sys.stderr().writeString(Stream.toPrint(err.position) + ': Unmatched ${err.type}\n');
Sys.exit(1);
return null;
} catch (err:Exception) {
Sys.stderr().writeString("Error: " + err.message + "\n");
Sys.stderr().writeString(err.stack.toString() + "\n");
Sys.exit(1);
return null;
}
#end
}
/**
Build macro: add fields to a class from a corresponding .kiss file
**/
public static function build(?kissFile:String, ?k:KissState, useClassFields = true):Array<Field> {
var classPath = Context.getPosInfos(Context.currentPos()).file;
// (load... ) relative to the original file
var loadingDirectory = Path.directory(classPath);
if (kissFile == null) {
kissFile = classPath.withoutDirectory().withoutExtension().withExtension("kiss");
}
return _try(() -> {
if (k == null)
k = defaultKissState();
if (useClassFields) {
k.fieldList = Context.getBuildFields();
for (field in k.fieldList) {
k.fieldDict[field.name] = field;
}
}
k.loadingDirectory = loadingDirectory;
var topLevelBegin = load(kissFile, k);
if (topLevelBegin != null) {
// If no main function is defined manually, Kiss expressions at the top of a file will be put in a main function.
// If a main function IS defined, this will result in an error
if (k.fieldDict.exists("main")) {
throw CompileError.fromExp(topLevelBegin, '$kissFile has expressions outside of field definitions, but already defines its own main function.');
}
var b = topLevelBegin.expBuilder();
// This doesn't need to be added to the fieldDict because all code generation is done
k.fieldList.push({
name: "main",
access: [AStatic],
kind: FFun(Helpers.makeFunction(
b.symbol("main"),
false,
b.list([]),
[topLevelBegin],
k,
"function")),
pos: topLevelBegin.macroPos()
});
}
k.fieldList;
});
}
public static function load(kissFile:String, k:KissState, ?loadingDirectory:String, loadAllExps = false):Null<ReaderExp> {
if (loadingDirectory == null)
loadingDirectory = k.loadingDirectory;
var fullPath = Path.join([loadingDirectory, kissFile]);
if (k.loadedFiles.exists(fullPath)) {
return k.loadedFiles[fullPath];
}
var stream = Stream.fromFile(fullPath);
var startPosition = stream.position();
var loadedExps = [];
Reader.readAndProcess(stream, k, (nextExp) -> {
#if test
Sys.println(nextExp.def.toString());
#end
// readerExpToHaxeExpr must be called to process readermacro, alias, and macro definitions
var expr = readerExpToHaxeExpr(nextExp, k);
// exps in the loaded file that actually become haxe expressions can be inserted into the
// file that loaded them at the position (load) was called.
// conditional compiler macros like (#when) tend to return empty blocks, or blocks containing empty blocks
// when they contain field forms, so this should also be ignored
function isEmpty(expr) {
switch (expr.expr) {
case EBlock([]):
case EBlock(blockExps):
for (exp in blockExps) {
if (!isEmpty(exp))
return false;
}
default:
return false;
}
return true;
}
// When calling from build(), we can't add all expressions to the (begin) returned by (load), because that will
// cause double-evaluation of field forms
if (loadAllExps) {
loadedExps.push(nextExp);
} else if (!isEmpty(expr)) {
// don't double-compile macros:
loadedExps.push(RawHaxe(expr.toString()).withPosOf(nextExp));
}
});
var exp = if (loadedExps.length > 0) {
CallExp(Symbol("begin").withPos(startPosition), loadedExps).withPos(startPosition);
} else {
null;
}
k.loadedFiles[fullPath] = exp;
return exp;
}
/**
* Build macro: add fields to a Haxe class by compiling multiple Kiss files in order with the same KissState
*/
public static function buildAll(kissFiles:Array<String>, ?k:KissState, useClassFields = true):Array<Field> {
if (k == null)
k = defaultKissState();
if (useClassFields) {
k.fieldList = Context.getBuildFields();
for (field in k.fieldList) {
k.fieldDict[field.name] = field;
}
}
for (file in kissFiles) {
build(file, k, false);
}
return k.fieldList;
}
public static function readerExpToHaxeExpr(exp:ReaderExp, k:KissState):Expr {
var macros = k.macros;
var fieldForms = k.fieldForms;
var specialForms = k.specialForms;
// Bind the table arguments of this function for easy recursive calling/passing
var convert = readerExpToHaxeExpr.bind(_, k);
if (k.hscript)
exp = Helpers.removeTypeAnnotations(exp);
var none = EBlock([]).withMacroPosOf(exp);
var expr = switch (exp.def) {
case None:
none;
case Symbol(alias) if (k.identAliases.exists(alias)):
readerExpToHaxeExpr(k.identAliases[alias].withPosOf(exp), k);
case Symbol(name):
try {
Context.parse(name, exp.macroPos());
} catch (err:haxe.Exception) {
throw CompileError.fromExp(exp, "invalid symbol");
};
case StrExp(s):
EConst(CString(s)).withMacroPosOf(exp);
case CallExp({pos: _, def: Symbol(ff)}, args) if (fieldForms.exists(ff)):
var field = fieldForms[ff](exp, args, k);
k.fieldList.push(field);
k.fieldDict[field.name] = field;
none; // Field forms are no-ops
case CallExp({pos: _, def: Symbol(mac)}, args) if (macros.exists(mac)):
var expanded = macros[mac](exp, args, k);
if (expanded != null) {
convert(expanded);
} else {
none;
};
case CallExp({pos: _, def: Symbol(specialForm)}, args) if (specialForms.exists(specialForm)):
specialForms[specialForm](exp, args, k);
case CallExp({pos: _, def: Symbol(alias)}, args) if (k.callAliases.exists(alias)):
convert(CallExp(k.callAliases[alias].withPosOf(exp), args).withPosOf(exp));
case CallExp(func, args):
ECall(convert(func), [for (argExp in args) convert(argExp)]).withMacroPosOf(exp);
case ListExp(elements):
var isMap = false;
var arrayDecl = EArrayDecl([
for (elementExp in elements) {
switch (elementExp.def) {
case KeyValueExp(_, _):
isMap = true;
default:
}
convert(elementExp);
}
]).withMacroPosOf(exp);
if (!isMap && k.wrapListExps && !k.hscript) {
ENew({
pack: ["kiss"],
name: "List"
}, [arrayDecl]).withMacroPosOf(exp);
} else {
arrayDecl;
};
case RawHaxe(code):
Context.parse(code, exp.macroPos());
case FieldExp(field, innerExp):
EField(convert(innerExp), field).withMacroPosOf(exp);
case KeyValueExp(keyExp, valueExp):
EBinop(OpArrow, convert(keyExp), convert(valueExp)).withMacroPosOf(exp);
case Quasiquote(innerExp):
// This statement actually turns into an HScript expression before running
macro {
Helpers.evalUnquotes($v{innerExp}, k, __args__).def;
};
default:
throw CompileError.fromExp(exp, 'conversion not implemented');
};
#if test
// Sys.println(expr.toString()); // For very fine-grained codegen inspection--slows compilation a lot.
#end
return expr;
}
// Return an identical Kiss State, but without type annotations or wrapping list expressions as kiss.List constructor calls.
public static function forHScript(k:KissState):KissState {
var copy = new Cloner().clone(k);
copy.hscript = true;
return copy;
}
// Return an identical Kiss State, but without wrapping list expressions as kiss.List constructor calls.
public static function withoutListWrapping(k:KissState) {
var copy = new Cloner().clone(k);
copy.wrapListExps = false;
return copy;
}
// Return an identical Kiss State, but prepared for parsing a branch pattern of a (case...) expression
public static function forCaseParsing(k:KissState):KissState {
var copy = withoutListWrapping(k);
copy.macros.remove("or");
copy.specialForms["or"] = SpecialForms.caseOr;
copy.specialForms["as"] = SpecialForms.caseAs;
return copy;
}
public static function convert(k:KissState, exp:ReaderExp) {
return readerExpToHaxeExpr(exp, k);
}
}
#end