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:
+86
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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 & ")"
|
||||
|
||||
Reference in New Issue
Block a user