feat: project build system with lib/bin separation

- Add src/project.nim: parse bara.edn manifest (name, version, lib, bins)
- Add cljnim build / cljnim build --mono commands
- Compile lib modules once to target/nim/lib_*.nim with exported defs
- Cross-module symbol resolution via from/import nil + fully qualified names
- Emitter: lib mode with * export markers, variadic seq[CljVal] handling
This commit is contained in:
2026-05-16 19:20:23 +03:00
parent ab29c74299
commit 7cb8aa1ee0
3 changed files with 472 additions and 13 deletions
+56 -9
View File
@@ -5,6 +5,7 @@ import macros
var requiredImports* = initHashSet[string]()
var emitLibMode* = false
var emitEntryProcName* = ""
var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
var scopeStack*: seq[HashSet[string]] = @[]
@@ -82,6 +83,21 @@ proc clearGlobals*() =
proc setNsAliases*(aliases: seq[(string, string)]) =
nsAliases = aliases
var libNsPrefixes*: seq[string] = @[]
proc setLibNsPrefixes*(prefixes: seq[string]) =
libNsPrefixes = prefixes
proc sanitizeNimIdent*(name: string): string
proc nsToNimModuleName(ns: string): string =
result = "lib_"
for c in ns:
case c
of '.', '-': result.add('_')
of 'a'..'z', 'A'..'Z', '0'..'9', '_': result.add(c)
else: result.add('_')
proc resolveNsAlias*(name: string): string =
# Resolve mu/square -> square (strip namespace prefix if alias exists)
let slashIdx = name.find('/')
@@ -90,6 +106,9 @@ proc resolveNsAlias*(name: string): string =
let suffix = name[slashIdx+1..^1]
for (alias, ns) in nsAliases:
if prefix == alias:
if prefix in libNsPrefixes:
# Return fully qualified name for lib module symbols
return nsToNimModuleName(ns) & ".clj_" & sanitizeNimIdent(suffix)
return suffix
return name
@@ -130,6 +149,10 @@ proc sanitizeNimIdent*(name: string): string =
result = "x" & result
if result.len == 0:
result = "val"
# Avoid conflicts with runtime functions: all-uppercase short names (GET, POST, etc)
# get a suffix so they don't collide with cljGet, cljSet, etc.
if result.len > 0 and result.allCharsInSet({'A'..'Z', '_'}) and result.len <= 7:
result.add("X")
# Nim keywords that would clash
let nimKeywords = ["in", "out", "var", "let", "proc", "func", "type", "ref", "ptr",
"object", "method", "template", "macro", "iterator", "converter",
@@ -732,7 +755,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
let mangled = mangleName(nameItem.symName)
registerGlobal(nameItem.symName)
let valCode = emitExpr(valItem, 0, needsValue = true)
return sp & "let " & mangled & " = " & valCode
let exportMarker = if emitLibMode: "*" else: ""
return sp & "let " & mangled & exportMarker & " = " & valCode
return sp & "cljNil()"
var name = items[1]
# Strip metadata: (with-meta sym meta) -> sym
@@ -748,6 +772,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
# Special handling: (def name (fn ...)) should emit raw proc, not cljFn wrapper
let isFnDef = valForm.kind == ckList and valForm.items.len > 0 and
valForm.items[0].kind == ckSymbol and valForm.items[0].symName == "fn"
let exportMarker = if emitLibMode: "*" else: ""
if indent == 0:
if isFnDef:
# Emit fn as raw proc for def context
@@ -755,9 +780,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
emitLibMode = false
let valCode = emitFnAsProc(valForm.items, 0)
emitLibMode = oldEmitLibMode
return sp & "let " & mangled & " = " & valCode
return sp & "let " & mangled & exportMarker & " = " & valCode
let valCode = emitExpr(items[2], 0, needsValue = true)
return sp & "let " & mangled & " = " & valCode
return sp & "let " & mangled & exportMarker & " = " & valCode
else:
# For nested def: use global var so it's accessible from closures
if isFnDef:
@@ -765,9 +790,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
emitLibMode = false
let valCode = emitFnAsProc(valForm.items, indent)
emitLibMode = oldEmitLibMode
return sp & "var " & mangled & " = " & valCode.strip() & "\n" & sp & mangled
return sp & "var " & mangled & exportMarker & " = " & valCode.strip() & "\n" & sp & mangled
let valCode = emitExpr(items[2], indent, needsValue = true)
return sp & "var " & mangled & " = " & valCode.strip() & "\n" & sp & mangled
return sp & "var " & mangled & exportMarker & " = " & valCode.strip() & "\n" & sp & mangled
of "defn":
if items.len < 3:
@@ -896,7 +921,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
else:
bodyCode = emitBlock(body, indent + 1)
popScope()
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
var result = sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
if emitLibMode and paramNames.len > 0 and not (name.symName in multiArityFns):
var wrapperArgs: seq[string] = @[]
for i in 0..<paramNames.len:
wrapperArgs.add("args[" & $i & "]")
result.add("\n" & sp & "proc " & procName & exportMarker & "(args: seq[CljVal]): CljVal =\n")
result.add(indentStr(indent + 1) & procName & "(" & wrapperArgs.join(", ") & ")\n")
return result
else:
var bodyCode = ""
if body.len == 1:
@@ -2181,6 +2213,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
var callOp = op
if callOp.endsWith("."):
callOp = callOp[0..^2]
let baseOp = if callOp.contains("/"): callOp.split("/")[1] else: callOp
let resolvedOp = resolveNsAlias(callOp)
if resolvedOp.contains("."):
return sp & resolvedOp & "(@[" & args.join(", ") & "])"
let mangled = mangleName(callOp)
# Multi-arity function: wrap args in seq
if callOp in multiArityFns:
@@ -2247,6 +2283,9 @@ proc emitExpr*(v: CljVal, indent: int = 0, needsValue: bool = false): string =
return sp & "cljKeyword(\"" & v.kwName & "\")"
of ckSymbol:
let symName = resolveNsAlias(v.symName)
# If symName contains a dot, it's a fully qualified lib module reference
if symName.contains("."):
return sp & symName
let rn = runtimeName(symName)
if rn.len > 0 and not isLocalVar(symName):
# Check if the runtime function is variadic (takes seq[CljVal])
@@ -2443,9 +2482,17 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
for form in mainForms:
lines.add(form)
else:
lines.add("when isMainModule:")
for form in mainForms:
lines.add(form)
if emitEntryProcName.len > 0:
lines.add("proc " & emitEntryProcName & "*(args: seq[CljVal] = @[]): CljVal =")
for form in mainForms:
lines.add(form)
lines.add("")
lines.add("when isMainModule:")
lines.add(" discard " & emitEntryProcName & "()")
else:
lines.add("when isMainModule:")
for form in mainForms:
lines.add(form)
return lines.join("\n") & "\n"