From 7cb8aa1ee0494aaf475fb58295a91cc519355d15 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 16 May 2026 19:20:23 +0300 Subject: [PATCH] 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 --- src/cljnim.nim | 321 +++++++++++++++++++++++++++++++++++++++++++++++- src/emitter.nim | 65 ++++++++-- src/project.nim | 99 +++++++++++++++ 3 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 src/project.nim diff --git a/src/cljnim.nim b/src/cljnim.nim index a4e0cb1..0039bcd 100644 --- a/src/cljnim.nim +++ b/src/cljnim.nim @@ -1,6 +1,6 @@ # Bara Lang — AI-First Compiler -import os, osproc, strutils, times -import reader, emitter, types, repl, macros, deps, ai_assist, tui +import os, osproc, strutils, times, tables +import reader, emitter, types, repl, macros, deps, ai_assist, tui, project var libPathOverride* = "" @@ -65,7 +65,7 @@ proc extractRequires(forms: seq[CljVal]): seq[(string, string)] = walk(form) return res -proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[string] = @[], libMode: bool = false) = +proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[string] = @[], libMode: bool = false, entryProcName: string = "", libs: Table[string, string] = initTable[string, string]()) = let source = readFile(inputPath) let forms = reader.readAll(source) @@ -88,10 +88,17 @@ proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[ var allForms: seq[CljVal] = @[] var visited: seq[string] = @[] var nsAliases: seq[(string, string)] = @[] # (alias, namespace) + var libImports: seq[string] = @[] proc resolveFile(nsName: string) = if nsName in visited: return visited.add(nsName) + if libs.hasKey(nsName): + # Use pre-compiled lib module instead of inlining + let nimModule = libs[nsName] + if nimModule notin libImports: + libImports.add(nimModule) + return let path = resolveNsToPath(nsName, searchPaths) if path.len == 0: stderr.writeLine("Warning: Cannot find file for namespace: " & nsName) @@ -118,13 +125,37 @@ proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[ # Set namespace aliases in emitter emitter.setNsAliases(nsAliases) + # Set lib prefixes for qualified name resolution + var libPrefixes: seq[string] = @[] + for (alias, nsName) in nsAliases: + if libs.hasKey(nsName): + libPrefixes.add(alias) + emitter.setLibNsPrefixes(libPrefixes) + # Add current file's forms (skip ns declaration) for f in forms: if not (f.kind == ckList and f.items.len >= 1 and f.items[0].kind == ckSymbol and f.items[0].symName == "ns"): allForms.add(f) - let nimCode = if libMode: emitter.emitProgramLib(allForms) else: emitter.emitProgram(allForms) + let oldEntryProc = emitter.emitEntryProcName + if entryProcName.len > 0: + emitter.emitEntryProcName = entryProcName + var nimCode = if libMode: emitter.emitProgramLib(allForms) else: emitter.emitProgram(allForms) + emitter.emitEntryProcName = oldEntryProc + + # Insert lib imports into generated Nim code + if libImports.len > 0: + var lines = nimCode.splitLines() + var insertIdx = 0 + for i, line in lines: + if line.startsWith("import "): + insertIdx = i + 1 + for imp in libImports: + lines.insert("from " & imp & " import nil", insertIdx) + insertIdx.inc + nimCode = lines.join("\n") + writeFile(outputPath, nimCode) echo "Generated: ", outputPath @@ -155,6 +186,269 @@ proc makeTempDir(): string = result = getTempDir() / "cljnim_build_" & $pid & "_" & $ts createDir(result) +proc sanitizeNimIdent(name: string): string = + ## Sanitize a project/bin name to valid Nim module name + result = "" + for c in name: + case c + of '-': result.add('_') + of 'a'..'z', 'A'..'Z', '0'..'9', '_': result.add(c) + else: result.add('_') + if result.len == 0: + result = "bin" + +proc buildLibs(proj: Project, includeExamples: bool, nimDir: string): Table[string, string] = + ## Discover and compile all project-local libraries required by bin files. + ## Returns mapping: namespace -> nim module name + result = initTable[string, string]() + var entries = proj.bins + if includeExamples: + entries.add(proj.examples) + + var visited: seq[string] = @[] + var queue: seq[string] = @[] + let searchPaths = @[proj.rootDir, proj.rootDir / "lib"] + + # Collect all required namespaces from bin files + for entry in entries: + let inputPath = proj.rootDir / entry.path + if not fileExists(inputPath): continue + let forms = reader.readAll(readFile(inputPath)) + let requires = extractRequires(forms) + for (nsName, _) in requires: + if nsName notin queue: + queue.add(nsName) + + # Iteratively process queue, adding transitive dependencies + var i = 0 + while i < queue.len: + let nsName = queue[i] + i.inc + if nsName in visited: continue + visited.add(nsName) + + let path = resolveNsToPath(nsName, searchPaths) + if path.len == 0 or not path.startsWith(proj.rootDir): + continue # External dependency, skip + + let forms = reader.readAll(readFile(path)) + let requires = extractRequires(forms) + for (depNs, _) in requires: + if depNs notin queue: + queue.add(depNs) + + # Compile libs in dependency order (simple topological sort) + var remaining = visited + while remaining.len > 0: + var madeProgress = false + var nextRemaining: seq[string] = @[] + + for nsName in remaining: + let path = resolveNsToPath(nsName, searchPaths) + if path.len == 0 or not path.startsWith(proj.rootDir): + continue + + let forms = reader.readAll(readFile(path)) + let requires = extractRequires(forms) + + var allDepsReady = true + var depLibs = initTable[string, string]() + for (depNs, _) in requires: + if depNs == nsName: continue + if result.hasKey(depNs): + depLibs[depNs] = result[depNs] + else: + let depPath = resolveNsToPath(depNs, searchPaths) + if depPath.len > 0 and depPath.startsWith(proj.rootDir): + allDepsReady = false + break + + if allDepsReady: + madeProgress = true + let nimName = "lib_" & sanitizeNimIdent(nsName.replace(".", "_").replace("-", "_")) + let nimPath = nimDir / nimName & ".nim" + if not fileExists(nimPath): + echo "Building lib: ", nsName, " → ", nimPath + compileFileInternal(path, nimPath, @[], true, "", depLibs) + result[nsName] = nimName + else: + nextRemaining.add(nsName) + + if not madeProgress and nextRemaining.len > 0: + # Circular dependency or bug, force compile first remaining + let nsName = nextRemaining[0] + nextRemaining.delete(0) + let path = resolveNsToPath(nsName, searchPaths) + if path.len > 0 and path.startsWith(proj.rootDir): + let nimName = "lib_" & sanitizeNimIdent(nsName.replace(".", "_").replace("-", "_")) + let nimPath = nimDir / nimName & ".nim" + if not fileExists(nimPath): + echo "Building lib (forced): ", nsName, " → ", nimPath + compileFileInternal(path, nimPath, @[], true, "", initTable[string, string]()) + result[nsName] = nimName + + remaining = nextRemaining + +proc buildBin(proj: Project, bin: project.BinEntry, outputDir: string, nimDir: string, nimcacheDir: string, release: bool, libs: Table[string, string]) = + let inputPath = proj.rootDir / bin.path + let nimName = "bin_" & sanitizeNimIdent(bin.name) + let nimPath = nimDir / nimName & ".nim" + let binPath = outputDir / bin.name + + if not fileExists(inputPath): + stderr.writeLine("Warning: Bin file not found: " & inputPath) + return + + echo "Building bin: ", bin.name, " → ", binPath + + # Set entry proc name so mono can call it later + let oldEntryProc = emitter.emitEntryProcName + emitter.emitEntryProcName = "clj_main_" & sanitizeNimIdent(bin.name) + defer: emitter.emitEntryProcName = oldEntryProc + + compileFileInternal(inputPath, nimPath, @[], false, "", libs) + + let libPath = getLibPath() + let compilerLib = getAppDir() / "lib" + var cmd = "nim c" + if release: + cmd.add(" -d:release") + if dirExists(compilerLib) and compilerLib != libPath: + cmd.add(" --path:" & quoteShell(compilerLib)) + cmd.add(" --path:" & quoteShell(libPath)) + cmd.add(" --path:" & quoteShell(nimDir)) + cmd.add(" --nimcache:" & quoteShell(nimcacheDir)) + cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath)) + echo " ", cmd + let (output, exitCode) = execCmdEx(cmd) + if exitCode != 0: + stderr.writeLine("Compilation failed for ", bin.name) + stderr.writeLine(output) + quit(1) + echo " ✓ ", binPath + +proc buildProject(proj: Project, release: bool, includeExamples: bool) = + let targetDir = proj.rootDir / "target" + let nimDir = targetDir / "nim" + let binDir = targetDir / "bin" + let nimcacheDir = targetDir / "nimcache" + createDir(nimDir) + createDir(binDir) + createDir(nimcacheDir) + + echo "Building project: ", proj.name, " v", proj.version + echo " Root: ", proj.rootDir + echo " Bins: ", proj.bins.len + + let libs = buildLibs(proj, includeExamples, nimDir) + if libs.len > 0: + echo " Libs: ", libs.len + + for bin in proj.bins: + buildBin(proj, bin, binDir, nimDir, nimcacheDir, release, libs) + + if includeExamples: + echo " Examples: ", proj.examples.len + for ex in proj.examples: + buildBin(proj, ex, binDir, nimDir, nimcacheDir, release, libs) + +proc buildMono(proj: Project, release: bool, includeExamples: bool) = + let targetDir = proj.rootDir / "target" + let nimDir = targetDir / "nim" + let binDir = targetDir / "bin" + let nimcacheDir = targetDir / "nimcache" + createDir(nimDir) + createDir(binDir) + createDir(nimcacheDir) + + var entries: seq[project.BinEntry] = proj.bins + if includeExamples: + entries.add(proj.examples) + + echo "Building mono binary: ", proj.name + echo " Entries: ", entries.len + + let libs = buildLibs(proj, includeExamples, nimDir) + if libs.len > 0: + echo " Libs: ", libs.len + + # First compile each entry to a Nim module with exported entry proc + var imports: seq[string] = @[] + var cases: seq[string] = @[] + + for entry in entries: + let inputPath = proj.rootDir / entry.path + let nimName = "bin_" & sanitizeNimIdent(entry.name) + let nimPath = nimDir / nimName & ".nim" + + if not fileExists(inputPath): + stderr.writeLine("Warning: Entry file not found: " & inputPath) + continue + + echo " Generating module: ", nimName + + let oldEntryProc = emitter.emitEntryProcName + emitter.emitEntryProcName = "clj_main_" & sanitizeNimIdent(entry.name) + defer: emitter.emitEntryProcName = oldEntryProc + + compileFileInternal(inputPath, nimPath, @[], false, "", libs) + imports.add(nimName) + cases.add(" of \"" & entry.name & "\": discard " & "clj_main_" & sanitizeNimIdent(entry.name) & "()") + + if imports.len == 0: + stderr.writeLine("Error: No valid entries to build") + quit(1) + + # Generate mono wrapper + let monoName = sanitizeNimIdent(proj.name) + let monoNimPath = nimDir / "mono.nim" + var monoCode = "# Generated by Bara Lang — Mono Binary Wrapper\n" + monoCode.add("import os\n") + monoCode.add("import cljnim_runtime\n") + for imp in imports: + monoCode.add("import " & imp & "\n") + monoCode.add("\nproc main() =\n") + monoCode.add(" if paramCount() < 1:\n") + monoCode.add(" echo \"Usage: " & proj.name & " \"\n") + monoCode.add(" echo \"Commands:\"\n") + for entry in entries: + if fileExists(proj.rootDir / entry.path): + monoCode.add(" echo \" " & entry.name & "\"\n") + monoCode.add(" quit(1)\n") + monoCode.add(" let cmd = paramStr(1)\n") + monoCode.add(" case cmd\n") + for c in cases: + monoCode.add(c & "\n") + monoCode.add(" else:\n") + monoCode.add(" echo \"Unknown command: \" & cmd\n") + monoCode.add(" quit(1)\n") + monoCode.add("\nwhen isMainModule:\n") + monoCode.add(" main()\n") + + writeFile(monoNimPath, monoCode) + echo " Generated: ", monoNimPath + + # Compile mono wrapper + let monoBinPath = binDir / monoName + let libPath = getLibPath() + let compilerLib = getAppDir() / "lib" + var cmd = "nim c" + if release: + cmd.add(" -d:release") + if dirExists(compilerLib) and compilerLib != libPath: + cmd.add(" --path:" & quoteShell(compilerLib)) + cmd.add(" --path:" & quoteShell(libPath)) + cmd.add(" --path:" & quoteShell(nimDir)) + cmd.add(" --nimcache:" & quoteShell(nimcacheDir)) + cmd.add(" -o:" & quoteShell(monoBinPath) & " " & quoteShell(monoNimPath)) + echo " Compiling mono: ", cmd + let (output, exitCode) = execCmdEx(cmd) + if exitCode != 0: + stderr.writeLine("Compilation failed for mono binary") + stderr.writeLine(output) + quit(1) + echo " ✓ ", monoBinPath + proc runFile*(inputPath: string) = let baseName = inputPath.splitFile.name let buildDir = makeTempDir() @@ -238,6 +532,7 @@ proc main() = echo " cljnim [--lib-path ] compile [output.nim]" echo " cljnim [--lib-path ] run " echo " cljnim [--lib-path ] read " + echo " cljnim build [--mono] [--release] [--examples]" echo " cljnim repl [--json]" echo " cljnim deps" echo " cljnim -e ''" @@ -272,6 +567,7 @@ proc main() = let nimPath = buildDir / "expr.nim" let binPath = buildDir / "expr" let forms = reader.readAll(code) + emitter.emitEntryProcName = "" let nimCode = emitter.emitProgram(forms) writeFile(nimPath, nimCode) let (compileResult, compileOut) = nimCompile(nimPath, binPath) @@ -296,6 +592,23 @@ proc main() = let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim") compileFile(inputPath, outputPath) + of "build": + var monoMode = false + var releaseMode = false + var includeExamples = false + for i in 1.. 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.. 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" diff --git a/src/project.nim b/src/project.nim new file mode 100644 index 0000000..eee5a4c --- /dev/null +++ b/src/project.nim @@ -0,0 +1,99 @@ +# Bara Lang Project Manifest Reader +# Reads bara.edn (Clojure/EDN format) for project configuration +import os +import types, reader + +type + BinEntry* = object + name*: string + path*: string + + LibEntry* = object + name*: string + path*: string + + Project* = object + name*: string + version*: string + rootDir*: string + lib*: LibEntry + bins*: seq[BinEntry] + examples*: seq[BinEntry] + +proc findProjectRoot*(startDir: string = getCurrentDir()): string = + ## Walk up from startDir looking for bara.edn + var dir = absolutePath(startDir) + while true: + let baraEdn = dir / "bara.edn" + if fileExists(baraEdn): + return dir + let parent = parentDir(dir) + if parent == dir: + break + dir = parent + return "" + +proc extractMapString(form: CljVal, key: string, defaultVal: string = ""): string = + if form.kind != ckMap: return defaultVal + for i in 0.. 0: + result.lib = parseLibEntry(libVal[0]) + elif true: + # Also check for single :lib map (not vector) + for i in 0..