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.Kiss; using kiss.Helpers; using kiss.Reader; using tink.MacroApi; using haxe.io.Path; typedef ExprConversion = (ReaderExp) -> Expr; typedef KissState = { className:String, file:String, readTable:ReadTable, startOfLineReadTable:ReadTable, startOfFileReadTable:ReadTable, endOfFileReadTable:ReadTable, fieldForms:Map, specialForms:Map, macros:Map, wrapListExps:Bool, loadedFiles:Map>, callAliases:Map, identAliases:Map, fieldList:Array, // 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, loadingDirectory:String, hscript:Bool, macroVars:Map, collectedBlocks:Map>, inStaticFunction:Bool }; class Kiss { public static function defaultKissState():KissState { var className = Context.getLocalClass().get().name; var k = { className: className, file: "", readTable: Reader.builtins(), startOfLineReadTable: new ReadTable(), startOfFileReadTable: new ReadTable(), endOfFileReadTable: new ReadTable(), fieldForms: FieldForms.builtins(), specialForms: SpecialForms.builtins(), macros: Macros.builtins(), wrapListExps: true, loadedFiles: new Map(), // 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"), "reverse" => Symbol("Prelude.reverse"), "memoize" => Symbol("Prelude.memoize"), "fsMemoize" => Symbol("Prelude.fsMemoize"), "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"), "libPath" => Symbol("Prelude.libPath"), "random" => Symbol("Std.random"), "walkDirectory" => Symbol("Prelude.walkDirectory"), "purgeDirectory" => Symbol("Prelude.purgeDirectory"), "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"), "zip" => Symbol("Prelude.zipThrow"), "zipKeep" => Symbol("Prelude.zipKeep"), "zipDrop" => Symbol("Prelude.zipDrop"), "zipThrow" => Symbol("Prelude.zipThrow"), "intersect" => Symbol("Prelude.intersect"), "joinPath" => Symbol("Prelude.joinPath"), "readDirectory" => Symbol("Prelude.readDirectory"), "substr" => Symbol("Prelude.substr"), "isListExp" => Symbol("Prelude.isListExp") ], fieldList: [], fieldDict: new Map(), loadingDirectory: "", hscript: false, macroVars: new Map(), collectedBlocks: new Map(), inStaticFunction: false }; return k; } public static function _try(operation:() -> T):Null { #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 { 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"); } //trace('kiss build $kissFile'); return _try(() -> { #if profileKiss haxe.Timer.measure(() -> { trace(kissFile); #end 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; #if profileKiss }); #end }); } public static function load(kissFile:String, k:KissState, ?loadingDirectory:String, loadAllExps = false):Null { if (loadingDirectory == null) loadingDirectory = k.loadingDirectory; var fullPath = if (Path.isAbsolute(kissFile)) { kissFile; } else { Path.join([loadingDirectory, kissFile]); }; var previousFile = k.file; k.file = fullPath; 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 macroUsed = false; 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: if (macroUsed) { loadedExps.push(RawHaxe(expr.toString()).withPosOf(nextExp)); } else { loadedExps.push(nextExp); } } }); var exp = if (loadedExps.length > 0) { CallExp(Symbol("begin").withPos(startPosition), loadedExps).withPos(startPosition); } else { null; } k.loadedFiles[fullPath] = exp; k.file = previousFile; 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, ?k:KissState, useClassFields = true):Array { 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; } static var macroUsed = false; 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)): macroUsed = true; 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): try { Context.parse(code, exp.macroPos()); } catch (err:Exception) { throw CompileError.fromExp(exp, 'Haxe parse error: $err'); }; 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}).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; } static function disableMacro(copy:KissState, m:String, reason:String) { copy.macros[m] = (wholeExp:ReaderExp, exps, k) -> { var b = wholeExp.expBuilder(); // have this throw during macroEXPANSION, not before (so assertThrows will catch it) b.throwCompileError('$m is unavailable in macros because $reason'); }; } // This doesn't clone k because k might be modified in important ways :( public static function forStaticFunction(k:KissState, inStaticFunction:Bool) { k.inStaticFunction = inStaticFunction; return k; } // 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; // disallow macros that will error when run in hscript: disableMacro(copy, "ifLet", "hscript doesn't support pattern-matching"); disableMacro(copy, "whenLet", "hscript doesn't support pattern-matching"); disableMacro(copy, "unlessLet", "hscript doesn't support pattern-matching"); copy.macros["cast"] = (wholeExp:ReaderExp, exps, k) -> { exps[0]; }; return copy; } public static function forMacroEval(k:KissState): KissState { var copy = k.forHScript(); // Catch accidental misuse of (set) on macroVars var setLocal = copy.specialForms["set"]; copy.specialForms["set"] = (wholeExp:ReaderExp, exps, k:KissState) -> { switch (exps[0].def) { case Symbol(varName) if (k.macroVars.exists(varName)): var b = wholeExp.expBuilder(); // have this throw during macroEXPANSION, not before (so assertThrows will catch it) copy.convert(b.throwCompileError('If you intend to change macroVar $varName, use setMacroVar instead. If not, rename your local variable for clarity.')); default: setLocal(wholeExp, exps, copy); }; }; // TODO should this also be in forHScript()? // In macro evaluation, copy.macros.remove("eval"); 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