From 5c7d2931c1ad33dffc7b5543a439305b955234ad Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Tue, 9 Nov 2021 18:55:33 -0700 Subject: [PATCH] Less hacky macro-time evaluation --- src/kiss/Helpers.hx | 107 ++++++++++++++++-------------- src/kiss/Kiss.hx | 5 +- src/kiss/KissInterp.hx | 9 +++ src/kiss/Macros.hx | 3 + src/test/cases/MacroTestCase.kiss | 11 +-- 5 files changed, 77 insertions(+), 58 deletions(-) diff --git a/src/kiss/Helpers.hx b/src/kiss/Helpers.hx index 4b58527..9047616 100644 --- a/src/kiss/Helpers.hx +++ b/src/kiss/Helpers.hx @@ -328,11 +328,6 @@ class Helpers { } } - // This stack will contain multiple references to the same interp--to count how many layers deep it is. - // This stack is like top in Inception. When empty, it proves that we're not running at compiletime yet. - // When we ARE running at compiletime already, the pre-existing interp will be used - static var interps:kiss.List = []; - public static function removeTypeAnnotations(exp:ReaderExp):ReaderExp { var def = switch (exp.def) { case Symbol(_) | StrExp(_) | RawHaxe(_) | Quasiquote(_): @@ -377,58 +372,67 @@ class Helpers { }); } - public static function runAtCompileTimeDynamic(exp:ReaderExp, k:KissState, ?args:Map):Dynamic { + static var parser = new Parser(); + static function compileTimeHScript(exp:ReaderExp, k:KissState) { var hscriptExp = mapForInterp(k.forHScript().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 parser = new Parser(); - if (interps.length == 0) { - 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("k", k.forHScript()); - interp.variables.set("Helpers", Helpers); - interp.variables.set("Macros", Macros); - for (name => value in k.macroVars) { - interp.variables.set(name, value); - } - // This is kind of a big deal: - interp.variables.set("eval", Helpers.runAtCompileTimeDynamic.bind(_, k)); - interp.variables.set("macroDepth", () -> interps.length); - - interps.push(interp); - } else { - interps.push(new Cloner().clone(interps[-1])); - } 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):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); } - interps[-1].variables.set("__args__", args); // trippy + function innerRunAtCompileTimeDynamic(innerExp:ReaderExp) { + // in case macroVars have changed + for (name => value in k.macroVars) { + interp.variables.set(name, value); + } + var value = interp.publicExprReturn(compileTimeHScript(innerExp, k)); + 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) { - interps[-1].variables.set(arg, value); + interp.variables.set(arg, value); } } - var value:Dynamic = if (interps.length == 1) { - interps[-1].execute(parsed); - } else { - interps[-1].expr(parsed); - }; - interps.pop(); + var value:Dynamic = interp.execute(parsed); if (value == null) { throw CompileError.fromExp(exp, "compile-time evaluation returned null"); } @@ -470,13 +474,13 @@ class Helpers { return e; } - static function evalUnquoteLists(l:Array, k:KissState, ?args:Map):Array { + static function evalUnquoteLists(l:Array, innerRunAtCompileTime:(ReaderExp)->Dynamic):Array { var idx = 0; while (idx < l.length) { switch (l[idx].def) { case UnquoteList(exp): l.splice(idx, 1); - var listToInsert:Dynamic = runAtCompileTime(exp, k, args); + var listToInsert:Dynamic = innerRunAtCompileTime(exp); // listToInsert could be either an array (from &rest) or a ListExp (from [list syntax]) var newElements:Array = if (Std.isOfType(listToInsert, Array)) { listToInsert; @@ -498,22 +502,23 @@ class Helpers { return l; } - public static function evalUnquotes(exp:ReaderExp, k:KissState, ?args:Map):ReaderExp { + 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(evalUnquotes(func, k, args), evalUnquoteLists(callArgs, k, args).map(evalUnquotes.bind(_, k, args))); + CallExp(recurse(func), evalUnquoteLists(callArgs, innerRunAtCompileTime).map(recurse)); case ListExp(elements): - ListExp(evalUnquoteLists(elements, k, args).map(evalUnquotes.bind(_, k, args))); + ListExp(evalUnquoteLists(elements, innerRunAtCompileTime).map(recurse)); case TypedExp(type, innerExp): - TypedExp(type, evalUnquotes(innerExp, k, args)); + TypedExp(type, recurse(innerExp)); case FieldExp(field, innerExp): - FieldExp(field, evalUnquotes(innerExp, k, args)); + FieldExp(field, recurse(innerExp)); case KeyValueExp(keyExp, valueExp): - KeyValueExp(evalUnquotes(keyExp, k, args), evalUnquotes(valueExp, k, args)); + KeyValueExp(recurse(keyExp), recurse(valueExp)); case Unquote(innerExp): - var unquoteValue:Dynamic = runAtCompileTime(innerExp, k, args); + var unquoteValue:Dynamic = innerRunAtCompileTime(innerExp); if (unquoteValue == null) { throw CompileError.fromExp(innerExp, "unquote evaluated to null"); } else if (Std.isOfType(unquoteValue, ReaderExpDef)) { @@ -524,7 +529,7 @@ class Helpers { throw CompileError.fromExp(exp, "unquote didn't evaluate to a ReaderExp or ReaderExpDef"); }; case MetaExp(meta, innerExp): - MetaExp(meta, evalUnquotes(innerExp, k, args)); + MetaExp(meta, recurse(innerExp)); default: throw CompileError.fromExp(exp, 'unquote evaluation not implemented'); }; diff --git a/src/kiss/Kiss.hx b/src/kiss/Kiss.hx index 78143fa..e21247e 100644 --- a/src/kiss/Kiss.hx +++ b/src/kiss/Kiss.hx @@ -360,7 +360,7 @@ class Kiss { case Quasiquote(innerExp): // This statement actually turns into an HScript expression before running macro { - Helpers.evalUnquotes($v{innerExp}, k, __args__).def; + Helpers.evalUnquotes($v{innerExp}).def; }; default: throw CompileError.fromExp(exp, 'conversion not implemented'); @@ -377,7 +377,7 @@ class Kiss { var copy = new Cloner().clone(k); copy.hscript = true; - // Also disallow macros that will error when run in hscript: + // disallow macros that will error when run in hscript: function disableMacro(m:String, reason:String) { copy.macros[m] = (wholeExp:ReaderExp, exps, k) -> { var b = wholeExp.expBuilder(); @@ -385,6 +385,7 @@ class Kiss { }; } + disableMacro("set", "you don't want your macros to be stateful"); disableMacro("ifLet", "hscript doesn't support pattern-matching"); disableMacro("whenLet", "hscript doesn't support pattern-matching"); disableMacro("unlessLet", "hscript doesn't support pattern-matching"); diff --git a/src/kiss/KissInterp.hx b/src/kiss/KissInterp.hx index 50b1418..c8b91c3 100644 --- a/src/kiss/KissInterp.hx +++ b/src/kiss/KissInterp.hx @@ -113,4 +113,13 @@ class KissInterp extends Interp { restore(old); } #end + + public function publicExprReturn(e) { + return exprReturn(e); + } + + // For debugging: + public function getLocals() { + return locals; + } } diff --git a/src/kiss/Macros.hx b/src/kiss/Macros.hx index be6c097..aeeee95 100644 --- a/src/kiss/Macros.hx +++ b/src/kiss/Macros.hx @@ -802,6 +802,8 @@ class Macros { var name = exps[0].symbolNameValue(); var lambdaExp = b.callSymbol("lambda", [exps[1]].concat(exps.slice(2))); + k.macroVars[name] = Helpers.runAtCompileTimeDynamic(lambdaExp, k); + // Run the definition AGAIN so it can capture itself recursively: k.macroVars[name] = Helpers.runAtCompileTimeDynamic(lambdaExp, k); return null; @@ -995,6 +997,7 @@ class Macros { stringifyExpList.push(b.the(b.symbol("String"), b.callSymbol("tink.Json.stringify", [b.the(b.symbol(type), bindingList[idx + 1])]))); parseBindingList.push(bindingList[idx]); + // This will be called in the context where __args__ is Sys.args() parseBindingList.push(b.callSymbol("tink.Json.parse", [b.callField("shift", b.symbol("__args__"), [])])); idx += 2; } diff --git a/src/test/cases/MacroTestCase.kiss b/src/test/cases/MacroTestCase.kiss index 866c334..3941637 100644 --- a/src/test/cases/MacroTestCase.kiss +++ b/src/test/cases/MacroTestCase.kiss @@ -56,17 +56,18 @@ (defMacro _testPrintAtMacroTimeMacro [] (printAtMacroTime)) -(function testPrintAtMacroTime [] - (_testPrintAtMacroTimeMacro)) +(function _testPrintAtMacroTime [] + (_testPrintAtMacroTimeMacro) + (Assert.pass)) (defMacroVar count 0) (defMacro _testSetMacroVarMacro [] - (set count (+ count 1)) + (assertThrows (set count (+ count 1))) (ReaderExp.StrExp (Std.string count))) (function _testSetMacroVar [] - (Assert.equals 1 (_testSetMacroVarMacro)) - (Assert.equals 2 (_testSetMacroVarMacro))) + (_testSetMacroVarMacro) + (Assert.pass)) // ifLet and its derivatives should be disabled in defMacro bodies: (defMacro _testIfLetDisabledMacro []