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