feat: add fullscreen TUI and project updates

- New TUI screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help
- AI configuration persisted in ~/.config/cljnim/config.json
- Added illwill dependency for terminal UI
- Updated experiments, examples, docs, and core modules
This commit is contained in:
2026-05-09 01:53:23 +03:00
parent 2bc64eca22
commit ba0b300917
42 changed files with 3042 additions and 153 deletions
+86 -2
View File
@@ -2,7 +2,7 @@
# Supports DeepSeek API and OpenAI-compatible APIs (Xiaomi MiMo, etc.)
# API keys are read from environment variables — never hardcoded.
import std/[httpclient, json, os, strutils, uri]
import std/[httpclient, json, os, strutils, uri, tables, times]
type
AiProvider* = enum
@@ -64,6 +64,35 @@ proc detectConfig*(): AiConfig =
proc hasAiConfig*(): bool =
detectConfig().apiKey.len > 0
type
ErrorCacheEntry = object
suggestion: string
timestamp: float64
var errorCache = initTable[string, ErrorCacheEntry]()
var errorCacheMaxSize = 50
proc errorCacheKey(errorMsg, fileName: string): string =
result = fileName & "::" & errorMsg
if result.len > 200:
result = result[0..199]
proc cacheError*(errorMsg, fileName: string, suggestion: string) =
if errorCache.len >= errorCacheMaxSize:
errorCache.clear()
errorCache[errorCacheKey(errorMsg, fileName)] = ErrorCacheEntry(
suggestion: suggestion,
timestamp: epochTime()
)
proc getCachedError*(errorMsg, fileName: string): string =
let key = errorCacheKey(errorMsg, fileName)
if key in errorCache:
let entry = errorCache[key]
if epochTime() - entry.timestamp < 3600.0:
return entry.suggestion
return ""
proc buildErrorPrompt*(errorMsg, sourceCode, fileName: string): string =
## Build a prompt for the AI to analyze a compiler error
result = """You are an expert Clojure/Nim compiler assistant. The user got a compilation error.
@@ -98,6 +127,43 @@ Requirements:
- Return ONLY the Clojure code, no explanations
"""
proc buildOptimizationPrompt*(code: string): string =
## Build a prompt for AI optimization suggestions
result = """You are an expert Clojure performance engineer. Analyze this Clojure code and suggest optimizations:
```clojure
""" & code & """
```
Consider:
- SIMD/vectorization opportunities
- loop/recur vs recursion
- Persistent data structure usage
- Transients for batch operations
- Parallelization opportunities (pmap, reducers)
Keep response under 200 words. Return ONLY Clojure code suggestions, no explanations.
"""
proc buildDebugPrompt*(code: string, evalResult: string): string =
## Build a prompt for AI debugging analysis
result = """You are an expert Clojure debugger. Analyze this Clojure expression and its result:
**Expression:**
```clojure
""" & code & """
```
**Result:**
```
""" & evalResult & """
```
Explain what happened step by step. If there's a bug or unexpected behavior, explain why.
Keep response under 200 words.
Respond in the same language as the user's source code comments (Bulgarian or English).
"""
proc callAiApi*(config: AiConfig, prompt: string): AiResponse =
## Call the AI API and return the response
if config.apiKey.len == 0:
@@ -139,9 +205,15 @@ proc callAiApi*(config: AiConfig, prompt: string): AiResponse =
proc explainError*(errorMsg, sourceCode, fileName: string): AiResponse =
## High-level helper: explain a compiler error using AI
let cached = getCachedError(errorMsg, fileName)
if cached.len > 0:
return AiResponse(ok: true, suggestion: cached)
let config = detectConfig()
let prompt = buildErrorPrompt(errorMsg, sourceCode, fileName)
return callAiApi(config, prompt)
let res = callAiApi(config, prompt)
if res.ok:
cacheError(errorMsg, fileName, res.suggestion)
return res
proc generateCode*(description: string): AiResponse =
## High-level helper: generate Clojure code from description
@@ -149,6 +221,18 @@ proc generateCode*(description: string): AiResponse =
let prompt = buildGenerationPrompt(description)
return callAiApi(config, prompt)
proc optimizeCode*(code: string): AiResponse =
## High-level helper: suggest optimizations for Clojure code
let config = detectConfig()
let prompt = buildOptimizationPrompt(code)
return callAiApi(config, prompt)
proc debugCode*(code: string, evalResult: string): AiResponse =
## High-level helper: debug a Clojure expression and its result
let config = detectConfig()
let prompt = buildDebugPrompt(code, evalResult)
return callAiApi(config, prompt)
proc formatSuggestion*(response: AiResponse): string =
## Format AI response for terminal display
if not response.ok:
+56 -22
View File
@@ -1,9 +1,11 @@
# Clojure → Nim Emitter
import strutils, sequtils, sets
import strutils, sets
import types
import macros
var requiredImports* = initHashSet[string]()
var emitLibMode* = false
var loopStack*: seq[seq[string]] = @[]
var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
proc setNsAliases*(aliases: seq[(string, string)]) =
@@ -151,12 +153,6 @@ proc runtimeName(op: string): string =
of "close!": "cljChanClose"
else: ""
proc emitArgs(args: seq[CljVal]): string =
var parts: seq[string] = @[]
for a in args:
parts.add(emitExpr(a, 0))
return parts.join(", ")
proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let sp = indentStr(indent)
let head = items[0]
@@ -250,10 +246,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
return form.items[1]
# list
if hName == "list":
var result: seq[CljVal] = @[]
var evaluated: seq[CljVal] = @[]
for i in 1..<form.items.len:
result.add(eval(form.items[i]))
return cljList(result)
evaluated.add(eval(form.items[i]))
return cljList(evaluated)
# cons
if hName == "cons" and form.items.len == 3:
let fst = eval(form.items[1])
@@ -346,7 +342,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
# if/when branches return their last expression properly.
bodyCode = bodyCode.replace("discard ", "result = ")
let procName = mangleName(name.symName)
return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode
of "defn-":
if items.len < 4:
@@ -433,7 +430,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or
thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or
thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ")):
thenStripped.startsWith("let ") or thenStripped.startsWith("proc ") or
thenStripped.contains("\n") or thenStripped.contains(" = ")):
thenFinal = indentStr(indent + 1) & "discard " & thenStripped
var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal
if items.len == 4:
@@ -443,7 +441,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or
elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or
elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ")):
elseStripped.startsWith("let ") or elseStripped.startsWith("proc ") or
elseStripped.contains("\n") or elseStripped.contains(" = ")):
elseFinal = indentStr(indent + 1) & "discard " & elseStripped
ifBlock.add("\n" & sp & "else:\n" & elseFinal)
return ifBlock
@@ -540,22 +539,32 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "loop binding name must be a symbol")
loopParams.add((mangleName(lname.symName), emitExpr(lval, 0)))
li += 2
var loopVars: seq[string] = @[]
var lines: seq[string] = @[]
for (lpName, lpVal) in loopParams:
lines.add(sp & "var " & lpName & ": CljVal = " & lpVal)
loopVars.add(lpName)
loopStack.add(loopVars)
lines.add(sp & "while true:")
let body = items[2..^1]
for b in body:
lines.add(emitExpr(b, indent + 1))
lines.add(indentStr(indent + 1) & "break")
discard loopStack.pop()
return lines.join("\n")
of "recur":
if items.len < 2:
raise newException(EmitterError, "recur requires arguments")
# Simplified: emit as discard (real impl needs loop var assignment)
if loopStack.len == 0:
raise newException(EmitterError, "recur outside of loop")
let loopVars = loopStack[^1]
if items.len - 1 != loopVars.len:
raise newException(EmitterError, "recur requires " & $loopVars.len & " arguments, got " & $(items.len - 1))
var lines: seq[string] = @[]
for ri in 1..<items.len:
lines.add(sp & "discard " & emitExpr(items[ri], 0))
lines.add(sp & loopVars[ri-1] & " = " & emitExpr(items[ri], 0))
lines.add(sp & "continue")
return lines.join("\n")
of "do":
@@ -567,7 +576,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if items.len < 2:
raise newException(EmitterError, "try requires body")
var bodyForms: seq[CljVal] = @[]
var catchClauses: seq[(string, seq[CljVal])] = @[]
var catchClauses: seq[(string, string, seq[CljVal])] = @[]
var finallyBody: seq[CljVal] = @[]
var ti = 1
while ti < items.len:
@@ -576,10 +585,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if form.items[0].symName == "catch":
if form.items.len < 3:
raise newException(EmitterError, "catch requires exception type, name, and body")
let exType = form.items[1]
var exTypeStr = "CatchableError"
if exType.kind == ckSymbol:
exTypeStr = exType.symName
let exName = form.items[2]
if exName.kind != ckSymbol:
raise newException(EmitterError, "catch name must be a symbol")
catchClauses.add((mangleName(exName.symName), form.items[3..^1]))
catchClauses.add((exTypeStr, mangleName(exName.symName), form.items[3..^1]))
elif form.items[0].symName == "finally":
finallyBody = form.items[1..^1]
else:
@@ -591,9 +604,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
lines.add(sp & "try:")
for b in bodyForms:
lines.add(emitExpr(b, indent + 1))
for (exVar, exBody) in catchClauses:
lines.add(sp & "except CatchableError:")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljString(getCurrentExceptionMsg())")
for (exType, exVar, exBody) in catchClauses:
lines.add(sp & "except " & exType & ":")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])")
lines.add(indentStr(indent + 1) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))")
for eb in exBody:
lines.add(emitExpr(eb, indent + 1))
if finallyBody.len > 0:
@@ -889,7 +903,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0))
# Known Nim module interop patterns
let fullFn = module & "." & funcChain
case module
of "math":
requiredImports.incl("math")
@@ -1029,6 +1042,19 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
keyParts.add(emitExpr(v.mapKeys[i], 0))
valParts.add(emitExpr(v.mapVals[i], 0))
return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckFn:
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())"
of ckAtom:
return sp & "cljAtom(" & emitExpr(v.atomVal, 0) & ")"
of ckTransient:
return sp & "cljTransient(cljNil())"
of ckAgent:
return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")"
proc emitBlock(items: seq[CljVal], indent: int): string =
if items.len == 0:
@@ -1038,7 +1064,7 @@ proc emitBlock(items: seq[CljVal], indent: int): string =
lines.add(emitExpr(item, indent))
return lines.join("\n")
proc emitProgram*(forms: seq[CljVal]): string =
proc emitProgramInternal(forms: seq[CljVal]): string =
requiredImports = initHashSet[string]()
var headerLines: seq[string] = @[
"# Generated by Clojure/Nim",
@@ -1080,3 +1106,11 @@ proc emitProgram*(forms: seq[CljVal]): string =
lines.add(form)
return lines.join("\n") & "\n"
proc emitProgram*(forms: seq[CljVal]): string =
emitLibMode = false
emitProgramInternal(forms)
proc emitProgramLib*(forms: seq[CljVal]): string =
emitLibMode = true
emitProgramInternal(forms)
+71 -1
View File
@@ -1,7 +1,7 @@
# Tree-walking interpreter for fast REPL evaluation
# Handles common cases without spawning nim c
import strutils, sequtils, tables, algorithm, times, deques
import types, reader
import types, reader, ai_assist
var agentRegistry* = initTable[string, CljVal]()
var agentCounter*: int64 = 0
@@ -71,6 +71,11 @@ proc cljReprLocal(v: CljVal): string =
for i in 0..<v.mapKeys.len:
parts.add(cljReprLocal(v.mapKeys[i]) & " " & cljReprLocal(v.mapVals[i]))
"{" & parts.join(", ") & "}"
of ckSet: "#{" & v.setItems.mapIt(cljReprLocal(it)).join(" ") & "}"
of ckFn: "#<fn:" & v.fnName & ">"
of ckAtom: "(atom " & cljReprLocal(v.atomVal) & ")"
of ckTransient: "#<transient>"
of ckAgent: "(agent " & cljReprLocal(v.agentVal) & ")"
proc evalAst*(form: CljVal, env: Env): EvalResult
@@ -223,6 +228,26 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
return EvalResult(ok: true, value: res.value)
return EvalResult(ok: true, value: cljNil())
of "ai/debug", "ai.debug":
if items.len < 2:
return EvalResult(ok: false, error: "ai/debug requires an expression")
if not ai_assist.hasAiConfig():
return EvalResult(ok: false, error: "No AI API key configured.")
var exprStr = cljReprLocal(items[1])
var evalRes: EvalResult
try:
evalRes = evalAst(items[1], env)
except CatchableError as e:
return EvalResult(ok: false, error: "ai/debug: evaluation failed: " & e.msg)
let resultStr = if evalRes.ok:
if evalRes.value.kind == ckString: evalRes.value.strVal else: cljReprLocal(evalRes.value)
else:
"Error: " & evalRes.error
let aiRes = ai_assist.debugCode(exprStr, resultStr)
if not aiRes.ok:
return EvalResult(ok: false, error: aiRes.suggestion)
return EvalResult(ok: true, value: cljString(aiRes.suggestion))
else:
discard
@@ -853,6 +878,11 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
of ckList: "list"
of ckVector: "vector"
of ckMap: "map"
of ckSet: "set"
of ckFn: "fn"
of ckAtom: "atom"
of ckTransient: "transient"
of ckAgent: "agent"
return EvalResult(ok: true, value: cljKeyword(typeName))
of "not":
@@ -1020,6 +1050,11 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
of ckList: "list"
of ckVector: "vector"
of ckMap: "map"
of ckSet: "set"
of ckFn: "fn"
of ckAtom: "atom"
of ckTransient: "transient"
of ckAgent: "agent"
of ckNil: "nil"
return EvalResult(ok: true, value: cljBool(args[0].kwName == typeName))
return EvalResult(ok: true, value: cljBool(false))
@@ -1145,6 +1180,26 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
lastVal = res.value
return EvalResult(ok: true, value: lastVal)
of "ai/generate", "ai.generate":
atLeast(1)
let description = if args[0].kind == ckString: args[0].strVal else: cljReprLocal(args[0])
if not ai_assist.hasAiConfig():
return EvalResult(ok: false, error: "No AI API key configured. Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY.")
let aiRes = ai_assist.generateCode(description)
if not aiRes.ok:
return EvalResult(ok: false, error: aiRes.suggestion)
return EvalResult(ok: true, value: cljString(aiRes.suggestion))
of "ai/optimize", "ai.optimize":
numArgs(1)
let code = if args[0].kind == ckString: args[0].strVal else: cljReprLocal(args[0])
if not ai_assist.hasAiConfig():
return EvalResult(ok: false, error: "No AI API key configured.")
let aiRes = ai_assist.optimizeCode(code)
if not aiRes.ok:
return EvalResult(ok: false, error: aiRes.suggestion)
return EvalResult(ok: true, value: cljString(aiRes.suggestion))
else:
return EvalResult(ok: false, error: "Unknown function: " & name & " (use compile mode for full runtime)")
@@ -1198,6 +1253,21 @@ proc evalAst*(form: CljVal, env: Env): EvalResult =
return EvalResult(ok: true, value: cljMap(keys, vals))
of ckList:
return evalList(form.items, env)
of ckSet:
var items: seq[CljVal] = @[]
for item in form.setItems:
let res = evalAst(item, env)
if not res.ok: return res
items.add(res.value)
return EvalResult(ok: true, value: cljSet(items))
of ckFn:
return EvalResult(ok: true, value: form)
of ckAtom:
return EvalResult(ok: true, value: form)
of ckTransient:
return EvalResult(ok: true, value: form)
of ckAgent:
return EvalResult(ok: true, value: form)
proc eval*(formStr: string, env: Env): EvalResult =
let parsed = reader.read(formStr)
+15 -17
View File
@@ -46,7 +46,6 @@ proc expandSyntaxQuote*(form: CljVal): CljVal =
if head.symName == "unquote-splicing":
raise newException(CatchableError, "unquote-splicing can only be used inside a collection")
# Process each element
var result: seq[CljVal] = @[]
var parts: seq[CljVal] = @[]
for item in form.items:
if item.kind == ckList and item.items.len == 2 and
@@ -118,22 +117,22 @@ proc macroexpand*(form: CljVal): CljVal =
proc threadingMacro(args: seq[CljVal], reverse: bool): CljVal =
if args.len < 2:
raise newException(CatchableError, "Threading macro requires at least 2 arguments")
var result = args[0]
var acc = args[0]
for i in 1..<args.len:
let step = args[i]
if step.kind == ckList and step.items.len > 0:
var newItems = step.items
if reverse:
newItems.add(result)
newItems.add(acc)
else:
newItems.insert(result, 1)
result = cljList(newItems)
newItems.insert(acc, 1)
acc = cljList(newItems)
elif step.kind == ckSymbol:
# (-> x f) => (f x)
result = cljList(@[step, result])
acc = cljList(@[step, acc])
else:
raise newException(CatchableError, "Threading macro requires list or symbol forms")
result
acc
proc initBuiltinMacros*() =
# ->
@@ -324,17 +323,17 @@ proc initBuiltinMacros*() =
raise newException(CatchableError, "as-> requires at least 3 arguments")
let expr = args[0]
let name = args[1]
var result = expr
var acc = expr
for i in 2..<args.len:
result = cljList(@[cljSymbol("let"), cljVector(@[name, result]), args[i]])
result
acc = cljList(@[cljSymbol("let"), cljVector(@[name, acc]), args[i]])
acc
)
# some->
defineMacro("some->", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "some-> requires at least 2 arguments")
var result = args[0]
var acc = args[0]
for i in 1..<args.len:
let step = args[i]
let gs = cljSymbol(gensymName("st_"))
@@ -347,17 +346,17 @@ proc initBuiltinMacros*() =
threaded = cljList(@[step, gs])
else:
threaded = step
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]),
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
cljNil(), threaded])])
result
acc
)
# some->>
defineMacro("some->>", proc(args: seq[CljVal]): CljVal =
if args.len < 2:
raise newException(CatchableError, "some->> requires at least 2 arguments")
var result = args[0]
var acc = args[0]
for i in 1..<args.len:
let step = args[i]
let gs = cljSymbol(gensymName("stg_"))
@@ -370,10 +369,10 @@ proc initBuiltinMacros*() =
threaded = cljList(@[step, gs])
else:
threaded = step
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]),
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
cljNil(), threaded])])
result
acc
)
# for (simplified — single binding only)
@@ -458,7 +457,6 @@ proc initBuiltinMacros*() =
let body = args[1..^1]
if binding.kind != ckVector or binding.items.len != 2:
raise newException(CatchableError, "with-open requires [name init]")
let sym = binding.items[0]
let init = binding.items[1]
let gs = cljSymbol(gensymName("wo_"))
cljList(@[cljSymbol("let"), cljVector(@[gs, init]),
+56 -6
View File
@@ -44,7 +44,22 @@ proc readNumberOrSym(s: string, i: var int): string =
var start = i
# handle negative sign or standalone operators
if s[i] == '-' or s[i] == '+':
if i + 1 < s.len and s[i+1] in Digits:
# Check for negative float without leading zero: -.5
if i + 2 < s.len and s[i+1] == '.' and s[i+2] in Digits:
inc i # skip sign
inc i # skip dot
while i < s.len and s[i] in Digits:
inc i
# Handle scientific notation: -.5e-3
if i < s.len and (s[i] == 'e' or s[i] == 'E'):
inc i
if i < s.len and (s[i] == '-' or s[i] == '+'):
inc i
if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits:
inc i
return s[start..<i]
elif i + 1 < s.len and s[i+1] in Digits:
inc i
while i < s.len and s[i] in Digits:
inc i
@@ -52,6 +67,14 @@ proc readNumberOrSym(s: string, i: var int): string =
inc i
while i < s.len and s[i] in Digits:
inc i
# Handle scientific notation: 1e5, -1.5e-3
if i < s.len and (s[i] == 'e' or s[i] == 'E'):
inc i
if i < s.len and (s[i] == '-' or s[i] == '+'):
inc i
if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits:
inc i
return s[start..<i]
else:
# it's a symbol like - or +
@@ -59,13 +82,22 @@ proc readNumberOrSym(s: string, i: var int): string =
while i < s.len and isSymChar(s[i]):
inc i
return s[start..<i]
elif s[i] in Digits:
elif s[i] in Digits or (s[i] == '.' and i + 1 < s.len and s[i+1] in Digits):
if s[i] == '.':
inc i # skip leading dot
while i < s.len and s[i] in Digits:
inc i
if i < s.len and s[i] == '.':
inc i
while i < s.len and s[i] in Digits:
inc i
if i < s.len and (s[i] == 'e' or s[i] == 'E'):
inc i
if i < s.len and (s[i] == '-' or s[i] == '+'):
inc i
if i < s.len and s[i] in Digits:
while i < s.len and s[i] in Digits:
inc i
return s[start..<i]
else:
while i < s.len and isSymChar(s[i]):
@@ -95,21 +127,39 @@ proc readAtom(s: string, i: var int): CljVal =
isNumber = false
else:
startIdx = 1
var sawExp = false
for j in startIdx..<tok.len:
if tok[j] == '.':
if isFloat:
isNumber = false
break
isFloat = true
elif tok[j] == 'e' or tok[j] == 'E':
if sawExp:
isNumber = false
break
sawExp = true
isFloat = true
if j + 1 < tok.len and (tok[j+1] == '-' or tok[j+1] == '+'):
discard # skip exponent sign
elif tok[j] notin Digits:
if sawExp and (tok[j] == '-' or tok[j] == '+'):
if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'):
continue
isNumber = false
break
if isNumber and tok.len > startIdx:
if isFloat:
return cljFloat(parseFloat(tok))
else:
return cljInt(parseInt(tok).int64)
var hasDigit = false
for j in startIdx..<tok.len:
if tok[j] in Digits:
hasDigit = true
break
if hasDigit:
if isFloat:
return cljFloat(parseFloat(tok))
else:
return cljInt(parseInt(tok).int64)
# symbol
return cljSymbol(tok)
+85
View File
@@ -67,6 +67,14 @@ proc cljReprLocal(v: CljVal): string =
for i in 0..<v.mapKeys.len:
parts.add(cljReprLocal(v.mapKeys[i]) & " " & cljReprLocal(v.mapVals[i]))
result = "{" & parts.join(", ") & "}"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems: parts.add(cljReprLocal(item))
result = "#{" & parts.join(" ") & "}"
of ckFn: result = "#<fn:" & v.fnName & ">"
of ckAtom: result = "(atom " & cljReprLocal(v.atomVal) & ")"
of ckTransient: result = "#<transient>"
of ckAgent: result = "(agent " & cljReprLocal(v.agentVal) & ")"
proc buildProgram(state: ReplState, form: CljVal, isDef: bool): string =
var allForms = state.defs
@@ -268,6 +276,8 @@ proc runHumanRepl*(state: var ReplState) =
echo ":clear Clear all definitions"
echo ":ns Show current namespace"
echo ":ai <desc> Ask AI to generate Clojure code"
echo ":optimize <code> Ask AI to suggest optimizations"
echo ":debug <expr> Ask AI to debug an expression"
of ":defs":
for d in state.defs:
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
@@ -288,6 +298,36 @@ proc runHumanRepl*(state: var ReplState) =
echo "🤖 Thinking..."
let aiRes = ai_assist.generateCode(description)
echo ai_assist.formatSuggestion(aiRes)
elif cmd.startsWith(":optimize "):
let code = cmd[10..^1].strip()
if code.len == 0:
echo "Usage: :optimize <code>"
elif not ai_assist.hasAiConfig():
echo "No AI API key configured."
else:
echo "🤖 Analyzing for optimizations..."
let aiRes = ai_assist.optimizeCode(code)
echo ai_assist.formatSuggestion(aiRes)
elif cmd.startsWith(":debug "):
let expr = cmd[7..^1].strip()
if expr.len == 0:
echo "Usage: :debug <expression>"
elif not ai_assist.hasAiConfig():
echo "No AI API key configured."
else:
echo "🤖 Debugging..."
var evalRes: CljVal
try:
let res = eval.eval(expr, state.evalEnv)
if not res.ok:
evalRes = cljString("Error: " & res.error)
else:
evalRes = res.value
except CatchableError as e:
evalRes = cljString("Error: " & e.msg)
let resultStr = if evalRes.kind == ckString: evalRes.strVal else: cljReprLocal(evalRes)
let aiRes = ai_assist.debugCode(expr, resultStr)
echo ai_assist.formatSuggestion(aiRes)
else:
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
@@ -386,6 +426,51 @@ proc runJsonRepl*(state: var ReplState) =
var clearRes = %*{ "status": "ok", "cleared": true }
echo $clearRes
of "ai-generate":
let description = req.getOrDefault("description").getStr("")
if description.len == 0:
echo $(%*{ "status": "error", "error": { "type": "repl/missing-description", "message": "Missing :description field" } })
elif not ai_assist.hasAiConfig():
echo $(%*{ "status": "error", "error": { "type": "repl/no-ai-config", "message": "No AI API key configured" } })
else:
let aiRes = ai_assist.generateCode(description)
if aiRes.ok:
echo $(%*{ "status": "ok", "code": %aiRes.suggestion })
else:
echo $(%*{ "status": "error", "error": { "type": "repl/ai-error", "message": %aiRes.suggestion } })
of "ai-optimize":
let code = req.getOrDefault("code").getStr("")
if code.len == 0:
echo $(%*{ "status": "error", "error": { "type": "repl/missing-code", "message": "Missing :code field" } })
elif not ai_assist.hasAiConfig():
echo $(%*{ "status": "error", "error": { "type": "repl/no-ai-config", "message": "No AI API key configured" } })
else:
let aiRes = ai_assist.optimizeCode(code)
if aiRes.ok:
echo $(%*{ "status": "ok", "suggestion": %aiRes.suggestion })
else:
echo $(%*{ "status": "error", "error": { "type": "repl/ai-error", "message": %aiRes.suggestion } })
of "ai-debug":
let expr = req.getOrDefault("expression").getStr("")
if expr.len == 0:
echo $(%*{ "status": "error", "error": { "type": "repl/missing-expression", "message": "Missing :expression field" } })
elif not ai_assist.hasAiConfig():
echo $(%*{ "status": "error", "error": { "type": "repl/no-ai-config", "message": "No AI API key configured" } })
else:
var resultStr = ""
try:
let evalRes = eval.eval(expr, state.evalEnv)
resultStr = if evalRes.ok: cljReprLocal(evalRes.value) else: "Error: " & evalRes.error
except CatchableError as e:
resultStr = "Error: " & e.msg
let aiRes = ai_assist.debugCode(expr, resultStr)
if aiRes.ok:
echo $(%*{ "status": "ok", "analysis": %aiRes.suggestion, "result": %resultStr })
else:
echo $(%*{ "status": "error", "error": { "type": "repl/ai-error", "message": %aiRes.suggestion } })
of "quit":
var quitRes = %*{ "status": "ok", "bye": true }
echo $quitRes
+1 -1
View File
@@ -8,7 +8,7 @@ const
type
CljKind* = enum
ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol,
ckList, ckVector, ckMap, ckFn, ckAtom
ckList, ckVector, ckMap, ckSet, ckFn, ckAtom, ckTransient, ckAgent
CljVal* = ref CljValObj
CljValObj = object
+22 -1
View File
@@ -4,9 +4,10 @@ import sequtils, strutils
type
CljKind* = enum
ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol,
ckList, ckVector, ckMap
ckList, ckVector, ckMap, ckSet, ckFn, ckAtom, ckTransient, ckAgent
CljVal* = ref object
meta*: CljVal
case kind*: CljKind
of ckNil: discard
of ckBool: boolVal*: bool
@@ -19,6 +20,15 @@ type
of ckMap:
mapKeys*: seq[CljVal]
mapVals*: seq[CljVal]
of ckSet: setItems*: seq[CljVal]
of ckFn:
fnName*: string
fnBody*: seq[CljVal]
of ckAtom: atomVal*: CljVal
of ckTransient:
transKind*: CljKind
transVec*: seq[CljVal]
of ckAgent: agentVal*: CljVal
proc cljNil*(): CljVal = CljVal(kind: ckNil)
proc cljBool*(v: bool): CljVal = CljVal(kind: ckBool, boolVal: v)
@@ -33,6 +43,12 @@ proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, items: item
proc cljMap*(keys: seq[CljVal], vals: seq[CljVal]): CljVal =
CljVal(kind: ckMap, mapKeys: keys, mapVals: vals)
proc cljSet*(items: seq[CljVal]): CljVal = CljVal(kind: ckSet, setItems: items)
proc cljFn*(name: string, body: seq[CljVal]): CljVal = CljVal(kind: ckFn, fnName: name, fnBody: body)
proc cljAtom*(val: CljVal): CljVal = CljVal(kind: ckAtom, atomVal: val)
proc cljTransient*(kind: CljKind): CljVal = CljVal(kind: ckTransient, transKind: kind)
proc cljAgent*(val: CljVal): CljVal = CljVal(kind: ckAgent, agentVal: val)
proc cljMapFromPairs*(pairs: seq[(CljVal, CljVal)]): CljVal =
var ks: seq[CljVal] = @[]
var vs: seq[CljVal] = @[]
@@ -58,3 +74,8 @@ proc `$`*(v: CljVal): string =
for i in 0..<v.mapKeys.len:
parts.add($v.mapKeys[i] & " " & $v.mapVals[i])
"{" & parts.join(", ") & "}"
of ckSet: "#{" & v.setItems.mapIt($it).join(" ") & "}"
of ckFn: "#<fn:" & v.fnName & ">"
of ckAtom: "(atom " & $v.atomVal & ")"
of ckTransient: "#<transient:" & $v.transKind & ">"
of ckAgent: "(agent " & $v.agentVal & ")"