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:
+317
-4
@@ -1,6 +1,6 @@
|
|||||||
# Bara Lang — AI-First Compiler
|
# Bara Lang — AI-First Compiler
|
||||||
import os, osproc, strutils, times
|
import os, osproc, strutils, times, tables
|
||||||
import reader, emitter, types, repl, macros, deps, ai_assist, tui
|
import reader, emitter, types, repl, macros, deps, ai_assist, tui, project
|
||||||
|
|
||||||
var libPathOverride* = ""
|
var libPathOverride* = ""
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
|
|||||||
walk(form)
|
walk(form)
|
||||||
return res
|
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 source = readFile(inputPath)
|
||||||
let forms = reader.readAll(source)
|
let forms = reader.readAll(source)
|
||||||
|
|
||||||
@@ -88,10 +88,17 @@ proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[
|
|||||||
var allForms: seq[CljVal] = @[]
|
var allForms: seq[CljVal] = @[]
|
||||||
var visited: seq[string] = @[]
|
var visited: seq[string] = @[]
|
||||||
var nsAliases: seq[(string, string)] = @[] # (alias, namespace)
|
var nsAliases: seq[(string, string)] = @[] # (alias, namespace)
|
||||||
|
var libImports: seq[string] = @[]
|
||||||
|
|
||||||
proc resolveFile(nsName: string) =
|
proc resolveFile(nsName: string) =
|
||||||
if nsName in visited: return
|
if nsName in visited: return
|
||||||
visited.add(nsName)
|
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)
|
let path = resolveNsToPath(nsName, searchPaths)
|
||||||
if path.len == 0:
|
if path.len == 0:
|
||||||
stderr.writeLine("Warning: Cannot find file for namespace: " & nsName)
|
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
|
# Set namespace aliases in emitter
|
||||||
emitter.setNsAliases(nsAliases)
|
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)
|
# Add current file's forms (skip ns declaration)
|
||||||
for f in forms:
|
for f in forms:
|
||||||
if not (f.kind == ckList and f.items.len >= 1 and
|
if not (f.kind == ckList and f.items.len >= 1 and
|
||||||
f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
|
f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
|
||||||
allForms.add(f)
|
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)
|
writeFile(outputPath, nimCode)
|
||||||
echo "Generated: ", outputPath
|
echo "Generated: ", outputPath
|
||||||
|
|
||||||
@@ -155,6 +186,269 @@ proc makeTempDir(): string =
|
|||||||
result = getTempDir() / "cljnim_build_" & $pid & "_" & $ts
|
result = getTempDir() / "cljnim_build_" & $pid & "_" & $ts
|
||||||
createDir(result)
|
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 & " <command>\"\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) =
|
proc runFile*(inputPath: string) =
|
||||||
let baseName = inputPath.splitFile.name
|
let baseName = inputPath.splitFile.name
|
||||||
let buildDir = makeTempDir()
|
let buildDir = makeTempDir()
|
||||||
@@ -238,6 +532,7 @@ proc main() =
|
|||||||
echo " cljnim [--lib-path <dir>] compile <file.clj> [output.nim]"
|
echo " cljnim [--lib-path <dir>] compile <file.clj> [output.nim]"
|
||||||
echo " cljnim [--lib-path <dir>] run <file.clj>"
|
echo " cljnim [--lib-path <dir>] run <file.clj>"
|
||||||
echo " cljnim [--lib-path <dir>] read <file.clj>"
|
echo " cljnim [--lib-path <dir>] read <file.clj>"
|
||||||
|
echo " cljnim build [--mono] [--release] [--examples]"
|
||||||
echo " cljnim repl [--json]"
|
echo " cljnim repl [--json]"
|
||||||
echo " cljnim deps"
|
echo " cljnim deps"
|
||||||
echo " cljnim -e '<code>'"
|
echo " cljnim -e '<code>'"
|
||||||
@@ -272,6 +567,7 @@ proc main() =
|
|||||||
let nimPath = buildDir / "expr.nim"
|
let nimPath = buildDir / "expr.nim"
|
||||||
let binPath = buildDir / "expr"
|
let binPath = buildDir / "expr"
|
||||||
let forms = reader.readAll(code)
|
let forms = reader.readAll(code)
|
||||||
|
emitter.emitEntryProcName = ""
|
||||||
let nimCode = emitter.emitProgram(forms)
|
let nimCode = emitter.emitProgram(forms)
|
||||||
writeFile(nimPath, nimCode)
|
writeFile(nimPath, nimCode)
|
||||||
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
|
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
|
||||||
@@ -296,6 +592,23 @@ proc main() =
|
|||||||
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
|
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
|
||||||
compileFile(inputPath, outputPath)
|
compileFile(inputPath, outputPath)
|
||||||
|
|
||||||
|
of "build":
|
||||||
|
var monoMode = false
|
||||||
|
var releaseMode = false
|
||||||
|
var includeExamples = false
|
||||||
|
for i in 1..<args.len:
|
||||||
|
if args[i] == "--mono":
|
||||||
|
monoMode = true
|
||||||
|
elif args[i] == "--release":
|
||||||
|
releaseMode = true
|
||||||
|
elif args[i] == "--examples":
|
||||||
|
includeExamples = true
|
||||||
|
let proj = project.loadProject()
|
||||||
|
if monoMode:
|
||||||
|
buildMono(proj, releaseMode, includeExamples)
|
||||||
|
else:
|
||||||
|
buildProject(proj, releaseMode, includeExamples)
|
||||||
|
|
||||||
of "compile-lib":
|
of "compile-lib":
|
||||||
if args.len < 2:
|
if args.len < 2:
|
||||||
stderr.writeLine("Error: Missing input file")
|
stderr.writeLine("Error: Missing input file")
|
||||||
|
|||||||
+53
-6
@@ -5,6 +5,7 @@ import macros
|
|||||||
|
|
||||||
var requiredImports* = initHashSet[string]()
|
var requiredImports* = initHashSet[string]()
|
||||||
var emitLibMode* = false
|
var emitLibMode* = false
|
||||||
|
var emitEntryProcName* = ""
|
||||||
var loopStack*: seq[seq[string]] = @[]
|
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]] = @[]
|
||||||
@@ -82,6 +83,21 @@ proc clearGlobals*() =
|
|||||||
proc setNsAliases*(aliases: seq[(string, string)]) =
|
proc setNsAliases*(aliases: seq[(string, string)]) =
|
||||||
nsAliases = aliases
|
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 =
|
proc resolveNsAlias*(name: string): string =
|
||||||
# Resolve mu/square -> square (strip namespace prefix if alias exists)
|
# Resolve mu/square -> square (strip namespace prefix if alias exists)
|
||||||
let slashIdx = name.find('/')
|
let slashIdx = name.find('/')
|
||||||
@@ -90,6 +106,9 @@ proc resolveNsAlias*(name: string): string =
|
|||||||
let suffix = name[slashIdx+1..^1]
|
let suffix = name[slashIdx+1..^1]
|
||||||
for (alias, ns) in nsAliases:
|
for (alias, ns) in nsAliases:
|
||||||
if prefix == alias:
|
if prefix == alias:
|
||||||
|
if prefix in libNsPrefixes:
|
||||||
|
# Return fully qualified name for lib module symbols
|
||||||
|
return nsToNimModuleName(ns) & ".clj_" & sanitizeNimIdent(suffix)
|
||||||
return suffix
|
return suffix
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@@ -130,6 +149,10 @@ proc sanitizeNimIdent*(name: string): string =
|
|||||||
result = "x" & result
|
result = "x" & result
|
||||||
if result.len == 0:
|
if result.len == 0:
|
||||||
result = "val"
|
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
|
# Nim keywords that would clash
|
||||||
let nimKeywords = ["in", "out", "var", "let", "proc", "func", "type", "ref", "ptr",
|
let nimKeywords = ["in", "out", "var", "let", "proc", "func", "type", "ref", "ptr",
|
||||||
"object", "method", "template", "macro", "iterator", "converter",
|
"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)
|
let mangled = mangleName(nameItem.symName)
|
||||||
registerGlobal(nameItem.symName)
|
registerGlobal(nameItem.symName)
|
||||||
let valCode = emitExpr(valItem, 0, needsValue = true)
|
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()"
|
return sp & "cljNil()"
|
||||||
var name = items[1]
|
var name = items[1]
|
||||||
# Strip metadata: (with-meta sym meta) -> sym
|
# 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
|
# Special handling: (def name (fn ...)) should emit raw proc, not cljFn wrapper
|
||||||
let isFnDef = valForm.kind == ckList and valForm.items.len > 0 and
|
let isFnDef = valForm.kind == ckList and valForm.items.len > 0 and
|
||||||
valForm.items[0].kind == ckSymbol and valForm.items[0].symName == "fn"
|
valForm.items[0].kind == ckSymbol and valForm.items[0].symName == "fn"
|
||||||
|
let exportMarker = if emitLibMode: "*" else: ""
|
||||||
if indent == 0:
|
if indent == 0:
|
||||||
if isFnDef:
|
if isFnDef:
|
||||||
# Emit fn as raw proc for def context
|
# Emit fn as raw proc for def context
|
||||||
@@ -755,9 +780,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
|
|||||||
emitLibMode = false
|
emitLibMode = false
|
||||||
let valCode = emitFnAsProc(valForm.items, 0)
|
let valCode = emitFnAsProc(valForm.items, 0)
|
||||||
emitLibMode = oldEmitLibMode
|
emitLibMode = oldEmitLibMode
|
||||||
return sp & "let " & mangled & " = " & valCode
|
return sp & "let " & mangled & exportMarker & " = " & valCode
|
||||||
let valCode = emitExpr(items[2], 0, needsValue = true)
|
let valCode = emitExpr(items[2], 0, needsValue = true)
|
||||||
return sp & "let " & mangled & " = " & valCode
|
return sp & "let " & mangled & exportMarker & " = " & valCode
|
||||||
else:
|
else:
|
||||||
# For nested def: use global var so it's accessible from closures
|
# For nested def: use global var so it's accessible from closures
|
||||||
if isFnDef:
|
if isFnDef:
|
||||||
@@ -765,9 +790,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
|
|||||||
emitLibMode = false
|
emitLibMode = false
|
||||||
let valCode = emitFnAsProc(valForm.items, indent)
|
let valCode = emitFnAsProc(valForm.items, indent)
|
||||||
emitLibMode = oldEmitLibMode
|
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)
|
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":
|
of "defn":
|
||||||
if items.len < 3:
|
if items.len < 3:
|
||||||
@@ -896,7 +921,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
|
|||||||
else:
|
else:
|
||||||
bodyCode = emitBlock(body, indent + 1)
|
bodyCode = emitBlock(body, indent + 1)
|
||||||
popScope()
|
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:
|
else:
|
||||||
var bodyCode = ""
|
var bodyCode = ""
|
||||||
if body.len == 1:
|
if body.len == 1:
|
||||||
@@ -2181,6 +2213,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false):
|
|||||||
var callOp = op
|
var callOp = op
|
||||||
if callOp.endsWith("."):
|
if callOp.endsWith("."):
|
||||||
callOp = callOp[0..^2]
|
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)
|
let mangled = mangleName(callOp)
|
||||||
# Multi-arity function: wrap args in seq
|
# Multi-arity function: wrap args in seq
|
||||||
if callOp in multiArityFns:
|
if callOp in multiArityFns:
|
||||||
@@ -2247,6 +2283,9 @@ proc emitExpr*(v: CljVal, indent: int = 0, needsValue: bool = false): string =
|
|||||||
return sp & "cljKeyword(\"" & v.kwName & "\")"
|
return sp & "cljKeyword(\"" & v.kwName & "\")"
|
||||||
of ckSymbol:
|
of ckSymbol:
|
||||||
let symName = resolveNsAlias(v.symName)
|
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)
|
let rn = runtimeName(symName)
|
||||||
if rn.len > 0 and not isLocalVar(symName):
|
if rn.len > 0 and not isLocalVar(symName):
|
||||||
# Check if the runtime function is variadic (takes seq[CljVal])
|
# Check if the runtime function is variadic (takes seq[CljVal])
|
||||||
@@ -2442,6 +2481,14 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
|
|||||||
# Lib mode: emit main forms at top level (no when isMainModule guard)
|
# Lib mode: emit main forms at top level (no when isMainModule guard)
|
||||||
for form in mainForms:
|
for form in mainForms:
|
||||||
lines.add(form)
|
lines.add(form)
|
||||||
|
else:
|
||||||
|
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:
|
else:
|
||||||
lines.add("when isMainModule:")
|
lines.add("when isMainModule:")
|
||||||
for form in mainForms:
|
for form in mainForms:
|
||||||
|
|||||||
@@ -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..<form.mapKeys.len:
|
||||||
|
let k = form.mapKeys[i]
|
||||||
|
if (k.kind == ckKeyword and k.kwName == key) or (k.kind == ckSymbol and k.symName == key):
|
||||||
|
let v = form.mapVals[i]
|
||||||
|
if v.kind == ckString: return v.strVal
|
||||||
|
return defaultVal
|
||||||
|
|
||||||
|
proc extractMapVector(form: CljVal, key: string): seq[CljVal] =
|
||||||
|
if form.kind != ckMap: return @[]
|
||||||
|
for i in 0..<form.mapKeys.len:
|
||||||
|
let k = form.mapKeys[i]
|
||||||
|
if (k.kind == ckKeyword and k.kwName == key) or (k.kind == ckSymbol and k.symName == key):
|
||||||
|
let v = form.mapVals[i]
|
||||||
|
if v.kind == ckVector: return v.items
|
||||||
|
if v.kind == ckList: return v.items
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
proc parseBinEntry(form: CljVal): BinEntry =
|
||||||
|
result.name = extractMapString(form, "name")
|
||||||
|
result.path = extractMapString(form, "path")
|
||||||
|
|
||||||
|
proc parseLibEntry(form: CljVal): LibEntry =
|
||||||
|
result.name = extractMapString(form, "name")
|
||||||
|
result.path = extractMapString(form, "path")
|
||||||
|
|
||||||
|
proc loadProject*(dir: string = getCurrentDir()): Project =
|
||||||
|
let root = findProjectRoot(dir)
|
||||||
|
if root.len == 0:
|
||||||
|
raise newException(ValueError, "bara.edn not found in " & dir & " or parent directories")
|
||||||
|
|
||||||
|
let path = root / "bara.edn"
|
||||||
|
let source = readFile(path)
|
||||||
|
let forms = reader.readAll(source)
|
||||||
|
if forms.len == 0:
|
||||||
|
raise newException(ValueError, "Empty bara.edn")
|
||||||
|
|
||||||
|
let map = forms[0]
|
||||||
|
if map.kind != ckMap:
|
||||||
|
raise newException(ValueError, "bara.edn must contain a map at top level")
|
||||||
|
|
||||||
|
result.rootDir = root
|
||||||
|
result.name = extractMapString(map, "name", "unknown")
|
||||||
|
result.version = extractMapString(map, "version", "0.1.0")
|
||||||
|
|
||||||
|
let libVal = extractMapVector(map, "lib")
|
||||||
|
if libVal.len > 0:
|
||||||
|
result.lib = parseLibEntry(libVal[0])
|
||||||
|
elif true:
|
||||||
|
# Also check for single :lib map (not vector)
|
||||||
|
for i in 0..<map.mapKeys.len:
|
||||||
|
let k = map.mapKeys[i]
|
||||||
|
if (k.kind == ckKeyword and k.kwName == "lib") or (k.kind == ckSymbol and k.symName == "lib"):
|
||||||
|
let v = map.mapVals[i]
|
||||||
|
if v.kind == ckMap:
|
||||||
|
result.lib = parseLibEntry(v)
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in extractMapVector(map, "bins"):
|
||||||
|
result.bins.add(parseBinEntry(item))
|
||||||
|
|
||||||
|
for item in extractMapVector(map, "examples"):
|
||||||
|
result.examples.add(parseBinEntry(item))
|
||||||
Reference in New Issue
Block a user