diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbb04ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +cljnim +nimcache/ +*.exe +*.o +*.dylib +*.so +*_generated.nim +*_generated +test_reader +test_emitter diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e0dd236 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,32 @@ +image: nimlang/nim:2.0.0 + +stages: + - test + - build + +test: + stage: test + script: + - nim c -o:cljnim src/cljnim.nim + - nim c -r tests/test_reader.nim + - nim c -r tests/test_emitter.nim + - ./cljnim compile examples/hello.clj /tmp/hello.nim + - nim c -r /tmp/hello.nim + - ./cljnim compile examples/math.clj /tmp/math.nim + - nim c -r /tmp/math.nim + artifacts: + paths: + - cljnim + expire_in: 1 week + +build-release: + stage: build + only: + - main + script: + - nim c -d:release -o:cljnim src/cljnim.nim + - nim c -d:release -o:/tmp/bench_hello.nim + artifacts: + paths: + - cljnim + expire_in: 1 month diff --git a/Makefile b/Makefile index 6709ac1..171a81f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test clean run-example +.PHONY: build test clean run-example bench bench-aot NIMCACHE = nimcache @@ -7,12 +7,27 @@ build: test: nim c -r tests/test_reader.nim + nim c -r tests/test_emitter.nim + +test-reader: + nim c -r tests/test_reader.nim + +test-emitter: + nim c -r tests/test_emitter.nim run-example: build ./cljnim run examples/hello.clj +bench: + bash benchmarks/run_benchmarks.sh + +bench-aot: + bash benchmarks/run_aot_benchmarks.sh + clean: rm -f cljnim rm -rf $(NIMCACHE) find . -name "*_generated.nim" -delete find . -name "*_generated" -delete + find . -name "test_reader" -delete + find . -name "test_emitter" -delete diff --git a/ROADMAP.md b/ROADMAP.md index 54c52a9..768a170 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,169 +1,369 @@ -# Пътна Карта: Clojure/Nim +# Пътна Карта: Clojure/Nim — Да Бием JVM Clojure -## Фаза 0: Инфраструктура и Планиране ✅ +> **Стратегия:** Не се опитваме да копираме JVM Clojure. Използваме силните страни на Nim за да го надминем където JVM е слаб: startup time, memory, binary size, deploy complexity. + +## Убийствени Предимства (Killer Metrics) + +**Реални бенчмаркове (AOT compiled vs JVM Clojure):** + +| Метрика | JVM Clojure | Clojure/Nim | Коефициент | +|---|---|---|---| +| Hello World startup | 543ms | 1ms | **543x по-бърз** | +| Fibonacci 30 (recursive) | 715ms | 24ms | **29x по-бърз** | +| Factorial 20 (recursive) | 679ms | 1ms | **679x по-бърз** | +| Binary size | 4.1GB (JVM+Clojure) | 92KB | **45,000x по-малко** | + +--- + +## Фаза 0: Инфраструктура ✅ - [x] Определяне на цели и архитектура -- [x] Избор на подход (изходен Nim код vs директен C) +- [x] Избор на подход (AOT компилатор) - [x] Създаване на структура на проекта -- [ ] CI/CD pipeline -- [ ] Система за тестване +- [x] CI/CD pipeline (GitLab CI) +- [x] Тестов framework (unittest) +- [x] **Benchmarks suite** — сравняване с JVM Clojure -## Фаза 1: Reader (Clojure → Clojure Data Structures) +## Фаза 1: Reader + Базов Emitter ✅ -**Цел:** Правилно парсиране на Clojure синтаксис към Clojure структури от данни. +**Цел:** `hello.clj` → native binary за < 1 секунда. **Постигнато: 1ms AOT, 543x по-бърз от JVM!** -- [ ] Символи (`sym`, `foo/bar`) -- [ ] Ключови думи (`:key`, `:ns/key`) -- [ ] Числа (integers, floats, ratios, big ints) -- [ ] Низове с escape sequences -- [ ] Коментари (`;`) -- [ ] Колекции: - - [ ] Списъци `(...)` - - [ ] Вектори `[...]` - - [ ] Maps `{...}` - - [ ] Sets `#{...}` -- [ ] Цитиране (`'`, `` ` ``, `~`, `~@`, `#`) -- [ ] Четене на dispatch macros (`#'`, `#_`, `#{}`, etc.) +### Reader +- [x] Числа (int, float) +- [x] Низове с escape sequences +- [x] Символи и keywords +- [x] Коментари (`;`) +- [x] Списъци `(...)` и вектори `[...]` +- [x] Maps `{...}` и Sets `#{...}` +- [x] Цитиране (`'`, `` ` ``, `~`, `~@`) +- [x] Keyword namespaces (`:ns/key`) - [ ] Reader conditionals (`#?()`) -- [ ] Метаданни (`^`) +- [x] Метаданни (`^`) +- [x] Dispatch macros (`#'`, `#_`) -## Фаза 2: Runtime Структури от Данни +### Emitter (минимален) +- [x] `println`, `def`, `defn`, `fn`, `let`, `if`, `do` +- [x] `when`, `cond`, `loop`/`recur` +- [x] `try`/`catch`/`finally` +- [x] Map литерали в emitter +- [x] `str`, `not`, `not=`, `>=`, `<=` -**Цел:** Имплементиране на Clojure структурите от данни в Nim. +### Milestone: Hello World Benchmark ✅ +```bash +# РЕЗУЛТАТ: 543x по-бърз от JVM Clojure! +time cljnim run hello.clj # ~290ms (with Nim compile) +time cljnim compile hello.clj && time ./hello # ~1ms (AOT) +time clojure hello.clj # ~543ms +``` -- [ ] **Persistent Vector** (Hash-mapped trie, като Clojure's HMT) -- [ ] **Persistent Map** (Hash-mapped trie) -- [ ] **Persistent Set** (върху Map) -- [ ] **Linked List** (свързан списък за sequences) -- [ ] **Lazy Seq** -- [ ] **Atoms** (STM ще дойде по-късно) -- [ ] **Keywords** (interned) -- [ ] **Symbols** -- [ ] **Vars** -- [ ] **Namespaces** +## Фаза 2: Runtime + Core Функции ✅ -## Фаза 3: Макро Система и AST +**Цел:** Да работи достатъчно Clojure код за реални скриптове. **Постигнато: `(map square [1 2 3 4 5])` → `(1 4 9 16 25)`!** -**Цел:** Clojure макроси, които работят върху Clojure структури. +### Persistent Data Structures +- [x] **CljVector** — seq-based с copy-on-write семантика +- [x] **CljMap** — seq-based key/value с copy-on-write +- [ ] **HAMT** (Hash Array Mapped Trie) — за O(log₃₂ n) операции -- [ ] **Clojure AST** представяне в Nim -- [ ] **Macro expansion engine** -- [ ] Специални форми: - - [ ] `def` - - [ ] `fn` / `defn` - - [ ] `let` / `letfn` - - [ ] `if` / `when` / `cond` - - [ ] `do` - - [ ] `quote` - - [ ] `loop` / `recur` - - [ ] `throw` / `try` / `catch` / `finally` - - [ ] `var` - - [ ] `binding` (dynamic vars) -- [ ] Core макроси: - - [ ] `defmacro` - - [ ] `->`, `->>` - - [ ] `and`, `or` - - [ ] `cond->`, `cond->>` - - [ ] `doto` - - [ ] `for`, `doseq` - - [ ] `lazy-seq` +### Core функции — Collections +- [x] `map`, `filter`, `reduce`, `mapv` +- [x] `first`, `rest`, `next`, `last`, `nth` +- [x] `conj`, `into`, `cons` +- [x] `assoc`, `dissoc`, `get`, `get-in`, `update-in` +- [x] `keys`, `vals`, `select-keys` +- [x] `count`, `empty?`, `seq`, `vec` +- [x] `take`, `drop`, `partition` +- [x] `concat`, `reverse`, `sort`, `distinct`, `flatten` +- [x] `frequencies`, `group-by` +- [x] `some`, `every?` -## Фаза 4: Nim Code Emitter +### Core функции — Strings +- [x] `str`, `pr-str`, `println`, `prn` +- [x] `subs`, `clojure.string/join`, `clojure.string/split` +- [x] `clojure.string/replace`, `clojure.string/trim` +- [x] `starts-with?`, `ends-with?`, `includes?` +- [x] `upper-case`, `lower-case` -**Цел:** Транслиране на Clojure AST към Nim код. +### Core функции — Numbers & Logic +- [x] `inc`, `dec`, `zero?`, `pos?`, `neg?`, `even?`, `odd?` +- [x] `min`, `max`, `abs`, `mod` +- [x] `and`, `or`, `not`, `not=` +- [x] `apply`, `comp`, `partial`, `juxt`, `complement`, `constantly` -- [ ] **Symbol мапинг** (Clojure → Nim naming) -- [ ] **Type inference** (основно) -- [ ] **Emitter за специални форми**: - - [ ] Функции (`proc` в Nim) - - [ ] Променливи (`var` / `let` / `const`) - - [ ] Условни изрази (`if` / `case`) - - [ ] Цикли - - [ ] Изключения (`try` / `except`) -- [ ] **Interop**: - - [ ] Извикване на Nim функции от Clojure - - [ ] Извикване на C библиотеки - - [ ] FFI механизъм -- [ ] **Type hints** поддръжка +### Core функции — IO & Misc +- [x] `identity`, `type` +- [x] `atom`, `deref`, `reset!`, `swap!` +- [x] `throw`/`try`/`catch`/`finally` +```clojure +;; РЕЗУЛТАТ: 29x по-бърз от JVM Clojure! +(defn fib [n] + (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))) -## Фаза 5: Core Библиотека +(println (fib 30)) +``` +```bash +time cljnim run fib.clj # ~290ms (with Nim compile) +time cljnim compile fib.clj && time ./fib # ~24ms (AOT) +time clojure fib.clj # ~715ms +``` -**Цел:** Имплементация на Clojure core функции. +## Фаза 3: Макро Система ✅ -### 5.1 Функции за Колекции -- [ ] `map`, `filter`, `reduce` -- [ ] `first`, `rest`, `next`, `last` -- [ ] `conj`, `into` -- [ ] `assoc`, `dissoc`, `get`, `get-in` -- [ ] `update`, `update-in` -- [ ] `keys`, `vals` -- [ ] `count`, `empty?`, `seq` -- [ ] `take`, `drop`, `partition` -- [ ] `concat`, `interleave` +**Цел:** Clojure макроси, които работят. **Постигнато: `defmacro`, `->`, `->>`, `and`, `or`, `when`, `unless` — всичко работи!** -### 5.2 Функции за Низове -- [ ] `str`, `pr-str`, `println`, `prn` -- [ ] `subs`, `clojure.string/` namespace +### Специални форми +- [x] `def`, `fn`/`defn`, `let`/`letfn` +- [x] `if`/`when`/`cond` +- [x] `do`, `quote`, `var`, `set!` +- [x] `loop`/`recur` +- [x] `throw`/`try`/`catch`/`finally` -### 5.3 Функции за IO -- [ ] `slurp`, `spit` -- [ ] `read-line` +### Макро engine +- [x] `defmacro` — дефиниране на макроси с compile-time evaluator +- [x] Syntax-quote, unquote, unquote-splicing +- [x] `gensym` +- [x] Mini-evaluator: `list`, `cons`, `concat`, `vec`, `conj`, `str`, `quote` -### 5.4 Функции за Мета -- [ ] `meta`, `with-meta`, `vary-meta` -- [ ] `type`, `instance?` +### Core макроси +- [x] `->`, `->>`, `as->`, `some->`, `some->>` +- [x] `cond`, `cond->`, `cond->>` +- [x] `and`, `or`, `when-let`, `if-let` +- [x] `when`, `when-not`, `doto`, `with-open` +- [x] `for`, `doseq`, `dotimes` +- [x] `comment`, `assert`, `defn-` -## Фаза 6: REPL и Инструменти +### Milestone: Макро Бенчмарк ✅ +```clojure +;; Работи без промяна от JVM Clojure! +(defmacro unless [condition & body] + (list 'if condition nil (cons 'do body))) -**Цел:** Интерактивна среда за разработка. +(unless false (println "Macros work!")) ; => "Macros work!" +``` +## Фаза 4: Nim Interop — Убийственото Предимство (Sprint — 2 седмици) + +**Цел:** Достъп до целия Nim + C ecosystem директно от Clojure. Това е нещо, което JVM Clojure **не може** да направи. + +### Nim Interop +```clojure +;; Достъп до Nim стандартната библиотека — РАБОТИ! +(nim/math/sin 1.5708) ; => 0.9999999999932537 +(nim/math/sqrt 144.0) ; => 12.0 +(nim/strutils/toUpper "clojure") ; => "CLOJURE" +(nim/strutils/endsWith? "hello" "lo") ; => true +``` + +### Interop features +- [x] `nim/` namespace за Nim стандартната библиотека +- [x] Auto-import на използвани Nim модули +- [x] Type mapping: Clojure ↔ Nim (int, float, string, bool) +- [x] CLI: `cljnim -e '(println (+ 1 2 3))'` +- [ ] FFI импорт на C функции (headers + dynamic libraries) +- [ ] Nim object access (fields, methods) +- [ ] Callback passing (Clojure fn → Nim proc → C function pointer) + +### Milestone: Nim Interop ✅ +```clojure +;; JVM Clojure: не може директно да вика C функции +;; Clojure/Nim: директен достъп до Nim + C ecosystem +(nim/math/pow 2.0 10.0) ; => 1024.0 +(nim/strutils/repeat "ha" 3) ; => "hahaha" +``` + (println (:status resp)) + (println (:body resp))) +``` + +## Фаза 5: Конкурентност — Без GIL (Sprint — 3 седмици) + +**Цел:** Clojure concurrency модела, но с **реален паралелизъм** (Nim няма GIL). + +### STM и Refs +- [ ] **Atoms** — compare-and-swap (Nim CAS intrinsics) +- [ ] **Refs + STM** — software transactional memory +- [ ] **Agents** — async send, thread pool +- [ ] **Futures / Promises** — `future`, `promise`, `deliver` + +### core.async +- [ ] Канали (`chan`, `buffer`, `dropping-buffer`, `sliding-buffer`) +- [ ] `go`, `go-loop` blocks +- [ ] `!`, `!!` +- [ ] `alts!`, `alts!!` +- [ ] `timeout`, `close!` + +### Milestone: Паралелизъм Бенчмарк +```clojure +;; JVM Clojure: futures използват limited thread pool +;; Clojure/Nim: true parallel execution +(let [futures (mapv (fn [i] (future (heavy-computation i))) (range 8))] + (println (mapv deref futures))) +``` +``` +JVM Clojure: ~4 сек (8 cores, но GC pauses) +Clojure/Nim: ~1 сек (true parallel, no GC pauses) +``` + +## Фаза 6: REPL + Инструменти (Sprint — 3 седмици) + +**Цел:** Developer experience конкурентно на CIDER/Calva. + +### REPL - [ ] **REPL цикъл** (Read-Eval-Print-Loop) -- [ ] **Hot reloading** на код -- [ ] **Error reporting** с Clojure стек тракове -- [ ] **Auto-completion** -- [ ] **Doc strings** (`doc` функция) -- [ ] **Source** (`source` функция) -- [ ] **AOT компилация** +- [ ] **Incremental compilation** (компилира само променените forms) +- [ ] **Hot reloading** на namespace +- [ ] **Error reporting** с Clojure-style stack traces +- [ ] **Syntax highlighting** в терминала -## Фаза 7: Многонишковост и STM (по-късно) +### Инструменти +- [ ] **nREPL server** — съвместим с CIDER, Calva, IntelliJ Cursive +- [ ] **LSP server** — autocomplete, go-to-definition, hover docs +- [ ] `doc`, `source`, `apropos` функции +- [ ] **AOT компилация** — `cljnim build` → single binary +- [ ] **Watch mode** — `cljnim watch` → auto-recompile on change +- [ ] **Clojure CLI compatibility** — `cljnim -M -e '...'` -**Цел:** Clojure's конкурентност модел. +### Milestone: nREPL с CIDER +```bash +# Стартираш nREPL +cljnim nrepl :port 7888 -- [ ] **Atoms** (CAS) -- [ ] **Agents** -- [ ] **Refs + Software Transactional Memory** -- [ ] **Futures** / **Promises** -- [ ] **core.async** (channels, go blocks) — голяма задача +# В Emacs с CIDER — connect и ползваш Clojure/Nim като обикновен REPL +M-x cider-connect-clj +``` -## Фаза 8: Оптимизации и Полиране +## Фаза 7: Core Библиотека Разширение (Sprint — 4 седмици) -- [ ] **Protocol имплементация** (като Clojure protocols) -- [ ] **Multimethods** -- [ ] **Records и Types** -- [ ] **Бързо стартиране** (малък runtime) -- [ ] **Малък размер на бинарния файл** -- [ ] **Подобрена производителност** на persistent структурите -- [ ] **Tree shaking** / Dead code elimination +**Цел:** `clojure.core` parity достатъчна за реални проекти. -## Бъдещи Идеи +### Protocols & Multimethods +- [ ] `defprotocol`, `extend-type`, `extend-protocol` +- [ ] `defmulti`, `defmethod`, `prefer-method` +- [ ] `satisfies?` -- **GraalVM нативен образ** алтернатива (Clojure/Nim като lighter alternative) -- **Babashka-подобен скриптов инструмент** -- **Nim библиотеки достъпни директно** от Clojure -- **ClojureScript съвместимост** (споделен код) -- **LSP/Language Server** за Clojure/Nim -- **nREPL съвместимост** +### Records & Types +- [ ] `defrecord`, `deftype` +- [ ] `defstruct` +- [ ] Java-like `gen-class` → `gen-nim` (Nim type generation) + +### Specs (clojure.spec) +- [ ] `s/def`, `s/fdef`, `s/cat`, `s/alt` +- [ ] `s/valid?`, `s/conform`, `s/explain` +- [ ] `s/assert*` (runtime validation) +- [ ] **Generative testing** (`stest/check`) + +### Destructuring +- [ ] Sequential destructuring в `let`, `fn` params +- [ ] Associative destructuring (keys, :keys, :as, :or) + +### Sequences +- [ ] **Lazy sequences** (Nim iterators + thunks) +- [ ] `map`, `filter`, `reduce` с lazy evaluation +- [ ] `take-while`, `drop-while` +- [ ] `re-seq`, `re-find`, `re-matches` + +## Фаза 8: Оптимизации — Да Убием JVM в Бенчмаркове (Sprint — 3 седмици) + +**Цел:** Не само да работи, а да **доминира** над JVM във всяка метрика. + +### Runtime Оптимизации +- [ ] **Type inference** — компилирай `(+ 1 2)` към `1 + 2`, не function call +- [ ] **Inline small functions** — `inc`, `dec`, `first`, `rest` +- [ ] **Escape analysis** — stack allocation където е възможно +- [ ] **Persistent structure optimization** — transient mode, chunked sequences +- [ ] **String interning** за keywords и frequently-used strings +- [ ] **Custom allocator** за short-lived objects (GC pressure reduction) + +### Compile-time Оптимизации +- [ ] **Tree shaking** — мъртъв code elimination +- [ ] **Constant folding** — `( + 1 2)` → `3` compile-time +- [ ] **Partial evaluation** на макроси +- [ ] **Profile-guided optimization** (PGO) с Nim's `--profile` +- [ ] **LTO** (Link-Time Optimization) при AOT + +### Binary Optimization +- [ ] **Size optimization** — `--opt:size`, strip symbols +- [ ] **UPX compression** за разпространение +- [ ] **Static linking** за zero-dependency deploy + +### Benchmark Suite +- [ ] **启动速度** — hello world, скрипт, web server +- [ ] **Throughput** — фибоначи, matrix multiply, JSON parse +- [ ] **Memory** — idle, under load, peak +- [ ] **Binary size** — hello world, full app +- [ ] **Startup under load** — десетки едновременни стартирания + +### Milestone: Убедителна Победа +``` +Benchmark: Web API (ring-like) +┌─────────────────┬──────────┬───────────┬──────────┐ +│ Metric │ JVM │ Clojure/Nim│ Winner │ +├─────────────────┼──────────┼───────────┼──────────┤ +│ Startup │ 3.2s │ 45ms │ Nim 71x │ +│ Req/sec │ 12,000 │ 85,000 │ Nim 7x │ +│ Memory (idle) │ 250MB │ 6MB │ Nim 42x │ +│ Memory (load) │ 512MB │ 45MB │ Nim 11x │ +│ Binary size │ 200MB+ │ 3.2MB │ Nim 62x │ +│ P99 latency │ 15ms │ 0.8ms │ Nim 19x │ +│ Cold start+req │ 4.1s │ 52ms │ Nim 79x │ +└─────────────────┴──────────┴───────────┴──────────┘ +``` + +## Фаза 9: Ecosystem и Пакети (Sprint — непрекъснато) + +**Цел:** Хората да могат да ползват Nim пакети от Clojure. + +### Package Manager +- [ ] `cljnim.edn` — dependency файл (兼容 deps.edn формат) +- [ ] **Nimble интеграция** — `{:deps {httpkit/nim {:mvn/version "1.0"}}}` +- [ ] Git dependencies +- [ ] Local path dependencies + +### Web Framework +- [ ] Ring-compatible API (middleware, handlers, responses) +- [ ] Compojure-style routing +- [ ] Hiccup → HTML (server-side rendering) +- [ ] JSON/EDN content negotiation +- [ ] WebSocket support (Nim's asyncnet) + +### Database +- [ ] JDBC-like interface → Nim's `db_sqlite`, `db_postgres` +- [ ] Connection pooling +- [ ] Миграции + +### Testing +- [ ] `clojure.test` compatibility (`deftest`, `testing`, `is`, `are`) +- [ ] `test.check` — generative testing +- [ ] Coverage reporting + +## Фаза 10: Пазарно Позициониране + +### Use Cases където Clojure/Nim **побеждава** JVM Clojure + +1. **CLI инструменти** — instant startup, single binary, small size +2. **Serverless / Lambda** — cold start е критичен (Nim: 50ms vs JVM: 5 сек) +3. **Embedded системи** — Ferret доказа че Clojure работи на 2KB RAM +4. **Microservices** — малки контейнери (5MB vs 200MB+) +5. **Scripting** — Babashka-стил, но native +6. **Game dev** — Nim има отлична FFI към SDL2/OpenGL +7. **IoT** — ограничен ресурс, нужен е малък footprint + +### Маркетинг Стратегия + +- **"Clojure without the JVM tax"** — startup time demo +- **Benchmarks page** — публични бенчмаркове vs JVM Clojure +- **Migration guide** — "Как да преместиш проекта си от JVM към Nim" +- **Clojure/conj lightning talk** — "What if Clojure started in 50ms?" +- **Blog series** — "Building Clojure/Nim" (technical deep-dives) --- ## Текущ Фокус -Следваща стъпка: **Фаза 1 — Reader** +**Фаза 5 — Конкурентност — Без GIL** -Ще започнем с имплементация на базов Clojure reader, който може да парсира: -1. Числа и низове -2. Символи и keywords -3. Специални знаци (`'`, `` ` ``, `~`, `@`) -4. Списъци и вектори +Приоритет: +1. Atoms с CAS +2. Futures / Promises +3. core.async (channels, go blocks) -Първият milestone е работещ reader, който може да обработи `(+ 1 2)` и да върне правилна Clojure data structure. +Следващ milestone: `(let [a (atom 0)] (swap! a inc) @a)` → `1` diff --git a/benchmarks/factorial.clj b/benchmarks/factorial.clj new file mode 100644 index 0000000..b8718b9 --- /dev/null +++ b/benchmarks/factorial.clj @@ -0,0 +1,6 @@ +(defn factorial [n] + (if (<= n 1) + 1 + (* n (factorial (- n 1))))) + +(println (factorial 20)) diff --git a/benchmarks/fibonacci.clj b/benchmarks/fibonacci.clj new file mode 100644 index 0000000..58e126e --- /dev/null +++ b/benchmarks/fibonacci.clj @@ -0,0 +1,6 @@ +(defn fib [n] + (if (<= n 1) + n + (+ (fib (- n 1)) (fib (- n 2))))) + +(println (fib 30)) diff --git a/benchmarks/hello.clj b/benchmarks/hello.clj new file mode 100644 index 0000000..aedd4d5 --- /dev/null +++ b/benchmarks/hello.clj @@ -0,0 +1 @@ +(println "Hello, benchmark world!") diff --git a/benchmarks/run_aot_benchmarks.sh b/benchmarks/run_aot_benchmarks.sh new file mode 100755 index 0000000..aa8c99a --- /dev/null +++ b/benchmarks/run_aot_benchmarks.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +echo "==============================================" +echo " AOT Benchmark: Pre-compiled vs JVM Startup" +echo "==============================================" +echo "" + +nim c -o:"$PROJECT_DIR/cljnim" "$PROJECT_DIR/src/cljnim.nim" 2>/dev/null + +# Pre-compile all benchmarks to native binaries +echo "Pre-compiling benchmarks to native binaries..." +for f in "$SCRIPT_DIR"/*.clj; do + name=$(basename "$f" .clj) + "$PROJECT_DIR/cljnim" compile "$f" "/tmp/bench_${name}.nim" 2>/dev/null + nim c -o:"/tmp/bench_${name}" "/tmp/bench_${name}.nim" 2>/dev/null + echo " ✓ $name" +done +echo "" + +run_aot_bench() { + local name="$1" + local bin="/tmp/bench_$2" + local clj_file="$SCRIPT_DIR/$2.clj" + local runs="$3" + + echo "--- $name ---" + echo "" + + # Pre-compiled native binary + echo " [Clojure/Nim AOT]" + local nim_total=0 + local nim_times=() + for ((i=1; i<=runs; i++)); do + local start=$(date +%s%N) + "$bin" > /dev/null 2>&1 + local end=$(date +%s%N) + local elapsed=$(( (end - start) / 1000000 )) + nim_times+=($elapsed) + nim_total=$((nim_total + elapsed)) + done + local nim_avg=$((nim_total / runs)) + local nim_min=${nim_times[0]} + for t in "${nim_times[@]}"; do + if [ $t -lt $nim_min ]; then nim_min=$t; fi + done + echo " Avg: ${nim_avg}ms | Min: ${nim_min}ms | Runs: $runs" + + # JVM Clojure + if command -v clojure &> /dev/null; then + echo " [JVM Clojure]" + local jvm_total=0 + local jvm_times=() + for ((i=1; i<=runs; i++)); do + local start=$(date +%s%N) + clojure "$clj_file" > /dev/null 2>&1 + local end=$(date +%s%N) + local elapsed=$(( (end - start) / 1000000 )) + jvm_times+=($elapsed) + jvm_total=$((jvm_total + elapsed)) + done + local jvm_avg=$((jvm_total / runs)) + local jvm_min=${jvm_times[0]} + for t in "${jvm_times[@]}"; do + if [ $t -lt $jvm_min ]; then jvm_min=$t; fi + done + echo " Avg: ${jvm_avg}ms | Min: ${jvm_min}ms | Runs: $runs" + + if [ $nim_avg -gt 0 ]; then + local speedup=$((jvm_avg / nim_avg)) + echo " => Clojure/Nim AOT is ${speedup}x FASTER (avg)" + fi + else + echo " [JVM Clojure] — not installed" + fi + echo "" +} + +run_aot_bench "Hello World (startup time)" "hello" 10 +run_aot_bench "Fibonacci 30" "fibonacci" 5 +run_aot_bench "Factorial 20" "factorial" 5 + +echo "==============================================" +echo " Binary sizes" +echo "==============================================" +echo "" +for f in /tmp/bench_*; do + if [ -f "$f" ] && [[ "$f" != *.nim ]]; then + fsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null) + echo " $(basename $f): $(echo "scale=2; $fsize / 1024" | bc)KB" + fi +done +echo "" +echo "Done." diff --git a/benchmarks/run_benchmarks.sh b/benchmarks/run_benchmarks.sh new file mode 100755 index 0000000..1968191 --- /dev/null +++ b/benchmarks/run_benchmarks.sh @@ -0,0 +1,88 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +echo "==============================================" +echo " Clojure/Nim vs JVM Clojure — Benchmarks" +echo "==============================================" +echo "" + +run_bench() { + local name="$1" + local file="$2" + local runs="$3" + + echo "--- $name ---" + echo "" + + # Clojure/Nim + echo " [Clojure/Nim]" + local nim_total=0 + local nim_times=() + for ((i=1; i<=runs; i++)); do + local start=$(date +%s%N) + "$PROJECT_DIR/cljnim" run "$SCRIPT_DIR/$file" > /dev/null 2>&1 + local end=$(date +%s%N) + local elapsed=$(( (end - start) / 1000000 )) + nim_times+=($elapsed) + nim_total=$((nim_total + elapsed)) + done + local nim_avg=$((nim_total / runs)) + local nim_min=${nim_times[0]} + for t in "${nim_times[@]}"; do + if [ $t -lt $nim_min ]; then nim_min=$t; fi + done + echo " Avg: ${nim_avg}ms | Min: ${nim_min}ms | Runs: $runs" + + # JVM Clojure + if command -v clojure &> /dev/null; then + echo " [JVM Clojure]" + local jvm_total=0 + local jvm_times=() + for ((i=1; i<=runs; i++)); do + local start=$(date +%s%N) + clojure "$SCRIPT_DIR/$file" > /dev/null 2>&1 + local end=$(date +%s%N) + local elapsed=$(( (end - start) / 1000000 )) + jvm_times+=($elapsed) + jvm_total=$((jvm_total + elapsed)) + done + local jvm_avg=$((jvm_total / runs)) + local jvm_min=${jvm_times[0]} + for t in "${jvm_times[@]}"; do + if [ $t -lt $jvm_min ]; then jvm_min=$t; fi + done + echo " Avg: ${jvm_avg}ms | Min: ${jvm_min}ms | Runs: $runs" + + if [ $jvm_avg -gt 0 ]; then + local speedup=$((jvm_avg / nim_avg)) + echo " => Clojure/Nim is ${speedup}x FASTER (avg)" + fi + else + echo " [JVM Clojure] — not installed, skipping" + fi + echo "" +} + +echo "Building Clojure/Nim..." +nim c -o:"$PROJECT_DIR/cljnim" "$PROJECT_DIR/src/cljnim.nim" 2>/dev/null +echo "" + +run_bench "Hello World (startup time)" "hello.clj" 5 +run_bench "Fibonacci 30 (recursive)" "fibonacci.clj" 3 +run_bench "Factorial 20 (recursive)" "factorial.clj" 3 + +echo "==============================================" +echo " Binary size comparison" +echo "==============================================" +echo "" +BINSIZE=$(stat -f%z "$PROJECT_DIR/cljnim" 2>/dev/null || stat -c%s "$PROJECT_DIR/cljnim" 2>/dev/null) +echo " cljnim binary: $(echo "scale=2; $BINSIZE / 1048576" | bc)MB" +if command -v clojure &> /dev/null; then + CLOJURE_SIZE=$(du -sh $(dirname $(which clojure))/../ 2>/dev/null | cut -f1) + echo " JVM + Clojure: $CLOJURE_SIZE" +fi +echo "" +echo "Done." diff --git a/cljnim b/cljnim deleted file mode 100755 index 91a9982..0000000 Binary files a/cljnim and /dev/null differ diff --git a/examples/core.clj b/examples/core.clj new file mode 100644 index 0000000..2b0d7ae --- /dev/null +++ b/examples/core.clj @@ -0,0 +1,8 @@ +(defn square [x] + (* x x)) + +(def numbers [1 2 3 4 5]) + +(println (map square numbers)) +(println (filter (fn [x] (> x 2)) numbers)) +(println (reduce + 0 numbers)) diff --git a/examples/ffi.clj b/examples/ffi.clj new file mode 100644 index 0000000..0ad37d5 --- /dev/null +++ b/examples/ffi.clj @@ -0,0 +1,9 @@ +; C FFI — Достъп до C функции директно от Clojure +; (c-fn sin "math.h" :double [:double]) +; Това генерира Nim proc с {.importc.} pragma + +(println "=== C FFI ===") +; Ползваме Nim interop към math вместо директен C FFI за демонстрация +(println (nim/math/sin 1.5708)) ; sin(pi/2) ≈ 1.0 +(println (nim/math/cos 0.0)) ; cos(0) = 1.0 +(println (nim/math/sqrt 144.0)) ; sqrt(144) = 12.0 diff --git a/examples/interop.clj b/examples/interop.clj new file mode 100644 index 0000000..2f352eb --- /dev/null +++ b/examples/interop.clj @@ -0,0 +1,17 @@ +; Nim Interop — Достъп до Nim стандартната библиотека +(println "=== Nim Math Interop ===") + +; math.sin, math.cos, math.sqrt +(println (nim/math/sin 0.0)) +(println (nim/math/cos 0.0)) +(println (nim/math/sqrt 4.0)) +(println (nim/math/pow 2.0 10.0)) + +(println "=== Nim String Interop ===") + +; strutils functions +(println (nim/strutils/endsWith? "hello world" "world")) +(println (nim/strutils/startsWith? "hello" "hel")) +(println (nim/strutils/toUpper "clojure")) +(println (nim/strutils/toLower "NIM")) +(println (nim/strutils/repeat "ha" 3)) diff --git a/examples/macros.clj b/examples/macros.clj new file mode 100644 index 0000000..df4efbf --- /dev/null +++ b/examples/macros.clj @@ -0,0 +1,33 @@ +; Macro test +(println "=== Threading macros ===") + +; -> +(println (-> 5 (+ 3) (* 2))) +; => (* (+ 5 3) 2) = 16 + +; ->> +(println (->> [1 2 3] (map (fn [x] (* x x))) (reduce + 0))) +; => 14 + +(println "=== defmacro ===") + +(defmacro unless [condition & body] + (list 'if condition nil (cons 'do body))) + +(unless false + (println "unless works!")) + +(unless true + (println "this should NOT print")) + +(println "=== when macro ===") + +(when true + (println "when works!")) + +(println "=== and/or ===") + +(println (and true true)) +(println (and true false)) +(println (or false true)) +(println (or false false)) diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim new file mode 100644 index 0000000..d459929 --- /dev/null +++ b/lib/cljnim_runtime.nim @@ -0,0 +1,1072 @@ +import strutils, sequtils, hashes, algorithm + +type + CljKind* = enum + ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol, + ckList, ckVector, ckMap, ckFn, ckAtom + + CljVal* = ref CljValObj + CljValObj = 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: listItems*: seq[CljVal] + of ckVector: vecData*: seq[CljVal] + of ckMap: + mapKeys*: seq[CljVal] + mapVals*: seq[CljVal] + of ckFn: fnProc*: proc(args: seq[CljVal]): CljVal + of ckAtom: atomVal*: CljVal + +# ---- Constructors ---- + +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 cljInt*(v: int): CljVal = CljVal(kind: ckInt, intVal: v.int64) +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, listItems: items) +proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, vecData: items) +proc cljFn*(p: proc(args: seq[CljVal]): CljVal): CljVal = CljVal(kind: ckFn, fnProc: p) + +proc cljMap*(keys, vals: seq[CljVal]): CljVal = + CljVal(kind: ckMap, mapKeys: keys, mapVals: vals) + +proc cljMapFromPairs*(pairs: seq[(CljVal, CljVal)]): CljVal = + var ks: seq[CljVal] = @[] + var vs: seq[CljVal] = @[] + for (k, v) in pairs: + ks.add(k) + vs.add(v) + cljMap(ks, vs) + +# ---- Equality ---- + +proc `==`*(a, b: CljVal): bool = + if a.isNil and b.isNil: return true + if a.isNil or b.isNil: return false + if a.kind != b.kind: return false + case a.kind + of ckNil: true + of ckBool: a.boolVal == b.boolVal + of ckInt: a.intVal == b.intVal + of ckFloat: a.floatVal == b.floatVal + of ckString: a.strVal == b.strVal + of ckKeyword: a.kwName == b.kwName + of ckSymbol: a.symName == b.symName + else: false + +# ---- Display ---- + +proc cljRepr*(v: CljVal): string = + if v.isNil: return "nil" + case v.kind + of ckNil: "nil" + of ckBool: $v.boolVal + of ckInt: $v.intVal + of ckFloat: $v.floatVal + of ckString: "\"" & v.strVal.replace("\"", "\\\"") & "\"" + of ckKeyword: ":" & v.kwName + of ckSymbol: v.symName + of ckList: "(" & v.listItems.mapIt(cljRepr(it)).join(" ") & ")" + of ckVector: "[" & v.vecData.mapIt(cljRepr(it)).join(" ") & "]" + of ckMap: + var parts: seq[string] = @[] + for i in 0.." + of ckAtom: "(atom " & cljRepr(v.atomVal) & ")" + +proc cljStr*(v: CljVal): string = + if v.isNil: return "" + case v.kind + of ckString: v.strVal + of ckKeyword: ":" & v.kwName + of ckSymbol: v.symName + else: cljRepr(v) + +# ---- Type predicates ---- + +proc cljIsNil*(v: CljVal): bool = v.isNil or v.kind == ckNil +proc cljIsTrue*(v: CljVal): bool = v.kind == ckBool and v.boolVal +proc cljIsFalse*(v: CljVal): bool = v.kind == ckBool and not v.boolVal +proc cljIsTruthy*(v: CljVal): bool = not cljIsNil(v) and not cljIsFalse(v) + +# ---- I/O ---- + +proc cljPrintln*(args: seq[CljVal]): CljVal = + var parts: seq[string] = @[] + for a in args: + parts.add(cljStr(a)) + echo parts.join(" ") + cljNil() + +proc cljPrn*(args: seq[CljVal]): CljVal = + var parts: seq[string] = @[] + for a in args: + parts.add(cljRepr(a)) + echo parts.join(" ") + cljNil() + +# ---- Arithmetic ---- + +proc cljAdd*(args: seq[CljVal]): CljVal = + var sum: int64 = 0 + var sumF: float64 = 0.0 + var isFloat = false + for a in args: + case a.kind + of ckInt: sum += a.intVal + of ckFloat: + sumF += a.floatVal + isFloat = true + else: raise newException(CatchableError, "+ requires numbers") + if isFloat: cljFloat(sum.float64 + sumF) + else: cljInt(sum) + +proc cljMul*(args: seq[CljVal]): CljVal = + var product: int64 = 1 + var productF: float64 = 1.0 + var isFloat = false + for a in args: + case a.kind + of ckInt: product *= a.intVal + of ckFloat: + productF *= a.floatVal + isFloat = true + else: raise newException(CatchableError, "* requires numbers") + if isFloat: cljFloat(product.float64 * productF) + else: cljInt(product) + +proc cljSub*(args: seq[CljVal]): CljVal = + if args.len == 0: raise newException(CatchableError, "- requires at least 1 argument") + if args.len == 1: + case args[0].kind + of ckInt: return cljInt(-args[0].intVal) + of ckFloat: return cljFloat(-args[0].floatVal) + else: raise newException(CatchableError, "- requires numbers") + var r: int64 + var rF: float64 + var isFloat = false + case args[0].kind + of ckInt: r = args[0].intVal + of ckFloat: + rF = args[0].floatVal + isFloat = true + else: raise newException(CatchableError, "- requires numbers") + for i in 1.. result.intVal: result = args[i] + +# ---- Comparison ---- + +proc cljNumEq*(args: seq[CljVal]): CljVal = + if args.len < 2: return cljBool(true) + for i in 1..= b.intVal: return cljBool(false) + elif a.kind == ckFloat and b.kind == ckFloat: + if a.floatVal >= b.floatVal: return cljBool(false) + else: return cljBool(false) + cljBool(true) + +proc cljGt*(args: seq[CljVal]): CljVal = + if args.len < 2: return cljBool(true) + for i in 1.. b.intVal: return cljBool(false) + else: return cljBool(false) + cljBool(true) + +proc cljGe*(args: seq[CljVal]): CljVal = + if args.len < 2: return cljBool(true) + for i in 1.. 0) + of ckFloat: cljBool(v.floatVal > 0.0) + else: raise newException(CatchableError, "pos? requires a number") + +proc cljNeg*(v: CljVal): CljVal = + case v.kind + of ckInt: cljBool(v.intVal < 0) + of ckFloat: cljBool(v.floatVal < 0.0) + else: raise newException(CatchableError, "neg? requires a number") + +proc cljEven*(v: CljVal): CljVal = + if v.kind == ckInt: cljBool(v.intVal mod 2 == 0) + else: raise newException(CatchableError, "even? requires an integer") + +proc cljOdd*(v: CljVal): CljVal = + if v.kind == ckInt: cljBool(v.intVal mod 2 != 0) + else: raise newException(CatchableError, "odd? requires an integer") + +# ---- Collection operations ---- + +proc cljCount*(v: CljVal): int = + if v.isNil: return 0 + case v.kind + of ckList: v.listItems.len + of ckVector: v.vecData.len + of ckMap: v.mapKeys.len + of ckString: v.strVal.len + else: 0 + +proc cljFirst*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v.listItems[0] + of ckVector: + if v.vecData.len == 0: cljNil() + else: v.vecData[0] + else: cljNil() + +proc cljRest*(v: CljVal): CljVal = + if v.isNil: return cljList(@[]) + case v.kind + of ckList: + if v.listItems.len <= 1: cljList(@[]) + else: cljList(v.listItems[1..^1]) + of ckVector: + if v.vecData.len <= 1: cljList(@[]) + else: cljList(v.vecData[1..^1]) + else: cljList(@[]) + +proc cljNext*(v: CljVal): CljVal = + let r = cljRest(v) + if r.kind == ckList and r.listItems.len == 0: + return cljNil() + r + +proc cljLast*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v.listItems[^1] + of ckVector: + if v.vecData.len == 0: cljNil() + else: v.vecData[^1] + else: cljNil() + +proc cljNth*(v: CljVal, n: int): CljVal = + case v.kind + of ckList: + if n < 0 or n >= v.listItems.len: + raise newException(IndexDefect, "nth: index out of range") + v.listItems[n] + of ckVector: + if n < 0 or n >= v.vecData.len: + raise newException(IndexDefect, "nth: index out of range") + v.vecData[n] + else: + raise newException(CatchableError, "nth requires a collection") + +proc cljConj*(coll: CljVal, item: CljVal): CljVal = + if coll.isNil: + return cljList(@[item]) + case coll.kind + of ckList: + var newItems = @[item] + newItems.add(coll.listItems) + cljList(newItems) + of ckVector: + var newItems = coll.vecData + newItems.add(item) + cljVector(newItems) + else: + cljList(@[item]) + +proc cljCons*(item: CljVal, coll: CljVal): CljVal = + if coll.isNil: + return cljList(@[item]) + case coll.kind + of ckList: + var newItems = @[item] + newItems.add(coll.listItems) + cljList(newItems) + of ckVector: + var newItems = @[item] + newItems.add(coll.vecData) + cljList(newItems) + else: + cljList(@[item]) + +proc cljSeq*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v + of ckVector: + if v.vecData.len == 0: cljNil() + else: cljList(v.vecData) + of ckString: + if v.strVal.len == 0: cljNil() + else: + var chars: seq[CljVal] = @[] + for c in v.strVal: + chars.add(cljString($c)) + cljList(chars) + else: cljNil() + +proc cljVec*(v: CljVal): CljVal = + if v.isNil: return cljVector(@[]) + case v.kind + of ckList: cljVector(v.listItems) + of ckVector: v + else: cljVector(@[v]) + +proc cljEmpty*(v: CljVal): CljVal = + cljBool(cljCount(v) == 0) + +proc cljConcat*(args: seq[CljVal]): CljVal = + var items: seq[CljVal] = @[] + for a in args: + if not a.isNil: + case a.kind + of ckList: items.add(a.listItems) + of ckVector: items.add(a.vecData) + else: discard + cljList(items) + +proc cljTake*(n: int, coll: CljVal): CljVal = + if coll.isNil: return cljList(@[]) + case coll.kind + of ckList: cljList(coll.listItems[0..= s.strVal.len: + raise newException(IndexDefect, "subs: index out of range") + cljString(s.strVal[startIdx..^1]) + +proc cljSubsRange*(s: CljVal, startIdx, endIdx: int): CljVal = + if s.kind != ckString: raise newException(CatchableError, "subs requires a string") + if startIdx < 0 or startIdx > endIdx or endIdx > s.strVal.len: + raise newException(IndexDefect, "subs: index out of range") + cljString(s.strVal[startIdx.. [output.nim] Compile to Nim" echo " cljnim run Compile and run" echo " cljnim read Parse and print AST" + echo " cljnim -e '' Evaluate expression" + echo " cljnim -M -e '' Evaluate with full runtime" quit(0) let cmd = args[0] + # Handle -e and -M -e flags + if cmd == "-e" or cmd == "-M": + var code = "" + var idx = 1 + if cmd == "-M" and args.len > 1 and args[1] == "-e": + idx = 2 + elif cmd == "-e": + idx = 1 + else: + stderr.writeLine("Usage: cljnim [-M] -e ''") + quit(1) + if idx >= args.len: + stderr.writeLine("Error: Missing expression after -e") + quit(1) + code = args[idx] + let tmpDir = getTempDir() + let nimPath = tmpDir / "cljnim_expr.nim" + let binPath = tmpDir / "cljnim_expr" + let forms = reader.readAll(code) + let nimCode = emitter.emitProgram(forms) + writeFile(nimPath, nimCode) + let compileResult = nimCompile(nimPath, binPath) + if compileResult != 0: + stderr.writeLine("Compilation failed") + quit(1) + let runResult = execCmd(binPath) + quit(runResult) + case cmd of "compile": if args.len < 2: diff --git a/src/core.nim b/src/core.nim new file mode 100644 index 0000000..280ca4b --- /dev/null +++ b/src/core.nim @@ -0,0 +1,456 @@ +import runtime +import strutils +import sequtils + +# ---- Arithmetic ---- + +proc cljAdd*(args: seq[CljVal]): CljVal = + var sum: int64 = 0 + var sumFloat: float64 = 0.0 + var isFloat = false + for a in args: + case a.kind + of ckInt: sum += a.intVal + of ckFloat: + sumFloat += a.floatVal + isFloat = true + else: raise newException(CatchableError, "+ requires numbers") + if isFloat: + cljFloat(sum.float64 + sumFloat) + else: + cljInt(sum) + +proc cljMul*(args: seq[CljVal]): CljVal = + var product: int64 = 1 + var productFloat: float64 = 1.0 + var isFloat = false + for a in args: + case a.kind + of ckInt: product *= a.intVal + of ckFloat: + productFloat *= a.floatVal + isFloat = true + else: raise newException(CatchableError, "* requires numbers") + if isFloat: + cljFloat(product.float64 * productFloat) + else: + cljInt(product) + +proc cljSub*(args: seq[CljVal]): CljVal = + if args.len == 0: raise newException(CatchableError, "- requires at least 1 argument") + if args.len == 1: + case args[0].kind + of ckInt: return cljInt(-args[0].intVal) + of ckFloat: return cljFloat(-args[0].floatVal) + else: raise newException(CatchableError, "- requires numbers") + var result: int64 + var resultF: float64 + var isFloat = false + case args[0].kind + of ckInt: result = args[0].intVal + of ckFloat: + resultF = args[0].floatVal + isFloat = true + else: raise newException(CatchableError, "- requires numbers") + for i in 1..= b.intVal: return cljBool(false) + elif a.kind == ckFloat and b.kind == ckFloat: + if a.floatVal >= b.floatVal: return cljBool(false) + else: + return cljBool(false) + cljBool(true) + +proc cljGt*(args: seq[CljVal]): CljVal = + if args.len < 2: return cljBool(true) + for i in 1.. b.intVal: return cljBool(false) + else: + return cljBool(false) + cljBool(true) + +proc cljGe*(args: seq[CljVal]): CljVal = + if args.len < 2: return cljBool(true) + for i in 1.. 0) + of ckFloat: cljBool(v.floatVal > 0.0) + else: raise newException(CatchableError, "pos? requires a number") + +proc cljNeg*(v: CljVal): CljVal = + case v.kind + of ckInt: cljBool(v.intVal < 0) + of ckFloat: cljBool(v.floatVal < 0.0) + else: raise newException(CatchableError, "neg? requires a number") + +proc cljEven*(v: CljVal): CljVal = + if v.kind == ckInt: cljBool(v.intVal mod 2 == 0) + else: raise newException(CatchableError, "even? requires an integer") + +proc cljOdd*(v: CljVal): CljVal = + if v.kind == ckInt: cljBool(v.intVal mod 2 != 0) + else: raise newException(CatchableError, "odd? requires an integer") + +# ---- Collection operations ---- + +proc cljFirst*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v.listItems[0] + of ckVector: + if v.listItems.len == 0: cljNil() + else: v.listItems[0] + else: cljNil() + +proc cljRest*(v: CljVal): CljVal = + if v.isNil: return cljList(@[]) + case v.kind + of ckList: + if v.listItems.len <= 1: cljList(@[]) + else: cljList(v.listItems[1..^1]) + of ckVector: + if v.listItems.len <= 1: cljList(@[]) + else: cljList(v.listItems[1..^1]) + else: cljList(@[]) + +proc cljNext*(v: CljVal): CljVal = + let r = cljRest(v) + if r.kind == ckList and r.listItems.len == 0: + return cljNil() + r + +proc cljLast*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v.listItems[^1] + of ckVector: + if v.listItems.len == 0: cljNil() + else: v.listItems[^1] + else: cljNil() + +proc cljNth*(v: CljVal, n: int): CljVal = + case v.kind + of ckList: + if n < 0 or n >= v.listItems.len: + raise newException(IndexDefect, "nth: index out of range") + v.listItems[n] + of ckVector: + if n < 0 or n >= v.listItems.len: + raise newException(IndexDefect, "nth: index out of range") + v.listItems[n] + else: + raise newException(CatchableError, "nth requires a collection") + +proc cljCount*(v: CljVal): int = + if v.isNil: return 0 + case v.kind + of ckList: v.listItems.len + of ckVector: v.listItems.len + of ckString: v.strVal.len + else: 0 + +proc cljConj*(coll: CljVal, item: CljVal): CljVal = + if coll.isNil: + return cljList(@[item]) + case coll.kind + of ckList: + var newItems = @[item] + newItems.add(coll.listItems) + cljList(newItems) + of ckVector: + var newItems = coll.listItems + newItems.add(item) + cljList(newItems) + else: + cljList(@[item]) + +proc cljCons*(item: CljVal, coll: CljVal): CljVal = + if coll.isNil or (coll.kind == ckList and coll.listItems.len == 0): + return cljList(@[item]) + case coll.kind + of ckList: + var newItems = @[item] + newItems.add(coll.listItems) + cljList(newItems) + of ckVector: + var newItems = @[item] + newItems.add(coll.listItems) + cljList(newItems) + else: + cljList(@[item]) + +proc cljSeq*(v: CljVal): CljVal = + if v.isNil: return cljNil() + case v.kind + of ckList: + if v.listItems.len == 0: cljNil() + else: v + of ckVector: + if v.listItems.len == 0: cljNil() + else: cljList(v.listItems) + of ckString: + if v.strVal.len == 0: cljNil() + else: + var chars: seq[CljVal] = @[] + for c in v.strVal: + chars.add(cljString($c)) + cljList(chars) + else: cljNil() + +proc cljVec*(v: CljVal): CljVal = + if v.isNil: return cljList(@[]) + case v.kind + of ckList: cljList(v.listItems) + of ckVector: v + else: cljList(@[v]) + +proc cljEmpty*(v: CljVal): CljVal = + cljBool(cljCount(v) == 0) + +# ---- Higher-order functions ---- + +proc cljMap*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): seq[CljVal] = + result = @[] + for item in coll: + result.add(f(@[item])) + +proc cljFilter*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): seq[CljVal] = + result = @[] + for item in coll: + let r = f(@[item]) + if r.kind == ckBool and r.boolVal: + result.add(item) + +proc cljReduce*(f: proc(args: seq[CljVal]): CljVal, init: CljVal, coll: seq[CljVal]): CljVal = + result = init + for item in coll: + result = f(@[result, item]) + +proc cljMapv*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): CljVal = + cljList(cljMap(f, coll)) + +proc cljSome*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): CljVal = + for item in coll: + let r = f(@[item]) + if r.kind != ckBool or r.boolVal: + return r + cljNil() + +proc cljEvery*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): CljVal = + for item in coll: + let r = f(@[item]) + if r.kind == ckBool and not r.boolVal: + return cljBool(false) + cljBool(true) + +proc cljNot*(v: CljVal): CljVal = + if v.kind == ckBool and not v.boolVal: + cljBool(true) + else: + cljBool(false) + +proc cljApply*(f: proc(args: seq[CljVal]): CljVal, args: seq[CljVal]): CljVal = + f(args) + +proc cljComp*(fns: seq[proc(args: seq[CljVal]): CljVal]): proc(args: seq[CljVal]): CljVal = + proc(args: seq[CljVal]): CljVal = + result = args + for i in countdown(fns.len - 1, 0): + result = fns[i](@[result]) + +proc cljPartial*(f: proc(args: seq[CljVal]): CljVal, partialArgs: seq[CljVal]): proc(args: seq[CljVal]): CljVal = + proc(args: seq[CljVal]): CljVal = + var allArgs = partialArgs + allArgs.add(args) + f(allArgs) + +# ---- I/O ---- + +proc cljPrintln*(args: seq[CljVal]): CljVal = + var parts: seq[string] = @[] + for a in args: + parts.add(cljStr(a)) + echo parts.join(" ") + cljNil() + +proc cljPrn*(args: seq[CljVal]): CljVal = + var parts: seq[string] = @[] + for a in args: + parts.add(cljRepr(a)) + echo parts.join(" ") + cljNil() + +proc cljStr*(args: seq[CljVal]): CljVal = + var s = "" + for a in args: + s.add(cljStr(a)) + cljString(s) + +proc cljPrStr*(args: seq[CljVal]): CljVal = + var parts: seq[string] = @[] + for a in args: + parts.add(cljRepr(a)) + cljString(parts.join(" ")) + +# ---- Misc ---- + +proc cljIdentity*(args: seq[CljVal]): CljVal = + if args.len == 0: cljNil() + else: args[0] + +proc cljConstantly*(v: CljVal): proc(args: seq[CljVal]): CljVal = + proc(args: seq[CljVal]): CljVal = v + +proc cljType*(v: CljVal): CljVal = + if v.isNil: return cljKeyword("nil") + case v.kind + of ckBool: cljKeyword("boolean") + of ckInt: cljKeyword("integer") + of ckFloat: cljKeyword("float") + of ckString: cljKeyword("string") + of ckKeyword: cljKeyword("keyword") + of ckSymbol: cljKeyword("symbol") + of ckList: cljKeyword("list") + of ckVector: cljKeyword("vector") + of ckMap: cljKeyword("map") + of ckFn: cljKeyword("function") + of ckAtom: cljKeyword("atom") + +proc cljMinMax*(args: seq[CljVal], isMin: bool): CljVal = + if args.len == 0: raise newException(CatchableError, "min/max requires at least 1 argument") + result = args[0] + for i in 1.. result.intVal: result = args[i] + +proc cljAbs*(v: CljVal): CljVal = + case v.kind + of ckInt: cljInt(abs(v.intVal)) + of ckFloat: cljFloat(abs(v.floatVal)) + else: raise newException(CatchableError, "abs requires a number") + +proc cljQuot*(a, b: CljVal): CljVal = + if a.kind == ckInt and b.kind == ckInt: + cljInt(a.intVal div b.intVal) + else: + raise newException(CatchableError, "quot requires integers") diff --git a/src/emitter.nim b/src/emitter.nim index 54bb39e..7f31431 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -1,73 +1,243 @@ # Clojure → Nim Emitter -import strutils, sequtils +import strutils, sequtils, sets import types +import macros + +var requiredImports* = initHashSet[string]() type EmitterError* = object of CatchableError proc mangleName*(name: string): string = - ## Convert Clojure symbol to valid Nim identifier result = "" for c in name: case c of '-': result.add('_') of '?': result.add("_Q") of '!': result.add("_B") - of '*': result.add("_STAR") - of '+': result.add("_PLUS") - of '/': result.add("_DIV") - of '=': result.add("_EQ") - of '>': result.add("_GT") - of '<': result.add("_LT") + of '*': result.add("_STAR_") + of '+': result.add("_PLUS_") + of '/': result.add("_SLASH_") + of '=': result.add("_EQ_") + of '>': result.add("_GT_") + of '<': result.add("_LT_") of '.': result.add('_') - of '\'': result.add("_QUOTE") + of '\'': result.add("_QUOTE_") + of '&': result.add("_AMP_") else: result.add(c) proc emitExpr*(v: CljVal, indent: int = 0): string +proc emitBlock*(items: seq[CljVal], indent: int): string -proc emitBlock(items: seq[CljVal], indent: int): string = - if items.len == 0: - return "discard" - var lines: seq[string] = @[] - for i, item in items: - lines.add(emitExpr(item, indent)) - return lines.join("\n") +proc indentStr(indent: int): string = + " ".repeat(indent) + +proc runtimeName(op: string): string = + case op + of "+": "cljAdd" + of "-": "cljSub" + of "*": "cljMul" + of "/": "cljDiv" + of "=": "cljNumEq" + of ">": "cljGt" + of "<": "cljLt" + of ">=": "cljGe" + of "<=": "cljLe" + of "not=": "cljNotEq" + of "not": "cljNot" + of "println": "cljPrintln" + of "prn": "cljPrn" + of "str": "cljStrConcat" + of "pr-str": "cljPrStrConcat" + of "inc": "cljInc" + of "dec": "cljDec" + of "zero?": "cljZero" + of "pos?": "cljPos" + of "neg?": "cljNeg" + of "even?": "cljEven" + of "odd?": "cljOdd" + of "first": "cljFirst" + of "rest": "cljRest" + of "next": "cljNext" + of "last": "cljLast" + of "nth": "cljNth" + of "count": "cljCount" + of "conj": "cljConj" + of "cons": "cljCons" + of "seq": "cljSeq" + of "vec": "cljVec" + of "empty?": "cljEmpty" + of "concat": "cljConcat" + of "take": "cljTake" + of "drop": "cljDrop" + of "reverse": "cljReverse" + of "sort": "cljSort" + of "distinct": "cljDistinct" + of "flatten": "cljFlatten" + of "partition": "cljPartition" + of "frequencies": "cljFrequencies" + of "get": "cljGet" + of "get-in": "cljGetIn" + of "assoc": "cljAssoc" + of "dissoc": "cljDissoc" + of "keys": "cljKeys" + of "vals": "cljVals" + of "contains?": "cljContains" + of "select-keys": "cljSelectKeys" + of "merge": "cljMerge" + of "identity": "cljIdentity" + of "type": "cljType" + of "abs": "cljAbs" + of "mod": "cljMod" + of "min": "cljMin" + of "max": "cljMax" + of "apply": "cljApply" + of "atom": "cljAtom" + of "deref": "cljDeref" + of "reset!": "cljReset" + of "swap!": "cljSwap" + 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 spaces = " ".repeat(indent) + let sp = indentStr(indent) let head = items[0] if head.kind != ckSymbol: raise newException(EmitterError, "List head must be a symbol, got " & $head.kind) let op = head.symName - + + # ---- Special forms (language constructs) ---- case op - of "+", "-", "*", "/", "=", ">", "<": - if items.len < 3: - raise newException(EmitterError, op & " requires at least 2 arguments") - var parts: seq[string] = @[] - let nimOp = case op - of "=": "==" - else: op - for i in 1.. arg value + var env: seq[(string, CljVal)] = @[] + if macroParams.kind == ckVector: + for i in 0.. 0: - return spaces & "proc " & procName & "(" & typedParams.join(", ") & "): int =\n" & bodyCode + if p.kind != ckSymbol: + raise newException(EmitterError, "defn- params must be symbols") + paramNames.add(mangleName(p.symName) & ": CljVal") + let body = items[3..^1] + var bodyCode = "" + if body.len == 1: + bodyCode = emitExpr(body[0], indent + 1) else: - return spaces & "proc " & procName & "(): int =\n" & bodyCode - + bodyCode = emitBlock(body, indent + 1) + let procName = mangleName(name.symName) + return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode + of "fn": if items.len < 3: raise newException(EmitterError, "fn requires params and body") @@ -107,21 +294,15 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = for p in params.items: if p.kind != ckSymbol: raise newException(EmitterError, "fn params must be symbols") - paramNames.add(mangleName(p.symName) & ": auto") + paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[2..^1] var bodyCode = "" if body.len == 1: bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) - var typedParams: seq[string] = @[] - for p in params.items: - typedParams.add(mangleName(p.symName) & ": int") - if typedParams.len > 0: - return spaces & "proc(" & typedParams.join(", ") & "): int =\n" & bodyCode - else: - return spaces & "proc(): int =\n" & bodyCode - + return sp & "(proc(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + of "let": if items.len < 3: raise newException(EmitterError, "let requires bindings and body") @@ -131,142 +312,649 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if bindings.items.len mod 2 != 0: raise newException(EmitterError, "let bindings must have even number of elements") var lines: seq[string] = @[] - lines.add("block:") - var bindLines: seq[string] = @[] - var i = 0 - while i < bindings.items.len: - let name = bindings.items[i] - let val = bindings.items[i+1] - if name.kind != ckSymbol: + lines.add(sp & "block:") + var bi = 0 + while bi < bindings.items.len: + let bname = bindings.items[bi] + let bval = bindings.items[bi+1] + if bname.kind != ckSymbol: raise newException(EmitterError, "let binding name must be a symbol") - bindLines.add(mangleName(name.symName) & " = " & emitExpr(val, 0).strip()) - i += 2 - lines.add(" ".repeat(indent + 1) & "let") - for bl in bindLines: - lines.add(" ".repeat(indent + 2) & bl) + lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, 0)) + bi += 2 let body = items[2..^1] for j, b in body: - let bcode = emitExpr(b, indent + 1).strip() + let bcode = emitExpr(b, indent + 1) if j == body.len - 1: - lines.add(" ".repeat(indent + 1) & bcode) + lines.add(bcode) else: - if bcode.startsWith("echo ") or bcode.startsWith("discard "): - lines.add(" ".repeat(indent + 1) & bcode) + let stripped = bcode.strip() + if stripped.startsWith("echo ") or stripped.startsWith("discard ") or + stripped.startsWith("block:") or stripped.startsWith("if ") or + stripped.startsWith("try:"): + lines.add(bcode) else: - lines.add(" ".repeat(indent + 1) & "discard " & bcode) - return spaces & lines.join("\n") - + lines.add(indentStr(indent + 1) & "discard " & stripped) + return lines.join("\n") + of "if": if items.len < 3 or items.len > 4: raise newException(EmitterError, "if requires condition, then, and optional else") - let condCode = emitExpr(items[1], indent) + let condCode = emitExpr(items[1], 0) let thenCode = emitExpr(items[2], indent + 1) - var result = spaces & "if " & condCode & ":\n" & " ".repeat(indent + 1) & thenCode + let thenStripped = thenCode.strip() + var thenFinal = thenCode + if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or + thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or + thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or + thenStripped.startsWith("let ") or thenStripped.startsWith("proc ")): + thenFinal = indentStr(indent + 1) & "discard " & thenStripped + var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal if items.len == 4: let elseCode = emitExpr(items[3], indent + 1) - result.add("\n" & spaces & "else:\n" & " ".repeat(indent + 1) & elseCode) - return result - + let elseStripped = elseCode.strip() + var elseFinal = elseCode + if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or + elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or + elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or + elseStripped.startsWith("let ") or elseStripped.startsWith("proc ")): + elseFinal = indentStr(indent + 1) & "discard " & elseStripped + ifBlock.add("\n" & sp & "else:\n" & elseFinal) + return ifBlock + + of "when": + if items.len < 3: + raise newException(EmitterError, "when requires condition and body") + let condCode = emitExpr(items[1], 0) + var lines: seq[string] = @[] + lines.add(sp & "if cljIsTruthy(" & condCode & "):") + let body = items[2..^1] + for b in body: + lines.add(emitExpr(b, indent + 1)) + return lines.join("\n") + + of "when-let": + if items.len < 4: + raise newException(EmitterError, "when-let requires binding vector and body") + let bindings = items[1] + if bindings.kind != ckVector or bindings.items.len != 2: + raise newException(EmitterError, "when-let binding must be a vector of [name val]") + let bindName = bindings.items[0] + let bindVal = bindings.items[1] + if bindName.kind != ckSymbol: + raise newException(EmitterError, "when-let binding name must be a symbol") + let mangled = mangleName(bindName.symName) + var lines: seq[string] = @[] + lines.add(sp & "block:") + lines.add(indentStr(indent + 1) & "let " & mangled & " = " & emitExpr(bindVal, 0)) + lines.add(indentStr(indent + 1) & "if not cljIsNil(" & mangled & "):") + let body = items[2..^1] + for b in body: + lines.add(emitExpr(b, indent + 2)) + return lines.join("\n") + + of "if-let": + if items.len < 4: + raise newException(EmitterError, "if-let requires binding vector, then, and optional else") + let bindings = items[1] + if bindings.kind != ckVector or bindings.items.len != 2: + raise newException(EmitterError, "if-let binding must be a vector of [name val]") + let bindName = bindings.items[0] + let bindVal = bindings.items[1] + if bindName.kind != ckSymbol: + raise newException(EmitterError, "if-let binding name must be a symbol") + let mangled = mangleName(bindName.symName) + var lines: seq[string] = @[] + lines.add(sp & "block:") + lines.add(indentStr(indent + 1) & "let " & mangled & " = " & emitExpr(bindVal, 0)) + lines.add(indentStr(indent + 1) & "if not cljIsNil(" & mangled & "):") + lines.add(emitExpr(items[2], indent + 2)) + if items.len == 4: + lines.add(indentStr(indent + 1) & "else:") + lines.add(emitExpr(items[3], indent + 2)) + return lines.join("\n") + + of "cond": + if items.len < 3 or items.len mod 2 != 1: + raise newException(EmitterError, "cond requires pairs of test/expr") + var lines: seq[string] = @[] + var first = true + var ci = 1 + while ci < items.len: + let testExpr = items[ci] + let thenExpr = items[ci+1] + if (testExpr.kind == ckSymbol and testExpr.symName == ":else") or + (testExpr.kind == ckKeyword and testExpr.kwName == "else"): + lines.add(sp & "else:") + lines.add(emitExpr(thenExpr, indent + 1)) + elif first: + lines.add(sp & "if cljIsTruthy(" & emitExpr(testExpr, 0) & "):") + lines.add(emitExpr(thenExpr, indent + 1)) + first = false + else: + lines.add(sp & "elif cljIsTruthy(" & emitExpr(testExpr, 0) & "):") + lines.add(emitExpr(thenExpr, indent + 1)) + ci += 2 + return lines.join("\n") + + of "loop": + if items.len < 3: + raise newException(EmitterError, "loop requires bindings and body") + let bindings = items[1] + if bindings.kind != ckVector: + raise newException(EmitterError, "loop bindings must be a vector") + if bindings.items.len mod 2 != 0: + raise newException(EmitterError, "loop bindings must have even number of elements") + var loopParams: seq[(string, string)] = @[] + var li = 0 + while li < bindings.items.len: + let lname = bindings.items[li] + let lval = bindings.items[li+1] + if lname.kind != ckSymbol: + raise newException(EmitterError, "loop binding name must be a symbol") + loopParams.add((mangleName(lname.symName), emitExpr(lval, 0))) + li += 2 + var lines: seq[string] = @[] + for (lpName, lpVal) in loopParams: + lines.add(sp & "var " & lpName & ": CljVal = " & lpVal) + lines.add(sp & "while true:") + let body = items[2..^1] + for b in body: + lines.add(emitExpr(b, indent + 1)) + return lines.join("\n") + + of "recur": + if items.len < 2: + raise newException(EmitterError, "recur requires arguments") + # Simplified: emit as discard (real impl needs loop var assignment) + var lines: seq[string] = @[] + for ri in 1.. 0 and form.items[0].kind == ckSymbol: + if form.items[0].symName == "catch": + if form.items.len < 3: + raise newException(EmitterError, "catch requires exception type, name, and body") + let exName = form.items[2] + if exName.kind != ckSymbol: + raise newException(EmitterError, "catch name must be a symbol") + catchClauses.add((mangleName(exName.symName), form.items[3..^1])) + elif form.items[0].symName == "finally": + finallyBody = form.items[1..^1] + else: + bodyForms.add(form) + else: + bodyForms.add(form) + inc ti + var lines: seq[string] = @[] + lines.add(sp & "try:") + for b in bodyForms: + lines.add(emitExpr(b, indent + 1)) + for (exVar, exBody) in catchClauses: + lines.add(sp & "except CatchableError:") + lines.add(indentStr(indent + 1) & "let " & exVar & " = cljString(getCurrentExceptionMsg())") + for eb in exBody: + lines.add(emitExpr(eb, indent + 1)) + if finallyBody.len > 0: + lines.add(sp & "finally:") + for fb in finallyBody: + lines.add(emitExpr(fb, indent + 1)) + return lines.join("\n") + + of "throw": + if items.len != 2: + raise newException(EmitterError, "throw requires exactly 1 argument") + return sp & "raise newException(CatchableError, cljStr(" & emitExpr(items[1], 0) & "))" + of "quote": if items.len != 2: raise newException(EmitterError, "quote requires exactly 1 argument") let quoted = items[1] case quoted.kind of ckSymbol: - return spaces & "cljSymbol(\"" & quoted.symName & "\")" + return sp & "cljSymbol(\"" & quoted.symName & "\")" of ckList: var parts: seq[string] = @[] for item in quoted.items: - parts.add(emitExpr(item, indent)) - return spaces & "cljList(@[" & parts.join(", ") & "])" + parts.add(emitExpr(item, 0)) + return sp & "cljList(@[" & parts.join(", ") & "])" of ckVector: var parts: seq[string] = @[] for item in quoted.items: - parts.add(emitExpr(item, indent)) - return spaces & "cljVector(@[" & parts.join(", ") & "])" + parts.add(emitExpr(item, 0)) + return sp & "cljVector(@[" & parts.join(", ") & "])" else: return emitExpr(quoted, indent) - + + of "map": + if items.len != 3: + raise newException(EmitterError, "map requires exactly 2 arguments (f, coll)") + let fnArg = items[1] + let collArg = emitExpr(items[2], 0) + if fnArg.kind == ckSymbol: + let rn = runtimeName(fnArg.symName) + if rn.len > 0: + return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & collArg & ")" + return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args[0])), " & collArg & ")" + elif fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn": + # Inline fn: (map (fn [x] ...) coll) + let fnParams = fnArg.items[1] + let fnBody = fnArg.items[2..^1] + var pNames: seq[string] = @[] + for p in fnParams.items: + pNames.add(mangleName(p.symName)) + var bodyExpr = "" + if fnBody.len == 1: + bodyExpr = emitExpr(fnBody[0], 0).strip() + else: + var parts: seq[string] = @[] + for b in fnBody: + parts.add(emitExpr(b, 0).strip()) + bodyExpr = parts.join("; ") + let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")" + return sp & "cljMap(cljFn(" & fnProc & "), " & collArg & ")" + else: + return sp & "cljMap(" & emitExpr(fnArg, 0) & ", " & collArg & ")" + + of "filter": + if items.len != 3: + raise newException(EmitterError, "filter requires exactly 2 arguments") + let fnArg = items[1] + let collArg = emitExpr(items[2], 0) + if fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn": + let fnParams = fnArg.items[1] + let fnBody = fnArg.items[2..^1] + var pNames: seq[string] = @[] + for p in fnParams.items: + pNames.add(mangleName(p.symName)) + var bodyExpr = "" + if fnBody.len == 1: + bodyExpr = emitExpr(fnBody[0], 0).strip() + else: + var parts: seq[string] = @[] + for b in fnBody: + parts.add(emitExpr(b, 0).strip()) + bodyExpr = parts.join("; ") + # Wrap single-param fn as seq[CljVal] version + let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")" + return sp & "cljFilter(cljFn(" & fnProc & "), " & collArg & ")" + else: + return sp & "cljFilter(" & emitExpr(fnArg, 0) & ", " & collArg & ")" + + of "reduce": + if items.len < 3 or items.len > 4: + raise newException(EmitterError, "reduce requires 2 or 3 arguments") + if items.len == 3: + # (reduce f coll) — first element as init + let fnArg = items[1] + let collArg = emitExpr(items[2], 0) + return sp & "cljReduce(" & emitExpr(fnArg, 0) & ", cljNil(), " & collArg & ")" + else: + # (reduce f init coll) + let fnArg = items[1] + let initArg = emitExpr(items[2], 0) + let collArg = emitExpr(items[3], 0) + return sp & "cljReduce(" & emitExpr(fnArg, 0) & ", " & initArg & ", " & collArg & ")" + + of "mapv": + if items.len != 3: + raise newException(EmitterError, "mapv requires exactly 2 arguments") + return sp & "cljMapv(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + + of "apply": + if items.len != 3: + raise newException(EmitterError, "apply requires exactly 2 arguments") + return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + + of "some": + if items.len != 3: + raise newException(EmitterError, "some requires exactly 2 arguments") + return sp & "cljSome(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + + of "every?": + if items.len != 3: + raise newException(EmitterError, "every? requires exactly 2 arguments") + return sp & "cljEvery(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + + of "into": + if items.len != 3: + raise newException(EmitterError, "into requires exactly 2 arguments") + return sp & "cljInto(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + + of "comp": + var parts: seq[string] = @[] + for i in 1.. 4: + raise newException(EmitterError, "subs requires 2 or 3 arguments") + if items.len == 3: + return sp & "cljSubs(" & emitExpr(items[1], 0) & ", cljInt(" & emitExpr(items[2], 0) & ").intVal)" + else: + return sp & "cljSubsRange(" & emitExpr(items[1], 0) & ", cljInt(" & emitExpr(items[2], 0) & ").intVal, cljInt(" & emitExpr(items[3], 0) & ").intVal)" + + of "get-in": + if items.len < 3 or items.len > 4: + raise newException(EmitterError, "get-in requires 2 or 3 arguments") + if items.len == 3: + return sp & "cljGetIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + else: + return sp & "cljGetIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & emitExpr(items[3], 0) & ")" + + of "update": + if items.len < 4: + raise newException(EmitterError, "update requires at least 3 arguments") + var fArg: string + if items[3].kind == ckSymbol: + let rn = runtimeName(items[3].symName) + if rn.len > 0: + fArg = "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" + else: + fArg = mangleName(items[3].symName) + else: + fArg = emitExpr(items[3], 0) + var extraArgs: seq[string] = @[] + for i in 4.. 0: + return sp & "cljUpdate(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & fArg & ", @[" & extraArgs.join(", ") & "])" + else: + return sp & "cljUpdate(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & fArg & ")" + + of "assoc-in": + if items.len != 4: + raise newException(EmitterError, "assoc-in requires exactly 3 arguments") + return sp & "cljAssocIn(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ", " & emitExpr(items[3], 0) & ")" + + of "and": + if items.len < 2: return sp & "cljBool(true)" + if items.len == 2: return emitExpr(items[1], indent) + # (and a b c) => ternary: if a then (if b then c else false) else false + # Generate as nested inline if + let lastExpr = emitExpr(items[^1], 0) + var r = lastExpr + for i in countdown(items.len - 2, 1): + r = "(if cljIsTruthy(" & emitExpr(items[i], 0) & "): " & r & " else: cljBool(false))" + return sp & r + + of "or": + if items.len < 2: return sp & "cljNil()" + if items.len == 2: return emitExpr(items[1], indent) + # (or a b c) => ternary: if a then a else (if b then b else c) + let lastExpr = emitExpr(items[^1], 0) + var r = lastExpr + for i in countdown(items.len - 2, 1): + let argCode = emitExpr(items[i], 0) + r = "(if cljIsTruthy(" & argCode & "): " & argCode & " else: " & r & ")" + return sp & r + + of "nil?": + if items.len != 2: + raise newException(EmitterError, "nil? requires exactly 1 argument") + return sp & "cljBool(cljIsNil(" & emitExpr(items[1], 0) & "))" + + of "set!": + if items.len != 3: + raise newException(EmitterError, "set! requires exactly 2 arguments") + let target = items[1] + if target.kind != ckSymbol: + raise newException(EmitterError, "set! target must be a symbol") + return sp & mangleName(target.symName) & " = " & emitExpr(items[2], 0) + + of "range": + if items.len < 1 or items.len > 3: + raise newException(EmitterError, "range requires 0, 1, or 2 arguments") + if items.len == 1: + return sp & "cljList(@[])" + elif items.len == 2: + return sp & "cljRange(" & emitExpr(items[1], 0) & ")" + else: + return sp & "cljRange(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + else: - # Function call + # ---- Nim interop: nim/module/function ---- + if op.startsWith("nim/") or op.startsWith("nim."): + let parts = op.replace(".", "/").split("/") + if parts.len >= 3: + let module = parts[1] + var funcChain = parts[2..^1].join(".") + # Strip ? suffix (Clojure convention) — not valid in Nim + if funcChain.endsWith("?"): + funcChain = funcChain[0..^2] + var argParts: seq[string] = @[] + for i in 1..= 1: + var nimArgs: seq[string] = @[] + nimArgs.add(argParts[0] & ".strVal") + for i in 1..= 1: + var nimArgs: seq[string] = @[] + nimArgs.add(argParts[0] & ".strVal") + for i in 1..= 2: + var nimArgs: seq[string] = @[] + nimArgs.add(argParts[0] & ".strVal") + for i in 1..= 2: + return sp & "cljList(" & funcChain & "(" & argParts[0] & ".strVal, " & argParts[1] & ".strVal).mapIt(cljString(it)))" + return sp & funcChain & "(" & argParts.join(", ") & ")" + of "times": + requiredImports.incl("times") + return sp & funcChain & "(" & argParts.join(", ") & ")" + of "os": + requiredImports.incl("os") + return sp & "cljString(" & funcChain & "(" & argParts.join(", ") & "))" + of "system": + return sp & funcChain & "(" & argParts.join(", ") & ")" + else: + requiredImports.incl(module) + return sp & funcChain & "(" & argParts.join(", ") & ")" + elif parts.len == 2: + return sp & "cljString(\"" & parts[1] & "\")" + + # ---- C FFI import: (c/import "header" :fn1 :fn2) ---- + if op == "c/import" or op == "c-ffi/import": + if items.len < 2: + raise newException(EmitterError, "c/import requires at least a header path") + let header = items[1] + if header.kind != ckString: + raise newException(EmitterError, "c/import header must be a string") + let headerPath = header.strVal + var ffiProcs: seq[string] = @[] + for i in 2.. 0: + var argParts: seq[string] = @[] + for i in 1..", "<", ">=", "<=", "not=", + "println", "prn", "print", "str", "pr-str", + "concat", "min", "max", "merge"] + var call: string + if variadic and argParts.len > 0: + call = rn & "(@[" & argParts.join(", ") & "])" + else: + call = rn & "(" & argParts.join(", ") & ")" + if op in ["println", "prn", "print"]: + return sp & "discard " & call + return sp & call + + # ---- User-defined function call ---- var args: seq[string] = @[] for i in 1.. 0: + # Known function symbol — emit as runtime fn reference + return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" + return sp & mangleName(v.symName) of ckList: if v.items.len == 0: - return spaces & "cljList(@[])" - return emitSpecialForm(v.items, indent) + return sp & "cljList(@[])" + # Macro-expand before emitting + let expanded = macroexpand(v) + if expanded.kind != ckList or expanded == v: + return emitSpecialForm(v.items, indent) + return emitExpr(expanded, indent) of ckVector: var parts: seq[string] = @[] for item in v.items: - parts.add(emitExpr(item, indent)) - return spaces & "@[" & parts.join(", ") & "]" + parts.add(emitExpr(item, 0)) + return sp & "cljVector(@[" & parts.join(", ") & "])" of ckMap: - raise newException(EmitterError, "Maps not yet supported in emitter") + if v.mapKeys.len == 0: + return sp & "cljMap(@[], @[])" + var keyParts: seq[string] = @[] + var valParts: seq[string] = @[] + for i in 0.. 0 and form.items[0].kind == ckSymbol and - form.items[0].symName in ["def", "defn"] + form.items[0].symName in ["def", "defn", "defn-"] if isDef: defs.add(emitExpr(form, 0)) else: - let code = emitExpr(form, 2).strip() - if code.startsWith("echo ") or code.startsWith("discard ") or code.startsWith("block:"): + let code = emitExpr(form, 2) + let stripped = code.strip() + if stripped.startsWith("echo ") or stripped.startsWith("discard ") or + stripped.startsWith("block:") or stripped.startsWith("if ") or + stripped.startsWith("try:") or stripped.startsWith("var "): mainForms.add(code) elif i == forms.len - 1: - # Last form - echo its result - mainForms.add("echo " & code) + mainForms.add(indentStr(2) & "echo " & stripped) else: - mainForms.add("discard " & code) - + mainForms.add(indentStr(2) & "discard " & stripped) + var lines = headerLines + # Add collected Nim imports + for imp in requiredImports: + lines.add("import " & imp) + lines.add("") lines.add(defs) - + if mainForms.len > 0: lines.add("") lines.add("when isMainModule:") for form in mainForms: - lines.add(" " & form) - + lines.add(form) + return lines.join("\n") & "\n" diff --git a/src/macros.nim b/src/macros.nim new file mode 100644 index 0000000..f7047cc --- /dev/null +++ b/src/macros.nim @@ -0,0 +1,468 @@ +import tables +import types + +type + MacroFn* = proc(args: seq[CljVal]): CljVal + +var macroTable* = initTable[string, MacroFn]() + +proc defineMacro*(name: string, fn: MacroFn) = + macroTable[name] = fn + +proc isMacro*(name: string): bool = + macroTable.hasKey(name) + +proc expandMacro*(name: string, args: seq[CljVal]): CljVal = + macroTable[name](args) + +proc expandSyntaxQuote*(form: CljVal): CljVal +proc macroexpand1*(form: CljVal): CljVal +proc macroexpand*(form: CljVal): CljVal + +proc gensymCounter*: int = + var counter {.global.} = 0 + inc counter + counter + +proc gensymName*(prefix: string = "G__"): string = + prefix & $gensymCounter() + +proc expandSyntaxQuote*(form: CljVal): CljVal = + if form.isNil: return cljNil() + case form.kind + of ckSymbol: + return cljList(@[cljSymbol("quote"), form]) + of ckKeyword: + return cljList(@[cljSymbol("quote"), form]) + of ckList: + if form.items.len == 0: + return cljList(@[cljSymbol("quote"), cljList(@[])]) + let head = form.items[0] + if head.kind == ckSymbol: + if head.symName == "unquote": + if form.items.len != 2: + raise newException(CatchableError, "unquote requires exactly 1 argument") + return form.items[1] + 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 + item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing": + parts.add(item.items[1]) + elif item.kind == ckList and item.items.len == 2 and + item.items[0].kind == ckSymbol and item.items[0].symName == "unquote": + parts.add(cljList(@[cljSymbol("list"), item.items[1]])) + else: + parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)])) + if parts.len == 0: + return cljList(@[cljSymbol("quote"), cljList(@[])]) + # Build (concat part1 part2 ...) + if parts.len == 1: + return parts[0] + return cljList(@[cljSymbol("concat")] & parts) + of ckVector: + var parts: seq[CljVal] = @[] + for item in form.items: + if item.kind == ckList and item.items.len == 2 and + item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing": + parts.add(item.items[1]) + else: + parts.add(cljList(@[cljSymbol("list"), expandSyntaxQuote(item)])) + return cljList(@[cljSymbol("vec"), + if parts.len == 0: cljList(@[cljSymbol("quote"), cljList(@[])]) + elif parts.len == 1: parts[0] + else: cljList(@[cljSymbol("concat")] & parts)]) + of ckMap: + # Maps: expand each key and value + return cljList(@[cljSymbol("quote"), form]) + else: + return cljList(@[cljSymbol("quote"), form]) + +proc macroexpand1*(form: CljVal): CljVal = + if form.isNil: return form + if form.kind != ckList or form.items.len == 0: + return form + let head = form.items[0] + if head.kind != ckSymbol: + return form + let name = head.symName + # Handle special forms that should not be macro-expanded + if name in ["def", "fn", "defn", "defn-", "let", "if", "when", "cond", + "do", "loop", "recur", "try", "catch", "finally", "throw", + "quote", "var", "set!", "ns", "defmacro", + "and", "or", "nil?", "range"]: + return form + # Handle syntax-quote + if name == "syntax-quote": + if form.items.len != 2: + raise newException(CatchableError, "syntax-quote requires exactly 1 argument") + return expandSyntaxQuote(form.items[1]) + # Check if it's a macro + if isMacro(name): + return expandMacro(name, form.items[1..^1]) + return form + +proc macroexpand*(form: CljVal): CljVal = + result = form + while true: + let expanded = macroexpand1(result) + if expanded == result: + return result + result = expanded + +# ---- Built-in macro implementations ---- + +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] + for i in 1.. 0: + var newItems = step.items + if reverse: + newItems.add(result) + else: + newItems.insert(result, 1) + result = cljList(newItems) + elif step.kind == ckSymbol: + # (-> x f) => (f x) + result = cljList(@[step, result]) + else: + raise newException(CatchableError, "Threading macro requires list or symbol forms") + result + +proc initBuiltinMacros*() = + # -> + defineMacro("fn*", proc(args: seq[CljVal]): CljVal = + # fn* is used internally by reader for #() + cljList(@[cljSymbol("fn")] & args) + ) + + # -> + defineMacro("->", proc(args: seq[CljVal]): CljVal = + threadingMacro(args, false) + ) + + # ->> + defineMacro("->>", proc(args: seq[CljVal]): CljVal = + threadingMacro(args, true) + ) + + # and + defineMacro("and", proc(args: seq[CljVal]): CljVal = + if args.len == 0: return cljBool(true) + if args.len == 1: return args[0] + # (and a b c) => (if a (if b c false) false) + var r = args[^1] + for i in countdown(args.len - 2, 0): + r = cljList(@[cljSymbol("if"), args[i], r, cljBool(false)]) + r + ) + + # or + defineMacro("or", proc(args: seq[CljVal]): CljVal = + if args.len == 0: return cljNil() + if args.len == 1: return args[0] + # (or a b c) => (if a a (if b b (if c c nil))) + var r = args[^1] + for i in countdown(args.len - 2, 0): + r = cljList(@[cljSymbol("if"), args[i], args[i], r]) + r + ) + + # when-let + defineMacro("when-let", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "when-let requires binding and body") + let binding = args[0] + let body = args[1..^1] + if binding.kind != ckVector or binding.items.len != 2: + raise newException(CatchableError, "when-let binding must be [name val]") + let sym = binding.items[0] + let val = binding.items[1] + let gs = cljSymbol(gensymName("wl_")) + cljList(@[cljSymbol("let"), cljVector(@[gs, val]), + cljList(@[cljSymbol("if"), gs, + cljList(@[cljSymbol("let"), cljVector(@[sym, gs])] & body)])]) + ) + + # if-let + defineMacro("if-let", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "if-let requires binding, then, and optional else") + let binding = args[0] + let thenExpr = args[1] + let elseExpr = if args.len > 2: args[2] else: cljNil() + if binding.kind != ckVector or binding.items.len != 2: + raise newException(CatchableError, "if-let binding must be [name val]") + let sym = binding.items[0] + let val = binding.items[1] + let gs = cljSymbol(gensymName("il_")) + cljList(@[cljSymbol("let"), cljVector(@[gs, val]), + cljList(@[cljSymbol("if"), gs, + cljList(@[cljSymbol("let"), cljVector(@[sym, gs]), thenExpr]), + elseExpr])]) + ) + + # when + defineMacro("when", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "when requires condition and body") + let test = args[0] + let body = args[1..^1] + cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("do")] & body)]) + ) + + # when-not + defineMacro("when-not", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "when-not requires condition and body") + let test = args[0] + let body = args[1..^1] + cljList(@[cljSymbol("if"), test, cljNil(), cljList(@[cljSymbol("do")] & body)]) + ) + + # cond + defineMacro("cond", proc(args: seq[CljVal]): CljVal = + if args.len == 0: return cljNil() + if args.len mod 2 != 0: + raise newException(CatchableError, "cond requires even number of forms") + let test = args[0] + let then = args[1] + if (test.kind == ckKeyword and test.kwName == "else") or + (test.kind == ckSymbol and test.symName == ":else"): + return then + let rest = args[2..^1] + if rest.len == 0: + return cljList(@[cljSymbol("if"), test, then]) + cljList(@[cljSymbol("if"), test, then, + cljList(@[cljSymbol("cond")] & rest)]) + ) + + # cond-> + defineMacro("cond->", proc(args: seq[CljVal]): CljVal = + if args.len < 1: + raise newException(CatchableError, "cond-> requires at least 1 argument") + let expr = args[0] + let gs = cljSymbol(gensymName("ct_")) + var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])] + var i = 1 + while i + 1 < args.len: + let test = args[i] + let step = args[i+1] + var threaded: CljVal + if step.kind == ckList and step.items.len > 0: + var newItems = step.items + newItems.insert(gs, 1) + threaded = cljList(newItems) + elif step.kind == ckSymbol: + threaded = cljList(@[step, gs]) + else: + threaded = step + forms.add(cljList(@[cljSymbol("if"), test, + cljList(@[cljSymbol("set!"), gs, threaded]), + gs])) + i += 2 + forms.add(gs) + cljList(forms) + ) + + # cond->> + defineMacro("cond->>", proc(args: seq[CljVal]): CljVal = + if args.len < 1: + raise newException(CatchableError, "cond->> requires at least 1 argument") + let expr = args[0] + let gs = cljSymbol(gensymName("ctg_")) + var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])] + var i = 1 + while i + 1 < args.len: + let test = args[i] + let step = args[i+1] + var threaded: CljVal + if step.kind == ckList and step.items.len > 0: + var newItems = step.items + newItems.add(gs) + threaded = cljList(newItems) + elif step.kind == ckSymbol: + threaded = cljList(@[step, gs]) + else: + threaded = step + forms.add(cljList(@[cljSymbol("if"), test, + cljList(@[cljSymbol("set!"), gs, threaded]), + gs])) + i += 2 + forms.add(gs) + cljList(forms) + ) + + # doto + defineMacro("doto", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "doto requires at least 2 arguments") + let x = args[0] + let gs = cljSymbol(gensymName("do_")) + var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, x])] + for i in 1.. 0: + var newItems = step.items + newItems.insert(gs, 1) + forms.add(cljList(newItems)) + elif step.kind == ckSymbol: + forms.add(cljList(@[step, gs])) + forms.add(gs) + cljList(forms) + ) + + # as-> + defineMacro("as->", proc(args: seq[CljVal]): CljVal = + if args.len < 3: + raise newException(CatchableError, "as-> requires at least 3 arguments") + let expr = args[0] + let name = args[1] + var result = 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] + for i in 1.. 0: + var newItems = step.items + newItems.insert(gs, 1) + threaded = cljList(newItems) + elif step.kind == ckSymbol: + threaded = cljList(@[step, gs]) + else: + threaded = step + result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]), + cljNil(), threaded])]) + result + ) + + # some->> + defineMacro("some->>", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "some->> requires at least 2 arguments") + var result = args[0] + for i in 1.. 0: + var newItems = step.items + newItems.add(gs) + threaded = cljList(newItems) + elif step.kind == ckSymbol: + threaded = cljList(@[step, gs]) + else: + threaded = step + result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]), + cljNil(), threaded])]) + result + ) + + # for (simplified — single binding only) + defineMacro("for", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "for requires binding vector and body") + let bindings = args[0] + let body = args[1..^1] + if bindings.kind != ckVector or bindings.items.len < 2: + raise newException(CatchableError, "for requires a binding vector [name coll]") + let sym = bindings.items[0] + let coll = bindings.items[1] + let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body) + let gs = cljSymbol(gensymName("f_")) + cljList(@[cljSymbol("let"), cljVector(@[gs, cljVector(@[])]), + cljList(@[cljSymbol("doseq"), cljVector(@[sym, coll]), + cljList(@[cljSymbol("set!"), gs, + cljList(@[cljSymbol("conj"), gs, bodyExpr])])]), + gs]) + ) + + # doseq (simplified — single binding only) + defineMacro("doseq", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "doseq requires binding vector and body") + let bindings = args[0] + let body = args[1..^1] + if bindings.kind != ckVector or bindings.items.len < 2: + raise newException(CatchableError, "doseq requires a binding vector [name coll]") + let sym = bindings.items[0] + let coll = bindings.items[1] + let gs = cljSymbol(gensymName("ds_")) + let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body) + cljList(@[cljSymbol("let"), cljVector(@[gs, coll]), + cljList(@[cljSymbol("loop"), cljVector(@[cljSymbol(gensymName("i_")), cljInt(0)]), + cljList(@[cljSymbol("when"), + cljList(@[cljSymbol("<"), cljSymbol(gensymName("i_")), cljList(@[cljSymbol("count"), gs])]), + cljList(@[cljSymbol("let"), cljVector(@[sym, cljList(@[cljSymbol("nth"), gs, cljSymbol(gensymName("i_"))])]), + bodyExpr]), + cljList(@[cljSymbol("recur"), + cljList(@[cljSymbol("inc"), cljSymbol(gensymName("i_"))])])])])]) + ) + + # dotimes + defineMacro("dotimes", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "dotimes requires binding vector and body") + let bindings = args[0] + let body = args[1..^1] + if bindings.kind != ckVector or bindings.items.len != 2: + raise newException(CatchableError, "dotimes requires [name n]") + let sym = bindings.items[0] + let n = bindings.items[1] + cljList(@[cljSymbol("doseq"), cljVector(@[sym, cljList(@[cljSymbol("range"), n])])] & body) + ) + + # defmacro + defineMacro("defmacro", proc(args: seq[CljVal]): CljVal = + # defmacro is handled specially in the compiler, this is a no-op + cljNil() + ) + + # comment + defineMacro("comment", proc(args: seq[CljVal]): CljVal = + cljNil() + ) + + # assert + defineMacro("assert", proc(args: seq[CljVal]): CljVal = + if args.len == 0: return cljNil() + let test = args[0] + let msg = if args.len > 1: args[1] else: cljString("Assert failed") + cljList(@[cljSymbol("when-not"), test, + cljList(@[cljSymbol("throw"), msg])]) + ) + + # with-open + defineMacro("with-open", proc(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "with-open requires binding and body") + let binding = args[0] + let body = args[1..^1] + if binding.kind != ckVector or binding.items.len != 2: + raise newException(CatchableError, "with-open requires [name init]") + let sym = binding.items[0] + let init = binding.items[1] + let gs = cljSymbol(gensymName("wo_")) + cljList(@[cljSymbol("let"), cljVector(@[gs, init]), + cljList(@[cljSymbol("try")] & body & @[ + cljList(@[cljSymbol("finally"), + cljList(@[cljSymbol(".close"), gs])])])]) + ) diff --git a/src/reader.nim b/src/reader.nim index bb8096f..2e26d7e 100644 --- a/src/reader.nim +++ b/src/reader.nim @@ -1,5 +1,5 @@ # Clojure Reader (EDN subset) -import strutils, sequtils +import strutils import types type @@ -17,7 +17,7 @@ proc skipWhitespaceAndComments(s: string, i: var int) = break proc isSymChar(c: char): bool = - c in Letters or c in Digits or c in {'+', '-', '*', '/', '_', '?', '!', '=', '<', '>', '.', '\'', '#', '%', '&'} + c in Letters or c in Digits or c in {'+', '-', '*', '/', '_', '?', '!', '=', '<', '>', '.', '\'', '#', '%', '&', ':'} proc readStringTok(s: string, i: var int): string = inc i # skip opening quote @@ -126,7 +126,9 @@ proc readList(s: string, i: var int): CljVal = if s[i] == ')': inc i break - items.add(readForm(s, i)) + let form = readForm(s, i) + if form != nil: + items.add(form) return cljList(items) proc readVector(s: string, i: var int): CljVal = @@ -139,9 +141,97 @@ proc readVector(s: string, i: var int): CljVal = if s[i] == ']': inc i break - items.add(readForm(s, i)) + let form = readForm(s, i) + if form != nil: + items.add(form) return cljVector(items) +proc readMap(s: string, i: var int): CljVal = + inc i # skip '{' + var pairs: seq[(CljVal, CljVal)] = @[] + while true: + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "Unterminated map") + if s[i] == '}': + inc i + break + let key = readForm(s, i) + if key == nil: + continue + skipWhitespaceAndComments(s, i) + if i >= s.len or s[i] == '}': + raise newException(ReaderError, "Map must have even number of forms") + let val = readForm(s, i) + if val == nil: + raise newException(ReaderError, "Map value cannot be nil") + pairs.add((key, val)) + return cljMapFromPairs(pairs) + +proc readSet(s: string, i: var int): CljVal = + inc i # skip '{' after # + var items: seq[CljVal] = @[] + while true: + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "Unterminated set") + if s[i] == '}': + inc i + break + let form = readForm(s, i) + if form != nil: + items.add(form) + return cljList(@[cljSymbol("set"), cljVector(items)]) + +proc readDispatch(s: string, i: var int): CljVal = + inc i # skip '#' + if i >= s.len: + raise newException(ReaderError, "Unexpected end after #") + let c = s[i] + case c + of '{': + return readSet(s, i) + of '(': + let fnBody = readList(s, i) + return cljList(@[cljSymbol("fn*"), cljVector(@[cljSymbol("%")]), fnBody]) + of '\'': + inc i + let sym = readForm(s, i) + return cljList(@[cljSymbol("var"), sym]) + of '_': + inc i + discard readForm(s, i) + return nil + else: + raise newException(ReaderError, "Unknown dispatch macro: #" & c) + +proc readWithMeta(s: string, i: var int): CljVal = + inc i # skip '^' + let meta = readForm(s, i) + let form = readForm(s, i) + return cljList(@[cljSymbol("with-meta"), form, meta]) + +proc readSyntaxQuote(s: string, i: var int): CljVal = + inc i # skip '`' + let form = readForm(s, i) + return cljList(@[cljSymbol("syntax-quote"), form]) + +proc readUnquoteSplicing(s: string, i: var int): CljVal = + inc i # skip '~' + inc i # skip '@' + let form = readForm(s, i) + return cljList(@[cljSymbol("unquote-splicing"), form]) + +proc readUnquote(s: string, i: var int): CljVal = + inc i # skip '~' + let form = readForm(s, i) + return cljList(@[cljSymbol("unquote"), form]) + +proc readDeref(s: string, i: var int): CljVal = + inc i # skip '@' + let form = readForm(s, i) + return cljList(@[cljSymbol("deref"), form]) + proc readForm(s: string, i: var int): CljVal = skipWhitespaceAndComments(s, i) if i >= s.len: @@ -153,12 +243,26 @@ proc readForm(s: string, i: var int): CljVal = return readList(s, i) of '[': return readVector(s, i) + of '{': + return readMap(s, i) of '"': return cljString(readStringTok(s, i)) of '\'': inc i # skip quote let form = readForm(s, i) return cljList(@[cljSymbol("quote"), form]) + of '#': + return readDispatch(s, i) + of '^': + return readWithMeta(s, i) + of '`': + return readSyntaxQuote(s, i) + of '~': + if i + 1 < s.len and s[i+1] == '@': + return readUnquoteSplicing(s, i) + return readUnquote(s, i) + of '@': + return readDeref(s, i) else: return readAtom(s, i) @@ -166,15 +270,25 @@ proc readOne*(s: string, i: var int): CljVal = skipWhitespaceAndComments(s, i) if i >= s.len: return nil - return readForm(s, i) + result = readForm(s, i) + while result == nil: + skipWhitespaceAndComments(s, i) + if i >= s.len: + return nil + result = readForm(s, i) proc read*(s: string): CljVal = var i = 0 - let result = readForm(s, i) + var res: CljVal = nil + while res == nil: + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "No form found") + res = readForm(s, i) skipWhitespaceAndComments(s, i) if i < s.len: raise newException(ReaderError, "Extra input after form: " & s[i..^1]) - return result + return res proc readAll*(s: string): seq[CljVal] = var i = 0 @@ -183,5 +297,7 @@ proc readAll*(s: string): seq[CljVal] = skipWhitespaceAndComments(s, i) if i >= s.len: break - forms.add(readForm(s, i)) + let form = readForm(s, i) + if form != nil: + forms.add(form) return forms diff --git a/src/runtime.nim b/src/runtime.nim new file mode 100644 index 0000000..e1840bf --- /dev/null +++ b/src/runtime.nim @@ -0,0 +1,253 @@ +import hashes + +const + Bits = 5 + Width = 1 shl Bits # 32 + Mask = Width - 1 + +type + CljKind* = enum + ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol, + ckList, ckVector, ckMap, ckFn, ckAtom + + CljVal* = ref CljValObj + CljValObj = 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: listItems*: seq[CljVal] + of ckVector: vecRoot*: VecNode + of ckMap: mapRoot*: MapNode + of ckFn: fnProc*: proc(args: seq[CljVal]): CljVal + of ckAtom: atomVal*: CljVal + + VecNodeKind = enum vnLeaf, vnInternal + VecNode = ref object + case kind: VecNodeKind + of vnLeaf: + leaf: array[Width, CljVal] + of vnInternal: + children: array[Width, VecNode] + + PersistentVector* = object + count*: int + shift*: int + root*: VecNode + tail*: seq[CljVal] + tailLen*: int + + MapNodeKind = enum mnLeaf, mnInternal, mnCollision + MapNode = ref object + case kind: MapNodeKind + of mnLeaf: + leafKey*: CljVal + leafVal*: CljVal + of mnInternal: + bitmap*: uint32 + children*: seq[MapNode] + of mnCollision: + collHash*: Hash + collPairs*: seq[(CljVal, CljVal)] + + PersistentMap* = object + count*: int + root*: MapNode + hasLeaf*: bool + rootLeaf*: MapNode + +# ---- Constructors ---- + +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 cljInt*(v: int): CljVal = CljVal(kind: ckInt, intVal: v.int64) +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 cljFn*(p: proc(args: seq[CljVal]): CljVal): CljVal = CljVal(kind: ckFn, fnProc: p) + +proc emptyVecNode(): VecNode = VecNode(kind: vnLeaf) +proc emptyMapNode(): MapNode = MapNode(kind: mnInternal, bitmap: 0, children: @[]) + +# ---- CljVal helpers ---- + +proc cljValHash*(v: CljVal): Hash = + case v.kind + of ckNil: hash(0) + of ckBool: hash(v.boolVal) + of ckInt: hash(v.intVal) + of ckFloat: hash(v.floatVal) + of ckString: hash(v.strVal) + of ckKeyword: hash(v.kwName) !& hash(":kw") + of ckSymbol: hash(v.symName) + else: hash(cast[uint](v)) + +proc `==`*(a, b: CljVal): bool = + if a.isNil and b.isNil: return true + if a.isNil or b.isNil: return false + if a.kind != b.kind: return false + case a.kind + of ckNil: true + of ckBool: a.boolVal == b.boolVal + of ckInt: a.intVal == b.intVal + of ckFloat: a.floatVal == b.floatVal + of ckString: a.strVal == b.strVal + of ckKeyword: a.kwName == b.kwName + of ckSymbol: a.symName == b.symName + else: false + +# ---- Persistent Vector (HAMT) ---- + +proc newPersistentVector*(): PersistentVector = + PersistentVector(count: 0, shift: Bits, root: emptyVecNode(), tail: @[], tailLen: 0) + +proc tailOff(count: int): int = + if count < Width: 0 + else: ((count - 1) shr Bits) shl Bits + +proc vecPush(v: PersistentVector, val: CljVal): PersistentVector = + if v.count - tailOff(v.count) < Width: + # room in tail + var newTail = v.tail + newTail.add(val) + return PersistentVector(count: v.count + 1, shift: v.shift, root: v.root, tail: newTail, tailLen: v.tailLen + 1) + # tail full, push into tree + let tailNode = VecNode(kind: vnLeaf, leaf: block: + var arr: array[Width, CljVal] + for i in 0.. (1 shl v.shift): + var newRootN = VecNode(kind: vnInternal) + newRootN.children[0] = v.root + newRootN.children[1] = tailNode + newRoot = newRootN + newShift += Bits + else: + # insert tail into tree + proc insertTail(node: VecNode, level: int, parentBitmap: uint32): VecNode = + discard + # simplified: just create leaf for now + newRoot = v.root + var newTailSeq = @[val] + return PersistentVector(count: v.count + 1, shift: newShift, root: newRoot, tail: newTailSeq, tailLen: 1) + +proc vecGet(v: PersistentVector, idx: int): CljVal = + if idx < 0 or idx >= v.count: + raise newException(IndexDefect, "Index out of range: " & $idx) + let to = tailOff(v.count) + if idx >= to: + return v.tail[idx - to] + var node = v.root + var level = v.shift + while level > 0: + node = node.children[(idx shr level) and Mask] + level -= Bits + return node.leaf[idx and Mask] + +# ---- Simplified seq-based persistent vector ---- +# For now use Nim seq with copy-on-write semantics via wrapper + +type + CljVec* = ref object + data*: seq[CljVal] + + CljMap* = ref object + keys*: seq[CljVal] + vals*: seq[CljVal] + +proc newCljVec*(): CljVec = CljVec(data: @[]) +proc newCljVec*(items: seq[CljVal]): CljVec = CljVec(data: items) + +proc cljVecLen*(v: CljVec): int = v.data.len +proc cljVecGet*(v: CljVec, i: int): CljVal = + if i < 0 or i >= v.data.len: + raise newException(IndexDefect, "Index out of range: " & $i) + v.data[i] + +proc cljVecConj*(v: CljVec, val: CljVal): CljVec = + var newData = v.data + newData.add(val) + newCljVec(newData) + +proc cljVecAssoc*(v: CljVec, i: int, val: CljVal): CljVec = + var newData = v.data + if i == newData.len: + newData.add(val) + elif i >= 0 and i < newData.len: + newData[i] = val + else: + raise newException(IndexDefect, "Index out of range: " & $i) + newCljVec(newData) + +proc newCljMap*(): CljMap = CljMap(keys: @[], vals: @[]) + +proc cljMapGet*(m: CljMap, key: CljVal): CljVal = + for i in 0.." + of ckAtom: "(atom " & cljRepr(v.atomVal) & ")" + +proc cljStr*(v: CljVal): string = + if v.isNil: return "" + case v.kind + of ckString: v.strVal + of ckKeyword: ":" & v.kwName + of ckSymbol: v.symName + else: cljRepr(v) diff --git a/src/types.nim b/src/types.nim index c60df3c..b5ee960 100644 --- a/src/types.nim +++ b/src/types.nim @@ -1,4 +1,4 @@ -# Clojure Value Types (Nim Runtime) +# Clojure Value Types (Compiler internal representation) import sequtils, strutils type @@ -16,9 +16,10 @@ type of ckKeyword: kwName*: string of ckSymbol: symName*: string of ckList, ckVector: items*: seq[CljVal] - of ckMap: pairs*: seq[(CljVal, CljVal)] + of ckMap: + mapKeys*: seq[CljVal] + mapVals*: seq[CljVal] -# Constructors 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) @@ -28,10 +29,20 @@ 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*(pairs: seq[(CljVal, CljVal)]): CljVal = CljVal(kind: ckMap, pairs: pairs) -# String representation (for debugging) +proc cljMap*(keys: seq[CljVal], vals: seq[CljVal]): CljVal = + CljVal(kind: ckMap, mapKeys: keys, mapVals: vals) + +proc cljMapFromPairs*(pairs: seq[(CljVal, CljVal)]): CljVal = + var ks: seq[CljVal] = @[] + var vs: seq[CljVal] = @[] + for (k, v) in pairs: + ks.add(k) + vs.add(v) + cljMap(ks, vs) + proc `$`*(v: CljVal): string = + if v.isNil: return "nil" case v.kind of ckNil: "nil" of ckBool: $v.boolVal @@ -44,6 +55,6 @@ proc `$`*(v: CljVal): string = of ckVector: "[" & v.items.mapIt($it).join(" ") & "]" of ckMap: var parts: seq[string] = @[] - for (k, v2) in v.pairs: - parts.add($k & " " & $v2) + for i in 0.. x 2)) [1 2 3])") + let code = emitExpr(v) + check "cljFilter" in code + check "cljFn" in code + + test "emit reduce": + let v = read("(reduce + 0 [1 2 3])") + let code = emitExpr(v) + check "cljReduce" in code diff --git a/tests/test_reader.nim b/tests/test_reader.nim new file mode 100644 index 0000000..3da2ef6 --- /dev/null +++ b/tests/test_reader.nim @@ -0,0 +1,160 @@ +import unittest +import ../src/types +import ../src/reader + +suite "Reader - Basic Types": + test "read nil": + let v = read("nil") + check v.kind == ckNil + + test "read true": + let v = read("true") + check v.kind == ckBool + check v.boolVal == true + + test "read false": + let v = read("false") + check v.kind == ckBool + check v.boolVal == false + + test "read integer": + let v = read("42") + check v.kind == ckInt + check v.intVal == 42 + + test "read negative integer": + let v = read("-7") + check v.kind == ckInt + check v.intVal == -7 + + test "read float": + let v = read("3.14") + check v.kind == ckFloat + check v.floatVal == 3.14 + + test "read string": + let v = read("\"hello\"") + check v.kind == ckString + check v.strVal == "hello" + + test "read string with escapes": + let v = read("\"hello\\nworld\"") + check v.kind == ckString + check v.strVal == "hello\nworld" + + test "read keyword": + let v = read(":key") + check v.kind == ckKeyword + check v.kwName == "key" + + test "read symbol": + let v = read("foo") + check v.kind == ckSymbol + check v.symName == "foo" + + test "read symbol with special chars": + let v = read("my-fn?") + check v.kind == ckSymbol + check v.symName == "my-fn?" + +suite "Reader - Collections": + test "read empty list": + let v = read("()") + check v.kind == ckList + check v.items.len == 0 + + test "read list": + let v = read("(+ 1 2)") + check v.kind == ckList + check v.items.len == 3 + check v.items[0].kind == ckSymbol + check v.items[0].symName == "+" + check v.items[1].kind == ckInt + check v.items[1].intVal == 1 + check v.items[2].kind == ckInt + check v.items[2].intVal == 2 + + test "read vector": + let v = read("[1 2 3]") + check v.kind == ckVector + check v.items.len == 3 + check v.items[0].intVal == 1 + check v.items[1].intVal == 2 + check v.items[2].intVal == 3 + + test "read empty vector": + let v = read("[]") + check v.kind == ckVector + check v.items.len == 0 + + test "read map": + let v = read("{:a 1 :b 2}") + check v.kind == ckMap + check v.mapKeys.len == 2 + check v.mapKeys[0].kind == ckKeyword + check v.mapKeys[0].kwName == "a" + check v.mapVals[0].kind == ckInt + check v.mapVals[0].intVal == 1 + + test "read empty map": + let v = read("{}") + check v.kind == ckMap + check v.mapKeys.len == 0 + + test "read nested list": + let v = read("(+ (- 3 1) 2)") + check v.kind == ckList + check v.items.len == 3 + check v.items[1].kind == ckList + check v.items[1].items.len == 3 + +suite "Reader - Quote and Syntax": + test "read quote": + let v = read("'foo") + check v.kind == ckList + check v.items.len == 2 + check v.items[0].kind == ckSymbol + check v.items[0].symName == "quote" + check v.items[1].kind == ckSymbol + check v.items[1].symName == "foo" + + test "read deref": + let v = read("@atom") + check v.kind == ckList + check v.items[0].symName == "deref" + + test "read unquote": + let v = read("~x") + check v.kind == ckList + check v.items[0].symName == "unquote" + + test "read unquote-splicing": + let v = read("~@x") + check v.kind == ckList + check v.items[0].symName == "unquote-splicing" + +suite "Reader - Comments": + test "skip comments": + let v = read("; comment\n42") + check v.kind == ckInt + check v.intVal == 42 + + test "inline comments": + let forms = readAll("(+ 1 2) ; addition\n(- 3 1)") + check forms.len == 2 + +suite "Reader - readAll": + test "read multiple forms": + let forms = readAll("1 2 3") + check forms.len == 3 + check forms[0].intVal == 1 + check forms[1].intVal == 2 + check forms[2].intVal == 3 + + test "read empty string": + let forms = readAll("") + check forms.len == 0 + + test "read whitespace only": + let forms = readAll(" \n \t ") + check forms.len == 0