# 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.. 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..= 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 [output.nim] Compile to Nim source" echo " cljnim run Compile and run binary" echo " cljnim read 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 '' Evaluate expression" echo " cljnim ai '' 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.. 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()