Add AI-first REPL with JSON mode

This commit is contained in:
2026-05-08 16:51:05 +03:00
parent 87d6028487
commit 5971c062d1
5 changed files with 595 additions and 363 deletions
+39 -23
View File
@@ -1,17 +1,15 @@
# Clojure/Nim Compiler
import os, osproc
import reader, emitter, types, macros
# Clojure/Nim — AI-First Clojure Compiler
import os, osproc, strutils
import reader, emitter, types, repl
proc getLibPath(): string =
let appDir = getAppDir()
# Try relative to binary location
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
# Fallback: assume lib is in current dir
return "lib"
proc compileFile*(inputPath: string, outputPath: string) =
@@ -55,36 +53,38 @@ proc runFile*(inputPath: string) =
quit(1)
proc main() =
initBuiltinMacros()
let args = commandLineParams()
if args.len == 0:
echo "Clojure/Nim Compiler"
echo "Clojure/Nim — AI-First Clojure Compiler"
echo ""
echo "Usage:"
echo " cljnim compile <file.clj> [output.nim] Compile to Nim"
echo " cljnim run <file.clj> Compile and run"
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 -e '<code>' Evaluate expression"
echo " cljnim -M -e '<code>' Evaluate with full runtime"
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 and -M -e flags
if cmd == "-e" or cmd == "-M":
var code = ""
var idx = 1
if cmd == "-M" and args.len > 1 and args[1] == "-e":
idx = 2
elif cmd == "-e":
idx = 1
else:
stderr.writeLine("Usage: cljnim [-M] -e '<code>'")
quit(1)
if idx >= args.len:
# Handle -e flag
if cmd == "-e":
if args.len < 2:
stderr.writeLine("Error: Missing expression after -e")
quit(1)
code = args[idx]
let code = args[1]
let tmpDir = getTempDir()
let nimPath = tmpDir / "cljnim_expr.nim"
let binPath = tmpDir / "cljnim_expr"
@@ -122,6 +122,22 @@ proc main() =
for form in forms:
echo $form
of "repl":
var mode = rmHuman
for i in 1..<args.len:
if args[i] == "--json":
mode = rmJson
elif args[i] == "--edn":
mode = rmEdn
var state = initReplState(mode)
try:
case mode
of rmHuman: runHumanRepl(state)
of rmJson: runJsonRepl(state)
of rmEdn: runJsonRepl(state)
finally:
state.cleanup()
else:
stderr.writeLine("Unknown command: " & cmd)
quit(1)
+2 -2
View File
@@ -940,9 +940,9 @@ proc emitProgram*(forms: seq[CljVal]): string =
stripped.startsWith("try:") or stripped.startsWith("var "):
mainForms.add(code)
elif i == forms.len - 1:
mainForms.add(indentStr(2) & "echo " & stripped)
mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")")
else:
mainForms.add(indentStr(2) & "discard " & stripped)
mainForms.add(indentStr(2) & "discard cljRepr(" & stripped & ")")
var lines = headerLines
# Add collected Nim imports
+283
View File
@@ -0,0 +1,283 @@
# AI-First REPL for Clojure/Nim
import os, osproc, strutils, json, times
import reader, emitter, types
type
ReplMode* = enum
rmHuman, rmJson, rmEdn
ReplState* = object
mode*: ReplMode
ns*: string
defs*: seq[CljVal]
tempDir*: string
sessionId*: string
libPath*: string
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 initReplState*(mode: ReplMode): ReplState =
result.mode = mode
result.ns = "user"
result.defs = @[]
result.tempDir = getTempDir() / "cljnim_repl_" & $getCurrentProcessId()
createDir(result.tempDir)
result.sessionId = $getCurrentProcessId() & "_" & $epochTime().int
result.libPath = getLibPath()
proc cleanup*(state: ReplState) =
try:
removeDir(state.tempDir)
except:
discard
proc buildProgram(state: ReplState, form: CljVal, isDef: bool): string =
var allForms = state.defs
if isDef:
allForms.add(form)
var header = @[
"# Generated by Clojure/Nim REPL",
"import cljnim_runtime",
"",
]
var mainCode = ""
if not isDef:
mainCode = emitter.emitExpr(form, 0)
var lines = header
for d in allForms:
lines.add(emitter.emitExpr(d, 0))
if not isDef and mainCode.len > 0:
lines.add("")
lines.add("when isMainModule:")
let stripped = mainCode.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or stripped.startsWith("block:"):
lines.add(" " & stripped)
else:
lines.add(" echo cljRepr(" & stripped & ")")
elif isDef:
lines.add("")
lines.add("when isMainModule:")
lines.add(" discard nil")
return lines.join("\n") & "\n"
proc compileAndRun(state: ReplState, nimCode: string): tuple[ok: bool, output: string, error: string] =
let sourceFile = state.tempDir / "repl_input.nim"
let binFile = state.tempDir / "repl_input"
writeFile(sourceFile, nimCode)
let compileCmd = "nim c --path:" & quoteShell(state.libPath) & " -o:" & binFile & " " & sourceFile
let (compileOut, compileExit) = execCmdEx(compileCmd)
if compileExit != 0 or not fileExists(binFile):
return (false, "", compileOut)
let (runOut, runExit) = execCmdEx(binFile)
if runExit != 0:
return (false, "", runOut)
return (true, runOut.strip(), "")
proc evalForm*(state: var ReplState, formStr: string): JsonNode =
let startTime = epochTime()
var parsed: CljVal
try:
parsed = reader.read(formStr)
except ReaderError as e:
return %*{
"status": "error",
"error": {
"type": "reader/error",
"message": e.msg,
"form": formStr
},
"meta": {
"ns": state.ns,
"ms": (epochTime() - startTime) * 1000
}
}
let isDef = parsed.kind == ckList and parsed.items.len > 0 and
parsed.items[0].kind == ckSymbol and
parsed.items[0].symName in ["def", "defn"]
let nimCode = buildProgram(state, parsed, isDef)
let (ok, output, error) = compileAndRun(state, nimCode)
let elapsed = (epochTime() - startTime) * 1000
if not ok:
return %*{
"status": "error",
"error": {
"type": "compiler/error",
"message": error,
"form": formStr
},
"meta": {
"ns": state.ns,
"ms": elapsed
}
}
if isDef:
state.defs.add(parsed)
let name = if parsed.items[1].kind == ckSymbol: parsed.items[1].symName else: "unknown"
return %*{
"status": "ok",
"result": {
"type": "var",
"name": name,
"ns": state.ns,
"printed": "#\"" & state.ns & "/" & name & "\""
},
"meta": {
"ns": state.ns,
"ms": elapsed,
"form": formStr
}
}
return %*{
"status": "ok",
"result": {
"type": "unknown",
"value": output,
"printed": output
},
"meta": {
"ns": state.ns,
"ms": elapsed,
"form": formStr
}
}
proc runHumanRepl*(state: var ReplState) =
echo "Clojure/Nim REPL"
echo "Type :quit to exit, :help for commands"
echo ""
while true:
stdout.write(state.ns & "> ")
stdout.flushFile()
let line = stdin.readLine()
if line.strip().len == 0:
continue
let cmd = line.strip()
case cmd
of ":quit", ":q":
echo "Bye!"
break
of ":help", ":h":
echo ":quit, :q Exit REPL"
echo ":defs List defined vars"
echo ":clear Clear all definitions"
echo ":ns Show current namespace"
of ":defs":
for d in state.defs:
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
echo " " & d.items[1].symName
of ":clear":
state.defs = @[]
echo "Definitions cleared."
of ":ns":
echo state.ns
else:
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
echo "=> " & res["result"]["printed"].getStr()
else:
echo "Error: " & res["error"]["message"].getStr()
proc runJsonRepl*(state: var ReplState) =
var readyMsg = %*{ "status": "ready", "ns": state.ns, "mode": "json" }
echo $readyMsg
stdout.flushFile()
while true:
let line = stdin.readLine()
if line.strip().len == 0:
continue
var req: JsonNode
try:
req = parseJson(line)
except JsonParsingError as e:
var err = %*{
"status": "error",
"error": { "type": "json/parse-error", "message": e.msg }
}
echo $err
stdout.flushFile()
continue
let op = req.getOrDefault("op").getStr("eval")
case op
of "eval":
let form = req.getOrDefault("form").getStr("")
if form.len == 0:
var err = %*{
"status": "error",
"error": { "type": "repl/missing-form", "message": "Missing :form field" }
}
echo $err
else:
let res = evalForm(state, form)
if req.hasKey("request-id"):
res["request-id"] = req["request-id"]
echo $res
of "eval-batch":
let forms = req.getOrDefault("forms")
var results: seq[JsonNode] = @[]
if forms.kind == JArray:
for f in forms:
let formStr = f.getStr("")
if formStr.len > 0:
results.add(evalForm(state, formStr))
var batchRes = %*{ "status": "ok", "results": results }
echo $batchRes
of "get-defs":
var defs: seq[string] = @[]
for d in state.defs:
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
defs.add(d.items[1].symName)
var defsRes = %*{ "status": "ok", "defs": defs, "ns": state.ns }
echo $defsRes
of "clear":
state.defs = @[]
var clearRes = %*{ "status": "ok", "cleared": true }
echo $clearRes
of "quit":
var quitRes = %*{ "status": "ok", "bye": true }
echo $quitRes
stdout.flushFile()
break
else:
var unkRes = %*{
"status": "error",
"error": { "type": "repl/unknown-op", "message": "Unknown op: " & op }
}
echo $unkRes
stdout.flushFile()