diff --git a/src/cljnim.nim b/src/cljnim.nim index d661e54..efbf7c4 100644 --- a/src/cljnim.nim +++ b/src/cljnim.nim @@ -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 = diff --git a/src/deps.nim b/src/deps.nim index 6f3e55f..1eec108 100644 --- a/src/deps.nim +++ b/src/deps.nim @@ -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: diff --git a/src/emitter.nim b/src/emitter.nim index 59497ad..67a625e 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -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.. 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..= 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.. 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.. 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.. 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"