Add AI-first REPL with JSON mode
This commit is contained in:
+39
-23
@@ -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
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user