fix: implement cljnim compiler recommendations (10 fixes)

- #1: Support docstrings in defn/defn-
- #2: Support multi-arity defn with dispatch by args.len
- #3: (:key map) keyword-as-function syntax (cljGet)
- #4: & rest parameters in defn/defn-
- #5: Nim interop name mangling for nim/ module calls
- #6: getLibPath() checks CLJNIM_LIB_PATH env and cwd first
- #7: Parse :paths from deps.edn
- #8: loop+if/else discard fix with loopResult variable
- #10: emitProgramLib skips when isMainModule guard
This commit is contained in:
2026-05-12 00:42:17 +03:00
parent 32dbd1fc5d
commit e36bf4e362
3 changed files with 270 additions and 31 deletions
+7 -2
View File
@@ -3,13 +3,18 @@ import os, osproc, strutils, times
import reader, emitter, types, repl, macros, deps, ai_assist, tui import reader, emitter, types, repl, macros, deps, ai_assist, tui
proc getLibPath(): string = proc getLibPath(): string =
# Check environment variable first
let envPath = getEnv("CLJNIM_LIB_PATH", "")
if envPath.len > 0 and dirExists(envPath):
return envPath
# Check current directory's lib first (project-local libs)
let candidate0 = getCurrentDir() / "lib"
if dirExists(candidate0): return candidate0
let appDir = getAppDir() let appDir = getAppDir()
let candidate1 = appDir / "lib" let candidate1 = appDir / "lib"
let candidate2 = appDir.parentDir / "lib" let candidate2 = appDir.parentDir / "lib"
let candidate3 = getCurrentDir() / "lib"
if dirExists(candidate1): return candidate1 if dirExists(candidate1): return candidate1
if dirExists(candidate2): return candidate2 if dirExists(candidate2): return candidate2
if dirExists(candidate3): return candidate3
return "lib" return "lib"
proc resolveNsToPath(nsName: string, searchPaths: seq[string]): string = proc resolveNsToPath(nsName: string, searchPaths: seq[string]): string =
+11
View File
@@ -17,6 +17,7 @@ type
DepsFile* = object DepsFile* = object
deps*: seq[Dependency] deps*: seq[Dependency]
paths*: seq[string]
DepsError* = object of CatchableError DepsError* = object of CatchableError
@@ -103,6 +104,11 @@ proc parseDepsFile*(filePath: string): DepsFile =
for (pk, pv) in pairs: for (pk, pv) in pairs:
if pk == "deps": if pk == "deps":
result.deps = parseDepsMap(pv) result.deps = parseDepsMap(pv)
elif pk == "paths":
if pv.kind == ckVector:
for p in pv.items:
if p.kind == ckString:
result.paths.add(p.strVal)
proc depsDir*(projectDir: string): string = proc depsDir*(projectDir: string): string =
## Return the .deps directory path for a project ## Return the .deps directory path for a project
@@ -139,6 +145,11 @@ proc gitCheckout*(repoDir, sha: string): bool =
proc resolveDeps*(depsFile: DepsFile, projectDir: string): seq[string] = proc resolveDeps*(depsFile: DepsFile, projectDir: string): seq[string] =
## Download/resolve all dependencies and return search paths ## Download/resolve all dependencies and return search paths
result = @[] result = @[]
# Add :paths from deps.edn
for p in depsFile.paths:
let absPath = if p.isAbsolute: p else: projectDir / p
if dirExists(absPath):
result.add(absPath)
for dep in depsFile.deps: for dep in depsFile.deps:
case dep.kind case dep.kind
of dkLocal: of dkLocal:
+252 -29
View File
@@ -1,5 +1,5 @@
# Bara Lang → Nim Emitter # Bara Lang → Nim Emitter
import strutils, sets, algorithm import strutils, sets, algorithm, sequtils
import types import types
import macros import macros
@@ -9,6 +9,8 @@ var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace) var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
var scopeStack*: seq[HashSet[string]] = @[] var scopeStack*: seq[HashSet[string]] = @[]
var definedGlobals*: HashSet[string] = initHashSet[string]() var definedGlobals*: HashSet[string] = initHashSet[string]()
var multiArityFns*: HashSet[string] = initHashSet[string]()
var loopResultVar*: string = "" # Set by loop handler when result capture is needed
proc registerGlobal*(name: string) = proc registerGlobal*(name: string) =
definedGlobals.incl(name) definedGlobals.incl(name)
@@ -454,6 +456,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let sp = indentStr(indent) let sp = indentStr(indent)
let head = items[0] let head = items[0]
if head.kind != ckSymbol: if head.kind != ckSymbol:
# (:key map) — keyword as function: lookup key in map
if head.kind == ckKeyword:
if items.len == 2:
return sp & "cljGet(" & emitExpr(items[1], 0) & ", cljKeyword(\"" & head.kwName & "\"))"
elif items.len == 3:
return sp & "cljGetDefault(" & emitExpr(items[1], 0) & ", cljKeyword(\"" & head.kwName & "\"), " & emitExpr(items[2], 0) & ")"
# Head is not a symbol - evaluate as function call: ((fn ...) args...) # Head is not a symbol - evaluate as function call: ((fn ...) args...)
var argParts: seq[string] = @[] var argParts: seq[string] = @[]
for i in 1..<items.len: for i in 1..<items.len:
@@ -706,19 +714,112 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if name.kind != ckSymbol: if name.kind != ckSymbol:
raise newException(EmitterError, "defn name must be a symbol") raise newException(EmitterError, "defn name must be a symbol")
registerGlobal(name.symName) registerGlobal(name.symName)
let params = items[2] # Skip docstring if present: (defn name "doc" [params] body...)
var paramsIdx = 2
if items.len > 3 and items[2].kind == ckString:
paramsIdx = 3
let params = items[paramsIdx]
# Multi-arity defn: (defn name ([a] ...) ([a b] ...))
if params.kind == ckList:
let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: ""
# Register as multi-arity BEFORE emitting arity bodies (so internal calls wrap correctly)
multiArityFns.incl(name.symName)
var arityProcs: seq[string] = @[]
var dispatchCases: seq[string] = @[]
for arityIdx in paramsIdx..<items.len:
let arityForm = items[arityIdx]
if arityForm.kind == ckList and arityForm.items.len >= 2 and
arityForm.items[0].kind == ckVector:
let arityParams = arityForm.items[0]
let arityBody = arityForm.items[1..^1]
let paramCount = arityParams.items.len
var paramNames: seq[string] = @[]
pushScope()
for p in arityParams.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
var bodyCode = ""
if arityBody.len == 1:
bodyCode = emitExpr(arityBody[0], indent + 1)
else:
bodyCode = emitBlock(arityBody, indent + 1)
popScope()
let arityName = procName & "_arity" & $paramCount
arityProcs.add(sp & "proc " & arityName & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode)
let argCall = (0..<paramCount).mapIt("args[" & $it & "]").join(", ")
dispatchCases.add(indentStr(indent + 1) & "of " & $paramCount & ": " & arityName & "(" & argCall & ")")
if arityProcs.len > 0:
var allLines: seq[string] = @[]
# Forward declaration for the dispatch proc
allLines.add(sp & "proc " & procName & exportMarker & "(args: seq[CljVal]): CljVal")
for ap in arityProcs:
allLines.add(ap)
allLines.add(sp & "proc " & procName & exportMarker & "(args: seq[CljVal]): CljVal =")
allLines.add(indentStr(indent + 1) & "case args.len")
for dc in dispatchCases:
allLines.add(dc)
allLines.add(indentStr(indent + 1) & "else: raise newException(CatchableError, \"Wrong number of args to " & name.symName & "\")")
# Register as multi-arity so call sites use cljApply
multiArityFns.incl(name.symName)
return allLines.join("\n")
if params.kind != ckVector: if params.kind != ckVector:
raise newException(EmitterError, "defn params must be a vector") raise newException(EmitterError, "defn params must be a vector")
var paramNames: seq[string] = @[] var paramNames: seq[string] = @[]
var hasRest = false
var restIdx = -1
pushScope() pushScope()
for p in params.items: # First pass: check for & rest params
if p.kind != ckSymbol: for pi, p in params.items:
raise newException(EmitterError, "defn params must be symbols") if p.kind == ckSymbol and p.symName == "&":
addToScope(p.symName) hasRest = true
paramNames.add(mangleName(p.symName) & ": CljVal") restIdx = pi
let body = items[3..^1] break
if hasRest:
# Collect named params before &
for pi in 0..<restIdx:
let p = params.items[pi]
if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName)
# The param after & is the rest param name
if restIdx + 1 < params.items.len:
let restParam = params.items[restIdx + 1]
if restParam.kind != ckSymbol:
raise newException(EmitterError, "defn rest param must be a symbol")
addToScope(restParam.symName)
else:
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[(paramsIdx+1)..^1]
let procName = mangleName(name.symName) let procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: "" let exportMarker = if emitLibMode: "*" else: ""
if hasRest:
# Rest params: generate proc with args: seq[CljVal]
# Register so call sites wrap args in @[...]
multiArityFns.incl(name.symName)
let namedCount = restIdx
var preambleLines: seq[string] = @[]
for pi in 0..<restIdx:
let p = params.items[pi]
preambleLines.add(indentStr(indent + 1) & "let " & mangleName(p.symName) & " = args[" & $pi & "]")
let restParamName = params.items[restIdx + 1].symName
preambleLines.add(indentStr(indent + 1) & "let " & mangleName(restParamName) & " = cljList(args[" & $namedCount & "..^1])")
var bodyCode = ""
if body.len == 0:
bodyCode = indentStr(indent + 1) & "cljNil()"
elif body.len == 1:
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
popScope()
let preamble = preambleLines.join("\n")
return sp & "proc " & procName & exportMarker & "(args: seq[CljVal]): CljVal =\n" & preamble & "\n" & bodyCode
if indent == 0: if indent == 0:
var bodyCode = "" var bodyCode = ""
if body.len == 0: if body.len == 0:
@@ -751,17 +852,62 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if name.kind != ckSymbol: if name.kind != ckSymbol:
raise newException(EmitterError, "defn- name must be a symbol") raise newException(EmitterError, "defn- name must be a symbol")
registerGlobal(name.symName) registerGlobal(name.symName)
let params = items[2] # Skip docstring if present: (defn- name "doc" [params] body...)
var paramsIdx = 2
if items.len > 3 and items[2].kind == ckString:
paramsIdx = 3
let params = items[paramsIdx]
if params.kind != ckVector: if params.kind != ckVector:
raise newException(EmitterError, "defn- params must be a vector") raise newException(EmitterError, "defn- params must be a vector")
var paramNames: seq[string] = @[] var paramNames: seq[string] = @[]
var hasRest = false
var restIdx = -1
pushScope() pushScope()
for p in params.items: # First pass: check for & rest params
if p.kind != ckSymbol: for pi, p in params.items:
raise newException(EmitterError, "defn- params must be symbols") if p.kind == ckSymbol and p.symName == "&":
addToScope(p.symName) hasRest = true
paramNames.add(mangleName(p.symName) & ": CljVal") restIdx = pi
let body = items[3..^1] break
if hasRest:
for pi in 0..<restIdx:
let p = params.items[pi]
if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols")
addToScope(p.symName)
if restIdx + 1 < params.items.len:
let restParam = params.items[restIdx + 1]
if restParam.kind != ckSymbol:
raise newException(EmitterError, "defn- rest param must be a symbol")
addToScope(restParam.symName)
else:
for p in params.items:
if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols")
addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[(paramsIdx+1)..^1]
let procName = mangleName(name.symName)
if hasRest:
# Rest params: generate proc with args: seq[CljVal]
multiArityFns.incl(name.symName)
let namedCount = restIdx
var preambleLines: seq[string] = @[]
for pi in 0..<restIdx:
let p = params.items[pi]
preambleLines.add(indentStr(indent + 1) & "let " & mangleName(p.symName) & " = args[" & $pi & "]")
let restParamName = params.items[restIdx + 1].symName
preambleLines.add(indentStr(indent + 1) & "let " & mangleName(restParamName) & " = cljList(args[" & $namedCount & "..^1])")
var bodyCode = ""
if body.len == 0:
bodyCode = indentStr(indent + 1) & "cljNil()"
elif body.len == 1:
bodyCode = emitExpr(body[0], indent + 1)
else:
bodyCode = emitBlock(body, indent + 1)
popScope()
let preamble = preambleLines.join("\n")
return sp & "proc " & procName & "(args: seq[CljVal]): CljVal {.used.} =\n" & preamble & "\n" & bodyCode
var bodyCode = "" var bodyCode = ""
if body.len == 0: if body.len == 0:
bodyCode = indentStr(indent + 1) & "cljNil()" bodyCode = indentStr(indent + 1) & "cljNil()"
@@ -770,7 +916,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
else: else:
bodyCode = emitBlock(body, indent + 1) bodyCode = emitBlock(body, indent + 1)
popScope() popScope()
let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode
of "fn": of "fn":
@@ -909,12 +1054,53 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
of "if": of "if":
if items.len < 3 or items.len > 4: if items.len < 3 or items.len > 4:
raise newException(EmitterError, "if requires condition, then, and optional else") raise newException(EmitterError, "if requires condition, then, and optional else")
# Recursively check if an if form has control flow in any branch
proc ifHasControlFlow(form: CljVal): bool =
if form.kind != ckList or form.items.len < 3: return false
if form.items[0].kind != ckSymbol or form.items[0].symName != "if": return false
let thenCode = emitExpr(form.items[2], 0)
if thenCode.strip().contains("continue") or thenCode.strip().contains("break"):
return true
if form.items.len == 4:
let elseCode = emitExpr(form.items[3], 0)
if elseCode.strip().contains("continue") or elseCode.strip().contains("break"):
return true
# Recurse into nested if in else branch
if form.items[3].kind == ckList and form.items[3].items.len > 0 and
form.items[3].items[0].kind == ckSymbol and form.items[3].items[0].symName == "if":
return ifHasControlFlow(form.items[3])
return false
let condCode = emitExpr(items[1], 0) let condCode = emitExpr(items[1], 0)
let thenCode = emitExpr(items[2], indent + 1) let thenCode = emitExpr(items[2], indent + 1)
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode let thenStripped = thenCode.strip()
let thenIsControl = thenStripped.contains("continue") or thenStripped.contains("break")
let hasCF = ifHasControlFlow(items[0])
if items.len == 4: if items.len == 4:
let elseCode = emitExpr(items[3], indent + 1) let elseCode = emitExpr(items[3], indent + 1)
let elseStripped = elseCode.strip()
let elseIsControl = elseStripped.contains("continue") or elseStripped.contains("break")
if thenIsControl or elseIsControl:
# Direct control flow in branches — use statement form
var lines: seq[string] = @[]
lines.add(sp & "if cljIsTruthy(" & condCode.strip() & "):")
if thenIsControl:
lines.add(thenCode)
elif loopResultVar.len > 0:
lines.add(indentStr(indent + 1) & loopResultVar & " = " & thenStripped)
else:
lines.add(indentStr(indent + 1) & "discard " & thenStripped)
lines.add(sp & "else:")
if elseIsControl:
lines.add(elseCode)
elif loopResultVar.len > 0:
lines.add(indentStr(indent + 1) & loopResultVar & " = " & elseStripped)
else:
lines.add(indentStr(indent + 1) & "discard " & elseStripped)
return lines.join("\n")
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
ifBlock.add("\n" & sp & "else:\n" & elseCode) ifBlock.add("\n" & sp & "else:\n" & elseCode)
return ifBlock
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
return ifBlock return ifBlock
of "when": of "when":
@@ -999,21 +1185,43 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(sp & "var " & lpName & ": CljVal = " & lpVal) lines.add(sp & "var " & lpName & ": CljVal = " & lpVal)
loopVars.add(lpName) loopVars.add(lpName)
loopStack.add(loopVars) loopStack.add(loopVars)
lines.add(sp & "while true:")
let body = items[2..^1] let body = items[2..^1]
# Check if the last form is an 'if' with control flow in branches (recursively)
proc ifHasCF(form: CljVal): bool =
if form.kind != ckList or form.items.len < 3: return false
if form.items[0].kind != ckSymbol or form.items[0].symName != "if": return false
let thenCode = emitExpr(form.items[2], 0)
if thenCode.strip().contains("continue") or thenCode.strip().contains("break"):
return true
if form.items.len == 4:
let elseCode = emitExpr(form.items[3], 0)
if elseCode.strip().contains("continue") or elseCode.strip().contains("break"):
return true
if form.items[3].kind == ckList and form.items[3].items.len > 0 and
form.items[3].items[0].kind == ckSymbol and form.items[3].items[0].symName == "if":
return ifHasCF(form.items[3])
return false
var needsResultVar = false
if body.len > 0:
needsResultVar = ifHasCF(body[^1])
if needsResultVar:
lines.add(sp & "var loopResult: CljVal = cljNil()")
loopResultVar = "loopResult"
lines.add(sp & "while true:")
for bi, b in body: for bi, b in body:
let bcode = emitExpr(b, indent + 1) let bcode = emitExpr(b, indent + 1)
let stripped = bcode.strip() let stripped = bcode.strip()
let isLast = (bi == body.len - 1) let isLast = (bi == body.len - 1)
let isStatement = stripped.startsWith("echo ") or stripped.startsWith("discard ") or if isLast and needsResultVar:
stripped.startsWith("var ") or stripped.startsWith("let ") or # Last form with control flow: the if handler uses loopResultVar
stripped.startsWith("proc ") or lines.add(bcode)
stripped.startsWith("if ") or stripped.startsWith("try:") or elif isLast or stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("while ") or stripped.contains("continue") stripped.startsWith("var ") or stripped.startsWith("let ") or
if isLast or isStatement: stripped.startsWith("proc ") or
stripped.startsWith("if ") or stripped.startsWith("try:") or
stripped.startsWith("while ") or stripped.contains("continue"):
lines.add(bcode) lines.add(bcode)
elif stripped.startsWith("block:"): elif stripped.startsWith("block:"):
# Multi-line block as non-last form: discard the last line
var blockLines = bcode.split("\n") var blockLines = bcode.split("\n")
var lastIdx = blockLines.len - 1 var lastIdx = blockLines.len - 1
while lastIdx >= 0 and blockLines[lastIdx].strip() == "": while lastIdx >= 0 and blockLines[lastIdx].strip() == "":
@@ -1029,6 +1237,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(indentStr(indent + 1) & "break") lines.add(indentStr(indent + 1) & "break")
discard loopStack.pop() discard loopStack.pop()
popScope() popScope()
if needsResultVar:
lines.add(sp & "loopResult")
loopResultVar = ""
return lines.join("\n") return lines.join("\n")
of "recur": of "recur":
@@ -1737,7 +1948,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let parts = op.replace(".", "/").split("/") let parts = op.replace(".", "/").split("/")
if parts.len >= 3: if parts.len >= 3:
let module = parts[1] let module = parts[1]
var funcChain = parts[2..^1].join(".") # Mangle each part of the function chain for Nim identifier validity
var mangledParts: seq[string] = @[]
for p in parts[2..^1]:
mangledParts.add(mangleName(p))
var funcChain = mangledParts.join(".")
# Strip ? suffix (Clojure convention) — not valid in Nim # Strip ? suffix (Clojure convention) — not valid in Nim
if funcChain.endsWith("?"): if funcChain.endsWith("?"):
funcChain = funcChain[0..^2] funcChain = funcChain[0..^2]
@@ -1867,6 +2082,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if callOp.endsWith("."): if callOp.endsWith("."):
callOp = callOp[0..^2] callOp = callOp[0..^2]
let mangled = mangleName(callOp) let mangled = mangleName(callOp)
# Multi-arity function: wrap args in seq
if callOp in multiArityFns:
return sp & mangled & "(@[" & args.join(", ") & "])"
if isLocalVar(callOp): if isLocalVar(callOp):
# Local value (may be fn, map, set, vector, keyword) — use runtime dispatch # Local value (may be fn, map, set, vector, keyword) — use runtime dispatch
return sp & "cljCall(" & mangled & ", @[" & args.join(", ") & "])" return sp & "cljCall(" & mangled & ", @[" & args.join(", ") & "])"
@@ -2141,9 +2359,14 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
if mainForms.len > 0: if mainForms.len > 0:
lines.add("") lines.add("")
lines.add("when isMainModule:") if emitLibMode:
for form in mainForms: # Lib mode: emit main forms at top level (no when isMainModule guard)
lines.add(form) for form in mainForms:
lines.add(form)
else:
lines.add("when isMainModule:")
for form in mainForms:
lines.add(form)
return lines.join("\n") & "\n" return lines.join("\n") & "\n"