nandi/clonimpublic Fork
⑂ 4332e9a
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/clonim.git
git clone ssh://git@rickub.com/nandi/clonim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: add user-defined macros

nandithebull committed 2026-09-22T16:05:25-07:00 Browse files
4332e9a parent: 4ae0db9
modified README.md +5 -4
@@ -59,7 +59,7 @@ loaded implicitly merely because their source root is available.
5959
6060 This is a static subset: one leading `ns` per file, literal top-level `require`,
6161 `: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.
6363 Missing dependencies, missing vars, and dependency cycles produce errors.
6464
6565 ## Pipeline
@@ -145,6 +145,7 @@ tail another seq still holds simply survives.
145145 ## What works
146146
147147 `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive)
148+`defmacro` (including variadic parameters, `&form`, and `&env`)
148149 `let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let`
149150 `do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote`
150151
@@ -164,9 +165,9 @@ library above returns them where Clojure does.
164165
165166 ## What doesn't (yet)
166167
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.
170171 - **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two
171172 cells per element and runs ~2× slower than the old eager path. 32-element
172173 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 ## Pipeline65 ## Pipeline
@@ -145,6 +145,7 @@ tail another seq still holds simply survives.
145 ## What works145 ## 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. User168+- **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 two171 - **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-element172 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]
1010 import runtime, reader, core
1111
1212 type
13+ MacroDef = object
14+ params: Value
15+ body: seq[Value]
1316 Namespace = ref object
1417 name: string
1518 aliases, refers: Table[string, string]
@@ -20,6 +23,7 @@ type
2023 spaces: Table[string, Namespace]
2124 active: seq[string]
2225 loaded: HashSet[string]
26+ macros: Table[string, MacroDef]
2327 output: seq[Value]
2428 gensym: int
2529
@@ -156,6 +160,110 @@ proc define(ns: Namespace, v: Value, private = false): Value =
156160
157161 proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
158162
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+
159267 proc bindNames(v: Value, locals: var HashSet[string]) =
160268 case v.kind
161269 of kSymbol:
@@ -234,6 +342,9 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
234342 else: return v
235343 var xs = v.items
236344 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)
237348 let h = ns.syntaxHead(v, locals)
238349 if h in ["quote", "comment"]:
239350 xs[0] = mkSymbol(h)
@@ -249,16 +360,29 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
249360 scope.incl simple(xs[1], "function name")
250361 start = 2
251362 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-":
253380 if xs.len < 3: fail(h & " requires a name and parameters")
254381 xs[1] = ns.define(xs[1], h == "defn-")
255382 var start = 2
256383 if start < xs.len and xs[start].kind == kStr: inc start
257384 if start < xs.len and xs[start].kind == kMap: inc start
258385 var scope = locals
259- if h == "defmacro":
260- scope.incl "&form"
261- scope.incl "&env"
262386 return mkList(xs[0 ..< start] & r.fnTail(ns, xs[start .. ^1], scope))
263387 of "def", "declare":
264388 if xs.len < 2: fail(h & " requires a name")
@@ -480,7 +604,8 @@ proc process(r: Resolver, forms: seq[Value], expected = "") =
480604 fail("dynamic require is unsupported; quote each libspec")
481605 r.requireSpec(ns, arg.items[1])
482606 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
484609 r.loaded.incl name
485610 finally:
486611 discard r.active.pop()
@@ -495,6 +620,14 @@ proc resolveSource*(src: string, sourceRoots: seq[string]): seq[Value] =
495620 ## require, except for the current namespace and implicit clojure.core.
496621 ## Unknown and externally private vars are rejected during analysis.
497622 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, core10 import runtime, reader, core
11 11
12 type12 type
13+ MacroDef = object
14+ params: Value
15+ body: seq[Value]
13 Namespace = ref object16 Namespace = ref object
14 name: string17 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: int28 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]): Value161 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.kind268 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 v342 else: return v
235 var xs = v.items343 var xs = v.items
236 if xs.len == 0: return v344 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 = 2361 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 = 2382 var start = 2
256 if start < xs.len and xs[start].kind == kStr: inc start383 if start < xs.len and xs[start].kind == kStr: inc start
257 if start < xs.len and xs[start].kind == kMap: inc start384 if start < xs.len and xs[start].kind == kMap: inc start
258 var scope = locals385 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 name609 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.output625+ 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":
3131 check "registerCoreCollections" in destructured
3232 check "\"nth\"" in destructured
3333
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+
3440 test "regex literals preserve their pattern source":
3541 let forms = readAll("#\"(?i)^did:[a-z0-9]+:\"")
3642 check forms.len == 1
@@ -102,8 +108,7 @@ suite "static namespace resolution":
102108 rejects("(comment (def imaginary 1)) imaginary", "unable to resolve symbol imaginary")
103109
104110 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
107112 checkForms("(let [when +] (-> 1 (when 2)))",
108113 "(let [when clojure.core/+] ((do when) 1 2))")
109114
@@ -155,6 +160,10 @@ suite "namespace source loader":
155160 (ns other (:require sample.lib-one))
156161 (def result sample.lib-one/value)
157162 """)
163+ writeFile(root / "sample" / "macros.clj", """
164+ (ns sample.macros)
165+ (defmacro unless [p a b] (list 'if p b a))
166+ """)
158167 teardown:
159168 removeDir(root)
160169
@@ -168,6 +177,13 @@ suite "namespace source loader":
168177 check prStr(forms[^1]) == "[sample.lib-one/value sample.lib-one/value sample.lib-one/value]"
169178 check prStr(forms[2]) == "(defn sample.lib-one/public [] (sample.lib-one/hidden))"
170179
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+
171187 test "plain require imports no unqualified names or aliases":
172188 rejects("(require 'sample.lib-one) value", "unable to resolve symbol value", @[root])
173189 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 destructured31 check "registerCoreCollections" in destructured
32 check "\"nth\"" in destructured32 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 == 142 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