Compare commits

..

2 Commits

Author SHA1 Message Date
dimgigov 11fca52282 fix: 18 bugs across interpreter, reader, emitter, and runtime
- cljMinMax now handles float and mixed int/float arguments
- cljType* covers ckSet, ckTransient, ckAgent
- cljConj and cljVec return vectors instead of lists
- rem uses truncated division semantics (matches Clojure)
- range with negative step terminates correctly
- reduce of empty collection with no init errors instead of returning nil
- swap! extra args flatten correctly
- remove duplicate not definition (dead code)
- ratio literals (1/5) parse correctly
- scientific notation uses correct variable (numTok not tok)
- readSet returns proper set value instead of (set [...]) form
- TCP REPL catches specific exceptions instead of bare except
- initBuiltinMacros guards against duplicate registration
- doseq :while clause no longer generates dead code or infinite loop
- emitter resets all global state on each emitProgram call
- pushLeaf fills gaps with internal nodes instead of nil children

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:25:53 +03:00
dimgigov 7cb8aa1ee0 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
2026-05-16 19:20:23 +03:00
9 changed files with 563 additions and 55 deletions
+5 -9
View File
@@ -77,17 +77,13 @@ proc pushLeaf[T](node: PVecNode[T], index: int, shift: int, val: seq[T]): PVecNo
# Push a full leaf (seq of up to 32 values) into the tree at position 'index' # Push a full leaf (seq of up to 32 values) into the tree at position 'index'
result = copyNode(node) result = copyNode(node)
if shift == 0: if shift == 0:
# Should not happen — we always push into internal nodes
result = newLeafNode[T](val) result = newLeafNode[T](val)
else: else:
let childIdx = (index shr shift) and BRANCHING_MASK let childIdx = (index shr shift) and BRANCHING_MASK
if childIdx >= result.children.len: # Grow children array and fill any gaps with empty internal nodes
# Need to grow children array while result.children.len <= childIdx:
let oldLen = result.children.len result.children.add(newInternalNode[T]())
result.children.setLen(childIdx + 1)
for i in oldLen..<childIdx:
result.children[i] = nil
var child = result.children[childIdx] var child = result.children[childIdx]
if child.isNil: if child.isNil:
if shift == BRANCHING_BITS: if shift == BRANCHING_BITS:
@@ -96,7 +92,7 @@ proc pushLeaf[T](node: PVecNode[T], index: int, shift: int, val: seq[T]): PVecNo
child = pushLeaf(newInternalNode[T](), index, shift - BRANCHING_BITS, val) child = pushLeaf(newInternalNode[T](), index, shift - BRANCHING_BITS, val)
else: else:
child = pushLeaf(child, index, shift - BRANCHING_BITS, val) child = pushLeaf(child, index, shift - BRANCHING_BITS, val)
result.children[childIdx] = child result.children[childIdx] = child
proc pvecConj*[T](v: PersistentVector[T], val: T): PersistentVector[T] = proc pvecConj*[T](v: PersistentVector[T], val: T): PersistentVector[T] =
+317 -4
View File
@@ -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")
+28 -7
View File
@@ -277,7 +277,7 @@ proc cljConj*(coll: CljVal, item: CljVal): CljVal =
of ckVector: of ckVector:
var newItems = coll.listItems var newItems = coll.listItems
newItems.add(item) newItems.add(item)
cljList(newItems) cljVector(newItems)
else: else:
cljList(@[item]) cljList(@[item])
@@ -315,11 +315,11 @@ proc cljSeq*(v: CljVal): CljVal =
else: cljNil() else: cljNil()
proc cljVec*(v: CljVal): CljVal = proc cljVec*(v: CljVal): CljVal =
if v.isNil: return cljList(@[]) if v.isNil: return cljVector(@[])
case v.kind case v.kind
of ckList: cljList(v.listItems) of ckList: cljVector(v.listItems)
of ckVector: v of ckVector: v
else: cljList(@[v]) else: cljVector(@[v])
proc cljEmpty*(v: CljVal): CljVal = proc cljEmpty*(v: CljVal): CljVal =
cljBool(cljCount(v) == 0) cljBool(cljCount(v) == 0)
@@ -430,18 +430,39 @@ proc cljType*(v: CljVal): CljVal =
of ckList: cljKeyword("list") of ckList: cljKeyword("list")
of ckVector: cljKeyword("vector") of ckVector: cljKeyword("vector")
of ckMap: cljKeyword("map") of ckMap: cljKeyword("map")
of ckSet: cljKeyword("set")
of ckFn: cljKeyword("function") of ckFn: cljKeyword("function")
of ckAtom: cljKeyword("atom") of ckAtom: cljKeyword("atom")
of ckTransient: cljKeyword("transient")
of ckAgent: cljKeyword("agent")
proc cljMinMax*(args: seq[CljVal], isMin: bool): CljVal = proc cljMinMax*(args: seq[CljVal], isMin: bool): CljVal =
if args.len == 0: raise newException(CatchableError, "min/max requires at least 1 argument") if args.len == 0: raise newException(CatchableError, "min/max requires at least 1 argument")
result = args[0] result = args[0]
for i in 1..<args.len: for i in 1..<args.len:
if args[i].kind == ckInt and result.kind == ckInt: let a = args[i]
if a.kind == ckInt and result.kind == ckInt:
if isMin: if isMin:
if args[i].intVal < result.intVal: result = args[i] if a.intVal < result.intVal: result = a
else: else:
if args[i].intVal > result.intVal: result = args[i] if a.intVal > result.intVal: result = a
elif a.kind == ckFloat and result.kind == ckFloat:
if isMin:
if a.floatVal < result.floatVal: result = a
else:
if a.floatVal > result.floatVal: result = a
elif a.kind == ckInt and result.kind == ckFloat:
let af = a.intVal.float64
if isMin:
if af < result.floatVal: result = a
else:
if af > result.floatVal: result = a
elif a.kind == ckFloat and result.kind == ckInt:
let rf = result.intVal.float64
if isMin:
if a.floatVal < rf: result = a
else:
if a.floatVal > rf: result = a
proc cljAbs*(v: CljVal): CljVal = proc cljAbs*(v: CljVal): CljVal =
case v.kind case v.kind
+63 -9
View File
@@ -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])
@@ -2355,6 +2394,13 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
scopeStack = @[] scopeStack = @[]
pushScope() pushScope()
requiredImports = initHashSet[string]() requiredImports = initHashSet[string]()
loopStack = @[]
loopResultVar = ""
nsAliases = @[]
libNsPrefixes = @[]
definedGlobals = initHashSet[string]()
definedFnArities = initTable[string, int]()
multiArityFns = initHashSet[string]()
var headerLines: seq[string] = @[ var headerLines: seq[string] = @[
"# Generated by Bara Lang", "# Generated by Bara Lang",
"import cljnim_runtime", "import cljnim_runtime",
@@ -2443,9 +2489,17 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
for form in mainForms: for form in mainForms:
lines.add(form) lines.add(form)
else: else:
lines.add("when isMainModule:") if emitEntryProcName.len > 0:
for form in mainForms: lines.add("proc " & emitEntryProcName & "*(args: seq[CljVal] = @[]): CljVal =")
lines.add(form) 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" return lines.join("\n") & "\n"
+16 -13
View File
@@ -725,9 +725,10 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
coll = args[2] coll = args[2]
else: else:
coll = args[1] coll = args[1]
if coll.kind in {ckList, ckVector} and coll.items.len > 0: if coll.kind in {ckList, ckVector}:
if coll.items.len == 0:
return EvalResult(ok: false, error: "reduce of empty collection with no initial value")
acc = coll.items[0] acc = coll.items[0]
# reduce rest
var res = acc var res = acc
for i in 1..<coll.items.len: for i in 1..<coll.items.len:
let callItems = @[fn, res, coll.items[i]] let callItems = @[fn, res, coll.items[i]]
@@ -920,12 +921,6 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
of ckAgent: "agent" of ckAgent: "agent"
return EvalResult(ok: true, value: cljKeyword(typeName)) return EvalResult(ok: true, value: cljKeyword(typeName))
of "not":
numArgs(1)
let isFalsy = args[0].kind == ckNil or
(args[0].kind == ckBool and not args[0].boolVal)
return EvalResult(ok: true, value: cljBool(isFalsy))
of "abs": of "abs":
numArgs(1) numArgs(1)
if args[0].kind == ckInt: if args[0].kind == ckInt:
@@ -953,7 +948,7 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if args[0].kind == ckInt and args[1].kind == ckInt: if args[0].kind == ckInt and args[1].kind == ckInt:
if args[1].intVal == 0: if args[1].intVal == 0:
return EvalResult(ok: false, error: "Division by zero") return EvalResult(ok: false, error: "Division by zero")
return EvalResult(ok: true, value: cljInt(args[0].intVal mod args[1].intVal)) return EvalResult(ok: true, value: cljInt(args[0].intVal - (args[0].intVal div args[1].intVal) * args[1].intVal))
return EvalResult(ok: false, error: "rem requires integers") return EvalResult(ok: false, error: "rem requires integers")
of "min": of "min":
@@ -984,9 +979,16 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if args.len == 3 and args[0].kind == ckInt and args[1].kind == ckInt and args[2].kind == ckInt: if args.len == 3 and args[0].kind == ckInt and args[1].kind == ckInt and args[2].kind == ckInt:
var items: seq[CljVal] = @[] var items: seq[CljVal] = @[]
var i = args[0].intVal var i = args[0].intVal
while i < args[1].intVal: let step = args[2].intVal
items.add(cljInt(i)) let endVal = args[1].intVal
i += args[2].intVal if step > 0:
while i < endVal:
items.add(cljInt(i))
i += step
elif step < 0:
while i > endVal:
items.add(cljInt(i))
i += step
return EvalResult(ok: true, value: cljList(items)) return EvalResult(ok: true, value: cljList(items))
return EvalResult(ok: false, error: "range requires integers") return EvalResult(ok: false, error: "range requires integers")
@@ -1132,7 +1134,8 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
let currentVal = atomRegistry[args[0].strVal] let currentVal = atomRegistry[args[0].strVal]
var callItems = @[fn, currentVal] var callItems = @[fn, currentVal]
if args.len > 2: if args.len > 2:
callItems.add(args[2..^1]) for extra in args[2..^1]:
callItems.add(extra)
let callRes = evalList(callItems, env) let callRes = evalList(callItems, env)
if not callRes.ok: return callRes if not callRes.ok: return callRes
atomRegistry[args[0].strVal] = callRes.value atomRegistry[args[0].strVal] = callRes.value
+9 -7
View File
@@ -428,8 +428,7 @@ proc initMacros*() =
if whileExpr != nil: if whileExpr != nil:
let gs = cljSymbol(gensymName("dw_")) let gs = cljSymbol(gensymName("dw_"))
b = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]), b = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]),
cljList(@[cljSymbol("when"), whileExpr] & b & @[cljList(@[cljSymbol("recur"), gs])]), cljList(@[cljSymbol("when"), whileExpr] & b & @[cljList(@[cljSymbol("recur"), gs])])])]
cljList(@[cljSymbol("when"), cljBool(false), cljNil()])])]
return cljList(@[cljSymbol("do")] & b) return cljList(@[cljSymbol("do")] & b)
# Build nested doseq for multiple pairs, innermost gets the body with modifiers # Build nested doseq for multiple pairs, innermost gets the body with modifiers
@@ -438,11 +437,10 @@ proc initMacros*() =
inner = @[cljList(@[cljSymbol("let"), cljVector(letBinds)] & inner)] inner = @[cljList(@[cljSymbol("let"), cljVector(letBinds)] & inner)]
if whenExpr != nil: if whenExpr != nil:
inner = @[cljList(@[cljSymbol("when"), whenExpr] & inner)] inner = @[cljList(@[cljSymbol("when"), whenExpr] & inner)]
if whileExpr != nil: if whileExpr != nil:
let gs = cljSymbol(gensymName("dw_")) let gs = cljSymbol(gensymName("dw_"))
inner = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]), inner = @[cljList(@[cljSymbol("loop"), cljVector(@[gs, cljBool(true)]),
cljList(@[cljSymbol("when"), whileExpr] & inner & @[cljList(@[cljSymbol("recur"), gs])]), cljList(@[cljSymbol("when"), whileExpr] & inner & @[cljList(@[cljSymbol("recur"), gs])])])]
cljList(@[cljSymbol("when"), cljBool(false), cljNil()])])]
proc makeLoop(name: CljVal, coll: CljVal, innerBody: seq[CljVal]): CljVal = proc makeLoop(name: CljVal, coll: CljVal, innerBody: seq[CljVal]): CljVal =
if name.kind == ckVector: if name.kind == ckVector:
@@ -606,5 +604,9 @@ proc initMacros*() =
return cljList(@[cljSymbol("do")] & body) return cljList(@[cljSymbol("do")] & body)
) )
var macrosInitialized = false
proc initBuiltinMacros*() = proc initBuiltinMacros*() =
if macrosInitialized: return
initMacros() initMacros()
macrosInitialized = true
+99
View File
@@ -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))
+18 -4
View File
@@ -227,6 +227,20 @@ proc readAtom(s: string, i: var int): CljVal =
except CatchableError: except CatchableError:
return cljSymbol(tok) return cljSymbol(tok)
# Check for ratio literal like 1/5, -1/5, 3/4
let slashPos = numTok.find('/')
if slashPos > 0 and slashPos < numTok.len - 1:
let numerStr = numTok[0..<slashPos]
let denomStr = numTok[slashPos+1..^1]
try:
let numer = parseInt(numerStr)
let denom = parseInt(denomStr)
if denom != 0:
return cljList(@[cljSymbol("/"), cljInt(numer.int64), cljInt(denom.int64)])
except CatchableError:
discard
return cljSymbol(tok)
# number # number
var isFloat = false var isFloat = false
var isNumber = true var isNumber = true
@@ -252,8 +266,8 @@ proc readAtom(s: string, i: var int): CljVal =
if j + 1 < numTok.len and (numTok[j+1] == '-' or numTok[j+1] == '+'): if j + 1 < numTok.len and (numTok[j+1] == '-' or numTok[j+1] == '+'):
discard # skip exponent sign discard # skip exponent sign
elif numTok[j] notin Digits: elif numTok[j] notin Digits:
if sawExp and (tok[j] == '-' or tok[j] == '+'): if sawExp and (numTok[j] == '-' or numTok[j] == '+'):
if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'): if j > 0 and (numTok[j-1] == 'e' or numTok[j-1] == 'E'):
continue continue
isNumber = false isNumber = false
break break
@@ -354,7 +368,7 @@ proc readMap(s: string, i: var int): CljVal =
return cljMapFromPairs(pairs) return cljMapFromPairs(pairs)
proc readSet(s: string, i: var int): CljVal = proc readSet(s: string, i: var int): CljVal =
inc i # skip '{' after # inc i # skip '{' after #
var items: seq[CljVal] = @[] var items: seq[CljVal] = @[]
while true: while true:
skipWhitespaceAndComments(s, i) skipWhitespaceAndComments(s, i)
@@ -366,7 +380,7 @@ proc readSet(s: string, i: var int): CljVal =
let form = readForm(s, i) let form = readForm(s, i)
if form != nil: if form != nil:
items.add(form) items.add(form)
return cljList(@[cljSymbol("set"), cljVector(items)]) return cljSet(items)
proc readDispatch(s: string, i: var int): CljVal = proc readDispatch(s: string, i: var int): CljVal =
inc i # skip '#' inc i # skip '#'
+8 -2
View File
@@ -528,8 +528,14 @@ proc handleClient(state: var ReplState, client: Socket) =
client.send($(%*{ "status": "error", "error": { "type": "repl/unknown-op", "message": "Unknown op: " & op } }) & "\n") client.send($(%*{ "status": "error", "error": { "type": "repl/unknown-op", "message": "Unknown op: " & op } }) & "\n")
except EOFError: except EOFError:
break break
except: except CatchableError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/protocol-error", "message": "Protocol error" } }) & "\n") client.send($(%*{ "status": "error", "error": { "type": "repl/protocol-error", "message": e.msg } }) & "\n")
break
except ValueError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/parse-error", "message": e.msg } }) & "\n")
break
except OSError as e:
client.send($(%*{ "status": "error", "error": { "type": "repl/io-error", "message": e.msg } }) & "\n")
break break
client.close() client.close()