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:
+468
@@ -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])])])])
|
||||
)
|
||||
Reference in New Issue
Block a user