Phase 1-4: Reader, Runtime, Macros, Nim Interop — 543x faster than JVM Clojure

- Reader: maps, sets, syntax-quote, dispatch macros, metadata
- Runtime: CljVal type system, 80+ core functions (collections, strings, math, IO)
- Emitter: full CljVal-based code generation, macro expansion, inline fns
- Macros: defmacro with compile-time evaluator, ->, ->>, and, or, when, cond, for, doseq
- Nim Interop: nim/module/function syntax, auto-import, type mapping
- CLI: cljnim -e '<code>' for quick evaluation
- Tests: 60 unit tests (reader + emitter)
- Benchmarks: AOT suite showing 543x startup, 679x factorial vs JVM Clojure
- CI: GitLab CI pipeline
- Runtime library: lib/cljnim_runtime.nim (1070+ lines)
This commit is contained in:
2026-05-08 16:34:39 +03:00
parent f89b381fdb
commit 87d6028487
24 changed files with 4244 additions and 275 deletions
+468
View File
@@ -0,0 +1,468 @@
import tables
import types
type
MacroFn* = proc(args: seq[CljVal]): CljVal
var macroTable* = initTable[string, MacroFn]()
proc defineMacro*(name: string, fn: MacroFn) =
macroTable[name] = fn
proc isMacro*(name: string): bool =
macroTable.hasKey(name)
proc expandMacro*(name: string, args: seq[CljVal]): CljVal =
macroTable[name](args)
proc expandSyntaxQuote*(form: CljVal): CljVal
proc macroexpand1*(form: CljVal): CljVal
proc macroexpand*(form: CljVal): CljVal
proc gensymCounter*: int =
var counter {.global.} = 0
inc counter
counter
proc gensymName*(prefix: string = "G__"): string =
prefix & $gensymCounter()
proc expandSyntaxQuote*(form: CljVal): CljVal =
if form.isNil: return cljNil()
case form.kind
of ckSymbol:
return cljList(@[cljSymbol("quote"), form])
of ckKeyword:
return cljList(@[cljSymbol("quote"), form])
of ckList:
if form.items.len == 0:
return cljList(@[cljSymbol("quote"), cljList(@[])])
let head = form.items[0]
if head.kind == ckSymbol:
if head.symName == "unquote":
if form.items.len != 2:
raise newException(CatchableError, "unquote requires exactly 1 argument")
return form.items[1]
if head.symName == "unquote-splicing":
raise newException(CatchableError, "unquote-splicing can only be used inside a collection")
# Process each element
var result: seq[CljVal] = @[]
var parts: seq[CljVal] = @[]
for item in form.items:
if item.kind == ckList and item.items.len == 2 and
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing":
parts.add(item.items[1])
elif item.kind == ckList and item.items.len == 2 and
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote":
parts.add(cljList(@[cljSymbol("list"), item.items[1]]))
else:
parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)]))
if parts.len == 0:
return cljList(@[cljSymbol("quote"), cljList(@[])])
# Build (concat part1 part2 ...)
if parts.len == 1:
return parts[0]
return cljList(@[cljSymbol("concat")] & parts)
of ckVector:
var parts: seq[CljVal] = @[]
for item in form.items:
if item.kind == ckList and item.items.len == 2 and
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing":
parts.add(item.items[1])
else:
parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)]))
return cljList(@[cljSymbol("vec"),
if parts.len == 0: cljList(@[cljSymbol("quote"), cljList(@[])])
elif parts.len == 1: parts[0]
else: cljList(@[cljSymbol("concat")] & parts)])
of ckMap:
# Maps: expand each key and value
return cljList(@[cljSymbol("quote"), form])
else:
return cljList(@[cljSymbol("quote"), form])
proc macroexpand1*(form: CljVal): CljVal =
if form.isNil: return form
if form.kind != ckList or form.items.len == 0:
return form
let head = form.items[0]
if head.kind != ckSymbol:
return form
let name = head.symName
# Handle special forms that should not be macro-expanded
if name in ["def", "fn", "defn", "defn-", "let", "if", "when", "cond",
"do", "loop", "recur", "try", "catch", "finally", "throw",
"quote", "var", "set!", "ns", "defmacro",
"and", "or", "nil?", "range"]:
return form
# Handle syntax-quote
if name == "syntax-quote":
if form.items.len != 2:
raise newException(CatchableError, "syntax-quote requires exactly 1 argument")
return expandSyntaxQuote(form.items[1])
# Check if it's a macro
if isMacro(name):
return expandMacro(name, form.items[1..^1])
return form
proc macroexpand*(form: CljVal): CljVal =
result = form
while true:
let expanded = macroexpand1(result)
if expanded == result:
return result
result = expanded
# ---- Built-in macro implementations ----
proc threadingMacro(args: seq[CljVal], reverse: bool): CljVal =
if args.len < 2:
raise newException(CatchableError, "Threading macro requires at least 2 arguments")
var result = args[0]
for i in 1..<args.len:
let step = args[i]
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
if reverse:
newItems.add(result)
else:
newItems.insert(result, 1)
result = cljList(newItems)
elif step.kind == ckSymbol:
# (-> x f) => (f x)
result = cljList(@[step, result])
else:
raise newException(CatchableError, "Threading macro requires list or symbol forms")
result
proc initBuiltinMacros*() =
# ->
defineMacro("fn*", proc(args: seq[CljVal]): CljVal =
# fn* is used internally by reader for #()
cljList(@[cljSymbol("fn")] & args)
)
# ->
defineMacro("->", proc(args: seq[CljVal]): CljVal =
threadingMacro(args, false)
)
# ->>
defineMacro("->>", proc(args: seq[CljVal]): CljVal =
threadingMacro(args, true)
)
# and
defineMacro("and", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljBool(true)
if args.len == 1: return args[0]
# (and a b c) => (if a (if b c false) false)
var r = args[^1]
for i in countdown(args.len - 2, 0):
r = cljList(@[cljSymbol("if"), args[i], r, cljBool(false)])
r
)
# or
defineMacro("or", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil()
if args.len == 1: return args[0]
# (or a b c) => (if a a (if b b (if c c nil)))
var r = args[^1]
for i in countdown(args.len - 2, 0):
r = cljList(@[cljSymbol("if"), args[i], args[i], r])
r
)
# when-let
defineMacro("when-let", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "when-let requires binding and body")
let binding = args[0]
let body = args[1..^1]
if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "when-let binding must be [name val]")
let sym = binding.items[0]
let val = binding.items[1]
let gs = cljSymbol(gensymName("wl_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
cljList(@[cljSymbol("if"), gs,
cljList(@[cljSymbol("let"), cljVector(@[sym, gs])] & body)])])
)
# if-let
defineMacro("if-let", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "if-let requires binding, then, and optional else")
let binding = args[0]
let thenExpr = args[1]
let elseExpr = if args.len > 2: args[2] else: cljNil()
if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "if-let binding must be [name val]")
let sym = binding.items[0]
let val = binding.items[1]
let gs = cljSymbol(gensymName("il_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
cljList(@[cljSymbol("if"), gs,
cljList(@[cljSymbol("let"), cljVector(@[sym, gs]), thenExpr]),
elseExpr])])
)
# when
defineMacro("when", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "when requires condition and body")
let test = args[0]
let body = args[1..^1]
cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("do")] & body)])
)
# when-not
defineMacro("when-not", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "when-not requires condition and body")
let test = args[0]
let body = args[1..^1]
cljList(@[cljSymbol("if"), test, cljNil(), cljList(@[cljSymbol("do")] & body)])
)
# cond
defineMacro("cond", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil()
if args.len mod 2 != 0:
raise newException(CatchableError, "cond requires even number of forms")
let test = args[0]
let then = args[1]
if (test.kind == ckKeyword and test.kwName == "else") or
(test.kind == ckSymbol and test.symName == ":else"):
return then
let rest = args[2..^1]
if rest.len == 0:
return cljList(@[cljSymbol("if"), test, then])
cljList(@[cljSymbol("if"), test, then,
cljList(@[cljSymbol("cond")] & rest)])
)
# cond->
defineMacro("cond->", proc(args: seq[CljVal]): CljVal =
if args.len < 1:
raise newException(CatchableError, "cond-> requires at least 1 argument")
let expr = args[0]
let gs = cljSymbol(gensymName("ct_"))
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])]
var i = 1
while i + 1 < args.len:
let test = args[i]
let step = args[i+1]
var threaded: CljVal
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
newItems.insert(gs, 1)
threaded = cljList(newItems)
elif step.kind == ckSymbol:
threaded = cljList(@[step, gs])
else:
threaded = step
forms.add(cljList(@[cljSymbol("if"), test,
cljList(@[cljSymbol("set!"), gs, threaded]),
gs]))
i += 2
forms.add(gs)
cljList(forms)
)
# cond->>
defineMacro("cond->>", proc(args: seq[CljVal]): CljVal =
if args.len < 1:
raise newException(CatchableError, "cond->> requires at least 1 argument")
let expr = args[0]
let gs = cljSymbol(gensymName("ctg_"))
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])]
var i = 1
while i + 1 < args.len:
let test = args[i]
let step = args[i+1]
var threaded: CljVal
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
newItems.add(gs)
threaded = cljList(newItems)
elif step.kind == ckSymbol:
threaded = cljList(@[step, gs])
else:
threaded = step
forms.add(cljList(@[cljSymbol("if"), test,
cljList(@[cljSymbol("set!"), gs, threaded]),
gs]))
i += 2
forms.add(gs)
cljList(forms)
)
# doto
defineMacro("doto", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "doto requires at least 2 arguments")
let x = args[0]
let gs = cljSymbol(gensymName("do_"))
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, x])]
for i in 1..<args.len:
let step = args[i]
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
newItems.insert(gs, 1)
forms.add(cljList(newItems))
elif step.kind == ckSymbol:
forms.add(cljList(@[step, gs]))
forms.add(gs)
cljList(forms)
)
# as->
defineMacro("as->", proc(args: seq[CljVal]): CljVal =
if args.len < 3:
raise newException(CatchableError, "as-> requires at least 3 arguments")
let expr = args[0]
let name = args[1]
var result = expr
for i in 2..<args.len:
result = cljList(@[cljSymbol("let"), cljVector(@[name, result]), args[i]])
result
)
# some->
defineMacro("some->", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "some-> requires at least 2 arguments")
var result = args[0]
for i in 1..<args.len:
let step = args[i]
let gs = cljSymbol(gensymName("st_"))
var threaded: CljVal
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
newItems.insert(gs, 1)
threaded = cljList(newItems)
elif step.kind == ckSymbol:
threaded = cljList(@[step, gs])
else:
threaded = step
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
cljNil(), threaded])])
result
)
# some->>
defineMacro("some->>", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "some->> requires at least 2 arguments")
var result = args[0]
for i in 1..<args.len:
let step = args[i]
let gs = cljSymbol(gensymName("stg_"))
var threaded: CljVal
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
newItems.add(gs)
threaded = cljList(newItems)
elif step.kind == ckSymbol:
threaded = cljList(@[step, gs])
else:
threaded = step
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
cljNil(), threaded])])
result
)
# for (simplified — single binding only)
defineMacro("for", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "for requires binding vector and body")
let bindings = args[0]
let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len < 2:
raise newException(CatchableError, "for requires a binding vector [name coll]")
let sym = bindings.items[0]
let coll = bindings.items[1]
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body)
let gs = cljSymbol(gensymName("f_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, cljVector(@[])]),
cljList(@[cljSymbol("doseq"), cljVector(@[sym, coll]),
cljList(@[cljSymbol("set!"), gs,
cljList(@[cljSymbol("conj"), gs, bodyExpr])])]),
gs])
)
# doseq (simplified — single binding only)
defineMacro("doseq", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "doseq requires binding vector and body")
let bindings = args[0]
let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len < 2:
raise newException(CatchableError, "doseq requires a binding vector [name coll]")
let sym = bindings.items[0]
let coll = bindings.items[1]
let gs = cljSymbol(gensymName("ds_"))
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body)
cljList(@[cljSymbol("let"), cljVector(@[gs, coll]),
cljList(@[cljSymbol("loop"), cljVector(@[cljSymbol(gensymName("i_")), cljInt(0)]),
cljList(@[cljSymbol("when"),
cljList(@[cljSymbol("<"), cljSymbol(gensymName("i_")), cljList(@[cljSymbol("count"), gs])]),
cljList(@[cljSymbol("let"), cljVector(@[sym, cljList(@[cljSymbol("nth"), gs, cljSymbol(gensymName("i_"))])]),
bodyExpr]),
cljList(@[cljSymbol("recur"),
cljList(@[cljSymbol("inc"), cljSymbol(gensymName("i_"))])])])])])
)
# dotimes
defineMacro("dotimes", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "dotimes requires binding vector and body")
let bindings = args[0]
let body = args[1..^1]
if bindings.kind != ckVector or bindings.items.len != 2:
raise newException(CatchableError, "dotimes requires [name n]")
let sym = bindings.items[0]
let n = bindings.items[1]
cljList(@[cljSymbol("doseq"), cljVector(@[sym, cljList(@[cljSymbol("range"), n])])] & body)
)
# defmacro
defineMacro("defmacro", proc(args: seq[CljVal]): CljVal =
# defmacro is handled specially in the compiler, this is a no-op
cljNil()
)
# comment
defineMacro("comment", proc(args: seq[CljVal]): CljVal =
cljNil()
)
# assert
defineMacro("assert", proc(args: seq[CljVal]): CljVal =
if args.len == 0: return cljNil()
let test = args[0]
let msg = if args.len > 1: args[1] else: cljString("Assert failed")
cljList(@[cljSymbol("when-not"), test,
cljList(@[cljSymbol("throw"), msg])])
)
# with-open
defineMacro("with-open", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "with-open requires binding and body")
let binding = args[0]
let body = args[1..^1]
if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "with-open requires [name init]")
let sym = binding.items[0]
let init = binding.items[1]
let gs = cljSymbol(gensymName("wo_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, init]),
cljList(@[cljSymbol("try")] & body & @[
cljList(@[cljSymbol("finally"),
cljList(@[cljSymbol(".close"), gs])])])])
)