feat: add user-defined macros
4332e9a parent: 4ae0db9 modified
README.md +5 -4 | @@ -59,7 +59,7 @@ loaded implicitly merely because their source root is available. | ||
| 59 | 59 | |
| 60 | 60 | This is a static subset: one leading `ns` per file, literal top-level `require`, |
| 61 | 61 | `:as`, explicit `:refer`, and `defn-` privacy. Dynamic namespace operations, |
| 62 | -reload options, `:refer :all`, and arbitrary macro expansion are unsupported. | |
| 62 | +reload options, and `:refer :all` are unsupported. | |
| 63 | 63 | Missing dependencies, missing vars, and dependency cycles produce errors. |
| 64 | 64 | |
| 65 | 65 | ## Pipeline |
| @@ -145,6 +145,7 @@ tail another seq still holds simply survives. | ||
| 145 | 145 | ## What works |
| 146 | 146 | |
| 147 | 147 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) |
| 148 | +`defmacro` (including variadic parameters, `&form`, and `&env`) | |
| 148 | 149 | `let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let` |
| 149 | 150 | `do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote` |
| 150 | 151 | |
| @@ -164,9 +165,9 @@ library above returns them where Clojure does. | ||
| 164 | 165 | |
| 165 | 166 | ## What doesn't (yet) |
| 166 | 167 | |
| 167 | -- **`defmacro`.** The macro set is fixed and expanded by the compiler. User | |
| 168 | - macros need the compiler to be able to *evaluate* code at compile time — | |
| 169 | - the honest fix is to bootstrap clonim in itself, or embed an interpreter. | |
| 168 | +- **Syntax-quote.** User macros can construct forms with `quote` and ordinary | |
| 169 | + core functions such as `list`, `vector`, and `apply`, but automatic | |
| 170 | + namespace qualification through syntax-quote/unquote is not implemented yet. | |
| 170 | 171 | - **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two |
| 171 | 172 | cells per element and runs ~2× slower than the old eager path. 32-element |
| 172 | 173 | chunking is the fix, at the cost of exact demand. |
| @@ -59,7 +59,7 @@ loaded implicitly merely because their source root is available. | |||
| 59 | 59 | ||
| 60 | This is a static subset: one leading `ns` per file, literal top-level `require`, | 60 | This is a static subset: one leading `ns` per file, literal top-level `require`, |
| 61 | `:as`, explicit `:refer`, and `defn-` privacy. Dynamic namespace operations, | 61 | `:as`, explicit `:refer`, and `defn-` privacy. Dynamic namespace operations, |
| 62 | -reload options, `:refer :all`, and arbitrary macro expansion are unsupported. | 62 | +reload options, and `:refer :all` are unsupported. |
| 63 | Missing dependencies, missing vars, and dependency cycles produce errors. | 63 | Missing dependencies, missing vars, and dependency cycles produce errors. |
| 64 | 64 | ||
| 65 | ## Pipeline | 65 | ## Pipeline |
| @@ -145,6 +145,7 @@ tail another seq still holds simply survives. | |||
| 145 | ## What works | 145 | ## What works |
| 146 | 146 | ||
| 147 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) | 147 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) |
| 148 | +`defmacro` (including variadic parameters, `&form`, and `&env`) | ||
| 148 | `let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let` | 149 | `let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let` |
| 149 | `do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote` | 150 | `do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote` |
| 150 | 151 | ||
| @@ -164,9 +165,9 @@ library above returns them where Clojure does. | |||
| 164 | 165 | ||
| 165 | ## What doesn't (yet) | 166 | ## What doesn't (yet) |
| 166 | 167 | ||
| 167 | -- **`defmacro`.** The macro set is fixed and expanded by the compiler. User | 168 | +- **Syntax-quote.** User macros can construct forms with `quote` and ordinary |
| 168 | - macros need the compiler to be able to *evaluate* code at compile time — | 169 | + core functions such as `list`, `vector`, and `apply`, but automatic |
| 169 | - the honest fix is to bootstrap clonim in itself, or embed an interpreter. | 170 | + namespace qualification through syntax-quote/unquote is not implemented yet. |
| 170 | - **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two | 171 | - **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two |
| 171 | cells per element and runs ~2× slower than the old eager path. 32-element | 172 | cells per element and runs ~2× slower than the old eager path. 32-element |
| 172 | chunking is the fix, at the cost of exact demand. | 173 | chunking is the fix, at the cost of exact demand. |
added
examples/user_macros.clj +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +(defmacro unless [test then else] | |
| 2 | + (list 'if test else then)) | |
| 3 | + | |
| 4 | +(defmacro call-list [& xs] | |
| 5 | + (apply list xs)) | |
| 6 | + | |
| 7 | +(defmacro form-and-env [& _] | |
| 8 | + (list 'quote (vector &form &env))) | |
| 9 | + | |
| 10 | +(defmacro nothing [] nil) | |
| 11 | + | |
| 12 | +(println (unless false :yes :no)) | |
| 13 | +(call-list println "variadic" 3) | |
| 14 | +(println (form-and-env)) | |
| 15 | +(println (nil? (nothing))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +(defmacro unless [test then else] | ||
| 2 | + (list 'if test else then)) | ||
| 3 | + | ||
| 4 | +(defmacro call-list [& xs] | ||
| 5 | + (apply list xs)) | ||
| 6 | + | ||
| 7 | +(defmacro form-and-env [& _] | ||
| 8 | + (list 'quote (vector &form &env))) | ||
| 9 | + | ||
| 10 | +(defmacro nothing [] nil) | ||
| 11 | + | ||
| 12 | +(println (unless false :yes :no)) | ||
| 13 | +(call-list println "variadic" 3) | ||
| 14 | +(println (form-and-env)) | ||
| 15 | +(println (nil? (nothing))) | ||
modified
src/namespaces.nim +141 -8 | @@ -10,6 +10,9 @@ import std/[os, tables, sets, strutils] | ||
| 10 | 10 | import runtime, reader, core |
| 11 | 11 | |
| 12 | 12 | type |
| 13 | + MacroDef = object | |
| 14 | + params: Value | |
| 15 | + body: seq[Value] | |
| 13 | 16 | Namespace = ref object |
| 14 | 17 | name: string |
| 15 | 18 | aliases, refers: Table[string, string] |
| @@ -20,6 +23,7 @@ type | ||
| 20 | 23 | spaces: Table[string, Namespace] |
| 21 | 24 | active: seq[string] |
| 22 | 25 | loaded: HashSet[string] |
| 26 | + macros: Table[string, MacroDef] | |
| 23 | 27 | output: seq[Value] |
| 24 | 28 | gensym: int |
| 25 | 29 | |
| @@ -156,6 +160,110 @@ proc define(ns: Namespace, v: Value, private = false): Value = | ||
| 156 | 160 | |
| 157 | 161 | proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value |
| 158 | 162 | |
| 163 | +proc macroKey(r: Resolver, ns: Namespace, raw: string): string = | |
| 164 | + let slash = raw.find('/') | |
| 165 | + if slash > 0: | |
| 166 | + let target = ns.aliases.getOrDefault(raw[0 ..< slash], raw[0 ..< slash]) | |
| 167 | + return target & raw[slash .. ^1] | |
| 168 | + if raw in ns.defs: return ns.name & "/" & raw | |
| 169 | + if ns.refers.hasKey(raw): return ns.refers[raw] | |
| 170 | + | |
| 171 | +proc bindMacroParams(params: Value, args: seq[Value]): Table[string, Value] = | |
| 172 | + if params.kind != kVector: fail("macro parameters must be a vector") | |
| 173 | + result = initTable[string, Value]() | |
| 174 | + var pi = 0 | |
| 175 | + var ai = 0 | |
| 176 | + while pi < params.items.len: | |
| 177 | + let p = params.items[pi] | |
| 178 | + if p.kind == kSymbol and p.s == "&": | |
| 179 | + if pi + 1 >= params.items.len or params.items[pi + 1].kind != kSymbol: | |
| 180 | + fail("macro & must be followed by a symbol") | |
| 181 | + result[params.items[pi + 1].s] = | |
| 182 | + (if ai < args.len: mkList(args[ai .. ^1]) else: NilV) | |
| 183 | + return | |
| 184 | + if p.kind != kSymbol: fail("macro parameters must be symbols") | |
| 185 | + if ai >= args.len: fail("not enough arguments passed to macro") | |
| 186 | + result[p.s] = args[ai] | |
| 187 | + inc pi | |
| 188 | + inc ai | |
| 189 | + if ai != args.len: fail("too many arguments passed to macro") | |
| 190 | + | |
| 191 | +proc evalMacro(v: Value, env: Table[string, Value]): Value | |
| 192 | + | |
| 193 | +proc evalMacroBody(body: seq[Value], env: Table[string, Value]): Value = | |
| 194 | + result = NilV | |
| 195 | + for form in body: result = evalMacro(form, env) | |
| 196 | + | |
| 197 | +proc evalMacro(v: Value, env: Table[string, Value]): Value = | |
| 198 | + case v.kind | |
| 199 | + of kSymbol: | |
| 200 | + if env.hasKey(v.s): return env[v.s] | |
| 201 | + if hasVar(v.s): return getVar(v.s) | |
| 202 | + fail("macro evaluation cannot resolve " & v.s) | |
| 203 | + of kVector: | |
| 204 | + var xs: seq[Value] | |
| 205 | + for x in v.items: xs.add evalMacro(x, env) | |
| 206 | + return mkVector(xs) | |
| 207 | + of kMap: | |
| 208 | + var ps: seq[(Value, Value)] | |
| 209 | + for (k, value) in v.pairs: ps.add (evalMacro(k, env), evalMacro(value, env)) | |
| 210 | + return mkMap(ps) | |
| 211 | + of kSet: | |
| 212 | + var xs: seq[Value] | |
| 213 | + for x in v.items: xs.add evalMacro(x, env) | |
| 214 | + return mkSet(xs) | |
| 215 | + of kList: discard | |
| 216 | + else: return v | |
| 217 | + let xs = v.items | |
| 218 | + if xs.len == 0: return v | |
| 219 | + let head = if xs[0].kind == kSymbol: xs[0].s else: "" | |
| 220 | + case head | |
| 221 | + of "quote": | |
| 222 | + if xs.len != 2: fail("quote in macro body expects one argument") | |
| 223 | + return xs[1] | |
| 224 | + of "if": | |
| 225 | + if xs.len notin 3 .. 4: fail("if in macro body expects two or three arguments") | |
| 226 | + if truthy(evalMacro(xs[1], env)): return evalMacro(xs[2], env) | |
| 227 | + return (if xs.len == 4: evalMacro(xs[3], env) else: NilV) | |
| 228 | + of "do": return evalMacroBody(xs[1 .. ^1], env) | |
| 229 | + of "let", "let*": | |
| 230 | + if xs.len < 3 or xs[1].kind != kVector or xs[1].items.len mod 2 != 0: | |
| 231 | + fail("let in macro body requires paired bindings") | |
| 232 | + var scope = env | |
| 233 | + var i = 0 | |
| 234 | + while i < xs[1].items.len: | |
| 235 | + let name = xs[1].items[i] | |
| 236 | + if name.kind != kSymbol: fail("macro let bindings must be symbols") | |
| 237 | + scope[name.s] = evalMacro(xs[1].items[i + 1], scope) | |
| 238 | + i += 2 | |
| 239 | + return evalMacroBody(xs[2 .. ^1], scope) | |
| 240 | + of "fn", "fn*": | |
| 241 | + if xs.len < 3 or xs[1].kind != kVector: | |
| 242 | + fail("fn in macro body requires a parameter vector") | |
| 243 | + let params = xs[1] | |
| 244 | + let body = xs[2 .. ^1] | |
| 245 | + let captured = env | |
| 246 | + return mkFn("macro-fn", proc (args: openArray[Value]): Value = | |
| 247 | + var scope = captured | |
| 248 | + let bound = bindMacroParams(params, @args) | |
| 249 | + for name, value in bound: scope[name] = value | |
| 250 | + evalMacroBody(body, scope)) | |
| 251 | + else: discard | |
| 252 | + let f = evalMacro(xs[0], env) | |
| 253 | + var args: seq[Value] | |
| 254 | + for x in xs[1 .. ^1]: args.add evalMacro(x, env) | |
| 255 | + call(f, args) | |
| 256 | + | |
| 257 | +proc expandMacro(r: Resolver, ns: Namespace, form: Value, | |
| 258 | + locals: HashSet[string]): (bool, Value) = | |
| 259 | + let key = r.macroKey(ns, form.items[0].s) | |
| 260 | + if key.len == 0 or not r.macros.hasKey(key): return (false, NilV) | |
| 261 | + let m = r.macros[key] | |
| 262 | + var env = bindMacroParams(m.params, form.items[1 .. ^1]) | |
| 263 | + env["&form"] = form | |
| 264 | + env["&env"] = mkMap(@[]) | |
| 265 | + result = (true, evalMacroBody(m.body, env)) | |
| 266 | + | |
| 159 | 267 | proc bindNames(v: Value, locals: var HashSet[string]) = |
| 160 | 268 | case v.kind |
| 161 | 269 | of kSymbol: |
| @@ -234,6 +342,9 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value | ||
| 234 | 342 | else: return v |
| 235 | 343 | var xs = v.items |
| 236 | 344 | if xs.len == 0: return v |
| 345 | + if xs[0].kind == kSymbol and xs[0].s notin locals: | |
| 346 | + let (isMacro, expanded) = r.expandMacro(ns, v, locals) | |
| 347 | + if isMacro: return r.walk(ns, expanded, locals) | |
| 237 | 348 | let h = ns.syntaxHead(v, locals) |
| 238 | 349 | if h in ["quote", "comment"]: |
| 239 | 350 | xs[0] = mkSymbol(h) |
| @@ -249,16 +360,29 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value | ||
| 249 | 360 | scope.incl simple(xs[1], "function name") |
| 250 | 361 | start = 2 |
| 251 | 362 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) |
| 252 | - of "defn", "defn-", "defmacro": | |
| 363 | + of "defmacro": | |
| 364 | + if xs.len < 4: fail("defmacro requires a name, parameters, and body") | |
| 365 | + xs[1] = ns.define(xs[1]) | |
| 366 | + var start = 2 | |
| 367 | + if xs[start].kind == kStr: inc start | |
| 368 | + if start < xs.len and xs[start].kind == kMap: inc start | |
| 369 | + if start >= xs.len or xs[start].kind != kVector: | |
| 370 | + fail("defmacro requires a parameter vector") | |
| 371 | + var scope = locals | |
| 372 | + bindNames(xs[start], scope) | |
| 373 | + scope.incl "&form" | |
| 374 | + scope.incl "&env" | |
| 375 | + var body: seq[Value] | |
| 376 | + for i in start + 1 ..< xs.len: body.add r.walk(ns, xs[i], scope) | |
| 377 | + r.macros[xs[1].s] = MacroDef(params: xs[start], body: body) | |
| 378 | + return mkList(@[mkSymbol("defmacro"), xs[1]]) | |
| 379 | + of "defn", "defn-": | |
| 253 | 380 | if xs.len < 3: fail(h & " requires a name and parameters") |
| 254 | 381 | xs[1] = ns.define(xs[1], h == "defn-") |
| 255 | 382 | var start = 2 |
| 256 | 383 | if start < xs.len and xs[start].kind == kStr: inc start |
| 257 | 384 | if start < xs.len and xs[start].kind == kMap: inc start |
| 258 | 385 | var scope = locals |
| 259 | - if h == "defmacro": | |
| 260 | - scope.incl "&form" | |
| 261 | - scope.incl "&env" | |
| 262 | 386 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) |
| 263 | 387 | of "def", "declare": |
| 264 | 388 | if xs.len < 2: fail(h & " requires a name") |
| @@ -480,7 +604,8 @@ proc process(r: Resolver, forms: seq[Value], expected = "") = | ||
| 480 | 604 | fail("dynamic require is unsupported; quote each libspec") |
| 481 | 605 | r.requireSpec(ns, arg.items[1]) |
| 482 | 606 | else: |
| 483 | - r.output.add r.walk(ns, f, initHashSet[string]()) | |
| 607 | + let walked = r.walk(ns, f, initHashSet[string]()) | |
| 608 | + if headName(walked) != "defmacro": r.output.add walked | |
| 484 | 609 | r.loaded.incl name |
| 485 | 610 | finally: |
| 486 | 611 | discard r.active.pop() |
| @@ -495,6 +620,14 @@ proc resolveSource*(src: string, sourceRoots: seq[string]): seq[Value] = | ||
| 495 | 620 | ## require, except for the current namespace and implicit clojure.core. |
| 496 | 621 | ## Unknown and externally private vars are rejected during analysis. |
| 497 | 622 | let r = Resolver(roots: sourceRoots, cores: registeredCoreNames(), |
| 498 | - hosts: registeredHostNames()) | |
| 499 | - r.process(readAll(src)) | |
| 500 | - r.output | |
| 623 | + hosts: registeredHostNames(), | |
| 624 | + macros: initTable[string, MacroDef]()) | |
| 625 | + let saved = globals | |
| 626 | + globals = initTable[string, VarCell]() | |
| 627 | + try: | |
| 628 | + registerCore() | |
| 629 | + registerNamespaceCore() | |
| 630 | + r.process(readAll(src)) | |
| 631 | + result = r.output | |
| 632 | + finally: | |
| 633 | + globals = saved | |
| @@ -10,6 +10,9 @@ import std/[os, tables, sets, strutils] | |||
| 10 | import runtime, reader, core | 10 | import runtime, reader, core |
| 11 | 11 | ||
| 12 | type | 12 | type |
| 13 | + MacroDef = object | ||
| 14 | + params: Value | ||
| 15 | + body: seq[Value] | ||
| 13 | Namespace = ref object | 16 | Namespace = ref object |
| 14 | name: string | 17 | name: string |
| 15 | aliases, refers: Table[string, string] | 18 | aliases, refers: Table[string, string] |
| @@ -20,6 +23,7 @@ type | |||
| 20 | spaces: Table[string, Namespace] | 23 | spaces: Table[string, Namespace] |
| 21 | active: seq[string] | 24 | active: seq[string] |
| 22 | loaded: HashSet[string] | 25 | loaded: HashSet[string] |
| 26 | + macros: Table[string, MacroDef] | ||
| 23 | output: seq[Value] | 27 | output: seq[Value] |
| 24 | gensym: int | 28 | gensym: int |
| 25 | 29 | ||
| @@ -156,6 +160,110 @@ proc define(ns: Namespace, v: Value, private = false): Value = | |||
| 156 | 160 | ||
| 157 | proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value | 161 | proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value |
| 158 | 162 | ||
| 163 | +proc macroKey(r: Resolver, ns: Namespace, raw: string): string = | ||
| 164 | + let slash = raw.find('/') | ||
| 165 | + if slash > 0: | ||
| 166 | + let target = ns.aliases.getOrDefault(raw[0 ..< slash], raw[0 ..< slash]) | ||
| 167 | + return target & raw[slash .. ^1] | ||
| 168 | + if raw in ns.defs: return ns.name & "/" & raw | ||
| 169 | + if ns.refers.hasKey(raw): return ns.refers[raw] | ||
| 170 | + | ||
| 171 | +proc bindMacroParams(params: Value, args: seq[Value]): Table[string, Value] = | ||
| 172 | + if params.kind != kVector: fail("macro parameters must be a vector") | ||
| 173 | + result = initTable[string, Value]() | ||
| 174 | + var pi = 0 | ||
| 175 | + var ai = 0 | ||
| 176 | + while pi < params.items.len: | ||
| 177 | + let p = params.items[pi] | ||
| 178 | + if p.kind == kSymbol and p.s == "&": | ||
| 179 | + if pi + 1 >= params.items.len or params.items[pi + 1].kind != kSymbol: | ||
| 180 | + fail("macro & must be followed by a symbol") | ||
| 181 | + result[params.items[pi + 1].s] = | ||
| 182 | + (if ai < args.len: mkList(args[ai .. ^1]) else: NilV) | ||
| 183 | + return | ||
| 184 | + if p.kind != kSymbol: fail("macro parameters must be symbols") | ||
| 185 | + if ai >= args.len: fail("not enough arguments passed to macro") | ||
| 186 | + result[p.s] = args[ai] | ||
| 187 | + inc pi | ||
| 188 | + inc ai | ||
| 189 | + if ai != args.len: fail("too many arguments passed to macro") | ||
| 190 | + | ||
| 191 | +proc evalMacro(v: Value, env: Table[string, Value]): Value | ||
| 192 | + | ||
| 193 | +proc evalMacroBody(body: seq[Value], env: Table[string, Value]): Value = | ||
| 194 | + result = NilV | ||
| 195 | + for form in body: result = evalMacro(form, env) | ||
| 196 | + | ||
| 197 | +proc evalMacro(v: Value, env: Table[string, Value]): Value = | ||
| 198 | + case v.kind | ||
| 199 | + of kSymbol: | ||
| 200 | + if env.hasKey(v.s): return env[v.s] | ||
| 201 | + if hasVar(v.s): return getVar(v.s) | ||
| 202 | + fail("macro evaluation cannot resolve " & v.s) | ||
| 203 | + of kVector: | ||
| 204 | + var xs: seq[Value] | ||
| 205 | + for x in v.items: xs.add evalMacro(x, env) | ||
| 206 | + return mkVector(xs) | ||
| 207 | + of kMap: | ||
| 208 | + var ps: seq[(Value, Value)] | ||
| 209 | + for (k, value) in v.pairs: ps.add (evalMacro(k, env), evalMacro(value, env)) | ||
| 210 | + return mkMap(ps) | ||
| 211 | + of kSet: | ||
| 212 | + var xs: seq[Value] | ||
| 213 | + for x in v.items: xs.add evalMacro(x, env) | ||
| 214 | + return mkSet(xs) | ||
| 215 | + of kList: discard | ||
| 216 | + else: return v | ||
| 217 | + let xs = v.items | ||
| 218 | + if xs.len == 0: return v | ||
| 219 | + let head = if xs[0].kind == kSymbol: xs[0].s else: "" | ||
| 220 | + case head | ||
| 221 | + of "quote": | ||
| 222 | + if xs.len != 2: fail("quote in macro body expects one argument") | ||
| 223 | + return xs[1] | ||
| 224 | + of "if": | ||
| 225 | + if xs.len notin 3 .. 4: fail("if in macro body expects two or three arguments") | ||
| 226 | + if truthy(evalMacro(xs[1], env)): return evalMacro(xs[2], env) | ||
| 227 | + return (if xs.len == 4: evalMacro(xs[3], env) else: NilV) | ||
| 228 | + of "do": return evalMacroBody(xs[1 .. ^1], env) | ||
| 229 | + of "let", "let*": | ||
| 230 | + if xs.len < 3 or xs[1].kind != kVector or xs[1].items.len mod 2 != 0: | ||
| 231 | + fail("let in macro body requires paired bindings") | ||
| 232 | + var scope = env | ||
| 233 | + var i = 0 | ||
| 234 | + while i < xs[1].items.len: | ||
| 235 | + let name = xs[1].items[i] | ||
| 236 | + if name.kind != kSymbol: fail("macro let bindings must be symbols") | ||
| 237 | + scope[name.s] = evalMacro(xs[1].items[i + 1], scope) | ||
| 238 | + i += 2 | ||
| 239 | + return evalMacroBody(xs[2 .. ^1], scope) | ||
| 240 | + of "fn", "fn*": | ||
| 241 | + if xs.len < 3 or xs[1].kind != kVector: | ||
| 242 | + fail("fn in macro body requires a parameter vector") | ||
| 243 | + let params = xs[1] | ||
| 244 | + let body = xs[2 .. ^1] | ||
| 245 | + let captured = env | ||
| 246 | + return mkFn("macro-fn", proc (args: openArray[Value]): Value = | ||
| 247 | + var scope = captured | ||
| 248 | + let bound = bindMacroParams(params, @args) | ||
| 249 | + for name, value in bound: scope[name] = value | ||
| 250 | + evalMacroBody(body, scope)) | ||
| 251 | + else: discard | ||
| 252 | + let f = evalMacro(xs[0], env) | ||
| 253 | + var args: seq[Value] | ||
| 254 | + for x in xs[1 .. ^1]: args.add evalMacro(x, env) | ||
| 255 | + call(f, args) | ||
| 256 | + | ||
| 257 | +proc expandMacro(r: Resolver, ns: Namespace, form: Value, | ||
| 258 | + locals: HashSet[string]): (bool, Value) = | ||
| 259 | + let key = r.macroKey(ns, form.items[0].s) | ||
| 260 | + if key.len == 0 or not r.macros.hasKey(key): return (false, NilV) | ||
| 261 | + let m = r.macros[key] | ||
| 262 | + var env = bindMacroParams(m.params, form.items[1 .. ^1]) | ||
| 263 | + env["&form"] = form | ||
| 264 | + env["&env"] = mkMap(@[]) | ||
| 265 | + result = (true, evalMacroBody(m.body, env)) | ||
| 266 | + | ||
| 159 | proc bindNames(v: Value, locals: var HashSet[string]) = | 267 | proc bindNames(v: Value, locals: var HashSet[string]) = |
| 160 | case v.kind | 268 | case v.kind |
| 161 | of kSymbol: | 269 | of kSymbol: |
| @@ -234,6 +342,9 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value | |||
| 234 | else: return v | 342 | else: return v |
| 235 | var xs = v.items | 343 | var xs = v.items |
| 236 | if xs.len == 0: return v | 344 | if xs.len == 0: return v |
| 345 | + if xs[0].kind == kSymbol and xs[0].s notin locals: | ||
| 346 | + let (isMacro, expanded) = r.expandMacro(ns, v, locals) | ||
| 347 | + if isMacro: return r.walk(ns, expanded, locals) | ||
| 237 | let h = ns.syntaxHead(v, locals) | 348 | let h = ns.syntaxHead(v, locals) |
| 238 | if h in ["quote", "comment"]: | 349 | if h in ["quote", "comment"]: |
| 239 | xs[0] = mkSymbol(h) | 350 | xs[0] = mkSymbol(h) |
| @@ -249,16 +360,29 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value | |||
| 249 | scope.incl simple(xs[1], "function name") | 360 | scope.incl simple(xs[1], "function name") |
| 250 | start = 2 | 361 | start = 2 |
| 251 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) | 362 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) |
| 252 | - of "defn", "defn-", "defmacro": | 363 | + of "defmacro": |
| 364 | + if xs.len < 4: fail("defmacro requires a name, parameters, and body") | ||
| 365 | + xs[1] = ns.define(xs[1]) | ||
| 366 | + var start = 2 | ||
| 367 | + if xs[start].kind == kStr: inc start | ||
| 368 | + if start < xs.len and xs[start].kind == kMap: inc start | ||
| 369 | + if start >= xs.len or xs[start].kind != kVector: | ||
| 370 | + fail("defmacro requires a parameter vector") | ||
| 371 | + var scope = locals | ||
| 372 | + bindNames(xs[start], scope) | ||
| 373 | + scope.incl "&form" | ||
| 374 | + scope.incl "&env" | ||
| 375 | + var body: seq[Value] | ||
| 376 | + for i in start + 1 ..< xs.len: body.add r.walk(ns, xs[i], scope) | ||
| 377 | + r.macros[xs[1].s] = MacroDef(params: xs[start], body: body) | ||
| 378 | + return mkList(@[mkSymbol("defmacro"), xs[1]]) | ||
| 379 | + of "defn", "defn-": | ||
| 253 | if xs.len < 3: fail(h & " requires a name and parameters") | 380 | if xs.len < 3: fail(h & " requires a name and parameters") |
| 254 | xs[1] = ns.define(xs[1], h == "defn-") | 381 | xs[1] = ns.define(xs[1], h == "defn-") |
| 255 | var start = 2 | 382 | var start = 2 |
| 256 | if start < xs.len and xs[start].kind == kStr: inc start | 383 | if start < xs.len and xs[start].kind == kStr: inc start |
| 257 | if start < xs.len and xs[start].kind == kMap: inc start | 384 | if start < xs.len and xs[start].kind == kMap: inc start |
| 258 | var scope = locals | 385 | var scope = locals |
| 259 | - if h == "defmacro": | ||
| 260 | - scope.incl "&form" | ||
| 261 | - scope.incl "&env" | ||
| 262 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) | 386 | return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope)) |
| 263 | of "def", "declare": | 387 | of "def", "declare": |
| 264 | if xs.len < 2: fail(h & " requires a name") | 388 | if xs.len < 2: fail(h & " requires a name") |
| @@ -480,7 +604,8 @@ proc process(r: Resolver, forms: seq[Value], expected = "") = | |||
| 480 | fail("dynamic require is unsupported; quote each libspec") | 604 | fail("dynamic require is unsupported; quote each libspec") |
| 481 | r.requireSpec(ns, arg.items[1]) | 605 | r.requireSpec(ns, arg.items[1]) |
| 482 | else: | 606 | else: |
| 483 | - r.output.add r.walk(ns, f, initHashSet[string]()) | 607 | + let walked = r.walk(ns, f, initHashSet[string]()) |
| 608 | + if headName(walked) != "defmacro": r.output.add walked | ||
| 484 | r.loaded.incl name | 609 | r.loaded.incl name |
| 485 | finally: | 610 | finally: |
| 486 | discard r.active.pop() | 611 | discard r.active.pop() |
| @@ -495,6 +620,14 @@ proc resolveSource*(src: string, sourceRoots: seq[string]): seq[Value] = | |||
| 495 | ## require, except for the current namespace and implicit clojure.core. | 620 | ## require, except for the current namespace and implicit clojure.core. |
| 496 | ## Unknown and externally private vars are rejected during analysis. | 621 | ## Unknown and externally private vars are rejected during analysis. |
| 497 | let r = Resolver(roots: sourceRoots, cores: registeredCoreNames(), | 622 | let r = Resolver(roots: sourceRoots, cores: registeredCoreNames(), |
| 498 | - hosts: registeredHostNames()) | 623 | + hosts: registeredHostNames(), |
| 499 | - r.process(readAll(src)) | 624 | + macros: initTable[string, MacroDef]()) |
| 500 | - r.output | 625 | + let saved = globals |
| 626 | + globals = initTable[string, VarCell]() | ||
| 627 | + try: | ||
| 628 | + registerCore() | ||
| 629 | + registerNamespaceCore() | ||
| 630 | + r.process(readAll(src)) | ||
| 631 | + result = r.output | ||
| 632 | + finally: | ||
| 633 | + globals = saved | ||
modified
tests/test_namespaces.nim +18 -2 | @@ -31,6 +31,12 @@ suite "static namespace resolution": | ||
| 31 | 31 | check "registerCoreCollections" in destructured |
| 32 | 32 | check "\"nth\"" in destructured |
| 33 | 33 | |
| 34 | + test "user macros expand before code generation": | |
| 35 | + let expanded = resolveSource( | |
| 36 | + "(defmacro unless [p a b] (list 'if p b a)) (unless false 1 2)", @[]) | |
| 37 | + check expanded.len == 1 | |
| 38 | + check prStr(expanded[0]) == "(if false 2 1)" | |
| 39 | + | |
| 34 | 40 | test "regex literals preserve their pattern source": |
| 35 | 41 | let forms = readAll("#\"(?i)^did:[a-z0-9]+:\"") |
| 36 | 42 | check forms.len == 1 |
| @@ -102,8 +108,7 @@ suite "static namespace resolution": | ||
| 102 | 108 | rejects("(comment (def imaginary 1)) imaginary", "unable to resolve symbol imaginary") |
| 103 | 109 | |
| 104 | 110 | test "macro bindings and threading keep their scopes": |
| 105 | - checkForms("(defmacro m [x] (list 'quote x &form &env))", | |
| 106 | - "(defmacro user/m [x] (clojure.core/list (quote quote) x &form &env))") | |
| 111 | + check resolveSource("(defmacro m [x] (list 'quote x &form &env))", @[]).len == 0 | |
| 107 | 112 | checkForms("(let [when +] (-> 1 (when 2)))", |
| 108 | 113 | "(let [when clojure.core/+] ((do when) 1 2))") |
| 109 | 114 | |
| @@ -155,6 +160,10 @@ suite "namespace source loader": | ||
| 155 | 160 | (ns other (:require sample.lib-one)) |
| 156 | 161 | (def result sample.lib-one/value) |
| 157 | 162 | """) |
| 163 | + writeFile(root / "sample" / "macros.clj", """ | |
| 164 | + (ns sample.macros) | |
| 165 | + (defmacro unless [p a b] (list 'if p b a)) | |
| 166 | + """) | |
| 158 | 167 | teardown: |
| 159 | 168 | removeDir(root) |
| 160 | 169 | |
| @@ -168,6 +177,13 @@ suite "namespace source loader": | ||
| 168 | 177 | check prStr(forms[^1]) == "[sample.lib-one/value sample.lib-one/value sample.lib-one/value]" |
| 169 | 178 | check prStr(forms[2]) == "(defn sample.lib-one/public [] (sample.lib-one/hidden))" |
| 170 | 179 | |
| 180 | + test "macros expand through aliases and refers": | |
| 181 | + checkForms(""" | |
| 182 | + (ns app (:require [sample.macros :as m :refer [unless]])) | |
| 183 | + (m/unless false 1 2) | |
| 184 | + (unless true 3 4) | |
| 185 | + """, "(if false 2 1) (if true 4 3)", @[root]) | |
| 186 | + | |
| 171 | 187 | test "plain require imports no unqualified names or aliases": |
| 172 | 188 | rejects("(require 'sample.lib-one) value", "unable to resolve symbol value", @[root]) |
| 173 | 189 | rejects("(require 'sample.lib-one) lib/value", "namespace lib is not required", @[root]) |
| @@ -31,6 +31,12 @@ suite "static namespace resolution": | |||
| 31 | check "registerCoreCollections" in destructured | 31 | check "registerCoreCollections" in destructured |
| 32 | check "\"nth\"" in destructured | 32 | check "\"nth\"" in destructured |
| 33 | 33 | ||
| 34 | + test "user macros expand before code generation": | ||
| 35 | + let expanded = resolveSource( | ||
| 36 | + "(defmacro unless [p a b] (list 'if p b a)) (unless false 1 2)", @[]) | ||
| 37 | + check expanded.len == 1 | ||
| 38 | + check prStr(expanded[0]) == "(if false 2 1)" | ||
| 39 | + | ||
| 34 | test "regex literals preserve their pattern source": | 40 | test "regex literals preserve their pattern source": |
| 35 | let forms = readAll("#\"(?i)^did:[a-z0-9]+:\"") | 41 | let forms = readAll("#\"(?i)^did:[a-z0-9]+:\"") |
| 36 | check forms.len == 1 | 42 | check forms.len == 1 |
| @@ -102,8 +108,7 @@ suite "static namespace resolution": | |||
| 102 | rejects("(comment (def imaginary 1)) imaginary", "unable to resolve symbol imaginary") | 108 | rejects("(comment (def imaginary 1)) imaginary", "unable to resolve symbol imaginary") |
| 103 | 109 | ||
| 104 | test "macro bindings and threading keep their scopes": | 110 | test "macro bindings and threading keep their scopes": |
| 105 | - checkForms("(defmacro m [x] (list 'quote x &form &env))", | 111 | + check resolveSource("(defmacro m [x] (list 'quote x &form &env))", @[]).len == 0 |
| 106 | - "(defmacro user/m [x] (clojure.core/list (quote quote) x &form &env))") | ||
| 107 | checkForms("(let [when +] (-> 1 (when 2)))", | 112 | checkForms("(let [when +] (-> 1 (when 2)))", |
| 108 | "(let [when clojure.core/+] ((do when) 1 2))") | 113 | "(let [when clojure.core/+] ((do when) 1 2))") |
| 109 | 114 | ||
| @@ -155,6 +160,10 @@ suite "namespace source loader": | |||
| 155 | (ns other (:require sample.lib-one)) | 160 | (ns other (:require sample.lib-one)) |
| 156 | (def result sample.lib-one/value) | 161 | (def result sample.lib-one/value) |
| 157 | """) | 162 | """) |
| 163 | + writeFile(root / "sample" / "macros.clj", """ | ||
| 164 | + (ns sample.macros) | ||
| 165 | + (defmacro unless [p a b] (list 'if p b a)) | ||
| 166 | + """) | ||
| 158 | teardown: | 167 | teardown: |
| 159 | removeDir(root) | 168 | removeDir(root) |
| 160 | 169 | ||
| @@ -168,6 +177,13 @@ suite "namespace source loader": | |||
| 168 | check prStr(forms[^1]) == "[sample.lib-one/value sample.lib-one/value sample.lib-one/value]" | 177 | check prStr(forms[^1]) == "[sample.lib-one/value sample.lib-one/value sample.lib-one/value]" |
| 169 | check prStr(forms[2]) == "(defn sample.lib-one/public [] (sample.lib-one/hidden))" | 178 | check prStr(forms[2]) == "(defn sample.lib-one/public [] (sample.lib-one/hidden))" |
| 170 | 179 | ||
| 180 | + test "macros expand through aliases and refers": | ||
| 181 | + checkForms(""" | ||
| 182 | + (ns app (:require [sample.macros :as m :refer [unless]])) | ||
| 183 | + (m/unless false 1 2) | ||
| 184 | + (unless true 3 4) | ||
| 185 | + """, "(if false 2 1) (if true 4 3)", @[root]) | ||
| 186 | + | ||
| 171 | test "plain require imports no unqualified names or aliases": | 187 | test "plain require imports no unqualified names or aliases": |
| 172 | rejects("(require 'sample.lib-one) value", "unable to resolve symbol value", @[root]) | 188 | rejects("(require 'sample.lib-one) value", "unable to resolve symbol value", @[root]) |
| 173 | rejects("(require 'sample.lib-one) lib/value", "namespace lib is not required", @[root]) | 189 | rejects("(require 'sample.lib-one) lib/value", "namespace lib is not required", @[root]) |
added
tests/user_macros.expected +4 -0 | new file mode 100644 | ||
| @@ -0,0 +1,4 @@ | ||
| 1 | +:yes | |
| 2 | +variadic 3 | |
| 3 | +[(form-and-env) {}] | |
| 4 | +true | |
| new file mode 100644 | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | +:yes | ||
| 2 | +variadic 3 | ||
| 3 | +[(form-and-env) {}] | ||
| 4 | +true | ||