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:
+7
-2
@@ -3,13 +3,18 @@ import os, osproc, strutils, times
|
||||
import reader, emitter, types, repl, macros, deps, ai_assist, tui
|
||||
|
||||
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 candidate1 = appDir / "lib"
|
||||
let candidate2 = appDir.parentDir / "lib"
|
||||
let candidate3 = getCurrentDir() / "lib"
|
||||
if dirExists(candidate1): return candidate1
|
||||
if dirExists(candidate2): return candidate2
|
||||
if dirExists(candidate3): return candidate3
|
||||
return "lib"
|
||||
|
||||
proc resolveNsToPath(nsName: string, searchPaths: seq[string]): string =
|
||||
|
||||
@@ -17,6 +17,7 @@ type
|
||||
|
||||
DepsFile* = object
|
||||
deps*: seq[Dependency]
|
||||
paths*: seq[string]
|
||||
|
||||
DepsError* = object of CatchableError
|
||||
|
||||
@@ -103,6 +104,11 @@ proc parseDepsFile*(filePath: string): DepsFile =
|
||||
for (pk, pv) in pairs:
|
||||
if pk == "deps":
|
||||
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 =
|
||||
## 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] =
|
||||
## Download/resolve all dependencies and return search paths
|
||||
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:
|
||||
case dep.kind
|
||||
of dkLocal:
|
||||
|
||||
+252
-29
@@ -1,5 +1,5 @@
|
||||
# Bara Lang → Nim Emitter
|
||||
import strutils, sets, algorithm
|
||||
import strutils, sets, algorithm, sequtils
|
||||
import types
|
||||
import macros
|
||||
|
||||
@@ -9,6 +9,8 @@ var loopStack*: seq[seq[string]] = @[]
|
||||
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
|
||||
var scopeStack*: seq[HashSet[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) =
|
||||
definedGlobals.incl(name)
|
||||
@@ -454,6 +456,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
let sp = indentStr(indent)
|
||||
let head = items[0]
|
||||
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...)
|
||||
var argParts: seq[string] = @[]
|
||||
for i in 1..<items.len:
|
||||
@@ -706,19 +714,112 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
if name.kind != ckSymbol:
|
||||
raise newException(EmitterError, "defn name must be a symbol")
|
||||
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:
|
||||
raise newException(EmitterError, "defn params must be a vector")
|
||||
var paramNames: seq[string] = @[]
|
||||
var hasRest = false
|
||||
var restIdx = -1
|
||||
pushScope()
|
||||
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[3..^1]
|
||||
# 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:
|
||||
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 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:
|
||||
var bodyCode = ""
|
||||
if body.len == 0:
|
||||
@@ -751,17 +852,62 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
if name.kind != ckSymbol:
|
||||
raise newException(EmitterError, "defn- name must be a symbol")
|
||||
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:
|
||||
raise newException(EmitterError, "defn- params must be a vector")
|
||||
var paramNames: seq[string] = @[]
|
||||
var hasRest = false
|
||||
var restIdx = -1
|
||||
pushScope()
|
||||
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[3..^1]
|
||||
# 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:
|
||||
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 = ""
|
||||
if body.len == 0:
|
||||
bodyCode = indentStr(indent + 1) & "cljNil()"
|
||||
@@ -770,7 +916,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
else:
|
||||
bodyCode = emitBlock(body, indent + 1)
|
||||
popScope()
|
||||
let procName = mangleName(name.symName)
|
||||
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode
|
||||
|
||||
of "fn":
|
||||
@@ -909,12 +1054,53 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
of "if":
|
||||
if items.len < 3 or items.len > 4:
|
||||
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 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:
|
||||
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)
|
||||
return ifBlock
|
||||
var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
|
||||
return ifBlock
|
||||
|
||||
of "when":
|
||||
@@ -999,21 +1185,43 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
lines.add(sp & "var " & lpName & ": CljVal = " & lpVal)
|
||||
loopVars.add(lpName)
|
||||
loopStack.add(loopVars)
|
||||
lines.add(sp & "while true:")
|
||||
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:
|
||||
let bcode = emitExpr(b, indent + 1)
|
||||
let stripped = bcode.strip()
|
||||
let isLast = (bi == body.len - 1)
|
||||
let isStatement = stripped.startsWith("echo ") or stripped.startsWith("discard ") or
|
||||
stripped.startsWith("var ") or stripped.startsWith("let ") or
|
||||
stripped.startsWith("proc ") or
|
||||
stripped.startsWith("if ") or stripped.startsWith("try:") or
|
||||
stripped.startsWith("while ") or stripped.contains("continue")
|
||||
if isLast or isStatement:
|
||||
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("proc ") or
|
||||
stripped.startsWith("if ") or stripped.startsWith("try:") or
|
||||
stripped.startsWith("while ") or stripped.contains("continue"):
|
||||
lines.add(bcode)
|
||||
elif stripped.startsWith("block:"):
|
||||
# Multi-line block as non-last form: discard the last line
|
||||
var blockLines = bcode.split("\n")
|
||||
var lastIdx = blockLines.len - 1
|
||||
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")
|
||||
discard loopStack.pop()
|
||||
popScope()
|
||||
if needsResultVar:
|
||||
lines.add(sp & "loopResult")
|
||||
loopResultVar = ""
|
||||
return lines.join("\n")
|
||||
|
||||
of "recur":
|
||||
@@ -1737,7 +1948,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
let parts = op.replace(".", "/").split("/")
|
||||
if parts.len >= 3:
|
||||
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
|
||||
if funcChain.endsWith("?"):
|
||||
funcChain = funcChain[0..^2]
|
||||
@@ -1867,6 +2082,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
||||
if callOp.endsWith("."):
|
||||
callOp = callOp[0..^2]
|
||||
let mangled = mangleName(callOp)
|
||||
# Multi-arity function: wrap args in seq
|
||||
if callOp in multiArityFns:
|
||||
return sp & mangled & "(@[" & args.join(", ") & "])"
|
||||
if isLocalVar(callOp):
|
||||
# Local value (may be fn, map, set, vector, keyword) — use runtime dispatch
|
||||
return sp & "cljCall(" & mangled & ", @[" & args.join(", ") & "])"
|
||||
@@ -2141,9 +2359,14 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
|
||||
|
||||
if mainForms.len > 0:
|
||||
lines.add("")
|
||||
lines.add("when isMainModule:")
|
||||
for form in mainForms:
|
||||
lines.add(form)
|
||||
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:")
|
||||
for form in mainForms:
|
||||
lines.add(form)
|
||||
|
||||
return lines.join("\n") & "\n"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user