feat: AI-assisted compilation and code generation

- New src/ai_assist.nim module with DeepSeek/OpenAI/MiMo API support
- AI explains compiler errors automatically when compilation fails
- New CLI command: cljnim ai '<description>' for code generation
- REPL :ai command for interactive AI assistance
- API keys read from environment vars (DEEPSEEK_API_KEY, OPENAI_API_KEY, MIMO_API_KEY)
- Tests for prompt building and response formatting
- Updated README with AI integration docs
This commit is contained in:
2026-05-08 22:21:27 +03:00
parent 6bed638300
commit 6249d8a251
7 changed files with 382 additions and 32 deletions
+2
View File
@@ -12,4 +12,6 @@ test_pvec
test_pmap
test_deps
test_eval
test_macros
test_ai_assist
books/
+16 -2
View File
@@ -1,9 +1,9 @@
.PHONY: build test clean run-example bench bench-aot
.PHONY: build test check clean run-example bench bench-aot
NIMCACHE = nimcache
build:
nim c -o:cljnim src/cljnim.nim
nim c -d:ssl -o:cljnim src/cljnim.nim
test:
nim c -r tests/test_reader.nim
@@ -11,7 +11,9 @@ test:
nim c -r tests/test_pvec.nim
nim c -r tests/test_pmap.nim
nim c -r tests/test_deps.nim
nim c -r tests/test_macros.nim
nim c -r tests/test_eval.nim
nim c --path:src -r tests/test_ai_assist.nim
test-reader:
nim c -r tests/test_reader.nim
@@ -19,6 +21,13 @@ test-reader:
test-emitter:
nim c -r tests/test_emitter.nim
check: build test
./cljnim run examples/hello.clj
./cljnim run examples/math.clj
./cljnim run examples/core.clj
./cljnim run examples/interop.clj
./cljnim run examples/macros.clj
run-example: build
./cljnim run examples/hello.clj
@@ -36,3 +45,8 @@ clean:
find . -name "test_reader" -delete
find . -name "test_emitter" -delete
find . -name "test_deps" -delete
find . -name "test_pvec" -delete
find . -name "test_pmap" -delete
find . -name "test_eval" -delete
find . -name "test_macros" -delete
find . -name "test_ai_assist" -delete
+72 -10
View File
@@ -103,6 +103,58 @@ $ ./cljnim repl --json
See [`docs/AI_FIRST.md`](docs/AI_FIRST.md) for the full AI integration design.
## AI-Powered Assistance
Clojure/Nim can use paid AI APIs (DeepSeek, OpenAI, Xiaomi MiMo) to help you write and debug code.
### Setup
Set one of these environment variables:
```bash
# DeepSeek (recommended, fast & cheap)
export DEEPSEEK_API_KEY="sk-..."
# OpenAI / OpenRouter
export OPENAI_API_KEY="sk-..."
# Xiaomi MiMo
export MIMO_API_KEY="..."
```
### Features
**Auto-error explanation** — When compilation fails, the compiler automatically asks AI for a fix:
```bash
$ ./cljnim run broken.clj
Compilation failed
Error: type mismatch: got <int> but expected <string>
💡 AI Suggestion:
The function `greet` expects a string but you passed an integer.
Fix: (defn greet [name] (str "Hello " name))
```
**REPL AI command** — Generate code interactively:
```bash
$ ./cljnim repl
user> :ai function that calculates fibonacci using loop/recur
🤖 Thinking...
💡 AI Suggestion:
(defn fib [n]
(loop [a 0 b 1 i n]
(if (= i 0)
a
(recur b (+ a b) (dec i)))))
```
**CLI code generation** — Generate code from terminal:
```bash
$ ./cljnim ai "function that reads a file line by line"
(defn read-lines [filename]
(let [content (slurp filename)]
(clojure.string/split-lines content)))
```
## Architecture
```
@@ -131,14 +183,25 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
│ ├── reader.nim # Clojure reader / EDN parser
│ ├── emitter.nim # Nim code generator
│ ├── repl.nim # Human & AI REPL
│ ├── eval.nim # Tree-walking interpreter
│ ├── deps.nim # Dependency resolution
│ ├── core.nim # Core runtime functions
│ ├── types.nim # Clojure value types
│ ├── macros.nim # Macro expansion engine
│ └── runtime.nim # Runtime implementation
├── lib/
── cljnim_runtime.nim # Clojure runtime in Nim
── cljnim_runtime.nim # Clojure runtime in Nim
│ ├── cljnim_pvec.nim # Persistent Vector (HAMT)
│ ├── cljnim_pmap.nim # Persistent Map (HAMT)
│ └── cljnim_async.nim # core.async channels
├── examples/
│ ├── hello.clj
── math.clj
── math.clj
│ ├── core.clj
│ ├── macros.clj
│ ├── ffi.clj
│ ├── interop.clj
│ └── ai_tools.clj
├── docs/ # Documentation (EN + BG)
├── tests/
├── Makefile
@@ -148,19 +211,18 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
## Supported Features
### Working Now ✅
- Reader: numbers, strings, symbols, keywords, lists `()`, vectors `[]`
- Reader: numbers, strings, symbols, keywords, lists `()`, vectors `[]`, maps `{}`
- Special forms: `def`, `defn`, `fn`, `let`, `if`, `do`, `quote`
- Arithmetic: `+`, `-`, `*`, `/`, `=`, `<`, `>`
- Functions: `println`
- Functions: `println`, `map`, `filter`, `reduce`
- Human REPL and JSON REPL
- AOT compilation to native binaries
### In Progress 🔄
- Persistent data structures (Vector, Map, Set)
- `defmacro` system
- Persistent data structures (Vector, Map, Set) via HAMT
- Macro system (`defmacro`, `->`, `->>`, `and`, `or`, `when`, `cond`)
- Nim/C interop
- File operations from REPL
- Git operations from REPL
- File and Git operations from REPL
- Tree-walking interpreter for fast REPL eval
- Atoms, Agents, and core.async channels
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap.
+160
View File
@@ -0,0 +1,160 @@
# AI Assistance for Clojure/Nim Compiler
# 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]
type
AiProvider* = enum
aiDeepSeek
aiOpenAiCompatible
AiConfig* = object
provider*: AiProvider
apiKey*: string
baseUrl*: string
model*: string
timeoutMs*: int
AiResponse* = object
ok*: bool
suggestion*: string
rawJson*: string
proc detectConfig*(): AiConfig =
## Auto-detect AI configuration from environment variables
result.timeoutMs = 15000
# DeepSeek
let deepseekKey = getEnv("DEEPSEEK_API_KEY", "")
if deepseekKey.len > 0:
return AiConfig(
provider: aiDeepSeek,
apiKey: deepseekKey,
baseUrl: "https://api.deepseek.com",
model: getEnv("DEEPSEEK_MODEL", "deepseek-chat"),
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
)
# OpenAI-compatible (Xiaomi MiMo, OpenRouter, etc.)
let openaiKey = getEnv("OPENAI_API_KEY", "")
if openaiKey.len > 0:
return AiConfig(
provider: aiOpenAiCompatible,
apiKey: openaiKey,
baseUrl: getEnv("OPENAI_BASE_URL", "https://api.openai.com"),
model: getEnv("OPENAI_MODEL", "gpt-4o-mini"),
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
)
# Xiaomi MiMo (OpenAI-compatible)
let mimoKey = getEnv("MIMO_API_KEY", "")
if mimoKey.len > 0:
return AiConfig(
provider: aiOpenAiCompatible,
apiKey: mimoKey,
baseUrl: getEnv("MIMO_BASE_URL", "https://api.mi-mo.ai"),
model: getEnv("MIMO_MODEL", "mimo-chat"),
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
)
# No API key found
return AiConfig(provider: aiDeepSeek, apiKey: "", baseUrl: "", model: "", timeoutMs: 0)
proc hasAiConfig*(): bool =
detectConfig().apiKey.len > 0
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.
**File:** """ & fileName & """
**Source code:**
```clojure
""" & sourceCode & """
```
**Compiler error:**
```
""" & errorMsg & """
```
Please explain the error in simple terms and suggest a fix. Keep your response under 200 words.
If the error is in Clojure code, show the corrected Clojure snippet.
Respond in the same language as the user's source code comments (Bulgarian or English).
"""
proc buildGenerationPrompt*(description: string): string =
## Build a prompt for AI code generation
result = """You are an expert Clojure programmer. Generate a Clojure function based on this description:
""" & description & """
Requirements:
- Use idiomatic Clojure
- Include docstring
- Use loop/recur instead of recursion if possible
- Return ONLY the Clojure code, no explanations
"""
proc callAiApi*(config: AiConfig, prompt: string): AiResponse =
## Call the AI API and return the response
if config.apiKey.len == 0:
return AiResponse(ok: false, suggestion: "No AI API key configured. Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY environment variable.")
let client = newHttpClient(timeout = config.timeoutMs)
defer: client.close()
let url = config.baseUrl & "/v1/chat/completions"
let body = %*{
"model": config.model,
"messages": [
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 800
}
client.headers["Authorization"] = "Bearer " & config.apiKey
client.headers["Content-Type"] = "application/json"
try:
let resp = client.post(url, body = $body)
let respBody = resp.body
result.rawJson = respBody
if resp.code.int != 200:
return AiResponse(ok: false, suggestion: "AI API error (HTTP " & $resp.code.int & "): " & respBody)
let jsonResp = parseJson(respBody)
if jsonResp.hasKey("choices") and jsonResp["choices"].len > 0:
let content = jsonResp["choices"][0]["message"]["content"].getStr("")
return AiResponse(ok: true, suggestion: content, rawJson: respBody)
else:
return AiResponse(ok: false, suggestion: "Unexpected AI API response format", rawJson: respBody)
except CatchableError as e:
return AiResponse(ok: false, suggestion: "AI request failed: " & e.msg)
proc explainError*(errorMsg, sourceCode, fileName: string): AiResponse =
## High-level helper: explain a compiler error using AI
let config = detectConfig()
let prompt = buildErrorPrompt(errorMsg, sourceCode, fileName)
return callAiApi(config, prompt)
proc generateCode*(description: string): AiResponse =
## High-level helper: generate Clojure code from description
let config = detectConfig()
let prompt = buildGenerationPrompt(description)
return callAiApi(config, prompt)
proc formatSuggestion*(response: AiResponse): string =
## Format AI response for terminal display
if not response.ok:
return "💡 AI: " & response.suggestion
var res = "💡 AI Suggestion:\n"
for line in response.suggestion.splitLines():
res.add(" " & line & "\n")
return res
+57 -9
View File
@@ -1,6 +1,6 @@
# Clojure/Nim — AI-First Clojure Compiler
import os, osproc, strutils, times
import reader, emitter, types, repl, macros, deps
import reader, emitter, types, repl, macros, deps, ai_assist
proc getLibPath(): string =
let appDir = getAppDir()
@@ -44,7 +44,7 @@ proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
alias = req.items[2].symName
result.add((libName, alias))
proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[string] = @[], libMode: bool = false) =
let source = readFile(inputPath)
let forms = reader.readAll(source)
@@ -97,11 +97,17 @@ proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string]
f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
allForms.add(f)
let nimCode = emitter.emitProgram(allForms)
let nimCode = if libMode: emitter.emitProgramLib(allForms) else: emitter.emitProgram(allForms)
writeFile(outputPath, nimCode)
echo "Generated: ", outputPath
proc nimCompile(nimPath: string, binPath: string, release: bool = false): int =
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:
@@ -109,7 +115,8 @@ proc nimCompile(nimPath: string, binPath: string, release: bool = false): int =
cmd.add(" --path:" & quoteShell(libPath))
cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath))
echo "Compiling: ", cmd
execCmd(cmd)
let (output, exitCode) = execCmdEx(cmd)
return (exitCode, output)
proc makeTempDir(): string =
let pid = getCurrentProcessId()
@@ -143,9 +150,14 @@ proc runFile*(inputPath: string) =
if useCache:
echo "Using cached: ", cacheNimPath
let compileResult = nimCompile(cacheNimPath, cacheBinPath)
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)
@@ -153,9 +165,14 @@ proc runFile*(inputPath: string) =
compileFile(inputPath, nimPath, depPaths)
let compileResult = nimCompile(nimPath, binPath)
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)
@@ -188,6 +205,7 @@ proc main() =
echo " cljnim repl [--json] Start REPL (human or AI mode)"
echo " cljnim deps Resolve dependencies from deps.edn"
echo " cljnim -e '<code>' Evaluate expression"
echo " cljnim ai '<description>' Generate Clojure code with AI"
echo ""
echo "REPL Commands:"
echo " :quit, :q Exit REPL"
@@ -215,15 +233,19 @@ proc main() =
let forms = reader.readAll(code)
let nimCode = emitter.emitProgram(forms)
writeFile(nimPath, nimCode)
let compileResult = nimCompile(nimPath, binPath)
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:
@@ -232,6 +254,14 @@ proc main() =
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:
@@ -298,6 +328,24 @@ proc main() =
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)
else:
stderr.writeLine("Unknown command: " & cmd)
quit(1)
+30 -11
View File
@@ -1,6 +1,6 @@
# AI-First REPL for Clojure/Nim
import os, osproc, strutils, json, times, net
import reader, emitter, types, macros, eval
import reader, emitter, types, macros, eval, ai_assist
type
ReplMode* = enum
@@ -193,13 +193,18 @@ proc evalForm*(state: var ReplState, formStr: string): JsonNode =
let elapsed = (epochTime() - startTime) * 1000
if not ok:
var errObj = %*{
"type": "compiler/error",
"message": error,
"form": formStr
}
if ai_assist.hasAiConfig():
let aiRes = ai_assist.explainError(error, formStr, "REPL input")
if aiRes.ok:
errObj["ai_suggestion"] = %aiRes.suggestion
return %*{
"status": "error",
"error": {
"type": "compiler/error",
"message": error,
"form": formStr
},
"error": errObj,
"meta": {
"ns": state.ns,
"ms": elapsed
@@ -262,6 +267,7 @@ proc runHumanRepl*(state: var ReplState) =
echo ":defs List defined vars"
echo ":clear Clear all definitions"
echo ":ns Show current namespace"
echo ":ai <desc> Ask AI to generate Clojure code"
of ":defs":
for d in state.defs:
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
@@ -272,11 +278,25 @@ proc runHumanRepl*(state: var ReplState) =
of ":ns":
echo state.ns
else:
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
echo "=> " & res["result"]["printed"].getStr()
if cmd.startsWith(":ai "):
let description = cmd[4..^1].strip()
if description.len == 0:
echo "Usage: :ai <description>"
elif not ai_assist.hasAiConfig():
echo "No AI API key configured. Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY."
else:
echo "🤖 Thinking..."
let aiRes = ai_assist.generateCode(description)
echo ai_assist.formatSuggestion(aiRes)
else:
echo "Error: " & res["error"]["message"].getStr()
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
echo "=> " & res["result"]["printed"].getStr()
else:
echo "Error: " & res["error"]["message"].getStr()
if res["error"].hasKey("ai_suggestion"):
echo ""
echo ai_assist.formatSuggestion(AiResponse(ok: true, suggestion: res["error"]["ai_suggestion"].getStr("")))
proc runJsonRepl*(state: var ReplState) =
var readyMsg = %*{ "status": "ready", "ns": state.ns, "mode": "json" }
@@ -385,7 +405,6 @@ proc handleClient(state: var ReplState, client: Socket) =
var readyMsg = $(%*{ "status": "ready", "ns": state.ns, "mode": "nrepl" }) & "\n"
client.send(readyMsg)
var buf = ""
while true:
try:
var line = ""
+45
View File
@@ -0,0 +1,45 @@
# Tests for AI assistance module (no real API calls)
import unittest, strutils
import os
import ai_assist
suite "AI Config Detection":
test "detectConfig returns empty when no env vars set":
# Note: this test assumes no API keys are set in test environment
# If keys ARE set, it will detect them
let cfg = detectConfig()
# At minimum, the function should not crash
check cfg.timeoutMs >= 0
test "hasAiConfig returns false when no keys set (if env empty)":
let key = getEnv("DEEPSEEK_API_KEY", "")
let okey = getEnv("OPENAI_API_KEY", "")
let mkey = getEnv("MIMO_API_KEY", "")
if key.len == 0 and okey.len == 0 and mkey.len == 0:
check hasAiConfig() == false
suite "Prompt Building":
test "buildErrorPrompt includes filename and source":
let prompt = buildErrorPrompt("type mismatch", "(defn foo [x] x)", "test.clj")
check "test.clj" in prompt
check "type mismatch" in prompt
check "(defn foo [x] x)" in prompt
test "buildGenerationPrompt includes description":
let prompt = buildGenerationPrompt("reverse a list")
check "reverse a list" in prompt
check "Clojure" in prompt
suite "Response Formatting":
test "formatSuggestion shows error when not ok":
let resp = AiResponse(ok: false, suggestion: "No key configured")
let formatted = formatSuggestion(resp)
check "💡 AI:" in formatted
check "No key configured" in formatted
test "formatSuggestion shows suggestion when ok":
let resp = AiResponse(ok: true, suggestion: "Use (reverse coll)")
let formatted = formatSuggestion(resp)
check "💡 AI Suggestion:" in formatted
check "Use (reverse coll)" in formatted