From 9bc0ed2011eb8f54ca45c46fc295f2337bda521e Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 31 Dec 2020 14:05:46 -0700 Subject: [PATCH] defmacro --- kiss/src/kiss/Macros.hx | 90 ++++++++++++++++++++++++++ kiss/src/kiss/Reader.hx | 1 - kiss/src/test/cases/BasicTestCase.hx | 4 ++ kiss/src/test/cases/BasicTestCase.kiss | 11 +++- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/kiss/src/kiss/Macros.hx b/kiss/src/kiss/Macros.hx index 5b41acaf..6892be10 100644 --- a/kiss/src/kiss/Macros.hx +++ b/kiss/src/kiss/Macros.hx @@ -197,6 +197,94 @@ class Macros { }; } + macros["defmacro"] = (wholeExp:ReaderExp, exps:Array, k:KissState) -> { + wholeExp.checkNumArgs(3, null, '(defmacro [name] [[args...]] [body...])'); + + var table = k.macros; + + 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 meta appears, no other arguments can be declared + var restIndex = -1; + var argNames = []; + + var macroCallForm = '($name'; + + for (arg in argList) { + if (restIndex != -1) { + throw CompileError.fromExp(arg, "macros cannot declare arguments after a &rest 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)}): + argNames.push(name); + macroCallForm += ' [$name...]'; + restIndex = maxArgs; + 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, k:KissState) -> { + wholeExp.checkNumArgs(minArgs, maxArgs, macroCallForm); + var innerArgNames = argNames.copy(); + + var args:Map = []; + 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) + args[innerArgNames.shift()] = innerExps.slice(restIndex); + + // Return the macro expansion: + var expDef:ReaderExpDef = Helpers.runAtCompileTime(CallExp(Symbol("begin").withPosOf(wholeExp), exps.slice(2)).withPosOf(wholeExp), k, args); + expDef.withPosOf(wholeExp); + }; + + null; + }; + macros["defreadermacro"] = (wholeExp:ReaderExp, exps:Array, k:KissState) -> { wholeExp.checkNumArgs(3, null, '(defreadermacro ["[startingString]" or [startingStrings...]] [[streamArgName]] [body...])'); @@ -223,6 +311,8 @@ class Macros { var body = CallExp(Symbol("begin").withPos(stream.position()), exps.slice(2)).withPos(stream.position()); Helpers.runAtCompileTime(body, k, [streamArgName => stream]); }; + 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]'); } diff --git a/kiss/src/kiss/Reader.hx b/kiss/src/kiss/Reader.hx index 42316ef8..85485e7e 100644 --- a/kiss/src/kiss/Reader.hx +++ b/kiss/src/kiss/Reader.hx @@ -56,7 +56,6 @@ class Reader { null; }; readTable["#|"] = (stream, k) -> RawHaxe(stream.expect("closing |#", () -> stream.takeUntilAndDrop("|#"))); - // For defmacrofuns, unquoting with , is syntactic sugar for calling a Quote (Void->T) readTable[":"] = (stream, k) -> TypedExp(nextToken(stream, "a type path"), assertRead(stream, k)); diff --git a/kiss/src/test/cases/BasicTestCase.hx b/kiss/src/test/cases/BasicTestCase.hx index a2609966..b6c6ca91 100644 --- a/kiss/src/test/cases/BasicTestCase.hx +++ b/kiss/src/test/cases/BasicTestCase.hx @@ -249,6 +249,10 @@ class BasicTestCase extends Test { function testTypeParsing() { _testTypeParsing(); } + + function testDefmacro() { + _testDefmacro(); + } } class BasicObject { diff --git a/kiss/src/test/cases/BasicTestCase.kiss b/kiss/src/test/cases/BasicTestCase.kiss index 97919cd2..f2985f49 100644 --- a/kiss/src/test/cases/BasicTestCase.kiss +++ b/kiss/src/test/cases/BasicTestCase.kiss @@ -370,4 +370,13 @@ (Assert.equals 5 (doSomething (lambda [i] i))) (Assert.equals 7 (doSomething (lambda [i] (+ i 2)))) // Pass null to the really crazy one because I'm lazy: - (Assert.equals "but it still compiles" (itsAMonster null))) \ No newline at end of file + (Assert.equals "but it still compiles" (itsAMonster null))) + +(defmacro defconstfunc [name const] `(defun ,name [] ,const)) + +(defconstfunc func5 5) +(defconstfunc funcHello "hello") + +(defun _testDefmacro [] + (Assert.equals 5 (func5)) + (Assert.equals "hello" (funcHello))) \ No newline at end of file