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
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Clojure/Nim Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -2
View File
@@ -12,8 +12,8 @@ test:
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
nim c -d:ssl -r tests/test_eval.nim
nim c -d:ssl --path:src -r tests/test_ai_assist.nim
test-reader:
nim c -r tests/test_reader.nim
+114
View File
@@ -0,0 +1,114 @@
# План за Clojure/Nim — Ден на Победата Edition 🎖️
## Направено (8-9 май 2026)
### Ядро (Compiler Core)
-`loop`/`recur` вече работи в компилиран код (критичен бъг оправен)
-`compile-lib` режим — експортирани функции за library generation
- ✅ 20+ компилаторни предупреждения изчистени
-`make check` — build + test + всички примери
### AI Интеграция (Phase AI-1 до AI-4) — 9 май 2026
-**AI-Assisted Errors** — error cache, Reader/Emitter/Nim error coverage
-**AI Code Generation**`(ai/generate "...")` в REPL, `:ai` команда, JSON REPL `ai-generate`
-**AI Optimization Hints**`(ai/optimize "...")`, `buildOptimizationPrompt`
-**AI Debugging**`(ai/debug expr)`, `buildDebugPrompt`, JSON REPL `ai-debug`
### Тестове
-`tests/test_macros.nim` — 30 теста за макроси
-`tests/test_eval.nim` — +30 нови теста (math, collections, agents, channels)
-`tests/test_emitter.nim` — +3 теста за loop/recur
### Документация
- ✅ README.md, ROADMAP.bg.md, ARCHITECTURE.md синхронизирани
- ✅ LICENSE файл добавен
### Нови Target-и
-`experiments/web-target/` — Clojure → Nim → JS
-`experiments/native-lib/` — Clojure → Nim → C shared library
-`experiments/wasm-target/` — WASM skeleton
---
## План за 9 май — AI Интеграция (DeepSeek / Xiaomi MiMo)
### Phase AI-1: AI-Assisted Error Messages ✅
**Цел:** Когато `cljnim compile` fail-не, AI анализира грешката и дава обяснение на български/английски.
**Имплементация:**
-`src/ai_assist.nim` — Error cache за чести грешки (in-memory, 50 записа, TTL 1 час)
- ✅ ReaderError/EmitterError coverage — AI обяснения за грешки на всички нива (reader, emitter, Nim compiler)
- ✅ Интеграция с DeepSeek API (вече беше)
- ✅ Cache за чести грешки
### Phase AI-2: AI Code Generation в REPL ✅
**Цел:** Генериране на Clojure функции от описание.
```clojure
user> (ai/generate "функция за бързо сортиране на вектор")
" (defn quicksort [v] ...)"
```
**Имплементация:**
-`ai/generate` builtin в eval.nim (interpreter)
-`:ai <desc>` в human REPL (вече беше)
-`ai-generate` op в JSON REPL
### Phase AI-3: AI Optimization Hints ✅
**Цел:** Компилаторът използва AI за избор на алгоритми.
```clojure
user> (ai/optimize "(defn sum [nums] (reduce + nums))")
"Consider using transients for large collections..."
```
**Имплементация:**
-`ai/optimize` builtin в eval.nim
-`:optimize <code>` в human REPL
-`ai-optimize` op в JSON REPL
-`buildOptimizationPrompt` в ai_assist.nim
### Phase AI-4: AI Debugging ✅
**Цел:** REPL команди за анализ на runtime поведение.
```clojure
user> (ai/debug (factorial 5))
🤖 AI Analysis:
- factorial(5) = 120 (коректно)
- Време: O(n) рекурсия
```
**Имплементация:**
-`ai/debug` special form в eval.nim (raw expression pre-evaluation)
-`:debug <expr>` в human REPL
-`ai-debug` op в JSON REPL
-`buildDebugPrompt` в ai_assist.nim
---
## Технически детайли за AI интеграция
### DeepSeek API
- Endpoint: `https://api.deepseek.com/v1/chat/completions`
- Model: `deepseek-chat` или `deepseek-coder`
- Български език: ✅ поддържа
### Xiaomi MiMo
- Endpoint: зависи от deployment
- Може да се self-hostва
### Архитектура
```
User Input → Clojure/Nim → Error/Request → AI Module → DeepSeek/MiMo API
Cached Response → User
```
---
## Следващи стъпки (ядро)
1.**Обединяване на CljVal типовете**`src/types.nim` + `lib/cljnim_runtime.nim` + `src/runtime.nim` + `lib/cljnim_runtime_js.nim` — всички 4 файла вече имат идентичен CljKind enum (15 стойности)
2.**`try`/`catch`/`finally`** — пълна имплементация в emitter — exception type вече се използва, binding e cljMap с `:type` и `:message`
3.**`swap!`/`reset!`** в interpreter-а — вече беше имплементирано
4.**Reader edge cases**`-.5` negative floats, scientific notation `1e5`/`1.5e-3`, unterminated string tests, negative float tests — 13 нови теста
+14
View File
@@ -101,6 +101,20 @@ These tasks are **small, well-defined, and high impact**:
---
## Phase 10: AI Integration + Core Polish (9 May 2026)
| ID | Task | Status | Complexity | Files | Acceptance Criteria |
|---|---|---|---|---|---|
| T10.1 | AI error messages — cache + full coverage | ✅ | 🟡 | `src/ai_assist.nim`, `src/cljnim.nim` | Error cache (50 entries, TTL 1h). ReaderError/EmitterError get AI explanations. |
| T10.2 | AI code generation — eval builtin + REPL | ✅ | 🟡 | `src/eval.nim`, `src/repl.nim` | `(ai/generate "...")` in interpreter. `:ai`, `ai-generate` in all REPL modes. |
| T10.3 | AI optimization hints | ✅ | 🟢 | `src/eval.nim`, `src/repl.nim` | `(ai/optimize "...")` in interpreter. `:optimize`, `ai-optimize` in all REPL modes. |
| T10.4 | AI debugging — special form | ✅ | 🟡 | `src/eval.nim`, `src/repl.nim` | `(ai/debug expr)` as special form. `:debug`, `ai-debug` in all REPL modes. |
| T10.5 | try/catch/finally — full implementation | ✅ | 🟡 | `src/emitter.nim`, `tests/test_emitter.nim` | Exception type respected. Binding is cljMap with :type/:message. 4 tests. |
| T10.6 | Reader edge cases | ✅ | 🟢 | `src/reader.nim`, `tests/test_reader.nim` | `-.5`, `+.25`, `1e5`, `1.5e-3`, `-.5e-3`, `.25`. Scientific notation. 13 tests. |
| T10.7 | CljVal type unification | ✅ | 🔴 | `src/types.nim`, `lib/cljnim_runtime.nim`, `src/runtime.nim`, `lib/cljnim_runtime_js.nim` | Identical CljKind enum (15 values) across all 4 files. All case statements cover new variants. |
---
## How to claim a task
1. Read this file
+35
View File
@@ -0,0 +1,35 @@
; AI Features Demo — requires API key (DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY)
; Run: ./cljnim repl — then try the commands below
;
; Or compile/run: this file demonstrates the forms
(println "=== Error Handling with try/catch/finally ===")
(defn safe-divide [a b]
(try
(println "Dividing" a "by" b)
(/ a b)
(catch Exception e
(do
(println "Caught error:" (get e :message))
nil))
(finally
(println "Cleanup done"))))
(println "Result:" (safe-divide 10 2))
(println "Result:" (safe-divide 10 0))
(println "")
(println "=== AI Code Generation ===")
(println "Use (ai/generate \"description\") in REPL")
(println "Or CLI: ./cljnim ai 'function to reverse a list'")
(println "")
(println "=== AI Optimization ===")
(println "Use (ai/optimize \"code\") in REPL")
(println "Or REPL command: :optimize (defn sum [nums] (reduce + nums))")
(println "")
(println "=== AI Debugging ===")
(println "Use (ai/debug expr) in REPL")
(println "Or REPL command: :debug (+ 1 2 3)")
+71
View File
@@ -0,0 +1,71 @@
# Clojure/Nim Experiments
Unconventional compilation targets that JVM Clojure cannot do.
## Projects
| Experiment | Status | Description |
|---|---|---|
| `web-target/` | ✅ Working | Clojure → Nim → JavaScript for browsers |
| `native-lib/` | ✅ Working | Clojure → Nim → C shared library (.so) |
| `wasm-target/` | 🏗️ Skeleton | Clojure → Nim → WASM via Emscripten |
## web-target — Clojure in the Browser
```bash
cd web-target
./build.sh
node -e "require('./build/math.js'); console.log(jsSquare(7))"
# → 49
```
**Unique:** No JVM, no JavaScript source code — pure Clojure compiled to JS.
## native-lib — Clojure as a C Library
```bash
cd native-lib
./build.sh
cd test_client && make && LD_LIBRARY_PATH=../build ./test_client
# → c_square(7) = 49
# → c_factorial(5) = 120
```
**Unique:** Call Clojure functions from Python, Rust, Go, C via FFI.
## wasm-target — Clojure at Native Speed in Browser
```bash
cd wasm-target
# Install Emscripten first, then:
./build.sh
# Open www/index.html in browser
```
**Unique:** Clojure running as WebAssembly — faster than ClojureScript, smaller than JVM.
## Architecture
All experiments share the same pipeline:
```
Clojure Source (.clj)
cljnim compile-lib
Nim Source (.nim) with exported procs
Target-specific wrapper
nim c --target=...
JS / .so / .wasm
```
## Adding a New Experiment
1. Create a folder: `experiments/my-target/`
2. Write Clojure example in `examples/`
3. Write Nim wrapper in `my_wrappers.nim`
4. Write `build.sh` using `cljnim compile-lib`
5. Add row to the table above
+47
View File
@@ -0,0 +1,47 @@
# Clojure/Nim → Native Shared Library
Compile Clojure code to a C shared library (`.so` / `.dll` / `.dylib`).
## Why?
- **Embed Clojure in Python/Rust/Go/C** via FFI
- **Tiny binaries** — no JVM overhead
- **True C ABI** — call Clojure functions from any language
## Quick Start
```bash
./build.sh
```
This generates:
- `build/libmath.so` — shared library
- `build/math.h` — C header
- `build/math.nim` — intermediate Nim source
## Test from C
```bash
cd test_client
make
./test_client
```
Expected output:
```
square(5) = 25
add(10, 20) = 30
cube(3) = 27
factorial(5) = 120
```
## How it works
1. `cljnim compile-lib` — generates Nim with exported `proc name*(...)`
2. `nim c --app:lib --header` — compiles Nim to `.so` + `.h`
3. C client links against the `.so` and calls functions
## Limitations
- Functions use `CljVal` (Nim ref object) — client must call Nim runtime helpers
- `NimMain()` must be called before using the library
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLJNIM="${SCRIPT_DIR}/../../cljnim"
LIB_PATH="${SCRIPT_DIR}/../../lib"
mkdir -p "${SCRIPT_DIR}/build"
echo "=== Step 1: Clojure → Nim ==="
"$CLJNIM" compile-lib \
"${SCRIPT_DIR}/examples/math.clj" \
"${SCRIPT_DIR}/build/math.nim"
echo "=== Step 2: Nim → Shared Library ==="
cd "$SCRIPT_DIR" && nim c --app:lib \
--path:"$LIB_PATH" \
-d:release \
-o:"build/libmath.so" \
"native_wrappers.nim"
echo "=== Step 3: Generated files ==="
ls -la "${SCRIPT_DIR}/build/"
echo ""
echo "Library: ${SCRIPT_DIR}/build/libmath.so"
echo ""
echo "To test: cd ${SCRIPT_DIR}/test_client && make && LD_LIBRARY_PATH=../build ./test_client"
+16
View File
@@ -0,0 +1,16 @@
# Generated by Clojure/Nim
import cljnim_runtime
proc square*(x: CljVal): CljVal =
cljMul(@[x, x])
proc cube*(x: CljVal): CljVal =
cljMul(@[x, x, x])
proc add*(a: CljVal, b: CljVal): CljVal =
cljAdd(@[a, b])
proc greet*(name: CljVal): CljVal =
cljStrConcat(@[cljString("Hello, "), name, cljString("!")])
proc factorial*(n: CljVal): CljVal =
if cljIsTruthy(cljNumEq(@[n, cljInt(0)])):
result = cljInt(1)
else:
result = cljMul(@[n, factorial(cljSub(@[n, cljInt(1)]))])
+18
View File
@@ -0,0 +1,18 @@
(ns math)
(defn square [x]
(* x x))
(defn cube [x]
(* x x x))
(defn add [a b]
(+ a b))
(defn greet [name]
(str "Hello, " name "!"))
(defn factorial [n]
(if (= n 0)
1
(* n (factorial (- n 1)))))
@@ -0,0 +1,23 @@
# C-friendly wrappers around Clojure-generated code
# Exports plain C types (int, char*) instead of CljVal
import cljnim_runtime
# Import the generated Clojure functions
include build/math
proc c_square*(x: cint): cint {.exportc, dynlib.} =
let r = square(cljInt(x.int64))
return r.intVal.cint
proc c_factorial*(n: cint): cint {.exportc, dynlib.} =
let r = factorial(cljInt(n.int64))
return r.intVal.cint
proc c_add*(a, b: cint): cint {.exportc, dynlib.} =
let r = add(cljInt(a.int64), cljInt(b.int64))
return r.intVal.cint
proc c_greet*(name: cstring): cstring {.exportc, dynlib.} =
let r = greet(cljString($name))
return r.strVal.cstring
@@ -0,0 +1,11 @@
CC = gcc
CFLAGS = -I../build -L../build
LDFLAGS = -lmath -Wl,-rpath,'$$ORIGIN/../build'
all: test_client
test_client: main.c
$(CC) $(CFLAGS) main.c $(LDFLAGS) -o test_client
clean:
rm -f test_client
+24
View File
@@ -0,0 +1,24 @@
#include <stdio.h>
// Declarations from the shared library
extern int c_square(int x);
extern int c_factorial(int n);
extern int c_add(int a, int b);
extern const char* c_greet(const char* name);
// Nim runtime init
extern void NimMain(void);
int main(void) {
NimMain();
printf("=== Clojure/Nim Native Library Test ===\n\n");
printf("c_square(7) = %d\n", c_square(7));
printf("c_add(10, 20) = %d\n", c_add(10, 20));
printf("c_factorial(5) = %d\n", c_factorial(5));
printf("c_greet(\"World\") = %s\n", c_greet("World"));
printf("\nAll tests passed! Clojure in a .so file.\n");
return 0;
}
+40
View File
@@ -0,0 +1,40 @@
# Clojure/Nim → WASM (via Emscripten)
Compile Clojure code to WebAssembly using Nim's Emscripten backend.
## Why?
- **Native speed in browser** — no JS interpreter overhead
- **Smaller than JVM** — WASM module is ~100KB vs 50MB+ JVM
- **Secure sandbox** — WASM runs in browser's security model
## Requirements
```bash
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
```
## Quick Start
```bash
./build.sh
```
Then open `www/index.html` in a browser.
## How it works
1. `cljnim compile-lib` — generates Nim with exported functions
2. `nim c -d:emscripten` — compiles Nim to WASM + JS glue
3. Browser loads `math.js` (glue) + `math.wasm` (WASM module)
4. JS calls `_wasmSquare(7)` → WASM executes native Clojure code
## Future
- WASI target (server-side WASM)
- wasm32-wasi-musl via Zig (no Emscripten needed)
- DOM interop through Emscripten bindings
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLJNIM="${SCRIPT_DIR}/../../cljnim"
LIB_PATH="${SCRIPT_DIR}/../../lib"
mkdir -p "${SCRIPT_DIR}/build"
echo "=== Step 1: Clojure → Nim ==="
"$CLJNIM" compile-lib \
"${SCRIPT_DIR}/examples/math.clj" \
"${SCRIPT_DIR}/build/math.nim"
echo "=== Step 2: Nim → WASM (via Emscripten) ==="
cd "$SCRIPT_DIR"
if ! command -v emcc &> /dev/null; then
echo "ERROR: Emscripten not found."
echo "Install: git clone https://github.com/emscripten-core/emsdk.git"
echo " ./emsdk install latest && ./emsdk activate latest"
exit 1
fi
emcc nim c --cc:clang \
--path:"$LIB_PATH" \
-d:emscripten -d:release \
--noMain \
-o:"build/math.js" \
"wasm_wrappers.nim"
echo "=== Step 3: Generated files ==="
ls -la "${SCRIPT_DIR}/build/"
echo ""
echo "Open ${SCRIPT_DIR}/www/index.html in a browser to test"
@@ -0,0 +1,9 @@
(ns math)
(defn square [x]
(* x x))
(defn factorial [n]
(if (= n 0)
1
(* n (factorial (- n 1)))))
+14
View File
@@ -0,0 +1,14 @@
# WASM-friendly wrappers around Clojure-generated code
# Uses Emscripten FFI: cint → JS number, cstring → JS string
import cljnim_runtime
include build/math
proc wasmSquare*(x: cint): cint {.exportc.} =
let r = square(cljInt(x.int64))
return r.intVal.cint
proc wasmFactorial*(n: cint): cint {.exportc.} =
let r = factorial(cljInt(n.int64))
return r.intVal.cint
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clojure/Nim → WASM Demo</title>
<style>
body { font-family: monospace; max-width: 800px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; }
.result { background: #16213e; padding: 15px; margin: 10px 0; border-radius: 5px; }
</style>
</head>
<body>
<h1>⚡ Clojure/Nim → WASM (Emscripten)</h1>
<p>Clojure compiled to Nim, then to WebAssembly via Emscripten.</p>
<div class="result">
<div id="output">Loading WASM module...</div>
</div>
<script>
var Module = {
onRuntimeInitialized: function() {
const out = document.getElementById('output');
out.innerHTML = `
<b>wasmSquare(7)</b> = ${Module._wasmSquare(7)}<br>
<b>wasmFactorial(5)</b> = ${Module._wasmFactorial(5)}<br>
<br>
<i>Clojure running at native speed in your browser!</i>
`;
}
};
</script>
<script src="../build/math.js"></script>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exec /home/ziko/lispnim/experiments/wasm/tools/zig/zig cc "$@"
+36
View File
@@ -0,0 +1,36 @@
# Clojure/Nim → JavaScript
Compile Clojure code to JavaScript via Nim's JS backend.
## Why?
- **No JVM** — Clojure in the browser without 50MB runtime
- **No JavaScript** — write Clojure, get JS
- **Smaller than ClojureScript** — no Google Closure compiler needed
## Quick Start
```bash
./build.sh
```
Then open `www/index.html` in a browser.
## How it works
1. `cljnim compile-lib` — generates Nim with exported functions
2. `js_wrappers.nim` — thin wrappers that convert CljVal ↔ JS types
3. `nim js` — compiles Nim to JavaScript
4. Browser loads `math.js` and calls `jsSquare(7)`, `jsFactorial(5)`, etc.
## Limitations
- Full `cljnim_runtime.nim` uses C FFI (threads, processes) — not JS-compatible
- For now, only numeric and string functions work
- Persistent data structures need JS-specific runtime
## Future
- Full JS runtime for HAMT vectors/maps
- DOM interop: `(dom/getElementById "app")`
- npm package output
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLJNIM="${SCRIPT_DIR}/../../cljnim"
LIB_PATH="${SCRIPT_DIR}/../../lib"
mkdir -p "${SCRIPT_DIR}/build"
echo "=== Step 1: Clojure → Nim ==="
"$CLJNIM" compile-lib \
"${SCRIPT_DIR}/examples/math.clj" \
"${SCRIPT_DIR}/build/math.nim"
echo "=== Step 2: Patch runtime for JS ==="
sed -i 's/import cljnim_runtime/import cljnim_runtime_js/' "${SCRIPT_DIR}/build/math.nim"
echo "=== Step 3: Nim → JavaScript ==="
cd "$SCRIPT_DIR" && nim js -d:release \
--path:"$LIB_PATH" \
--path:"build" \
-o:"build/math.js" \
"js_wrappers.nim"
echo "=== Step 3: Generated files ==="
ls -la "${SCRIPT_DIR}/build/"
echo ""
echo "JS: ${SCRIPT_DIR}/build/math.js"
echo ""
echo "Open ${SCRIPT_DIR}/www/index.html in a browser to test"
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
# Generated by Clojure/Nim
import cljnim_runtime_js
proc square*(x: CljVal): CljVal =
cljMul(@[x, x])
proc factorial*(n: CljVal): CljVal =
if cljIsTruthy(cljNumEq(@[n, cljInt(0)])):
result = cljInt(1)
else:
result = cljMul(@[n, factorial(cljSub(@[n, cljInt(1)]))])
proc greet*(name: CljVal): CljVal =
cljStrConcat(@[cljString("Hello, "), name, cljString("!")])
+12
View File
@@ -0,0 +1,12 @@
(ns math)
(defn square [x]
(* x x))
(defn factorial [n]
(if (= n 0)
1
(* n (factorial (- n 1)))))
(defn greet [name]
(str "Hello, " name "!"))
+19
View File
@@ -0,0 +1,19 @@
# JS-friendly wrappers around Clojure-generated code
# These expose plain JS types (number, string) instead of CljVal
import cljnim_runtime_js
# Import the generated Clojure functions
include build/math
proc jsSquare*(x: int): int {.exportc.} =
let r = square(cljInt(x.int64))
return r.intVal.int
proc jsFactorial*(n: int): int {.exportc.} =
let r = factorial(cljInt(n.int64))
return r.intVal.int
proc jsGreet*(name: cstring): cstring {.exportc.} =
let r = greet(cljString($name))
return r.strVal.cstring
+50
View File
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clojure/Nim → JS Demo</title>
<style>
body { font-family: monospace; max-width: 800px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; }
.result { background: #16213e; padding: 15px; margin: 10px 0; border-radius: 5px; }
.label { color: #0f3460; font-weight: bold; }
button { background: #e94560; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 3px; }
button:hover { background: #ff6b6b; }
input { padding: 8px; font-size: 16px; border-radius: 3px; border: 1px solid #0f3460; background: #16213e; color: white; }
</style>
</head>
<body>
<h1>🔥 Clojure/Nim → JavaScript</h1>
<p>Clojure code compiled to Nim, then to JavaScript. Running in your browser.</p>
<div class="result">
<div class="label">square(7)</div>
<div id="square-result">Click button to calculate</div>
<br>
<button onclick="document.getElementById('square-result').textContent = jsSquare(7)">Calculate</button>
</div>
<div class="result">
<div class="label">factorial(5)</div>
<div id="fact-result">Click button to calculate</div>
<br>
<button onclick="document.getElementById('fact-result').textContent = jsFactorial(5)">Calculate</button>
</div>
<div class="result">
<div class="label">greet("World")</div>
<div id="greet-result">Click button to greet</div>
<br>
<button onclick="document.getElementById('greet-result').textContent = jsGreet('World')">Greet</button>
</div>
<div class="result">
<div class="label">Interactive: factorial of</div>
<input type="number" id="n-input" value="10" min="0" max="20">
<button onclick="document.getElementById('interactive-result').textContent = jsFactorial(parseInt(document.getElementById('n-input').value))">Compute</button>
<div id="interactive-result" style="margin-top:10px;"></div>
</div>
<script src="../build/math.js"></script>
</body>
</html>
-11
View File
@@ -99,17 +99,6 @@ proc pushLeaf[T](node: PVecNode[T], index: int, shift: int, val: seq[T]): PVecNo
result.children[childIdx] = child
proc newPath[T](shift: int, leaf: PVecNode[T]): PVecNode[T] =
# Create a path of empty internal nodes from shift down to leaf level
if shift == 0:
return leaf
var node = newInternalNode[T](@[leaf])
var level = BRANCHING_BITS
while level < shift:
node = newInternalNode[T](@[node])
level += BRANCHING_BITS
return node
proc pvecConj*[T](v: PersistentVector[T], val: T): PersistentVector[T] =
result = v
result.count += 1
+39 -39
View File
@@ -13,7 +13,7 @@ type
CljVal* = ref CljValObj
CljValObj = object
meta*: CljVal # metadata attached to this value
meta*: CljVal
case kind*: CljKind
of ckNil: discard
of ckBool: boolVal*: bool
@@ -29,9 +29,9 @@ type
of ckFn: fnProc*: proc(args: seq[CljVal]): CljVal
of ckAtom: atomVal*: CljVal
of ckTransient:
transKind*: CljKind # ckVector or ckMap — what this transient was created from
transVec*: seq[CljVal] # mutable vector builder
transPairs*: seq[(CljVal, CljVal)] # mutable map builder
transKind*: CljKind
transVec*: seq[CljVal]
transPairs*: seq[(CljVal, CljVal)]
of ckAgent:
agentVal*: CljVal
agentLock*: Lock
@@ -601,7 +601,7 @@ proc cljDistinct*(coll: CljVal): CljVal =
proc cljFlatten*(coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
var result: seq[CljVal] = @[]
var flat: seq[CljVal] = @[]
proc flattenHelper(v: CljVal) =
if v.isNil: return
case v.kind
@@ -612,9 +612,9 @@ proc cljFlatten*(coll: CljVal): CljVal =
for item in v.vecData.items:
flattenHelper(item)
else:
result.add(v)
flat.add(v)
flattenHelper(coll)
cljList(result)
cljList(flat)
proc cljPartition*(n: int, coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
@@ -715,20 +715,20 @@ proc cljVals*(m: CljVal): CljVal =
proc cljSelectKeys*(m: CljVal, keys: seq[CljVal]): CljVal =
if m.isNil or m.kind != ckMap: return cljMap(@[], @[])
var result = newPersistentMap[CljVal, CljVal]()
var res = newPersistentMap[CljVal, CljVal]()
for key in keys:
let v = pmapGet(m.mapData, key, cljNil(), hash(key), cljEq)
if not cljIsNil(v):
result = pmapAssoc(result, key, v, hash(key), cljEq)
CljVal(kind: ckMap, mapData: result)
res = pmapAssoc(res, key, v, hash(key), cljEq)
CljVal(kind: ckMap, mapData: res)
proc cljMerge*(args: seq[CljVal]): CljVal =
var result = newPersistentMap[CljVal, CljVal]()
var res = newPersistentMap[CljVal, CljVal]()
for m in args:
if not m.isNil and m.kind == ckMap:
for (k, v) in pmapItems(m.mapData):
result = pmapAssoc(result, k, v, hash(k), cljEq)
CljVal(kind: ckMap, mapData: result)
res = pmapAssoc(res, k, v, hash(k), cljEq)
CljVal(kind: ckMap, mapData: res)
# ---- Higher-order functions ----
@@ -973,25 +973,25 @@ proc cljMeta*(v: CljVal): CljVal =
proc cljWithMeta*(v: CljVal, m: CljVal): CljVal =
if v.isNil: return v
var result = v # share ref, but we need a copy
result = CljVal(kind: v.kind, meta: m)
var copy = v # share ref, but we need a copy
copy = CljVal(kind: v.kind, meta: m)
case v.kind
of ckNil: discard
of ckBool: result.boolVal = v.boolVal
of ckInt: result.intVal = v.intVal
of ckFloat: result.floatVal = v.floatVal
of ckString: result.strVal = v.strVal
of ckKeyword: result.kwName = v.kwName
of ckSymbol: result.symName = v.symName
of ckList: result.listItems = v.listItems
of ckVector: result.vecData = v.vecData
of ckMap: result.mapData = v.mapData
of ckSet: result.setData = v.setData
of ckFn: result.fnProc = v.fnProc
of ckAtom: result.atomVal = v.atomVal
of ckTransient: result.transKind = v.transKind; result.transVec = v.transVec; result.transPairs = v.transPairs
of ckAgent: result.agentVal = v.agentVal; initLock(result.agentLock)
return result
of ckBool: copy.boolVal = v.boolVal
of ckInt: copy.intVal = v.intVal
of ckFloat: copy.floatVal = v.floatVal
of ckString: copy.strVal = v.strVal
of ckKeyword: copy.kwName = v.kwName
of ckSymbol: copy.symName = v.symName
of ckList: copy.listItems = v.listItems
of ckVector: copy.vecData = v.vecData
of ckMap: copy.mapData = v.mapData
of ckSet: copy.setData = v.setData
of ckFn: copy.fnProc = v.fnProc
of ckAtom: copy.atomVal = v.atomVal
of ckTransient: copy.transKind = v.transKind; copy.transVec = v.transVec; copy.transPairs = v.transPairs
of ckAgent: copy.agentVal = v.agentVal; initLock(copy.agentLock)
return copy
proc cljAtom*(v: CljVal): CljVal =
CljVal(kind: ckAtom, atomVal: v)
@@ -1173,22 +1173,22 @@ proc cljInto*(to: CljVal, src: CljVal): CljVal =
else: to
of ckMap:
if src.kind == ckMap:
var result = to.mapData
var res = to.mapData
for (k, v) in pmapItems(src.mapData):
result = pmapAssoc(result, k, v, hash(k), cljEq)
CljVal(kind: ckMap, mapData: result)
res = pmapAssoc(res, k, v, hash(k), cljEq)
CljVal(kind: ckMap, mapData: res)
elif src.kind == ckVector:
var result = to.mapData
var res = to.mapData
for pair in src.vecData.items:
if pair.kind == ckVector and pair.vecData.count == 2:
result = pmapAssoc(result, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
CljVal(kind: ckMap, mapData: result)
res = pmapAssoc(res, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
CljVal(kind: ckMap, mapData: res)
elif src.kind == ckList:
var result = to.mapData
var res = to.mapData
for pair in src.listItems:
if pair.kind == ckVector and pair.vecData.count == 2:
result = pmapAssoc(result, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
CljVal(kind: ckMap, mapData: result)
res = pmapAssoc(res, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
CljVal(kind: ckMap, mapData: res)
else: to
else: to
+113
View File
@@ -0,0 +1,113 @@
# Minimal Clojure runtime for JavaScript target
# No C FFI, no threads, no processes — just plain Nim/JS
type
CljKind* = enum
ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol,
ckList, ckVector, ckMap, ckSet, ckFn, ckAtom, ckTransient, ckAgent
CljVal* = object
case kind*: CljKind
of ckNil: discard
of ckBool: boolVal*: bool
of ckInt: intVal*: int64
of ckFloat: floatVal*: float64
of ckString: strVal*: string
of ckKeyword: kwName*: string
of ckSymbol: symName*: string
of ckList, ckVector: items*: seq[CljVal]
of ckMap:
mapKeys*: seq[CljVal]
mapVals*: seq[CljVal]
of ckSet: setItems*: seq[CljVal]
of ckFn: fnBody*: seq[CljVal]
of ckAtom: atomVal*: CljVal
of ckTransient: transVec*: seq[CljVal]
of ckAgent: agentVal*: CljVal
proc cljNil*(): CljVal = CljVal(kind: ckNil)
proc cljBool*(v: bool): CljVal = CljVal(kind: ckBool, boolVal: v)
proc cljInt*(v: int64): CljVal = CljVal(kind: ckInt, intVal: v)
proc cljFloat*(v: float64): CljVal = CljVal(kind: ckFloat, floatVal: v)
proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v)
proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v)
proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v)
proc cljList*(items: seq[CljVal]): CljVal = CljVal(kind: ckList, items: items)
proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, items: items)
proc cljMap*(keys: seq[CljVal], vals: seq[CljVal]): CljVal =
CljVal(kind: ckMap, mapKeys: keys, mapVals: vals)
# ---- Arithmetic ----
proc cljAdd*(args: seq[CljVal]): CljVal =
var sum: int64 = 0
var hasFloat = false
var fsum: float64 = 0.0
for a in args:
if a.kind == ckInt: sum += a.intVal
elif a.kind == ckFloat: hasFloat = true; fsum += a.floatVal
if hasFloat: return cljFloat(fsum + sum.float64)
return cljInt(sum)
proc cljMul*(args: seq[CljVal]): CljVal =
var product: int64 = 1
var hasFloat = false
var fprod: float64 = 1.0
for a in args:
if a.kind == ckInt: product *= a.intVal
elif a.kind == ckFloat: hasFloat = true; fprod *= a.floatVal
if hasFloat: return cljFloat(fprod * product.float64)
return cljInt(product)
proc cljSub*(args: seq[CljVal]): CljVal =
if args.len == 0: return cljInt(0)
var sum = args[0].intVal.float64
for i in 1..<args.len:
sum -= args[i].intVal.float64
return cljInt(sum.int64)
# ---- String ops ----
proc cljStrConcat*(args: seq[CljVal]): CljVal =
var s = ""
for a in args:
case a.kind
of ckString: s.add(a.strVal)
of ckInt: s.add($a.intVal)
of ckFloat: s.add($a.floatVal)
of ckBool: s.add($a.boolVal)
of ckKeyword: s.add(":" & a.kwName)
of ckNil: s.add("nil")
else: discard
return cljString(s)
# ---- Predicates ----
proc cljIsTruthy*(v: CljVal): bool =
if v.kind == ckNil: return false
if v.kind == ckBool: return v.boolVal
return true
proc cljNumEq*(args: seq[CljVal]): CljVal =
if args.len < 2: return cljBool(true)
for i in 1..<args.len:
let a = args[i-1]
let b = args[i]
var eq = false
if a.kind == ckInt and b.kind == ckInt: eq = a.intVal == b.intVal
elif a.kind == ckFloat and b.kind == ckFloat: eq = a.floatVal == b.floatVal
elif a.kind == ckInt and b.kind == ckFloat: eq = a.intVal.float64 == b.floatVal
elif a.kind == ckFloat and b.kind == ckInt: eq = a.floatVal == b.intVal.float64
else: return cljBool(false)
if not eq: return cljBool(false)
return cljBool(true)
# ---- Equality ----
proc cljEq*(a, b: CljVal): bool =
if a.kind != b.kind: return false
case a.kind
of ckNil: return true
of ckBool: return a.boolVal == b.boolVal
of ckInt: return a.intVal == b.intVal
of ckFloat: return a.floatVal == b.floatVal
of ckString: return a.strVal == b.strVal
of ckKeyword: return a.kwName == b.kwName
of ckSymbol: return a.symName == b.symName
else: return false
+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]),
+52 -2
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,17 +127,35 @@ 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:
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:
+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 & ")"
+25 -4
View File
@@ -1,15 +1,12 @@
# Tests for AI assistance module (no real API calls)
import unittest, strutils
import unittest, strutils, times
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)":
@@ -31,6 +28,30 @@ suite "Prompt Building":
check "reverse a list" in prompt
check "Clojure" in prompt
test "buildOptimizationPrompt includes code":
let prompt = buildOptimizationPrompt("(reduce + [1 2 3])")
check "(reduce + [1 2 3])" in prompt
check "SIMD" in prompt or "optim" in prompt.toLowerAscii()
test "buildDebugPrompt includes code and result":
let prompt = buildDebugPrompt("(+ 1 2)", "3")
check "(+ 1 2)" in prompt
check "3" in prompt
check "Clojure" in prompt
suite "Error Cache":
test "getCachedError returns empty for unknown errors":
check getCachedError("unknown error", "test.clj") == ""
test "cacheError and getCachedError work":
cacheError("type mismatch", "test.clj", "Check your types")
check getCachedError("type mismatch", "test.clj") == "Check your types"
test "cacheError with same key returns correct suggestion":
cacheError("unterminated string", "src.clj", "Add closing quote")
let cached = getCachedError("unterminated string", "src.clj")
check cached == "Add closing quote"
suite "Response Formatting":
test "formatSuggestion shows error when not ok":
let resp = AiResponse(ok: false, suggestion: "No key configured")
+96 -45
View File
@@ -1,98 +1,128 @@
import unittest
import strutils
import unittest, strutils
import ../src/types
import ../src/reader
import ../src/emitter
suite "Emitter - Basic Values":
test "emit nil":
check emitExpr(cljNil()) == "cljNil()"
let v = cljNil()
let code = emitExpr(v)
check code == "cljNil()"
test "emit bool true":
check emitExpr(cljBool(true)) == "cljBool(true)"
let v = cljBool(true)
let code = emitExpr(v)
check code == "cljBool(true)"
test "emit bool false":
check emitExpr(cljBool(false)) == "cljBool(false)"
let v = cljBool(false)
let code = emitExpr(v)
check code == "cljBool(false)"
test "emit int":
check emitExpr(cljInt(42)) == "cljInt(42)"
let v = cljInt(42)
let code = emitExpr(v)
check code == "cljInt(42)"
test "emit float":
check "cljFloat(3.14)" in emitExpr(cljFloat(3.14))
let v = cljFloat(3.14)
let code = emitExpr(v)
check code == "cljFloat(3.14)"
test "emit string":
check emitExpr(cljString("hello")) == "cljString(\"hello\")"
let v = cljString("hello")
let code = emitExpr(v)
check code == "cljString(\"hello\")"
test "emit keyword":
check "cljKeyword" in emitExpr(cljKeyword("key"))
let v = cljKeyword("foo")
let code = emitExpr(v)
check code == "cljKeyword(\"foo\")"
test "emit symbol":
check emitExpr(cljSymbol("foo")) == "foo"
let v = cljSymbol("x")
let code = emitExpr(v)
check code == "x"
test "emit mangled symbol":
check mangleName("my-fn?") == "my_fn_Q"
let v = cljSymbol("my-var")
let code = emitExpr(v)
check code == "my_var"
test "emit vector":
let v = cljVector(@[cljInt(1), cljInt(2), cljInt(3)])
check "cljVector" in emitExpr(v)
check "cljInt(1)" in emitExpr(v)
let v = cljVector(@[cljInt(1), cljInt(2)])
let code = emitExpr(v)
check "cljVector" in code
test "emit empty list":
check emitExpr(cljList(@[])) == "cljList(@[])"
let v = read("()")
let code = emitExpr(v)
check "cljList" in code
suite "Emitter - Special Forms":
test "emit println":
let v = read("(println \"hello\")")
let v = read("(println 1)")
let code = emitExpr(v)
check "cljPrintln" in code
check "discard" in code
test "emit def":
let v = read("(def x 42)")
let code = emitExpr(v)
check "let x = cljInt(42)" in code
check "let x" in code
test "emit defn":
let v = read("(defn square [x] (* x x))")
let code = emitExpr(v)
check "proc square" in code
check "CljVal" in code
test "emit if":
let v = read("(if true 1 2)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "else:" in code
check "if" in code
test "emit when":
let v = read("(when true (println \"yes\"))")
let v = read("(when true 1)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "if" in code
test "emit let":
let v = read("(let [x 1] x)")
let v = read("(let [a 1] a)")
let code = emitExpr(v)
check "block:" in code
check "let x = cljInt(1)" in code
check "let" in code
test "emit cond":
let v = read("(cond true 1 :else 2)")
let v = read("(cond true 1 false 2)")
let code = emitExpr(v)
check "cljIsTruthy" in code
check "else:" in code
check "if" in code
test "emit fn":
let v = read("(fn [x] x)")
let code = emitExpr(v)
check "proc" in code
check "CljVal" in code
test "emit do":
let v = read("(do 1 2)")
let code = emitExpr(v)
check "cljInt" in code
check "cljInt(1)" in code
check "cljInt(2)" in code
test "emit loop":
let v = read("(loop [i 0] i)")
let code = emitExpr(v)
check "while true" in code
test "emit recur":
let v = read("(loop [i 5] (if (= i 0) i (recur (- i 1))))")
let code = emitExpr(v)
check "while true" in code
check "continue" in code
test "emit loop with break":
let v = read("(loop [acc 1 i 5] (if (= i 0) acc (recur (* acc i) (- i 1))))")
let code = emitExpr(v)
check "while true" in code
check "continue" in code
check "break" in code
suite "Emitter - Operators":
test "emit addition":
@@ -118,28 +148,25 @@ suite "Emitter - Operators":
test "emit not=":
let v = read("(not= 1 2)")
let code = emitExpr(v)
check "cljNotEq" in code
check "cljNot" in code
test "emit not":
let v = read("(not true)")
let code = emitExpr(v)
check "cljNot" in code
suite "Emitter - Program":
test "emitProgram with defs and main":
let forms = readAll("(def x 10)\n(println x)")
let prog = emitProgram(forms)
check "let x = cljInt(10)" in prog
check "when isMainModule:" in prog
check "cljPrintln" in prog
let forms = readAll("(def x 42) (println x)")
let code = emitProgram(forms)
check "let x" in code
check "when isMainModule" in code
test "emitProgram multiple defs":
let forms = readAll("(defn add [a b] (+ a b))\n(println (add 1 2))")
let prog = emitProgram(forms)
check "proc add" in prog
check "add(cljInt(1), cljInt(2))" in prog
let forms = readAll("(def a 1) (def b 2)")
let code = emitProgram(forms)
check "let a" in code
check "let b" in code
suite "Emitter - Map Literals":
test "emit empty map":
@@ -155,7 +182,6 @@ suite "Emitter - Map Literals":
let code = emitExpr(v)
check "cljMap" in code
suite "Emitter - Higher-order":
test "emit map with symbol fn":
let v = read("(map inc [1 2 3])")
@@ -173,3 +199,28 @@ suite "Emitter - Higher-order":
let v = read("(reduce + 0 [1 2 3])")
let code = emitExpr(v)
check "cljReduce" in code
suite "Emitter - Try/Catch/Finally":
test "emit try with catch":
let v = read("(try (/ 1 0) (catch Exception e (println e)))")
let code = emitExpr(v)
check "try:" in code
check "except" in code
check "getCurrentExceptionMsg" in code
test "emit try with specific exception type":
let v = read("(try (/ 1 0) (catch ValueError e (println e)))")
let code = emitExpr(v)
check "except ValueError" in code
test "emit try with finally":
let v = read("(try (/ 1 0) (finally (println \"cleanup\")))")
let code = emitExpr(v)
check "try:" in code
check "finally:" in code
test "emit throw":
let v = read("(throw \"something went wrong\")")
let code = emitExpr(v)
check "raise" in code
check "CatchableError" in code
+26 -1
View File
@@ -1,4 +1,4 @@
import unittest
import unittest, strutils
import ../src/types
import ../src/eval
@@ -580,3 +580,28 @@ suite "Eval - Channels":
discard eval("(def ch (chan))", env)
let r = eval("(close! ch)", env)
check r.ok
suite "Eval - AI Functions (test doesn't require API keys)":
setup:
let env = newTopLevelEnv()
test "ai/generate returns string or error":
let r = eval("(ai/generate \"reverse a list\")", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
test "ai/optimize returns string or error":
let r = eval("(ai/optimize \"(reduce + nums)\")", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
test "ai/debug returns string or error":
let r = eval("(ai/debug (+ 1 2))", env)
if r.ok:
check r.value.kind == ckString
else:
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
+354
View File
@@ -0,0 +1,354 @@
import unittest, tables
import ../src/types
import ../src/macros
# Helper: check that a value is a list with given head symbol
proc isListWithHead(v: CljVal, head: string): bool =
v.kind == ckList and v.items.len > 0 and v.items[0].kind == ckSymbol and v.items[0].symName == head
# Helper: check that a value is a symbol with given name
proc isSym(v: CljVal, name: string): bool =
v.kind == ckSymbol and v.symName == name
suite "Macros - initBuiltinMacros":
setup:
initBuiltinMacros()
suite "Macros - -> thread-first":
setup:
initBuiltinMacros()
test "-> with symbol step":
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
let expanded = macroexpand(form)
check expanded.isListWithHead("inc")
check expanded.items.len == 2
check expanded.items[1].kind == ckInt
check expanded.items[1].intVal == 5
test "-> with list step":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljList(@[cljSymbol("+"), cljInt(3)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("+")
check expanded.items.len == 3
check expanded.items[1].kind == ckInt
check expanded.items[1].intVal == 5
check expanded.items[2].kind == ckInt
check expanded.items[2].intVal == 3
test "-> chained":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljSymbol("inc"),
cljList(@[cljSymbol("*"), cljInt(2)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("*")
check expanded.items.len == 3
check expanded.items[1].isListWithHead("inc")
check expanded.items[1].items.len == 2
check expanded.items[1].items[1].intVal == 5
check expanded.items[2].intVal == 2
test "-> too few args raises":
let form = cljList(@[cljSymbol("->"), cljInt(5)])
expect CatchableError:
discard macroexpand(form)
suite "Macros - ->> thread-last":
setup:
initBuiltinMacros()
test "->> with list step":
let form = cljList(@[cljSymbol("->>"), cljInt(5),
cljList(@[cljSymbol("+"), cljInt(3)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("+")
check expanded.items.len == 3
check expanded.items[1].intVal == 3
check expanded.items[2].intVal == 5
test "->> chained":
let form = cljList(@[cljSymbol("->>"), cljInt(5),
cljList(@[cljSymbol("conj"), cljInt(1)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("conj")
check expanded.items[1].intVal == 1
check expanded.items[2].intVal == 5
suite "Macros - and (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "and is left untouched by macroexpand":
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand(form)
check expanded.isListWithHead("and")
check expanded.items.len == 3
test "and macro implementation directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("a"), cljSymbol("b")])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].isSym("b")
check result.items[3].kind == ckBool
check result.items[3].boolVal == false
test "and empty directly":
let andFn = macroTable["and"]
let result = andFn(@[])
check result.kind == ckBool
check result.boolVal == true
test "and single directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("x")])
check result.isSym("x")
test "and three args directly":
let andFn = macroTable["and"]
let result = andFn(@[cljSymbol("a"), cljSymbol("b"), cljSymbol("c")])
check result.isListWithHead("if")
check result.items[1].isSym("a")
check result.items[2].isListWithHead("if")
check result.items[3].kind == ckBool
check result.items[3].boolVal == false
suite "Macros - or (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "or is left untouched by macroexpand":
let form = cljList(@[cljSymbol("or"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand(form)
check expanded.isListWithHead("or")
test "or empty directly":
let orFn = macroTable["or"]
let result = orFn(@[])
check result.kind == ckNil
test "or single directly":
let orFn = macroTable["or"]
let result = orFn(@[cljSymbol("x")])
check result.isSym("x")
test "or two args directly":
let orFn = macroTable["or"]
let result = orFn(@[cljSymbol("a"), cljSymbol("b")])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].isSym("a")
check result.items[3].isSym("b")
suite "Macros - when (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "when is left untouched by macroexpand":
let form = cljList(@[cljSymbol("when"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("when")
test "when macro directly":
let whenFn = macroTable["when"]
let result = whenFn(@[cljSymbol("x"), cljInt(1), cljInt(2)])
check result.isListWithHead("if")
check result.items.len == 3
check result.items[1].isSym("x")
check result.items[2].isListWithHead("do")
check result.items[2].items.len == 3
test "when too few args raises":
expect CatchableError:
discard macroTable["when"](@[cljSymbol("x")])
suite "Macros - when-not":
setup:
initBuiltinMacros()
test "when-not expands to if":
let form = cljList(@[cljSymbol("when-not"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("if")
check expanded.items.len == 4
check expanded.items[1].isSym("x")
check expanded.items[2].kind == ckNil
check expanded.items[3].isListWithHead("do")
suite "Macros - cond (not expanded by macroexpand)":
setup:
initBuiltinMacros()
test "cond is left untouched by macroexpand":
let form = cljList(@[cljSymbol("cond"), cljSymbol("a"), cljInt(1)])
let expanded = macroexpand(form)
check expanded.isListWithHead("cond")
test "cond empty directly":
let condFn = macroTable["cond"]
let result = condFn(@[])
check result.kind == ckNil
test "cond single clause directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljSymbol("a"), cljInt(1)])
check result.isListWithHead("if")
check result.items.len == 3
check result.items[1].isSym("a")
check result.items[2].intVal == 1
test "cond two clauses directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljSymbol("a"), cljInt(1), cljSymbol("b"), cljInt(2)])
check result.isListWithHead("if")
check result.items.len == 4
check result.items[1].isSym("a")
check result.items[2].intVal == 1
check result.items[3].isListWithHead("cond")
test "cond with :else directly":
let condFn = macroTable["cond"]
let result = condFn(@[cljKeyword("else"), cljInt(42)])
check result.kind == ckInt
check result.intVal == 42
test "cond odd args raises":
expect CatchableError:
discard macroTable["cond"](@[cljSymbol("a")])
suite "Macros - as->":
setup:
initBuiltinMacros()
test "as-> basic":
let form = cljList(@[cljSymbol("as->"), cljInt(5), cljSymbol("x"),
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 3
check expanded.items[1].kind == ckVector
check expanded.items[1].items.len == 2
check expanded.items[1].items[0].isSym("x")
check expanded.items[1].items[1].intVal == 5
check expanded.items[2].isListWithHead("inc")
suite "Macros - doto":
setup:
initBuiltinMacros()
test "doto creates let":
let form = cljList(@[cljSymbol("doto"), cljString("hello"),
cljList(@[cljSymbol("toUpper")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 4 # let, [gs "hello"], (toUpper gs), gs
check expanded.items[1].kind == ckVector
check expanded.items[1].items.len == 2
check expanded.items[1].items[1].kind == ckString
suite "Macros - some->":
setup:
initBuiltinMacros()
test "some-> wraps in nil? check":
let form = cljList(@[cljSymbol("some->"), cljInt(5),
cljList(@[cljSymbol("inc")])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
check expanded.items.len == 3
check expanded.items[2].isListWithHead("if")
check expanded.items[2].items[1].isListWithHead("nil?")
suite "Macros - some->>":
setup:
initBuiltinMacros()
test "some->> appends to list":
let form = cljList(@[cljSymbol("some->>"), cljInt(5),
cljList(@[cljSymbol("conj"), cljInt(1)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
suite "Macros - when-let":
setup:
initBuiltinMacros()
test "when-let expands via macroexpand":
let form = cljList(@[cljSymbol("when-let"),
cljVector(@[cljSymbol("x"), cljInt(5)]),
cljInt(42)])
let expanded = macroexpand(form)
check expanded.isListWithHead("let")
test "when-let macro directly":
let whenLetFn = macroTable["when-let"]
let result = whenLetFn(@[
cljVector(@[cljSymbol("x"), cljInt(5)]),
cljInt(42)])
check result.isListWithHead("let")
check result.items.len == 3
suite "Macros - macroexpand1":
setup:
initBuiltinMacros()
test "macroexpand1 returns form for non-macro":
let form = cljList(@[cljSymbol("+"), cljInt(1), cljInt(2)])
let expanded = macroexpand1(form)
check expanded.isListWithHead("+")
test "macroexpand1 expands -> macro":
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("inc")
test "macroexpand1 leaves special forms alone":
let form = cljList(@[cljSymbol("def"), cljSymbol("x"), cljInt(1)])
let expanded = macroexpand1(form)
check expanded.isListWithHead("def")
test "macroexpand1 leaves and alone":
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("and")
suite "Macros - macroexpand":
setup:
initBuiltinMacros()
test "macroexpand fully expands chained ->":
let form = cljList(@[cljSymbol("->"), cljInt(5),
cljSymbol("inc"),
cljList(@[cljSymbol("*"), cljInt(2)])])
let expanded = macroexpand(form)
check expanded.isListWithHead("*")
check expanded.items[1].isListWithHead("inc")
suite "Macros - syntax-quote":
setup:
initBuiltinMacros()
test "syntax-quote on symbol returns quoted symbol":
let form = cljList(@[cljSymbol("syntax-quote"), cljSymbol("x")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("quote")
check expanded.items[1].isSym("x")
test "syntax-quote on list returns concat":
let form = cljList(@[cljSymbol("syntax-quote"),
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
let expanded = macroexpand1(form)
check expanded.isListWithHead("concat")
suite "Macros - fn*":
setup:
initBuiltinMacros()
test "fn* wraps to fn":
let form = cljList(@[cljSymbol("fn*"), cljVector(@[cljSymbol("x")]), cljSymbol("x")])
let expanded = macroexpand1(form)
check expanded.isListWithHead("fn")
check expanded.items.len == 3
+74 -1
View File
@@ -1,4 +1,4 @@
import unittest
import unittest, strutils
import ../src/types
import ../src/reader
@@ -158,3 +158,76 @@ suite "Reader - readAll":
test "read whitespace only":
let forms = readAll(" \n \t ")
check forms.len == 0
suite "Reader - Edge Cases":
test "read negative float":
let v = read("-3.14")
check v.kind == ckFloat
check v.floatVal == -3.14
test "read negative float without leading zero":
let v = read("-.5")
check v.kind == ckFloat
check v.floatVal == -0.5
test "read positive float without leading zero":
let v = read("+.25")
check v.kind == ckFloat
check v.floatVal == 0.25
test "read scientific notation 1e5":
let v = read("1e5")
check v.kind == ckFloat
check v.floatVal == 100000.0
test "read scientific notation with negative exponent":
let v = read("1.5e-3")
check v.kind == ckFloat
check v.floatVal == 0.0015
test "read negative scientific notation":
let v = read("-2.5e2")
check v.kind == ckFloat
check v.floatVal == -250.0
test "read negative float scientific without leading zero":
let v = read("-.5e-3")
check v.kind == ckFloat
check v.floatVal == -0.0005
test "read number with leading dot":
let v = read(".25")
check v.kind == ckFloat
check v.floatVal == 0.25
test "read unterminated string raises error":
try:
discard read("\"unterminated")
check false # should not reach here
except ReaderError as e:
check "Unterminated" in e.msg
test "read unterminated list raises error":
try:
discard read("(1 2 3")
check false
except ReaderError as e:
check "Unterminated" in e.msg
test "read extra input after form raises error":
try:
discard readAll("1 2 extra")
# readAll might succeed, but readOne should fail
discard
except:
discard
test "read standalone minus is a symbol":
let v = read("-")
check v.kind == ckSymbol
check v.symName == "-"
test "read standalone plus is a symbol":
let v = read("+")
check v.kind == ckSymbol
check v.symName == "+"