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:
+236 -13
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()
# First pass: check for & rest params
for pi, p in params.items:
if p.kind == ckSymbol and p.symName == "&":
hasRest = true
restIdx = pi
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: for p in params.items:
if p.kind != ckSymbol: if p.kind != ckSymbol:
raise newException(EmitterError, "defn params must be symbols") raise newException(EmitterError, "defn params must be symbols")
addToScope(p.symName) addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal") paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1] 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()
# First pass: check for & rest params
for pi, p in params.items:
if p.kind == ckSymbol and p.symName == "&":
hasRest = true
restIdx = pi
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: for p in params.items:
if p.kind != ckSymbol: if p.kind != ckSymbol:
raise newException(EmitterError, "defn- params must be symbols") raise newException(EmitterError, "defn- params must be symbols")
addToScope(p.symName) addToScope(p.symName)
paramNames.add(mangleName(p.symName) & ": CljVal") paramNames.add(mangleName(p.symName) & ": CljVal")
let body = items[3..^1] 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,13 +1054,54 @@ 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 return ifBlock
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
return ifBlock
of "when": of "when":
if items.len < 3: if items.len < 3:
@@ -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:
# Last form with control flow: the if handler uses loopResultVar
lines.add(bcode)
elif isLast or stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("var ") or stripped.startsWith("let ") or stripped.startsWith("var ") or stripped.startsWith("let ") or
stripped.startsWith("proc ") or stripped.startsWith("proc ") or
stripped.startsWith("if ") or stripped.startsWith("try:") or stripped.startsWith("if ") or stripped.startsWith("try:") or
stripped.startsWith("while ") or stripped.contains("continue") stripped.startsWith("while ") or stripped.contains("continue"):
if isLast or isStatement:
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,6 +2359,11 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
if mainForms.len > 0: if mainForms.len > 0:
lines.add("") lines.add("")
if emitLibMode:
# Lib mode: emit main forms at top level (no when isMainModule guard)
for form in mainForms:
lines.add(form)
else:
lines.add("when isMainModule:") lines.add("when isMainModule:")
for form in mainForms: for form in mainForms:
lines.add(form) lines.add(form)