Files
bara-lang/src/emitter.nim
T
dimgigov ba0b300917 feat: add fullscreen TUI and project updates
- New TUI screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help
- AI configuration persisted in ~/.config/cljnim/config.json
- Added illwill dependency for terminal UI
- Updated experiments, examples, docs, and core modules
2026-05-09 01:53:23 +03:00

1117 lines
42 KiB
Nim

# Clojure → Nim Emitter
import strutils, sets
import types
import macros
var requiredImports* = initHashSet[string]()
var emitLibMode* = false
var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
proc setNsAliases*(aliases: seq[(string, string)]) =
nsAliases = aliases
proc resolveNsAlias*(name: string): string =
# Resolve mu/square -> square (strip namespace prefix if alias exists)
let slashIdx = name.find('/')
if slashIdx < 0: return name
let prefix = name[0..<slashIdx]
let suffix = name[slashIdx+1..^1]
for (alias, ns) in nsAliases:
if prefix == alias:
return suffix
return name
type
EmitterError* = object of CatchableError
proc mangleName*(name: string): string =
result = ""
for c in name:
case c
of '-': result.add('_')
of '?': result.add("_Q")
of '!': result.add("_B")
of '*': result.add("_STAR_")
of '+': result.add("_PLUS_")
of '/': result.add("_SLASH_")
of '=': result.add("_EQ_")
of '>': result.add("_GT_")
of '<': result.add("_LT_")
of '.': result.add('_')
of '\'': result.add("_QUOTE_")
of '&': result.add("_AMP_")
else: result.add(c)
proc emitExpr*(v: CljVal, indent: int = 0): string
proc emitBlock*(items: seq[CljVal], indent: int): string
proc indentStr(indent: int): string =
" ".repeat(indent)
proc runtimeName(op: string): string =
case op
of "+": "cljAdd"
of "-": "cljSub"
of "*": "cljMul"
of "/": "cljDiv"
of "=": "cljNumEq"
of ">": "cljGt"
of "<": "cljLt"
of ">=": "cljGe"
of "<=": "cljLe"
of "not=": "cljNotEq"
of "not": "cljNot"
of "println": "cljPrintln"
of "prn": "cljPrn"
of "str": "cljStrConcat"
of "pr-str": "cljPrStrConcat"
of "inc": "cljInc"
of "dec": "cljDec"
of "zero?": "cljZero"
of "pos?": "cljPos"
of "neg?": "cljNeg"
of "even?": "cljEven"
of "odd?": "cljOdd"
of "first": "cljFirst"
of "rest": "cljRest"
of "next": "cljNext"
of "last": "cljLast"
of "nth": "cljNth"
of "count": "cljCount"
of "conj": "cljConj"
of "cons": "cljCons"
of "seq": "cljSeq"
of "vec": "cljVec"
of "empty?": "cljEmpty"
of "concat": "cljConcat"
of "take": "cljTake"
of "drop": "cljDrop"
of "reverse": "cljReverse"
of "sort": "cljSort"
of "distinct": "cljDistinct"
of "flatten": "cljFlatten"
of "partition": "cljPartition"
of "frequencies": "cljFrequencies"
of "get": "cljGet"
of "get-in": "cljGetIn"
of "assoc": "cljAssoc"
of "dissoc": "cljDissoc"
of "keys": "cljKeys"
of "vals": "cljVals"
of "contains?": "cljContains"
of "select-keys": "cljSelectKeys"
of "merge": "cljMerge"
of "identity": "cljIdentity"
of "type": "cljType"
of "abs": "cljAbs"
of "mod": "cljMod"
of "min": "cljMin"
of "max": "cljMax"
of "apply": "cljApply"
of "atom": "cljAtom"
of "deref": "cljDeref"
of "reset!": "cljReset"
of "swap!": "cljSwap"
# ---- File operations ----
of "file/read": "cljFileRead"
of "file/write": "cljFileWrite"
of "file/append": "cljFileAppend"
of "file/ls": "cljFileLs"
of "file/exists?": "cljFileExists"
# ---- Git operations ----
of "git/status": "cljGitStatus"
of "git/commit": "cljGitCommit"
of "git/push": "cljGitPush"
of "git/diff": "cljGitDiff"
of "git/log": "cljGitLog"
of "disj": "cljDisj"
of "slurp": "cljFileRead"
of "spit": "cljFileWrite"
of "read-line": "cljReadLine"
of "repeat": "cljRepeat"
of "cycle": "cljCycle"
of "iterate": "cljIterate"
of "interleave": "cljInterleave"
of "quot": "cljQuot"
of "rem": "cljRem"
of "instance?": "cljInstanceP"
of "meta": "cljMeta"
of "with-meta": "cljWithMeta"
of "transient": "cljTransient"
of "persistent!": "cljPersistent"
of "conj!": "cljConjB"
of "assoc!": "cljAssocB"
# ---- Agent operations ----
of "agent": "cljAgent"
of "send": "cljAgentSend"
of "await": "cljAgentAwait"
of "agent-error": "cljAgentError"
of "shutdown-agents": "cljAgentShutdown"
# ---- Channel operations (core.async) ----
of "chan": "cljChan"
of "close!": "cljChanClose"
else: ""
proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let sp = indentStr(indent)
let head = items[0]
if head.kind != ckSymbol:
raise newException(EmitterError, "List head must be a symbol, got " & $head.kind)
let op = resolveNsAlias(head.symName)
# ---- Special forms (language constructs) ----
case op
of "ns":
# Namespace declaration: (ns my.app (:require [other.lib :as lib]))
if items.len < 2:
raise newException(EmitterError, "ns requires a namespace name")
let nsName = items[1]
if nsName.kind != ckSymbol:
raise newException(EmitterError, "ns name must be a symbol")
var lines: seq[string] = @[]
lines.add(sp & "# Namespace: " & nsName.symName)
# Process require clauses
for ri in 2..<items.len:
let clause = items[ri]
let isRequire = (clause.kind == ckList and clause.items.len > 0 and
((clause.items[0].kind == ckSymbol and clause.items[0].symName == ":require") or
(clause.items[0].kind == ckKeyword and clause.items[0].kwName == "require")))
if isRequire:
for ci in 1..<clause.items.len:
let req = clause.items[ci]
if req.kind == ckVector and req.items.len >= 1:
let libName = req.items[0]
if libName.kind == ckSymbol:
var alias = libName.symName
# Check for :as
if req.items.len >= 3 and req.items[1].kind == ckKeyword and req.items[1].kwName == "as":
if req.items[2].kind == ckSymbol:
alias = req.items[2].symName
# Convert namespace to file path: my.app -> my/app
let filePath = libName.symName.replace(".", "/") & ".clj"
lines.add(sp & "# require: " & libName.symName & " as " & alias & " from " & filePath)
return lines.join("\n")
of "defmacro":
# Register user-defined macro
if items.len < 4:
raise newException(EmitterError, "defmacro requires name, params, and body")
let name = items[1]
if name.kind != ckSymbol:
raise newException(EmitterError, "defmacro name must be a symbol")
let macroName = name.symName
let macroParams = items[2]
let macroBody = items[3..^1]
defineMacro(macroName, proc(args: seq[CljVal]): CljVal =
# Build env: param name -> arg value
var env: seq[(string, CljVal)] = @[]
if macroParams.kind == ckVector:
for i in 0..<macroParams.items.len:
if macroParams.items[i].kind == ckSymbol:
let pName = macroParams.items[i].symName
if pName == "&":
if i + 1 < macroParams.items.len:
let restName = macroParams.items[i+1].symName
env.add((restName, cljList(args[i..^1])))
break
elif i < args.len:
env.add((pName, args[i]))
# Mini-evaluator for macro body
proc eval(form: CljVal): CljVal =
if form.isNil: return cljNil()
case form.kind
of ckSymbol:
for (n, v) in env:
if form.symName == n: return v
return form
of ckKeyword: return form
of ckInt: return form
of ckFloat: return form
of ckString: return form
of ckBool: return form
of ckNil: return form
of ckVector:
var newItems: seq[CljVal] = @[]
for item in form.items:
newItems.add(eval(item))
return cljVector(newItems)
of ckList:
if form.items.len == 0: return cljList(@[])
let head = form.items[0]
if head.kind == ckSymbol:
let hName = head.symName
# quote
if hName == "quote" and form.items.len == 2:
return form.items[1]
# list
if hName == "list":
var evaluated: seq[CljVal] = @[]
for i in 1..<form.items.len:
evaluated.add(eval(form.items[i]))
return cljList(evaluated)
# cons
if hName == "cons" and form.items.len == 3:
let fst = eval(form.items[1])
let rst = eval(form.items[2])
var res = @[fst]
if rst.kind == ckList:
res.add(rst.items)
return cljList(res)
# concat
if hName == "concat":
var res: seq[CljVal] = @[]
for i in 1..<form.items.len:
let v = eval(form.items[i])
if v.kind == ckList:
res.add(v.items)
return cljList(res)
# vec
if hName == "vec" and form.items.len == 2:
let v = eval(form.items[1])
if v.kind == ckList: return cljVector(v.items)
return v
# conj
if hName == "conj" and form.items.len == 3:
let coll = eval(form.items[2])
let item = eval(form.items[1])
if coll.kind == ckList:
var newItems = @[item]
newItems.add(coll.items)
return cljList(newItems)
return coll
# str
if hName == "str":
var s = ""
for i in 1..<form.items.len:
let v = eval(form.items[i])
case v.kind
of ckString: s.add(v.strVal)
else: s.add($v)
return cljString(s)
# Apply: evaluate all items, first must be fn
var evalItems: seq[CljVal] = @[]
for item in form.items:
evalItems.add(eval(item))
return cljList(evalItems)
# Non-symbol head: evaluate all
var evalItems: seq[CljVal] = @[]
for item in form.items:
evalItems.add(eval(item))
return cljList(evalItems)
else: return form
# Evaluate body
if macroBody.len == 1:
return eval(macroBody[0])
var resultItems: seq[CljVal] = @[cljSymbol("do")]
for b in macroBody:
resultItems.add(eval(b))
cljList(resultItems)
)
return sp & "discard cljNil()"
of "def":
if items.len != 3:
raise newException(EmitterError, "def requires exactly 2 arguments")
let name = items[1]
if name.kind != ckSymbol:
raise newException(EmitterError, "def name must be a symbol")
return sp & "let " & mangleName(name.symName) & " = " & emitExpr(items[2], 0)
of "defn":
if items.len < 4:
raise newException(EmitterError, "defn requires name, params, and body")
let name = items[1]
if name.kind != ckSymbol:
raise newException(EmitterError, "defn name must be a symbol")
let params = items[2]
if params.kind != ckVector:
raise newException(EmitterError, "defn params must be a vector")
var paramNames: seq[string] = @[]
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols")
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1]
var bodyCode = ""
if body.len == 1:
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
# In a proc body, replace "discard " with "result = " so that
# if/when branches return their last expression properly.
bodyCode = bodyCode.replace("discard ", "result = ")
let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
of "defn-":
if items.len < 4:
raise newException(EmitterError, "defn- requires name, params, and body")
let name = items[1]
if name.kind != ckSymbol:
raise newException(EmitterError, "defn- name must be a symbol")
let params = items[2]
if params.kind != ckVector:
raise newException(EmitterError, "defn- params must be a vector")
var paramNames: seq[string] = @[]
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols")
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1]
var bodyCode = ""
if body.len == 1:
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode
of "fn":
if items.len < 3:
raise newException(EmitterError, "fn requires params and body")
let params = items[1]
if params.kind != ckVector:
raise newException(EmitterError, "fn params must be a vector")
var paramNames: seq[string] = @[]
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "fn params must be symbols")
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[2..^1]
var bodyCode = ""
if body.len == 1:
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
return sp & "(proc(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")"
of "let":
if items.len < 3:
raise newException(EmitterError, "let requires bindings and body")
let bindings = items[1]
if bindings.kind != ckVector:
raise newException(EmitterError, "let bindings must be a vector")
if bindings.items.len mod 2 != 0:
raise newException(EmitterError, "let bindings must have even number of elements")
var lines: seq[string] = @[]
lines.add(sp & "block:")
var bi = 0
while bi < bindings.items.len:
let bname = bindings.items[bi]
let bval = bindings.items[bi+1]
if bname.kind != ckSymbol:
raise newException(EmitterError, "let binding name must be a symbol")
lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, 0))
bi += 2
let body = items[2..^1]
for j, b in body:
let bcode = emitExpr(b, indent + 1)
if j == body.len - 1:
lines.add(bcode)
else:
let stripped = bcode.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or
stripped.startsWith("try:"):
lines.add(bcode)
else:
lines.add(indentStr(indent + 1) & "discard " & stripped)
return lines.join("\n")
of "if":
if items.len < 3 or items.len > 4:
raise newException(EmitterError, "if requires condition, then, and optional else")
let condCode = emitExpr(items[1], 0)
let thenCode = emitExpr(items[2], indent + 1)
let thenStripped = thenCode.strip()
var thenFinal = thenCode
if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or
thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or
thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ") or
thenStripped.contains("\n") or thenStripped.contains(" = ")):
thenFinal = indentStr(indent + 1) & "discard " & thenStripped
var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal
if items.len == 4:
let elseCode = emitExpr(items[3], indent + 1)
let elseStripped = elseCode.strip()
var elseFinal = elseCode
if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or
elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or
elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ") or
elseStripped.contains("\n") or elseStripped.contains(" = ")):
elseFinal = indentStr(indent + 1) & "discard " & elseStripped
ifBlock.add("\n" & sp & "else:\n" & elseFinal)
return ifBlock
of "when":
if items.len < 3:
raise newException(EmitterError, "when requires condition and body")
let condCode = emitExpr(items[1], 0)
var lines: seq[string] = @[]
lines.add(sp & "if cljIsTruthy(" & condCode & "):")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 1))
return lines.join("\n")
of "when-let":
if items.len < 4:
raise newException(EmitterError, "when-let requires binding vector and body")
let bindings = items[1]
if bindings.kind != ckVector or bindings.items.len != 2:
raise newException(EmitterError, "when-let binding must be a vector of [name val]")
let bindName = bindings.items[0]
let bindVal = bindings.items[1]
if bindName.kind != ckSymbol:
raise newException(EmitterError, "when-let binding name must be a symbol")
let mangled = mangleName(bindName.symName)
var lines: seq[string] = @[]
lines.add(sp & "block:")
lines.add(indentStr(indent + 1) & "let " & mangled & " = " & emitExpr(bindVal, 0))
lines.add(indentStr(indent + 1) & "if not cljIsNil(" & mangled & "):")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 2))
return lines.join("\n")
of "if-let":
if items.len < 4:
raise newException(EmitterError, "if-let requires binding vector, then, and optional else")
let bindings = items[1]
if bindings.kind != ckVector or bindings.items.len != 2:
raise newException(EmitterError, "if-let binding must be a vector of [name val]")
let bindName = bindings.items[0]
let bindVal = bindings.items[1]
if bindName.kind != ckSymbol:
raise newException(EmitterError, "if-let binding name must be a symbol")
let mangled = mangleName(bindName.symName)
var lines: seq[string] = @[]
lines.add(sp & "block:")
lines.add(indentStr(indent + 1) & "let " & mangled & " = " & emitExpr(bindVal, 0))
lines.add(indentStr(indent + 1) & "if not cljIsNil(" & mangled & "):")
lines.add(emitExpr(items[2], indent + 2))
if items.len == 4:
lines.add(indentStr(indent + 1) & "else:")
lines.add(emitExpr(items[3], indent + 2))
return lines.join("\n")
of "cond":
if items.len < 3 or items.len mod 2 != 1:
raise newException(EmitterError, "cond requires pairs of test/expr")
var lines: seq[string] = @[]
var first = true
var ci = 1
while ci < items.len:
let testExpr = items[ci]
let thenExpr = items[ci+1]
if (testExpr.kind == ckSymbol and testExpr.symName == ":else") or
(testExpr.kind == ckKeyword and testExpr.kwName == "else"):
lines.add(sp & "else:")
lines.add(emitExpr(thenExpr, indent + 1))
elif first:
lines.add(sp & "if cljIsTruthy(" & emitExpr(testExpr, 0) & "):")
lines.add(emitExpr(thenExpr, indent + 1))
first = false
else:
lines.add(sp & "elif cljIsTruthy(" & emitExpr(testExpr, 0) & "):")
lines.add(emitExpr(thenExpr, indent + 1))
ci += 2
return lines.join("\n")
of "loop":
if items.len < 3:
raise newException(EmitterError, "loop requires bindings and body")
let bindings = items[1]
if bindings.kind != ckVector:
raise newException(EmitterError, "loop bindings must be a vector")
if bindings.items.len mod 2 != 0:
raise newException(EmitterError, "loop bindings must have even number of elements")
var loopParams: seq[(string, string)] = @[]
var li = 0
while li < bindings.items.len:
let lname = bindings.items[li]
let lval = bindings.items[li+1]
if lname.kind != ckSymbol:
raise newException(EmitterError, "loop binding name must be a symbol")
loopParams.add((mangleName(lname.symName), emitExpr(lval, 0)))
li += 2
var loopVars: seq[string] = @[]
var lines: seq[string] = @[]
for (lpName, lpVal) in loopParams:
lines.add(sp & "var " & lpName & ": CljVal = " & lpVal)
loopVars.add(lpName)
loopStack.add(loopVars)
lines.add(sp & "while true:")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 1))
lines.add(indentStr(indent + 1) & "break")
discard loopStack.pop()
return lines.join("\n")
of "recur":
if items.len < 2:
raise newException(EmitterError, "recur requires arguments")
if loopStack.len == 0:
raise newException(EmitterError, "recur outside of loop")
let loopVars = loopStack[^1]
if items.len - 1 != loopVars.len:
raise newException(EmitterError, "recur requires " & $loopVars.len & " arguments, got " & $(items.len - 1))
var lines: seq[string] = @[]
for ri in 1..<items.len:
lines.add(sp & loopVars[ri-1] & " = " & emitExpr(items[ri], 0))
lines.add(sp & "continue")
return lines.join("\n")
of "do":
if items.len < 2:
return sp & "discard cljNil()"
return emitBlock(items[1..^1], indent)
of "try":
if items.len < 2:
raise newException(EmitterError, "try requires body")
var bodyForms: seq[CljVal] = @[]
var catchClauses: seq[(string, string, seq[CljVal])] = @[]
var finallyBody: seq[CljVal] = @[]
var ti = 1
while ti < items.len:
let form = items[ti]
if form.kind == ckList and form.items.len > 0 and form.items[0].kind == ckSymbol:
if form.items[0].symName == "catch":
if form.items.len < 3:
raise newException(EmitterError, "catch requires exception type, name, and body")
let exType = form.items[1]
var exTypeStr = "CatchableError"
if exType.kind == ckSymbol:
exTypeStr = exType.symName
let exName = form.items[2]
if exName.kind != ckSymbol:
raise newException(EmitterError, "catch name must be a symbol")
catchClauses.add((exTypeStr, mangleName(exName.symName), form.items[3..^1]))
elif form.items[0].symName == "finally":
finallyBody = form.items[1..^1]
else:
bodyForms.add(form)
else:
bodyForms.add(form)
inc ti
var lines: seq[string] = @[]
lines.add(sp & "try:")
for b in bodyForms:
lines.add(emitExpr(b, indent + 1))
for (exType, exVar, exBody) in catchClauses:
lines.add(sp & "except " & exType & ":")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])")
lines.add(indentStr(indent + 1) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))")
for eb in exBody:
lines.add(emitExpr(eb, indent + 1))
if finallyBody.len > 0:
lines.add(sp & "finally:")
for fb in finallyBody:
lines.add(emitExpr(fb, indent + 1))
return lines.join("\n")
of "throw":
if items.len != 2:
raise newException(EmitterError, "throw requires exactly 1 argument")
return sp & "raise newException(CatchableError, cljStr(" & emitExpr(items[1], 0) & "))"
of "quote":
if items.len != 2:
raise newException(EmitterError, "quote requires exactly 1 argument")
let quoted = items[1]
case quoted.kind
of ckSymbol:
return sp & "cljSymbol(\"" & quoted.symName & "\")"
of ckList:
var parts: seq[string] = @[]
for item in quoted.items:
parts.add(emitExpr(item, 0))
return sp & "cljList(@[" & parts.join(", ") & "])"
of ckVector:
var parts: seq[string] = @[]
for item in quoted.items:
parts.add(emitExpr(item, 0))
return sp & "cljVector(@[" & parts.join(", ") & "])"
else:
return emitExpr(quoted, indent)
of "map":
if items.len != 3:
raise newException(EmitterError, "map requires exactly 2 arguments (f, coll)")
let fnArg = items[1]
let collArg = emitExpr(items[2], 0)
if fnArg.kind == ckSymbol:
let rn = runtimeName(fnArg.symName)
if rn.len > 0:
return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & collArg & ")"
return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args[0])), " & collArg & ")"
elif fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn":
# Inline fn: (map (fn [x] ...) coll)
let fnParams = fnArg.items[1]
let fnBody = fnArg.items[2..^1]
var pNames: seq[string] = @[]
for p in fnParams.items:
pNames.add(mangleName(p.symName))
var bodyExpr = ""
if fnBody.len == 1:
bodyExpr = emitExpr(fnBody[0], 0).strip()
else:
var parts: seq[string] = @[]
for b in fnBody:
parts.add(emitExpr(b, 0).strip())
bodyExpr = parts.join("; ")
let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")"
return sp & "cljMap(cljFn(" & fnProc & "), " & collArg & ")"
else:
return sp & "cljMap(" & emitExpr(fnArg, 0) & ", " & collArg & ")"
of "filter":
if items.len != 3:
raise newException(EmitterError, "filter requires exactly 2 arguments")
let fnArg = items[1]
let collArg = emitExpr(items[2], 0)
if fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn":
let fnParams = fnArg.items[1]
let fnBody = fnArg.items[2..^1]
var pNames: seq[string] = @[]
for p in fnParams.items:
pNames.add(mangleName(p.symName))
var bodyExpr = ""
if fnBody.len == 1:
bodyExpr = emitExpr(fnBody[0], 0).strip()
else:
var parts: seq[string] = @[]
for b in fnBody:
parts.add(emitExpr(b, 0).strip())
bodyExpr = parts.join("; ")
# Wrap single-param fn as seq[CljVal] version
let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")"
return sp & "cljFilter(cljFn(" & fnProc & "), " & collArg & ")"
else:
return sp & "cljFilter(" & emitExpr(fnArg, 0) & ", " & collArg & ")"
of "reduce":
if items.len < 3 or items.len > 4:
raise newException(EmitterError, "reduce requires 2 or 3 arguments")
if items.len == 3:
# (reduce f coll) — first element as init
let fnArg = items[1]
let collArg = emitExpr(items[2], 0)
return sp & "cljReduce(" & emitExpr(fnArg, 0) & ", cljNil(), " & collArg & ")"
else:
# (reduce f init coll)
let fnArg = items[1]
let initArg = emitExpr(items[2], 0)
let collArg = emitExpr(items[3], 0)
return sp & "cljReduce(" & emitExpr(fnArg, 0) & ", " & initArg & ", " & collArg & ")"
of "mapv":
if items.len != 3:
raise newException(EmitterError, "mapv requires exactly 2 arguments")
return sp & "cljMapv(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "apply":
if items.len != 3:
raise newException(EmitterError, "apply requires exactly 2 arguments")
return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "some":
if items.len != 3:
raise newException(EmitterError, "some requires exactly 2 arguments")
return sp & "cljSome(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "every?":
if items.len != 3:
raise newException(EmitterError, "every? requires exactly 2 arguments")
return sp & "cljEvery(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "into":
if items.len != 3:
raise newException(EmitterError, "into requires exactly 2 arguments")
return sp & "cljInto(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "comp":
var parts: seq[string] = @[]
for i in 1..<items.len:
parts.add(emitExpr(items[i], 0))
return sp & "cljComp(@[" & parts.join(", ") & "])"
of "partial":
var parts: seq[string] = @[]
for i in 1..<items.len:
parts.add(emitExpr(items[i], 0))
return sp & "cljPartial(" & parts[0] & ", @[" & parts[1..^1].join(", ") & "])"
of "juxt":
var parts: seq[string] = @[]
for i in 1..<items.len:
parts.add(emitExpr(items[i], 0))
return sp & "cljJuxt(@[" & parts.join(", ") & "])"
of "complement":
if items.len != 2:
raise newException(EmitterError, "complement requires exactly 1 argument")
return sp & "cljComplement(" & emitExpr(items[1], 0) & ")"
of "constantly":
if items.len != 2:
raise newException(EmitterError, "constantly requires exactly 1 argument")
return sp & "cljConstantly(" & emitExpr(items[1], 0) & ")"
of "group-by":
if items.len != 3:
raise newException(EmitterError, "group-by requires exactly 2 arguments")
return sp & "cljGroupBy(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
of "subs":
if items.len < 3 or items.len > 4:
raise newException(EmitterError, "subs requires 2 or 3 arguments")
if items.len == 3:
return sp & "cljSubs(" & emitExpr(items[1], 0) & ", cljInt(" & emitExpr(items[2], 0) & ").intVal)"
else:
return sp & "cljSubsRange(" & emitExpr(items[1], 0) & ", cljInt(" & emitExpr(items[2], 0) & ").intVal, cljInt(" & emitExpr(items[3], 0) & ").intVal)"
of "get-in":
if items.len < 3 or items.len > 4:
raise newException(EmitterError, "get-in requires 2 or 3 arguments")
if items.len == 3:
return sp & "cljGetIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
else:
return sp & "cljGetIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & emitExpr(items[3], 0) & ")"
of "update":
if items.len < 4:
raise newException(EmitterError, "update requires at least 3 arguments")
var fArg: string
if items[3].kind == ckSymbol:
let rn = runtimeName(items[3].symName)
if rn.len > 0:
fArg = "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
else:
fArg = mangleName(items[3].symName)
else:
fArg = emitExpr(items[3], 0)
var extraArgs: seq[string] = @[]
for i in 4..<items.len:
extraArgs.add(emitExpr(items[i], 0))
if extraArgs.len > 0:
return sp & "cljUpdate(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & fArg & ", @[" & extraArgs.join(", ") & "])"
else:
return sp & "cljUpdate(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & fArg & ")"
of "assoc-in":
if items.len != 4:
raise newException(EmitterError, "assoc-in requires exactly 3 arguments")
return sp & "cljAssocIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & emitExpr(items[3], 0) & ")"
of "and":
if items.len < 2: return sp & "cljBool(true)"
if items.len == 2: return emitExpr(items[1], indent)
# (and a b c) => ternary: if a then (if b then c else false) else false
# Generate as nested inline if
let lastExpr = emitExpr(items[^1], 0)
var r = lastExpr
for i in countdown(items.len - 2, 1):
r = "(if cljIsTruthy(" & emitExpr(items[i], 0) & "): " & r & " else: cljBool(false))"
return sp & r
of "or":
if items.len < 2: return sp & "cljNil()"
if items.len == 2: return emitExpr(items[1], indent)
# (or a b c) => ternary: if a then a else (if b then b else c)
let lastExpr = emitExpr(items[^1], 0)
var r = lastExpr
for i in countdown(items.len - 2, 1):
let argCode = emitExpr(items[i], 0)
r = "(if cljIsTruthy(" & argCode & "): " & argCode & " else: " & r & ")"
return sp & r
of "nil?":
if items.len != 2:
raise newException(EmitterError, "nil? requires exactly 1 argument")
return sp & "cljBool(cljIsNil(" & emitExpr(items[1], 0) & "))"
of "set!":
if items.len != 3:
raise newException(EmitterError, "set! requires exactly 2 arguments")
let target = items[1]
if target.kind != ckSymbol:
raise newException(EmitterError, "set! target must be a symbol")
return sp & mangleName(target.symName) & " = " & emitExpr(items[2], 0)
of "set":
if items.len != 2:
raise newException(EmitterError, "set requires exactly 1 argument")
let arg = items[1]
case arg.kind
of ckVector:
var parts: seq[string] = @[]
for item in arg.items:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckList:
var parts: seq[string] = @[]
for item in arg.items:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
else:
return sp & "cljSet(@[" & emitExpr(arg, 0) & "])"
of "range":
if items.len < 1 or items.len > 4:
raise newException(EmitterError, "range requires 0, 1, 2, or 3 arguments")
if items.len == 1:
return sp & "cljList(@[])"
elif items.len == 2:
return sp & "cljRange(" & emitExpr(items[1], 0) & ")"
elif items.len == 3:
return sp & "cljRange(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")"
else:
return sp & "cljRange3(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & emitExpr(items[3], 0) & ")"
of "iterate":
if items.len != 4:
raise newException(EmitterError, "iterate requires 3 arguments (n, f, x)")
let fnArg = items[2]
let nArg = emitExpr(items[1], 0)
let xArg = emitExpr(items[3], 0)
if fnArg.kind == ckSymbol:
let rn = runtimeName(fnArg.symName)
if rn.len > 0:
return sp & "cljIterate(" & nArg & ", cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & xArg & ")"
return sp & "cljIterate(" & nArg & ", cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args[0])), " & xArg & ")"
else:
return sp & "cljIterate(" & nArg & ", " & emitExpr(fnArg, 0) & ", " & xArg & ")"
else:
# ---- Nim interop: nim/module/function ----
if op.startsWith("nim/") or op.startsWith("nim."):
let parts = op.replace(".", "/").split("/")
if parts.len >= 3:
let module = parts[1]
var funcChain = parts[2..^1].join(".")
# Strip ? suffix (Clojure convention) — not valid in Nim
if funcChain.endsWith("?"):
funcChain = funcChain[0..^2]
var argParts: seq[string] = @[]
for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0))
# Known Nim module interop patterns
case module
of "math":
requiredImports.incl("math")
var nimArgs: seq[string] = @[]
for a in argParts:
nimArgs.add(a & ".floatVal")
return sp & "cljFloat(" & funcChain & "(" & nimArgs.join(", ") & "))"
of "strutils":
requiredImports.incl("strutils")
let strFns = ["endsWith", "startsWith", "contains", "find"]
let strRetFns = ["replace", "strip", "toLower", "toUpper", "join"]
let strIntFns = ["repeat", "count"]
if funcChain in strFns:
if argParts.len >= 1:
var nimArgs: seq[string] = @[]
nimArgs.add(argParts[0] & ".strVal")
for i in 1..<argParts.len:
nimArgs.add(argParts[i] & ".strVal")
return sp & "cljBool(" & funcChain & "(" & nimArgs.join(", ") & "))"
elif funcChain in strRetFns:
if argParts.len >= 1:
var nimArgs: seq[string] = @[]
nimArgs.add(argParts[0] & ".strVal")
for i in 1..<argParts.len:
nimArgs.add(argParts[i] & ".strVal")
return sp & "cljString(" & funcChain & "(" & nimArgs.join(", ") & "))"
elif funcChain in strIntFns:
# repeat(s, n), count(s, sub) — first arg string, rest int
if argParts.len >= 2:
var nimArgs: seq[string] = @[]
nimArgs.add(argParts[0] & ".strVal")
for i in 1..<argParts.len:
nimArgs.add(argParts[i] & ".intVal")
return sp & "cljString(" & funcChain & "(" & nimArgs.join(", ") & "))"
elif funcChain == "split":
if argParts.len >= 2:
return sp & "cljList(" & funcChain & "(" & argParts[0] & ".strVal, " & argParts[1] & ".strVal).mapIt(cljString(it)))"
return sp & funcChain & "(" & argParts.join(", ") & ")"
of "times":
requiredImports.incl("times")
return sp & funcChain & "(" & argParts.join(", ") & ")"
of "os":
requiredImports.incl("os")
return sp & "cljString(" & funcChain & "(" & argParts.join(", ") & "))"
of "system":
return sp & funcChain & "(" & argParts.join(", ") & ")"
else:
requiredImports.incl(module)
return sp & funcChain & "(" & argParts.join(", ") & ")"
elif parts.len == 2:
return sp & "cljString(\"" & parts[1] & "\")"
# ---- C FFI import: (c/import "header" :fn1 :fn2) ----
if op == "c/import" or op == "c-ffi/import":
if items.len < 2:
raise newException(EmitterError, "c/import requires at least a header path")
let header = items[1]
if header.kind != ckString:
raise newException(EmitterError, "c/import header must be a string")
let headerPath = header.strVal
var ffiProcs: seq[string] = @[]
for i in 2..<items.len:
if items[i].kind == ckKeyword:
let fnName = items[i].kwName
ffiProcs.add("proc " & fnName & "*(): clong {.importc, header: \"" & headerPath & "\".}")
elif items[i].kind == ckSymbol:
let fnName = items[i].symName
ffiProcs.add("proc " & fnName & "*(): clong {.importc, header: \"" & headerPath & "\".}")
return ffiProcs.join("\n" & sp)
# ---- Known runtime functions ----
let rn = runtimeName(op)
if rn.len > 0:
var argParts: seq[string] = @[]
for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0))
# Variadic functions take seq[CljVal]
let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=",
"println", "prn", "print", "str", "pr-str",
"concat", "min", "max", "merge", "interleave"]
var call: string
if variadic and argParts.len > 0:
call = rn & "(@[" & argParts.join(", ") & "])"
else:
call = rn & "(" & argParts.join(", ") & ")"
if op in ["println", "prn", "print"]:
return sp & "discard " & call
return sp & call
# ---- User-defined function call ----
var args: seq[string] = @[]
for i in 1..<items.len:
args.add(emitExpr(items[i], 0))
return sp & mangleName(op) & "(" & args.join(", ") & ")"
proc emitExpr*(v: CljVal, indent: int = 0): string =
let sp = indentStr(indent)
case v.kind
of ckNil:
return sp & "cljNil()"
of ckBool:
return sp & "cljBool(" & $v.boolVal & ")"
of ckInt:
return sp & "cljInt(" & $v.intVal & ")"
of ckFloat:
return sp & "cljFloat(" & $v.floatVal & ")"
of ckString:
return sp & "cljString(\"" & v.strVal.replace("\"", "\\\"") & "\")"
of ckKeyword:
return sp & "cljKeyword(\"" & v.kwName & "\")"
of ckSymbol:
let symName = resolveNsAlias(v.symName)
let rn = runtimeName(symName)
if rn.len > 0:
# Known function symbol — emit as runtime fn reference
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
return sp & mangleName(symName)
of ckList:
if v.items.len == 0:
return sp & "cljList(@[])"
# Macro-expand before emitting
let expanded = macroexpand(v)
if expanded.kind != ckList or expanded == v:
return emitSpecialForm(v.items, indent)
return emitExpr(expanded, indent)
of ckVector:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitExpr(item, 0))
return sp & "cljVector(@[" & parts.join(", ") & "])"
of ckMap:
if v.mapKeys.len == 0:
return sp & "cljMap(@[], @[])"
var keyParts: seq[string] = @[]
var valParts: seq[string] = @[]
for i in 0..<v.mapKeys.len:
keyParts.add(emitExpr(v.mapKeys[i], 0))
valParts.add(emitExpr(v.mapVals[i], 0))
return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckFn:
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())"
of ckAtom:
return sp & "cljAtom(" & emitExpr(v.atomVal, 0) & ")"
of ckTransient:
return sp & "cljTransient(cljNil())"
of ckAgent:
return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")"
proc emitBlock(items: seq[CljVal], indent: int): string =
if items.len == 0:
return indentStr(indent) & "discard cljNil()"
var lines: seq[string] = @[]
for item in items:
lines.add(emitExpr(item, indent))
return lines.join("\n")
proc emitProgramInternal(forms: seq[CljVal]): string =
requiredImports = initHashSet[string]()
var headerLines: seq[string] = @[
"# Generated by Clojure/Nim",
"import cljnim_runtime",
]
var defs: seq[string] = @[]
var mainForms: seq[string] = @[]
for i, form in forms:
let isDef = form.kind == ckList and form.items.len > 0 and
form.items[0].kind == ckSymbol and
form.items[0].symName in ["def", "defn", "defn-"]
if isDef:
defs.add(emitExpr(form, 0))
else:
let code = emitExpr(form, 2)
let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or
stripped.startsWith("try:") or stripped.startsWith("var "):
mainForms.add(code)
elif i == forms.len - 1:
mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")")
else:
mainForms.add(indentStr(2) & "discard cljRepr(" & stripped & ")")
var lines = headerLines
# Add collected Nim imports
for imp in requiredImports:
lines.add("import " & imp)
lines.add("")
lines.add(defs)
if mainForms.len > 0:
lines.add("")
lines.add("when isMainModule:")
for form in mainForms:
lines.add(form)
return lines.join("\n") & "\n"
proc emitProgram*(forms: seq[CljVal]): string =
emitLibMode = false
emitProgramInternal(forms)
proc emitProgramLib*(forms: seq[CljVal]): string =
emitLibMode = true
emitProgramInternal(forms)