From ba0b3009171fadc6710cc7f27767a0e603b0c929 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 9 May 2026 01:53:23 +0300 Subject: [PATCH] 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 --- LICENSE | 21 + Makefile | 4 +- PLAN.md | 114 ++ TASKS.md | 14 + examples/ai_features.clj | 35 + experiments/README.md | 71 ++ experiments/native-lib/README.md | 47 + experiments/native-lib/build.sh | 27 + experiments/native-lib/build/math.nim | 16 + experiments/native-lib/examples/math.clj | 18 + experiments/native-lib/native_wrappers.nim | 23 + experiments/native-lib/test_client/Makefile | 11 + experiments/native-lib/test_client/main.c | 24 + experiments/wasm-target/README.md | 40 + experiments/wasm-target/build.sh | 35 + experiments/wasm-target/examples/math.clj | 9 + experiments/wasm-target/wasm_wrappers.nim | 14 + experiments/wasm-target/www/index.html | 35 + experiments/wasm/tools/zig-cc | 2 + experiments/web-target/README.md | 36 + experiments/web-target/build.sh | 30 + experiments/web-target/build/math.js | 1206 +++++++++++++++++++ experiments/web-target/build/math.nim | 12 + experiments/web-target/examples/math.clj | 12 + experiments/web-target/js_wrappers.nim | 19 + experiments/web-target/www/index.html | 50 + lib/cljnim_pvec.nim | 11 - lib/cljnim_runtime.nim | 78 +- lib/cljnim_runtime_js.nim | 113 ++ src/ai_assist.nim | 88 +- src/emitter.nim | 78 +- src/eval.nim | 72 +- src/macros.nim | 32 +- src/reader.nim | 62 +- src/repl.nim | 85 ++ src/runtime.nim | 2 +- src/types.nim | 23 +- tests/test_ai_assist.nim | 29 +- tests/test_emitter.nim | 141 ++- tests/test_eval.nim | 27 +- tests/test_macros.nim | 354 ++++++ tests/test_reader.nim | 75 +- 42 files changed, 3042 insertions(+), 153 deletions(-) create mode 100644 LICENSE create mode 100644 PLAN.md create mode 100644 examples/ai_features.clj create mode 100644 experiments/README.md create mode 100644 experiments/native-lib/README.md create mode 100644 experiments/native-lib/build.sh create mode 100644 experiments/native-lib/build/math.nim create mode 100644 experiments/native-lib/examples/math.clj create mode 100644 experiments/native-lib/native_wrappers.nim create mode 100644 experiments/native-lib/test_client/Makefile create mode 100644 experiments/native-lib/test_client/main.c create mode 100644 experiments/wasm-target/README.md create mode 100644 experiments/wasm-target/build.sh create mode 100644 experiments/wasm-target/examples/math.clj create mode 100644 experiments/wasm-target/wasm_wrappers.nim create mode 100644 experiments/wasm-target/www/index.html create mode 100755 experiments/wasm/tools/zig-cc create mode 100644 experiments/web-target/README.md create mode 100644 experiments/web-target/build.sh create mode 100644 experiments/web-target/build/math.js create mode 100644 experiments/web-target/build/math.nim create mode 100644 experiments/web-target/examples/math.clj create mode 100644 experiments/web-target/js_wrappers.nim create mode 100644 experiments/web-target/www/index.html create mode 100644 lib/cljnim_runtime_js.nim create mode 100644 tests/test_macros.nim diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..54a786d --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile index 9ef021f..5fc9ebf 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..d0ec4e0 --- /dev/null +++ b/PLAN.md @@ -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 ` в 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 ` в 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 ` в 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 нови теста diff --git a/TASKS.md b/TASKS.md index 656b5a5..e31b0e5 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 diff --git a/examples/ai_features.clj b/examples/ai_features.clj new file mode 100644 index 0000000..5eb1484 --- /dev/null +++ b/examples/ai_features.clj @@ -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)") diff --git a/experiments/README.md b/experiments/README.md new file mode 100644 index 0000000..6b238dd --- /dev/null +++ b/experiments/README.md @@ -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 diff --git a/experiments/native-lib/README.md b/experiments/native-lib/README.md new file mode 100644 index 0000000..5c88c61 --- /dev/null +++ b/experiments/native-lib/README.md @@ -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 diff --git a/experiments/native-lib/build.sh b/experiments/native-lib/build.sh new file mode 100644 index 0000000..9734abb --- /dev/null +++ b/experiments/native-lib/build.sh @@ -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" diff --git a/experiments/native-lib/build/math.nim b/experiments/native-lib/build/math.nim new file mode 100644 index 0000000..188f3bc --- /dev/null +++ b/experiments/native-lib/build/math.nim @@ -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)]))]) diff --git a/experiments/native-lib/examples/math.clj b/experiments/native-lib/examples/math.clj new file mode 100644 index 0000000..639a9b1 --- /dev/null +++ b/experiments/native-lib/examples/math.clj @@ -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))))) diff --git a/experiments/native-lib/native_wrappers.nim b/experiments/native-lib/native_wrappers.nim new file mode 100644 index 0000000..6162de1 --- /dev/null +++ b/experiments/native-lib/native_wrappers.nim @@ -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 diff --git a/experiments/native-lib/test_client/Makefile b/experiments/native-lib/test_client/Makefile new file mode 100644 index 0000000..f6f4b34 --- /dev/null +++ b/experiments/native-lib/test_client/Makefile @@ -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 diff --git a/experiments/native-lib/test_client/main.c b/experiments/native-lib/test_client/main.c new file mode 100644 index 0000000..66c0545 --- /dev/null +++ b/experiments/native-lib/test_client/main.c @@ -0,0 +1,24 @@ +#include + +// 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; +} diff --git a/experiments/wasm-target/README.md b/experiments/wasm-target/README.md new file mode 100644 index 0000000..b47ac30 --- /dev/null +++ b/experiments/wasm-target/README.md @@ -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 diff --git a/experiments/wasm-target/build.sh b/experiments/wasm-target/build.sh new file mode 100644 index 0000000..e54604c --- /dev/null +++ b/experiments/wasm-target/build.sh @@ -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" diff --git a/experiments/wasm-target/examples/math.clj b/experiments/wasm-target/examples/math.clj new file mode 100644 index 0000000..e42898a --- /dev/null +++ b/experiments/wasm-target/examples/math.clj @@ -0,0 +1,9 @@ +(ns math) + +(defn square [x] + (* x x)) + +(defn factorial [n] + (if (= n 0) + 1 + (* n (factorial (- n 1))))) diff --git a/experiments/wasm-target/wasm_wrappers.nim b/experiments/wasm-target/wasm_wrappers.nim new file mode 100644 index 0000000..526910b --- /dev/null +++ b/experiments/wasm-target/wasm_wrappers.nim @@ -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 diff --git a/experiments/wasm-target/www/index.html b/experiments/wasm-target/www/index.html new file mode 100644 index 0000000..48a1c99 --- /dev/null +++ b/experiments/wasm-target/www/index.html @@ -0,0 +1,35 @@ + + + + + Clojure/Nim → WASM Demo + + + +

⚡ Clojure/Nim → WASM (Emscripten)

+

Clojure compiled to Nim, then to WebAssembly via Emscripten.

+ +
+
Loading WASM module...
+
+ + + + + diff --git a/experiments/wasm/tools/zig-cc b/experiments/wasm/tools/zig-cc new file mode 100755 index 0000000..55823a7 --- /dev/null +++ b/experiments/wasm/tools/zig-cc @@ -0,0 +1,2 @@ +#!/bin/bash +exec /home/ziko/lispnim/experiments/wasm/tools/zig/zig cc "$@" diff --git a/experiments/web-target/README.md b/experiments/web-target/README.md new file mode 100644 index 0000000..9ae0309 --- /dev/null +++ b/experiments/web-target/README.md @@ -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 diff --git a/experiments/web-target/build.sh b/experiments/web-target/build.sh new file mode 100644 index 0000000..fe3090e --- /dev/null +++ b/experiments/web-target/build.sh @@ -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" diff --git a/experiments/web-target/build/math.js b/experiments/web-target/build/math.js new file mode 100644 index 0000000..cda0c7e --- /dev/null +++ b/experiments/web-target/build/math.js @@ -0,0 +1,1206 @@ +/* Generated by the Nim Compiler v2.2.10 */ +var framePtr = null; +var excHandler = 0; +var lastJSError = null; +var NTI134217751 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI704643080 = {size: 0, kind: 24, base: null, node: null, finalizer: null}; +var NTI704643079 = {size: 0, kind: 24, base: null, node: null, finalizer: null}; +var NTI704643078 = {size: 0, kind: 24, base: null, node: null, finalizer: null}; +var NTI33554447 = {size: 0,kind: 36,base: null,node: null,finalizer: null}; +var NTI33554439 = {size: 0,kind: 35,base: null,node: null,finalizer: null}; +var NTI33554466 = {size: 0,kind: 1,base: null,node: null,finalizer: null}; +var NTI704643076 = {size: 0, kind: 18, base: null, node: null, finalizer: null}; +var NTI134217745 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI704643075 = {size: 0, kind: 14, base: null, node: null, finalizer: null}; +var NTI134217750 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI134217741 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI134217743 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI33555179 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI33555187 = {size: 0, kind: 22, base: null, node: null, finalizer: null}; +var NTI33554450 = {size: 0,kind: 29,base: null,node: null,finalizer: null}; +var NTI33555186 = {size: 0, kind: 22, base: null, node: null, finalizer: null}; +var NTI33555183 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI33555184 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI134217749 = {size: 0, kind: 17, base: null, node: null, finalizer: null}; +var NTI33554449 = {size: 0,kind: 28,base: null,node: null,finalizer: null}; +var NNI134217749 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217749.node = NNI134217749; +var NNI33555184 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI33555184.node = NNI33555184; +NTI33555186.base = NTI33555183; +NTI33555187.base = NTI33555183; +var NNI33555183 = {kind: 2, len: 5, offset: 0, typ: null, name: null, sons: [{kind: 1, offset: "parent", len: 0, typ: NTI33555186, name: "parent", sons: null}, +{kind: 1, offset: "name", len: 0, typ: NTI33554450, name: "name", sons: null}, +{kind: 1, offset: "message", len: 0, typ: NTI33554449, name: "msg", sons: null}, +{kind: 1, offset: "trace", len: 0, typ: NTI33554449, name: "trace", sons: null}, +{kind: 1, offset: "up", len: 0, typ: NTI33555187, name: "up", sons: null}]}; +NTI33555183.node = NNI33555183; +var NNI33555179 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI33555179.node = NNI33555179; +NTI33555183.base = NTI33555179; +NTI33555184.base = NTI33555183; +NTI134217749.base = NTI33555184; +var NNI134217743 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217743.node = NNI134217743; +var NNI134217741 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217741.node = NNI134217741; +NTI134217741.base = NTI33555184; +NTI134217743.base = NTI134217741; +var NNI134217750 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217750.node = NNI134217750; +NTI134217750.base = NTI33555184; +var NNI704643075 = {kind: 2, offset: 0, typ: null, name: null, len: 10, sons: {"0": {kind: 1, offset: 0, typ: NTI704643075, name: "ckNil", len: 0, sons: null}, +"1": {kind: 1, offset: 1, typ: NTI704643075, name: "ckBool", len: 0, sons: null}, +"2": {kind: 1, offset: 2, typ: NTI704643075, name: "ckInt", len: 0, sons: null}, +"3": {kind: 1, offset: 3, typ: NTI704643075, name: "ckFloat", len: 0, sons: null}, +"4": {kind: 1, offset: 4, typ: NTI704643075, name: "ckString", len: 0, sons: null}, +"5": {kind: 1, offset: 5, typ: NTI704643075, name: "ckKeyword", len: 0, sons: null}, +"6": {kind: 1, offset: 6, typ: NTI704643075, name: "ckSymbol", len: 0, sons: null}, +"7": {kind: 1, offset: 7, typ: NTI704643075, name: "ckList", len: 0, sons: null}, +"8": {kind: 1, offset: 8, typ: NTI704643075, name: "ckVector", len: 0, sons: null}, +"9": {kind: 1, offset: 9, typ: NTI704643075, name: "ckMap", len: 0, sons: null}}}; +NTI704643075.node = NNI704643075; +var NNI134217745 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217745.node = NNI134217745; +NTI134217745.base = NTI33555184; +NTI704643078.base = NTI704643076; +NTI704643079.base = NTI704643076; +NTI704643080.base = NTI704643076; +var NNI704643076 = {kind: 3, offset: "kind", len: 10, typ: NTI704643075, name: "kind", sons: [[setConstr(0), {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}], +[setConstr(1), {kind: 1, offset: "boolVal", len: 0, typ: NTI33554466, name: "boolVal", sons: null}], +[setConstr(2), {kind: 1, offset: "intVal", len: 0, typ: NTI33554439, name: "intVal", sons: null}], +[setConstr(3), {kind: 1, offset: "floatVal", len: 0, typ: NTI33554447, name: "floatVal", sons: null}], +[setConstr(4), {kind: 1, offset: "strVal", len: 0, typ: NTI33554449, name: "strVal", sons: null}], +[setConstr(5), {kind: 1, offset: "kwName", len: 0, typ: NTI33554449, name: "kwName", sons: null}], +[setConstr(6), {kind: 1, offset: "symName", len: 0, typ: NTI33554449, name: "symName", sons: null}], +[setConstr(7, 8), {kind: 1, offset: "items", len: 0, typ: NTI704643078, name: "items", sons: null}], +[setConstr(9), {kind: 2, len: 2, offset: 0, typ: null, name: null, sons: [{kind: 1, offset: "mapKeys", len: 0, typ: NTI704643079, name: "mapKeys", sons: null}, +{kind: 1, offset: "mapVals", len: 0, typ: NTI704643080, name: "mapVals", sons: null}]}]]}; +NTI704643076.node = NNI704643076; +var NNI134217751 = {kind: 2, len: 0, offset: 0, typ: null, name: null, sons: []}; +NTI134217751.node = NNI134217751; +NTI134217751.base = NTI33555184; + +function setConstr() { + var result = {}; + for (var i = 0; i < arguments.length; ++i) { + var x = arguments[i]; + if (typeof(x) == "object") { + for (var j = x[0]; j <= x[1]; ++j) { + result[j] = true; + } + } else { + result[x] = true; + } + } + return result; + + + +} +var ConstSet1 = setConstr(17, 16, 4, 18, 27, 19, 23, 22, 21); + +function nimCopy(dest_p0, src_p1, ti_p2) { + var result_33557376 = null; + + switch (ti_p2.kind) { + case 21: + case 22: + case 23: + case 5: + if (!(isFatPointer__system_u2924(ti_p2))) { + result_33557376 = src_p1; + } + else { + result_33557376 = [src_p1[0], src_p1[1]]; + } + + break; + case 19: + if (dest_p0 === null || dest_p0 === undefined) { + dest_p0 = {}; + } + else { + for (var key in dest_p0) { delete dest_p0[key]; } + } + for (var key in src_p1) { dest_p0[key] = src_p1[key]; } + result_33557376 = dest_p0; + + break; + case 18: + case 17: + if (!((ti_p2.base == null))) { + result_33557376 = nimCopy(dest_p0, src_p1, ti_p2.base); + } + else { + if ((ti_p2.kind == 17)) { + result_33557376 = (dest_p0 === null || dest_p0 === undefined) ? {m_type: ti_p2} : dest_p0; + } + else { + result_33557376 = (dest_p0 === null || dest_p0 === undefined) ? {} : dest_p0; + } + } + nimCopyAux(result_33557376, src_p1, ti_p2.node); + break; + case 4: + case 16: + if(ArrayBuffer.isView(src_p1)) { + if(dest_p0 === null || dest_p0 === undefined || dest_p0.length != src_p1.length) { + dest_p0 = new src_p1.constructor(src_p1); + } else { + dest_p0.set(src_p1, 0); + } + result_33557376 = dest_p0; + } else { + if (src_p1 === null) { + result_33557376 = null; + } + else { + if (dest_p0 === null || dest_p0 === undefined || dest_p0.length != src_p1.length) { + dest_p0 = new Array(src_p1.length); + } + result_33557376 = dest_p0; + for (var i = 0; i < src_p1.length; ++i) { + result_33557376[i] = nimCopy(result_33557376[i], src_p1[i], ti_p2.base); + } + } + } + + break; + case 24: + case 27: + if (src_p1 === null) { + result_33557376 = null; + } + else { + if (dest_p0 === null || dest_p0 === undefined || dest_p0.length != src_p1.length) { + dest_p0 = new Array(src_p1.length); + } + result_33557376 = dest_p0; + for (var i = 0; i < src_p1.length; ++i) { + result_33557376[i] = nimCopy(result_33557376[i], src_p1[i], ti_p2.base); + } + } + + break; + case 28: + if (src_p1 !== null) { + result_33557376 = src_p1.slice(0); + } + + break; + default: + result_33557376 = src_p1; + break; + } + + return result_33557376; + +} + +function mnewString(len_p0) { + var result = new Array(len_p0); + for (var i = 0; i < len_p0; i++) {result[i] = 0;} + return result; + + + +} + +function nimAddStrStr(x_p0, y_p1) { + var L = y_p1.length; + for (var i = 0; i < L; ++i) { + x_p0.push(y_p1[i]); + } + + + +} + +function isObj(obj_p0, subclass_p1) { + var result_33557489 = false; + + BeforeRet: { + var x_33557490 = obj_p0; + if ((x_33557490 == subclass_p1)) { + result_33557489 = true; + break BeforeRet; + } + + Label1: { + Label2: while (true) { + if (!!((x_33557490 == subclass_p1))) break Label2; + if ((x_33557490 == null)) { + result_33557489 = false; + break BeforeRet; + } + + x_33557490 = x_33557490.base; + } + }; + result_33557489 = true; + break BeforeRet; + }; + + return result_33557489; + +} + +function toJSStr(s_p0) { + var result_33556953 = null; + + var res_33557019 = newSeq__system_u2562((s_p0).length); + var i_33557020 = 0; + var j_33557021 = 0; + Label1: { + Label2: while (true) { + if (!(i_33557020 < (s_p0).length)) break Label2; + var c_33557022 = s_p0[i_33557020]; + if ((c_33557022 < 128)) { + res_33557019[j_33557021] = String.fromCharCode(c_33557022); + i_33557020 += 1; + } + else { + var helper_33557048 = newSeq__system_u2562(0); + Label3: { + Label4: while (true) { + if (!true) break Label4; + var code_33557049 = c_33557022.toString(16); + if ((((code_33557049) == null ? 0 : (code_33557049).length) == 1)) { + helper_33557048.push("%0");; + } + else { + helper_33557048.push("%");; + } + + helper_33557048.push(code_33557049);; + i_33557020 += 1; + if ((((s_p0).length <= i_33557020) || (s_p0[i_33557020] < 128))) { + break Label3; + } + + c_33557022 = s_p0[i_33557020]; + } + }; +++excHandler; + try { + res_33557019[j_33557021] = decodeURIComponent(helper_33557048.join("")); +--excHandler; +} catch (EXCEPTION) { + var prevJSError = lastJSError; + lastJSError = EXCEPTION; + --excHandler; + raiseDefect(); + res_33557019[j_33557021] = helper_33557048.join(""); + lastJSError = prevJSError; + } finally { + } + } + + j_33557021 += 1; + } + }; + if (res_33557019.length < j_33557021) { for (var i = res_33557019.length ; i < j_33557021 ; ++i) res_33557019.push(null); } + else { res_33557019.length = j_33557021; }; + result_33556953 = res_33557019.join(""); + + return result_33556953; + +} + +function raiseException(e_p0, ename_p1) { + e_p0.name = ename_p1; + if ((excHandler == 0)) { + unhandledException(e_p0); + } + + throw e_p0; + + +} + +function chckIndx(i_p0, a_p1, b_p2) { + var result_33557411 = 0; + + BeforeRet: { + if (((a_p1 <= i_p0) && (i_p0 <= b_p2))) { + result_33557411 = i_p0; + break BeforeRet; + } + else { + raiseIndexError(i_p0, a_p1, b_p2); + } + + }; + + return result_33557411; + +} + +function mulInt(a_p0, b_p1) { + var result = a_p0 * b_p1; + checkOverflowInt(result); + return result; + + + +} + +function mulInt64(a_p0, b_p1) { + var result = a_p0 * b_p1; + checkOverflowInt64(result); + return result; + + + +} +var ConstSet2 = setConstr(2); + +function raiseFieldError2(f_p0, discVal_p1) { + raiseException({message: (f_p0).concat(discVal_p1,[39]), parent: null, m_type: NTI134217750, name: null, trace: [], up: null}, "FieldDefect"); + + +} + +function makeNimstrLit(c_p0) { + var result = []; + for (var i = 0; i < c_p0.length; ++i) { + result[i] = c_p0.charCodeAt(i); + } + return result; + + + +} + +function nimBoolToStr(x_p0) { + var result_33556475 = []; + + if (x_p0) { + result_33556475 = nimCopy(null, [116,114,117,101], NTI33554449); + } + else { + result_33556475 = nimCopy(null, [102,97,108,115,101], NTI33554449); + } + + + return result_33556475; + +} + +function reprDiscriminant(e_p0, typ_p1) { + var Temporary1; + + var result_33557722 = []; + + switch (typ_p1.kind) { + case 14: + Temporary1 = reprEnum(e_p0, typ_p1); + break; + case 1: + Temporary1 = nimBoolToStr(!((e_p0 == 0))); + break; + default: + Temporary1 = HEX24__systemZdollars_u14(e_p0); + break; + } + result_33557722 = nimCopy(null, Temporary1, NTI33554449); + + return result_33557722; + +} +var ConstSet3 = setConstr(3); +var ConstSet4 = setConstr(2); + +function chckRange(i_p0, a_p1, b_p2) { + var result_33557416 = 0; + + BeforeRet: { + if (((a_p1 <= i_p0) && (i_p0 <= b_p2))) { + result_33557416 = i_p0; + break BeforeRet; + } + else { + raiseRangeError(); + } + + }; + + return result_33557416; + +} +var ConstSet5 = setConstr(1); + +function subInt(a_p0, b_p1) { + var result = a_p0 - b_p1; + checkOverflowInt(result); + return result; + + + +} +var ConstSet6 = setConstr(2); +var ConstSet7 = setConstr(2); +var ConstSet8 = setConstr(3); +var ConstSet9 = setConstr(3); +var ConstSet10 = setConstr(2); +var ConstSet11 = setConstr(3); +var ConstSet12 = setConstr(3); +var ConstSet13 = setConstr(2); + +function addInt(a_p0, b_p1) { + var result = a_p0 + b_p1; + checkOverflowInt(result); + return result; + + + +} +var ConstSet14 = setConstr(2); +var ConstSet15 = setConstr(2); +var ConstSet16 = setConstr(2); +var ConstSet17 = setConstr(4); +var ConstSet18 = setConstr(2); +var ConstSet19 = setConstr(3); +var ConstSet20 = setConstr(1); +var ConstSet21 = setConstr(5); + +function cstrToNimstr(c_p0) { + var ln = c_p0.length; + var result = new Array(ln); + var r = 0; + for (var i = 0; i < ln; ++i) { + var ch = c_p0.charCodeAt(i); + + if (ch < 128) { + result[r] = ch; + } + else { + if (ch < 2048) { + result[r] = (ch >> 6) | 192; + } + else { + if (ch < 55296 || ch >= 57344) { + result[r] = (ch >> 12) | 224; + } + else { + ++i; + ch = 65536 + (((ch & 1023) << 10) | (c_p0.charCodeAt(i) & 1023)); + result[r] = (ch >> 18) | 240; + ++r; + result[r] = ((ch >> 12) & 63) | 128; + } + ++r; + result[r] = ((ch >> 6) & 63) | 128; + } + ++r; + result[r] = (ch & 63) | 128; + } + ++r; + } + return result; + + + +} +var ConstSet22 = setConstr(4); + +function isFatPointer__system_u2924(ti_p0) { + var result_33557358 = false; + + BeforeRet: { + result_33557358 = !((ConstSet1[ti_p0.base.kind] != undefined)); + break BeforeRet; + }; + + return result_33557358; + +} + +function nimCopyAux(dest_p0, src_p1, n_p2) { + switch (n_p2.kind) { + case 0: + break; + case 1: + dest_p0[n_p2.offset] = nimCopy(dest_p0[n_p2.offset], src_p1[n_p2.offset], n_p2.typ); + + break; + case 2: + for (var i = 0; i < n_p2.sons.length; i++) { + nimCopyAux(dest_p0, src_p1, n_p2.sons[i]); + } + + break; + case 3: + dest_p0[n_p2.offset] = nimCopy(dest_p0[n_p2.offset], src_p1[n_p2.offset], n_p2.typ); + for (var i = 0; i < n_p2.sons.length; ++i) { + nimCopyAux(dest_p0, src_p1, n_p2.sons[i][1]); + } + + break; + } + + +} + +function addChars__stdZprivateZdigitsutils_u202(result_p0, result_p0_Idx, x_p1, start_p2, n_p3) { + var Temporary1; + + var old_335544528 = (result_p0[result_p0_Idx]).length; + if (result_p0[result_p0_Idx].length < (Temporary1 = (old_335544528 + n_p3), Temporary1)) { for (var i = result_p0[result_p0_Idx].length; i < Temporary1; ++i) result_p0[result_p0_Idx].push(0); } + else {result_p0[result_p0_Idx].length = Temporary1; }; + Label2: { + var iHEX60gensym4_335544542 = 0; + var i_570425429 = 0; + Label3: { + Label4: while (true) { + if (!(i_570425429 < n_p3)) break Label4; + iHEX60gensym4_335544542 = i_570425429; + result_p0[result_p0_Idx][(old_335544528 + iHEX60gensym4_335544542)] = x_p1.charCodeAt((start_p2 + iHEX60gensym4_335544542)); + i_570425429 += 1; + } + }; + }; + + +} + +function addChars__stdZprivateZdigitsutils_u198(result_p0, result_p0_Idx, x_p1) { + addChars__stdZprivateZdigitsutils_u202(result_p0, result_p0_Idx, x_p1, 0, ((x_p1) == null ? 0 : (x_p1).length)); + + +} + +function addInt__stdZprivateZdigitsutils_u223(result_p0, result_p0_Idx, x_p1) { + addChars__stdZprivateZdigitsutils_u198(result_p0, result_p0_Idx, ((x_p1) + "")); + + +} + +function addInt__stdZprivateZdigitsutils_u241(result_p0, result_p0_Idx, x_p1) { + addInt__stdZprivateZdigitsutils_u223(result_p0, result_p0_Idx, BigInt(x_p1)); + + +} + +function HEX24__systemZdollars_u14(xHEX60gensym0_p0) { + var result_402653200 = [[]]; + + result_402653200[0] = nimCopy(null, [], NTI33554449); + addInt__stdZprivateZdigitsutils_u241(result_402653200, 0, xHEX60gensym0_p0); + + return result_402653200[0]; + +} + +function add__system_u1972(x_p0, x_p0_Idx, y_p1) { + if (x_p0[x_p0_Idx] === null) { x_p0[x_p0_Idx] = []; } + var off = x_p0[x_p0_Idx].length; + x_p0[x_p0_Idx].length += y_p1.length; + for (var i = 0; i < y_p1.length; ++i) { + x_p0[x_p0_Idx][off+i] = y_p1.charCodeAt(i); + } + + + +} + +function newSeq__system_u2562(len_p0) { + var result_33556998 = []; + + result_33556998 = new Array(len_p0); for (var i = 0 ; i < len_p0 ; ++i) { result_33556998[i] = null; } + return result_33556998; + +} + +function isNimException__system_u2047() { + return lastJSError && lastJSError.m_type; + + +} + +function getCurrentException() { + var result_33556482 = null; + + if (isNimException__system_u2047()) { + result_33556482 = lastJSError; + } + + + return result_33556482; + +} + +function raiseDefect() { + if (isNimException__system_u2047()) { + var e_33556698 = getCurrentException(); + if (isObj(e_33556698.m_type, NTI33555184)) { + if ((excHandler == 0)) { + unhandledException(e_33556698); + } + + throw e_33556698; + } + + } + + + +} + +function unhandledException(e_p0) { + var buf_33556692 = [[]]; + if (!(((e_p0.message).length == 0))) { + nimAddStrStr(buf_33556692[0], [69,114,114,111,114,58,32,117,110,104,97,110,100,108,101,100,32,101,120,99,101,112,116,105,111,110,58,32]);; + nimAddStrStr(buf_33556692[0], e_p0.message);; + } + else { + nimAddStrStr(buf_33556692[0], [69,114,114,111,114,58,32,117,110,104,97,110,100,108,101,100,32,101,120,99,101,112,116,105,111,110]);; + } + + nimAddStrStr(buf_33556692[0], [32,91]);; + add__system_u1972(buf_33556692, 0, e_p0.name); + nimAddStrStr(buf_33556692[0], [93,10]);; + var cbuf_33556693 = toJSStr(buf_33556692[0]); + if (typeof(Error) !== "undefined") { + throw new Error(cbuf_33556693); + } + else { + throw cbuf_33556693; + } + + + +} + +function raiseIndexError(i_p0, a_p1, b_p2) { + var Temporary1; + + if ((b_p2 < a_p1)) { + Temporary1 = [105,110,100,101,120,32,111,117,116,32,111,102,32,98,111,117,110,100,115,44,32,116,104,101,32,99,111,110,116,97,105,110,101,114,32,105,115,32,101,109,112,116,121]; + } + else { + Temporary1 = ([105,110,100,101,120,32]).concat(HEX24__systemZdollars_u14(i_p0),[32,110,111,116,32,105,110,32],HEX24__systemZdollars_u14(a_p1),[32,46,46,32],HEX24__systemZdollars_u14(b_p2)); + } + + raiseException({message: nimCopy(null, Temporary1, NTI33554449), parent: null, m_type: NTI134217749, name: null, trace: [], up: null}, "IndexDefect"); + + +} + +function raiseOverflow() { + raiseException({message: [111,118,101,114,45,32,111,114,32,117,110,100,101,114,102,108,111,119], parent: null, m_type: NTI134217743, name: null, trace: [], up: null}, "OverflowDefect"); + + +} + +function checkOverflowInt(a_p0) { + if (a_p0 > 2147483647 || a_p0 < -2147483648) raiseOverflow(); + + + +} + +function checkOverflowInt64(a_p0) { + if (a_p0 > 9223372036854775807n || a_p0 < -9223372036854775808n) raiseOverflow(); + + + +} + +function HEX2AHEX3D__OOZOOZlibZcljnim95runtime95js_u211(x_p0, x_p0_Idx, y_p1) { + x_p0[x_p0_Idx] = mulInt64(x_p0[x_p0_Idx], y_p1); + + +} + +function reprEnum(e_p0, typ_p1) { + var result_33557684 = []; + + var tmp_33557685 = false; + var item_33557686 = typ_p1.node.sons[e_p0]; + tmp_33557685 = item_33557686 !== undefined; + if (tmp_33557685) { + result_33557684 = nimCopy(null, makeNimstrLit(item_33557686.name), NTI33554449); + } + else { + result_33557684 = nimCopy(null, (HEX24__systemZdollars_u14(e_p0)).concat([32,40,105,110,118,97,108,105,100,32,100,97,116,97,33,41]), NTI33554449); + } + + + return result_33557684; + +} + +function HEX2AHEX3D__OOZOOZlibZcljnim95runtime95js_u219(x_p0, x_p0_Idx, y_p1) { + x_p0[x_p0_Idx] = (x_p0[x_p0_Idx] * y_p1); + + +} + +function sysFatal__stdZassertions_u46(message_p1) { + raiseException({message: nimCopy(null, message_p1, NTI33554449), m_type: NTI134217745, parent: null, name: null, trace: [], up: null}, "AssertionDefect"); + + +} + +function raiseAssert__stdZassertions_u44(msg_p0) { + sysFatal__stdZassertions_u46(msg_p0); + + +} + +function failedAssertImpl__stdZassertions_u86(msg_p0) { + raiseAssert__stdZassertions_u44(msg_p0); + + +} + +function cljFloat__OOZOOZlibZcljnim95runtime95js_u97(v_p0) { + var result_704643171 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_704643171 = nimCopy(result_704643171, {kind: 3, floatVal: v_p0, boolVal: false, intVal: 0n, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}, NTI704643076); + + return result_704643171; + +} + +function cljInt__OOZOOZlibZcljnim95runtime95js_u94(v_p0) { + var result_704643168 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_704643168 = nimCopy(result_704643168, {kind: 2, intVal: v_p0, boolVal: false, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}, NTI704643076); + + return result_704643168; + +} + +function cljMul__OOZOOZlibZcljnim95runtime95js_u197(args_p0) { + var Temporary4; + var Temporary5; + + var result_704643271 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + BeforeRet: { + var product_704643272 = [1n]; + var hasFloat_704643273 = false; + var fprod_704643274 = [1.0]; + Label1: { + var a_704643278 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + var i_570425422 = 0; + var L_570425423 = (args_p0).length; + Label2: { + Label3: while (true) { + if (!(i_570425422 < L_570425423)) break Label3; + a_704643278 = args_p0[chckIndx(i_570425422, 0, (args_p0).length - 1)]; + if ((a_704643278.kind == 2)) { + var Temporary4 = a_704643278; + if (ConstSet2[Temporary4.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary4.kind, NTI704643075)); } + HEX2AHEX3D__OOZOOZlibZcljnim95runtime95js_u211(product_704643272, 0, Temporary4.intVal); + } + else { + if ((a_704643278.kind == 3)) { + hasFloat_704643273 = true; + var Temporary5 = a_704643278; + if (ConstSet3[Temporary5.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary5.kind, NTI704643075)); } + HEX2AHEX3D__OOZOOZlibZcljnim95runtime95js_u219(fprod_704643274, 0, Temporary5.floatVal); + } + } + i_570425422 += 1; + if (!(((args_p0).length == L_570425423))) { + failedAssertImpl__stdZassertions_u86(makeNimstrLit("iterators.nim(254, 11) `len(a) == L` the length of the seq changed while iterating over it")); + } + + } + }; + }; + if (hasFloat_704643273) { + result_704643271 = nimCopy(result_704643271, cljFloat__OOZOOZlibZcljnim95runtime95js_u97((fprod_704643274[0] * Number(product_704643272[0]))), NTI704643076); + break BeforeRet; + } + + result_704643271 = nimCopy(result_704643271, cljInt__OOZOOZlibZcljnim95runtime95js_u94(product_704643272[0]), NTI704643076); + break BeforeRet; + }; + + return result_704643271; + +} + +function square__js95wrappers_u3(x_p0) { + var result_570425349 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_570425349 = nimCopy(result_570425349, cljMul__OOZOOZlibZcljnim95runtime95js_u197([nimCopy(null, x_p0, NTI704643076), nimCopy(null, x_p0, NTI704643076)]), NTI704643076); + + return result_570425349; + +} + +function raiseRangeError() { + raiseException({message: [118,97,108,117,101,32,111,117,116,32,111,102,32,114,97,110,103,101], parent: null, m_type: NTI134217751, name: null, trace: [], up: null}, "RangeDefect"); + + +} + +function jsSquare(x_p0) { + var Temporary1; + + var result_570425410 = 0; + + BeforeRet: { + var result_570425411 = square__js95wrappers_u3(cljInt__OOZOOZlibZcljnim95runtime95js_u94(BigInt(x_p0))); + var Temporary1 = result_570425411; + if (ConstSet4[Temporary1.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary1.kind, NTI704643075)); } + result_570425410 = chckRange(Number(Temporary1.intVal), (-2147483648), 2147483647); + break BeforeRet; + }; + + return result_570425410; + +} + +function cljIsTruthy__OOZOOZlibZcljnim95runtime95js_u253(v_p0) { + var Temporary1; + + var result_704643327 = false; + + BeforeRet: { + if ((v_p0.kind == 0)) { + result_704643327 = false; + break BeforeRet; + } + + if ((v_p0.kind == 1)) { + var Temporary1 = v_p0; + if (ConstSet5[Temporary1.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'boolVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary1.kind, NTI704643075)); } + result_704643327 = Temporary1.boolVal; + break BeforeRet; + } + + result_704643327 = true; + break BeforeRet; + }; + + return result_704643327; + +} + +function cljBool__OOZOOZlibZcljnim95runtime95js_u91(v_p0) { + var result_704643165 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_704643165 = nimCopy(result_704643165, {kind: 1, boolVal: v_p0, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}, NTI704643076); + + return result_704643165; + +} + +function cljNumEq__OOZOOZlibZcljnim95runtime95js_u264(args_p0) { + var Temporary4; + var Temporary5; + var Temporary6; + var Temporary7; + var Temporary8; + var Temporary9; + var Temporary10; + var Temporary11; + + var result_704643338 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + BeforeRet: { + if (((args_p0).length < 2)) { + result_704643338 = nimCopy(result_704643338, cljBool__OOZOOZlibZcljnim95runtime95js_u91(true), NTI704643076); + break BeforeRet; + } + + Label1: { + var i_704643349 = 0; + var colontmp__570425435 = 0; + colontmp__570425435 = (args_p0).length; + var i_570425436 = 1; + Label2: { + Label3: while (true) { + if (!(i_570425436 < colontmp__570425435)) break Label3; + i_704643349 = i_570425436; + var a_704643350 = nimCopy(null, args_p0[chckIndx(subInt(i_704643349, 1), 0, (args_p0).length - 1)], NTI704643076); + var b_704643351 = nimCopy(null, args_p0[chckIndx(i_704643349, 0, (args_p0).length - 1)], NTI704643076); + var eq_704643352 = false; + if (((a_704643350.kind == 2) && (b_704643351.kind == 2))) { + var Temporary4 = a_704643350; + if (ConstSet6[Temporary4.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary4.kind, NTI704643075)); } + var Temporary5 = b_704643351; + if (ConstSet7[Temporary5.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary5.kind, NTI704643075)); } + eq_704643352 = (Temporary4.intVal == Temporary5.intVal); + } + else { + if (((a_704643350.kind == 3) && (b_704643351.kind == 3))) { + var Temporary6 = a_704643350; + if (ConstSet8[Temporary6.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary6.kind, NTI704643075)); } + var Temporary7 = b_704643351; + if (ConstSet9[Temporary7.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary7.kind, NTI704643075)); } + eq_704643352 = (Temporary6.floatVal == Temporary7.floatVal); + } + else { + if (((a_704643350.kind == 2) && (b_704643351.kind == 3))) { + var Temporary8 = a_704643350; + if (ConstSet10[Temporary8.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary8.kind, NTI704643075)); } + var Temporary9 = b_704643351; + if (ConstSet11[Temporary9.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary9.kind, NTI704643075)); } + eq_704643352 = (Number(Temporary8.intVal) == Temporary9.floatVal); + } + else { + if (((a_704643350.kind == 3) && (b_704643351.kind == 2))) { + var Temporary10 = a_704643350; + if (ConstSet12[Temporary10.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary10.kind, NTI704643075)); } + var Temporary11 = b_704643351; + if (ConstSet13[Temporary11.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary11.kind, NTI704643075)); } + eq_704643352 = (Temporary10.floatVal == Number(Temporary11.intVal)); + } + else { + result_704643338 = nimCopy(result_704643338, cljBool__OOZOOZlibZcljnim95runtime95js_u91(false), NTI704643076); + break BeforeRet; + } + }}} + if (!(eq_704643352)) { + result_704643338 = nimCopy(result_704643338, cljBool__OOZOOZlibZcljnim95runtime95js_u91(false), NTI704643076); + break BeforeRet; + } + + i_570425436 = addInt(i_570425436, 1); + } + }; + }; + result_704643338 = nimCopy(result_704643338, cljBool__OOZOOZlibZcljnim95runtime95js_u91(true), NTI704643076); + break BeforeRet; + }; + + return result_704643338; + +} + +function HEX2DHEX3D__OOZOOZlibZcljnim95runtime95js_u238(x_p0, x_p0_Idx, y_p1) { + x_p0[x_p0_Idx] = (x_p0[x_p0_Idx] - y_p1); + + +} + +function cljSub__OOZOOZlibZcljnim95runtime95js_u223(args_p0) { + var Temporary1; + var Temporary5; + + var result_704643297 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + BeforeRet: { + if (((args_p0).length == 0)) { + result_704643297 = nimCopy(result_704643297, cljInt__OOZOOZlibZcljnim95runtime95js_u94(0n), NTI704643076); + break BeforeRet; + } + + var Temporary1 = args_p0[chckIndx(0, 0, (args_p0).length - 1)]; + if (ConstSet14[Temporary1.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary1.kind, NTI704643075)); } + var sum_704643301 = [Number(Temporary1.intVal)]; + Label2: { + var i_704643309 = 0; + var colontmp__570425439 = 0; + colontmp__570425439 = (args_p0).length; + var i_570425440 = 1; + Label3: { + Label4: while (true) { + if (!(i_570425440 < colontmp__570425439)) break Label4; + i_704643309 = i_570425440; + var Temporary5 = args_p0[chckIndx(i_704643309, 0, (args_p0).length - 1)]; + if (ConstSet15[Temporary5.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary5.kind, NTI704643075)); } + HEX2DHEX3D__OOZOOZlibZcljnim95runtime95js_u238(sum_704643301, 0, Number(Temporary5.intVal)); + i_570425440 = addInt(i_570425440, 1); + } + }; + }; + result_704643297 = nimCopy(result_704643297, cljInt__OOZOOZlibZcljnim95runtime95js_u94(chckRange(BigInt(Math.trunc(sum_704643301[0])), (-9223372036854775808n), 9223372036854775807n)), NTI704643076); + break BeforeRet; + }; + + return result_704643297; + +} + +function factorial__js95wrappers_u26(n_p0) { + var result_570425372 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + if (cljIsTruthy__OOZOOZlibZcljnim95runtime95js_u253(cljNumEq__OOZOOZlibZcljnim95runtime95js_u264([nimCopy(null, n_p0, NTI704643076), cljInt__OOZOOZlibZcljnim95runtime95js_u94(0n)]))) { + result_570425372 = nimCopy(result_570425372, cljInt__OOZOOZlibZcljnim95runtime95js_u94(1n), NTI704643076); + } + else { + result_570425372 = nimCopy(result_570425372, cljMul__OOZOOZlibZcljnim95runtime95js_u197([nimCopy(null, n_p0, NTI704643076), factorial__js95wrappers_u26(cljSub__OOZOOZlibZcljnim95runtime95js_u223([nimCopy(null, n_p0, NTI704643076), cljInt__OOZOOZlibZcljnim95runtime95js_u94(1n)]))]), NTI704643076); + } + + + return result_570425372; + +} + +function jsFactorial(n_p0) { + var Temporary1; + + var result_570425414 = 0; + + BeforeRet: { + var result_570425415 = factorial__js95wrappers_u26(cljInt__OOZOOZlibZcljnim95runtime95js_u94(BigInt(n_p0))); + var Temporary1 = result_570425415; + if (ConstSet16[Temporary1.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary1.kind, NTI704643075)); } + result_570425414 = chckRange(Number(Temporary1.intVal), (-2147483648), 2147483647); + break BeforeRet; + }; + + return result_570425414; + +} + +function HEX24__systemZdollars_u34(xHEX60gensym4_p0) { + var result_402653220 = [[]]; + + result_402653220[0] = nimCopy(null, [], NTI33554449); + addInt__stdZprivateZdigitsutils_u223(result_402653220, 0, xHEX60gensym4_p0); + + return result_402653220[0]; + +} + +function nimFloatToString__stdZformatfloat_u130(a_p0) { + var result_419430532 = null; + + function nimOnlyDigitsOrMinus(n) { + return n.toString().match(/^-?\d+$/); + } + if (Number.isSafeInteger(a_p0)) + result_419430532 = a_p0 === 0 && 1 / a_p0 < 0 ? "-0.0" : a_p0+".0"; + else if (isNaN(a_p0)) // Number.isNaN is since ES6 + result_419430532 = "nan"; // or it'll be "NaN" + else if (!isFinite(a_p0)) // Number.isFinite newer but unnecessary here + result_419430532 = a_p0 > 0 ? "inf" : "-inf"; // or it'll be [-]Infinity + else { + result_419430532 = a_p0+""; + if(nimOnlyDigitsOrMinus(result_419430532)){ + result_419430532 = a_p0+".0"; + } + } + + + return result_419430532; + +} + +function addFloat__system_u3227(result_p0, result_p0_Idx, x_p1) { + add__system_u1972(result_p0, result_p0_Idx, nimFloatToString__stdZformatfloat_u130(x_p1)); + + +} + +function HEX24__system_u3223(x_p0) { + var result_33557658 = [[]]; + + result_33557658[0] = nimCopy(null, [], NTI33554449); + addFloat__system_u3227(result_33557658, 0, x_p0); + + return result_33557658[0]; + +} + +function cljString__OOZOOZlibZcljnim95runtime95js_u100(v_p0) { + var result_704643174 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_704643174 = nimCopy(result_704643174, {kind: 4, strVal: nimCopy(null, v_p0, NTI33554449), boolVal: false, intVal: 0n, floatVal: 0.0, kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}, NTI704643076); + + return result_704643174; + +} + +function cljStrConcat__OOZOOZlibZcljnim95runtime95js_u242(args_p0) { + var Temporary4; + var Temporary5; + var Temporary6; + var Temporary7; + var Temporary8; + + var result_704643316 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + BeforeRet: { + var s_704643317 = []; + Label1: { + var a_704643321 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + var i_570425443 = 0; + var L_570425444 = (args_p0).length; + Label2: { + Label3: while (true) { + if (!(i_570425443 < L_570425444)) break Label3; + a_704643321 = args_p0[chckIndx(i_570425443, 0, (args_p0).length - 1)]; + switch (a_704643321.kind) { + case 4: + var Temporary4 = a_704643321; + if (ConstSet17[Temporary4.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'strVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary4.kind, NTI704643075)); } + nimAddStrStr(s_704643317, Temporary4.strVal);; + break; + case 2: + var Temporary5 = a_704643321; + if (ConstSet18[Temporary5.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'intVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary5.kind, NTI704643075)); } + nimAddStrStr(s_704643317, HEX24__systemZdollars_u34(Temporary5.intVal));; + break; + case 3: + var Temporary6 = a_704643321; + if (ConstSet19[Temporary6.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'floatVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary6.kind, NTI704643075)); } + nimAddStrStr(s_704643317, HEX24__system_u3223(Temporary6.floatVal));; + break; + case 1: + var Temporary7 = a_704643321; + if (ConstSet20[Temporary7.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'boolVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary7.kind, NTI704643075)); } + nimAddStrStr(s_704643317, nimBoolToStr(Temporary7.boolVal));; + break; + case 5: + var Temporary8 = a_704643321; + if (ConstSet21[Temporary8.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'kwName\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary8.kind, NTI704643075)); } + nimAddStrStr(s_704643317, ([58]).concat(Temporary8.kwName));; + break; + case 0: + nimAddStrStr(s_704643317, [110,105,108]);; + break; + default: + break; + } + i_570425443 += 1; + if (!(((args_p0).length == L_570425444))) { + failedAssertImpl__stdZassertions_u86(makeNimstrLit("iterators.nim(254, 11) `len(a) == L` the length of the seq changed while iterating over it")); + } + + } + }; + }; + result_704643316 = nimCopy(result_704643316, cljString__OOZOOZlibZcljnim95runtime95js_u100(s_704643317), NTI704643076); + break BeforeRet; + }; + + return result_704643316; + +} + +function greet__js95wrappers_u41(name_p0) { + var result_570425387 = ({kind: 0, boolVal: false, intVal: 0n, floatVal: 0.0, strVal: [], kwName: [], symName: [], items: [], mapKeys: [], mapVals: []}); + + result_570425387 = nimCopy(result_570425387, cljStrConcat__OOZOOZlibZcljnim95runtime95js_u242([cljString__OOZOOZlibZcljnim95runtime95js_u100([72,101,108,108,111,44,32]), nimCopy(null, name_p0, NTI704643076), cljString__OOZOOZlibZcljnim95runtime95js_u100([33])]), NTI704643076); + + return result_570425387; + +} + +function jsGreet(name_p0) { + var Temporary1; + + var result_570425418 = null; + + BeforeRet: { + var result_570425419 = greet__js95wrappers_u41(cljString__OOZOOZlibZcljnim95runtime95js_u100(cstrToNimstr(name_p0))); + var Temporary1 = result_570425419; + if (ConstSet22[Temporary1.kind]===undefined) { raiseFieldError2(makeNimstrLit("field \'strVal\' is not accessible for type \'CljVal\' using \'kind = "), reprDiscriminant(Temporary1.kind, NTI704643075)); } + result_570425418 = toJSStr(Temporary1.strVal); + break BeforeRet; + }; + + return result_570425418; + +} diff --git a/experiments/web-target/build/math.nim b/experiments/web-target/build/math.nim new file mode 100644 index 0000000..28c9f1a --- /dev/null +++ b/experiments/web-target/build/math.nim @@ -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("!")]) diff --git a/experiments/web-target/examples/math.clj b/experiments/web-target/examples/math.clj new file mode 100644 index 0000000..790f2ea --- /dev/null +++ b/experiments/web-target/examples/math.clj @@ -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 "!")) diff --git a/experiments/web-target/js_wrappers.nim b/experiments/web-target/js_wrappers.nim new file mode 100644 index 0000000..76d1dec --- /dev/null +++ b/experiments/web-target/js_wrappers.nim @@ -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 diff --git a/experiments/web-target/www/index.html b/experiments/web-target/www/index.html new file mode 100644 index 0000000..9714daf --- /dev/null +++ b/experiments/web-target/www/index.html @@ -0,0 +1,50 @@ + + + + + Clojure/Nim → JS Demo + + + +

🔥 Clojure/Nim → JavaScript

+

Clojure code compiled to Nim, then to JavaScript. Running in your browser.

+ +
+
square(7)
+
Click button to calculate
+
+ +
+ +
+
factorial(5)
+
Click button to calculate
+
+ +
+ +
+
greet("World")
+
Click button to greet
+
+ +
+ +
+
Interactive: factorial of
+ + +
+
+ + + + diff --git a/lib/cljnim_pvec.nim b/lib/cljnim_pvec.nim index 2761810..ee6bca7 100644 --- a/lib/cljnim_pvec.nim +++ b/lib/cljnim_pvec.nim @@ -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 diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index 7e29aa7..ab64203 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -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 diff --git a/lib/cljnim_runtime_js.nim b/lib/cljnim_runtime_js.nim new file mode 100644 index 0000000..5d04269 --- /dev/null +++ b/lib/cljnim_runtime_js.nim @@ -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.. 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: diff --git a/src/emitter.nim b/src/emitter.nim index 89d9bbe..0501071 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -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.. 0: @@ -889,7 +903,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = for i in 1.." + of ckAtom: "(atom " & cljReprLocal(v.atomVal) & ")" + of ckTransient: "#" + 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) diff --git a/src/macros.nim b/src/macros.nim index f7047cc..f81d24c 100644 --- a/src/macros.nim +++ b/src/macros.nim @@ -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.. 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.. 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..> 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.. 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.." + of ckAtom: result = "(atom " & cljReprLocal(v.atomVal) & ")" + of ckTransient: result = "#" + 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 Ask AI to generate Clojure code" + echo ":optimize Ask AI to suggest optimizations" + echo ":debug 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 " + 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 " + 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 diff --git a/src/runtime.nim b/src/runtime.nim index e1840bf..cf81e17 100644 --- a/src/runtime.nim +++ b/src/runtime.nim @@ -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 diff --git a/src/types.nim b/src/types.nim index b5ee960..a9c6e6f 100644 --- a/src/types.nim +++ b/src/types.nim @@ -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.." + of ckAtom: "(atom " & $v.atomVal & ")" + of ckTransient: "#" + of ckAgent: "(agent " & $v.agentVal & ")" diff --git a/tests/test_ai_assist.nim b/tests/test_ai_assist.nim index 6e4c5ec..9ce1961 100644 --- a/tests/test_ai_assist.nim +++ b/tests/test_ai_assist.nim @@ -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") diff --git a/tests/test_emitter.nim b/tests/test_emitter.nim index 63f8be5..be8736e 100644 --- a/tests/test_emitter.nim +++ b/tests/test_emitter.nim @@ -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 diff --git a/tests/test_eval.nim b/tests/test_eval.nim index 6106c1c..746f0d0 100644 --- a/tests/test_eval.nim +++ b/tests/test_eval.nim @@ -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) diff --git a/tests/test_macros.nim b/tests/test_macros.nim new file mode 100644 index 0000000..0d41ea5 --- /dev/null +++ b/tests/test_macros.nim @@ -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 diff --git a/tests/test_reader.nim b/tests/test_reader.nim index 3da2ef6..4942e2a 100644 --- a/tests/test_reader.nim +++ b/tests/test_reader.nim @@ -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 == "+"