Files
bara-lang/src/cljnim.nim
T
dimgigov 2bc64eca22 feat: add fullscreen TUI with illwill
- New screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help
- AI configuration persisted in ~/.config/cljnim/config.json
- Navigate with arrow keys, Enter, Escape
- illwill added as dependency in cljnim.nimble
2026-05-09 01:51:28 +03:00

359 lines
12 KiB
Nim

# Clojure/Nim — AI-First Clojure Compiler
import os, osproc, strutils, times
import reader, emitter, types, repl, macros, deps, ai_assist, tui
proc getLibPath(): string =
let appDir = getAppDir()
let candidate1 = appDir / "lib"
let candidate2 = appDir.parentDir / "lib"
let candidate3 = getCurrentDir() / "lib"
if dirExists(candidate1): return candidate1
if dirExists(candidate2): return candidate2
if dirExists(candidate3): return candidate3
return "lib"
proc resolveNsToPath(nsName: string, searchPaths: seq[string]): string =
# Convert namespace name to file path: my.app -> my/app.clj
# Clojure convention: hyphens in ns names become underscores in filenames
let relPath = nsName.replace("-", "_").replace(".", "/") & ".clj"
for sp in searchPaths:
let candidate = sp / relPath
if fileExists(candidate):
return candidate
return ""
proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
# Extract require declarations: returns (namespace, alias) pairs
result = @[]
for form in forms:
if form.kind == ckList and form.items.len >= 2 and
form.items[0].kind == ckSymbol and form.items[0].symName == "ns":
for ri in 2..<form.items.len:
let clause = form.items[ri]
let isRequire = (clause.kind == ckList and clause.items.len > 0 and
((clause.items[0].kind == ckSymbol and clause.items[0].symName == ":require") or
(clause.items[0].kind == ckKeyword and clause.items[0].kwName == "require")))
if isRequire:
for ci in 1..<clause.items.len:
let req = clause.items[ci]
if req.kind == ckVector and req.items.len >= 1 and req.items[0].kind == ckSymbol:
let libName = req.items[0].symName
var alias = libName
if req.items.len >= 3 and req.items[1].kind == ckKeyword and req.items[1].kwName == "as":
if req.items[2].kind == ckSymbol:
alias = req.items[2].symName
result.add((libName, alias))
proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[string] = @[], libMode: bool = false) =
let source = readFile(inputPath)
let forms = reader.readAll(source)
if forms.len == 0:
stderr.writeLine("Error: No forms found in " & inputPath)
quit(1)
# Resolve requires and collect all forms from required files
let inputDir = inputPath.parentDir
var searchPaths: seq[string] = @[inputDir, getCurrentDir()]
for p in extraPaths:
if p notin searchPaths:
searchPaths.add(p)
let requires = extractRequires(forms)
var allForms: seq[CljVal] = @[]
var visited: seq[string] = @[]
var nsAliases: seq[(string, string)] = @[] # (alias, namespace)
proc resolveFile(nsName: string) =
if nsName in visited: return
visited.add(nsName)
let path = resolveNsToPath(nsName, searchPaths)
if path.len == 0:
stderr.writeLine("Warning: Cannot find file for namespace: " & nsName)
return
let src = readFile(path)
let fileForms = reader.readAll(src)
# Recursively resolve nested requires
let nestedRequires = extractRequires(fileForms)
for (nestedNs, _) in nestedRequires:
resolveFile(nestedNs)
# Add non-ns forms
for f in fileForms:
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)
# Collect namespace aliases
for (nsName, alias) in requires:
nsAliases.add((alias, nsName))
resolveFile(nsName)
# Set namespace aliases in emitter
emitter.setNsAliases(nsAliases)
# 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)
writeFile(outputPath, nimCode)
echo "Generated: ", outputPath
proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
compileFileInternal(inputPath, outputPath, extraPaths, false)
proc compileFileLib*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
compileFileInternal(inputPath, outputPath, extraPaths, true)
proc nimCompile(nimPath: string, binPath: string, release: bool = false): tuple[exitCode: int, output: string] =
let libPath = getLibPath()
var cmd = "nim c"
if release:
cmd.add(" -d:release")
cmd.add(" --path:" & quoteShell(libPath))
cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath))
echo "Compiling: ", cmd
let (output, exitCode) = execCmdEx(cmd)
return (exitCode, output)
proc makeTempDir(): string =
let pid = getCurrentProcessId()
let ts = epochTime().int
result = getTempDir() / "cljnim_build_" & $pid & "_" & $ts
createDir(result)
proc runFile*(inputPath: string) =
let baseName = inputPath.splitFile.name
let buildDir = makeTempDir()
let nimPath = buildDir / baseName & ".nim"
let binPath = buildDir / baseName
# Resolve project dependencies
let inputDir = inputPath.parentDir
let depPaths = deps.loadAndResolve(if inputDir.len > 0: inputDir else: getCurrentDir())
# Check nimcache for cached output
let projectDir = inputPath.parentDir
let cacheDir = projectDir / "nimcache"
let cacheSubDir = cacheDir / baseName
let cacheNimPath = cacheSubDir / baseName & ".nim"
let cacheBinPath = cacheSubDir / baseName
var useCache = false
if fileExists(cacheNimPath) and fileExists(cacheBinPath):
let srcTime = getLastModificationTime(inputPath).toUnix
let cacheTime = getLastModificationTime(cacheNimPath).toUnix
if srcTime <= cacheTime:
useCache = true
if useCache:
echo "Using cached: ", cacheNimPath
let (compileResult, compileOut) = nimCompile(cacheNimPath, cacheBinPath)
if compileResult != 0:
stderr.writeLine("Compilation failed (cached)")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let source = readFile(inputPath)
let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
quit(1)
try: removeDir(buildDir) except: discard
let runResult = execCmd(cacheBinPath)
quit(runResult)
compileFile(inputPath, nimPath, depPaths)
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
if compileResult != 0:
stderr.writeLine("Compilation failed")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let source = readFile(inputPath)
let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard
quit(1)
# Save to cache
try:
createDir(cacheSubDir)
copyFile(nimPath, cacheNimPath)
copyFile(binPath, cacheBinPath)
echo "Cached: ", cacheNimPath
except:
discard
let runResult = execCmd(binPath)
try: removeDir(buildDir) except: discard
if runResult != 0:
stderr.writeLine("Execution failed")
quit(1)
proc main() =
initBuiltinMacros()
let args = commandLineParams()
if args.len == 0:
echo "Clojure/Nim — AI-First Clojure Compiler"
echo ""
echo "Usage:"
echo " cljnim compile <file.clj> [output.nim] Compile to Nim source"
echo " cljnim run <file.clj> Compile and run binary"
echo " cljnim read <file.clj> Parse and print AST"
echo " cljnim repl [--json] Start REPL (human or AI mode)"
echo " cljnim deps Resolve dependencies from deps.edn"
echo " cljnim -e '<code>' Evaluate expression"
echo " cljnim ai '<description>' Generate Clojure code with AI"
echo " cljnim tui Start interactive TUI"
echo ""
echo "REPL Commands:"
echo " :quit, :q Exit REPL"
echo " :defs List defined vars"
echo " :clear Clear definitions"
echo " :ns Show current namespace"
echo ""
echo "AI Mode (JSON):"
echo " cljnim repl --json"
echo " Input: {\"op\":\"eval\",\"form\":\"(+ 1 2)\"}"
echo " Output: {\"status\":\"ok\",\"result\":{...},\"meta\":{...}}"
quit(0)
let cmd = args[0]
# Handle -e flag
if cmd == "-e":
if args.len < 2:
stderr.writeLine("Error: Missing expression after -e")
quit(1)
let code = args[1]
let buildDir = makeTempDir()
let nimPath = buildDir / "expr.nim"
let binPath = buildDir / "expr"
let forms = reader.readAll(code)
let nimCode = emitter.emitProgram(forms)
writeFile(nimPath, nimCode)
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
if compileResult != 0:
stderr.writeLine("Compilation failed")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let aiRes = ai_assist.explainError(compileOut, code, "-e expression")
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard
quit(1)
let runResult = execCmd(binPath)
try: removeDir(buildDir) except: discard
quit(runResult)
case cmd
of "compile":
if args.len < 2:
stderr.writeLine("Error: Missing input file")
quit(1)
let inputPath = args[1]
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
compileFile(inputPath, outputPath)
of "compile-lib":
if args.len < 2:
stderr.writeLine("Error: Missing input file")
quit(1)
let inputPath = args[1]
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
compileFileLib(inputPath, outputPath)
of "run":
if args.len < 2:
stderr.writeLine("Error: Missing input file")
quit(1)
runFile(args[1])
of "read":
if args.len < 2:
stderr.writeLine("Error: Missing input file")
quit(1)
let source = readFile(args[1])
let forms = reader.readAll(source)
for form in forms:
echo $form
of "repl":
var mode = rmHuman
var tcpPort = -1
for i in 1..<args.len:
if args[i] == "--json":
mode = rmJson
elif args[i] == "--edn":
mode = rmEdn
elif args[i] == "--tcp":
if i + 1 < args.len:
try:
tcpPort = parseInt(args[i+1])
except ValueError:
stderr.writeLine("Invalid TCP port: " & args[i+1])
quit(1)
elif args[i].startsWith("--tcp="):
try:
tcpPort = parseInt(args[i][6..^1])
except ValueError:
stderr.writeLine("Invalid TCP port: " & args[i][6..^1])
quit(1)
var state = initReplState(mode)
try:
if tcpPort > 0:
runTcpRepl(state, tcpPort)
else:
case mode
of rmHuman: runHumanRepl(state)
of rmJson: runJsonRepl(state)
of rmEdn: runJsonRepl(state)
finally:
state.cleanup()
of "deps":
let projectDir = getCurrentDir()
let depsPath = deps.findDepsFile(projectDir)
if depsPath.len == 0:
echo "No deps.edn or project.clj found"
quit(0)
echo "Found: ", depsPath
let depsFile = deps.parseDepsFile(depsPath)
echo "Dependencies: ", depsFile.deps.len
let paths = deps.resolveDeps(depsFile, depsPath.parentDir)
if paths.len > 0:
echo "Resolved paths:"
for p in paths:
echo " ", p
else:
echo "No dependencies to resolve"
of "ai":
if not ai_assist.hasAiConfig():
stderr.writeLine("Error: No AI API key configured.")
stderr.writeLine("Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY environment variable.")
quit(1)
if args.len < 2:
stderr.writeLine("Error: Missing description for AI generation")
stderr.writeLine("Usage: cljnim ai 'function that reverses a list'")
quit(1)
let description = args[1]
echo "🤖 Asking AI to generate code..."
let aiRes = ai_assist.generateCode(description)
if aiRes.ok:
echo aiRes.suggestion
else:
stderr.writeLine(aiRes.suggestion)
quit(1)
of "tui":
runTui()
else:
stderr.writeLine("Unknown command: " & cmd)
quit(1)
when isMainModule:
main()