commit f89b381fdbb07eb02f73cee58df76efb3544047d Author: dimgigov Date: Fri May 8 14:51:47 2026 +0300 Initial commit diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a340891 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,169 @@ +# Архитектура: Истински Clojure върху Nim + +## Философия + +Това не е "Clojure-подобен" език. Това е **Clojure диалект**, който компилира към Nim. +Макросите, структурите от данни и семантиката са съвместими с Clojure (JVM). + +### Защо истински Clojure? + +| Аспект | Clojure-inspired Lisp | Истински Clojure диалект | +|---|---|---| +| **Библиотеки** | Пишеш всичко на ново | Можеш да пренесеш `clojure.core`, `clojure.string`, `clojure.set` | +| **Разработчици** | Учат нов език | Всеки Clojure програмист може да го ползва веднага | +| **Инструменти** | Собствени | Работи с `clojure-mode`, CIDER, Calva (с някои адаптации) | +| **Финтех/Indустрия** | Не е опция | Може да се ползва за реални проекти | + +### Който го е правил успешно + +- **ClojureScript** — компилира Clojure до JS. Използва Google Closure. Пълна съвместимост. +- **Babashka** — GraalVM native image на Clojure. Бърз старт, скриптове, портативен. +- **Ferret** — Clojure → C++11 за embedded системи. Работи на 2KB RAM. +- **ClojErl** — Clojure върху BEAM. Жертва част от performance заради семантиката. + +## Архитектура на Компилатора + +``` +┌─────────────────┐ +│ .clj Файл │ ← Стандартен Clojure код +└────────┬────────┘ + │ + ┌────▼────┐ + │ Reader │ ← EDN парсер, Clojure symbols, quoted forms + └────┬────┘ + │ + ┌────▼────┐ + │ Macro │ ← defmacro, syntax-quote, unquote, unquote-splicing + │ Expansion│ (работи върху Clojure структури) + └────┬────┘ + │ + ┌────▼────┐ + │ Analyzer│ ← Специални форми, locals, recur targets, + │ │ closures, type hints + └────┬────┘ + │ + ┌────▼────┐ + │ Emitter │ ← Генерира Nim AST (не Nim текст!) + │ (Nim) │ Използва Nim макроси за компилация + └────┬────┘ + │ + ┌────▼────┐ + │ Nim │ ← C код, после машинен код + │Compiler │ оптимизация, tree-shaking, LTO + └────┬────┘ + │ + ┌────▼────┐ + │ Бинарен │ ← Единичен файл, < 10MB, бърз старт + │ Файл │ + └─────────┘ +``` + +## Ключови Решения + +### 1. Компилатор, не Интерпретатор + +Изборът е **AOT компилатор**, подобно на ClojureScript. Няма да пишем интерпретатор в Nim. + +- **Предимство:** Бърз runtime, малък footprint, компилира до C. +- **Недостатък:** REPL е по-труден (но не невъзможен — виж ClojureScript self-hosted). +- **Урок от calcit-runner:** Те започнаха с интерпретатор в Nim и се провалиха. Ние директно компилираме. + +### 2. Clojure Макроси на Clojure Структури + +```clojure +;; Това трябва да работи без промяна: +(defmacro unless [condition & body] + `(if (not ~condition) + (do ~@body))) + +(unless false + (println "Работи!")) +``` + +Макросите се разширяват **преди** да стигнем до Nim. Nim никога не вижда Clojure макрос. + +### 3. Persistent Структури от Данни в Nim + +Clojure's Hash Array Mapped Trie (HAMT) трябва да се имплементира в Nim: + +```nim +# Nim runtime за Clojure Vector +type + NodeKind = enum nkLeaf, nkInternal + Node[T] = ref object + case kind: NodeKind + of nkLeaf: values: seq[T] + of nkInternal: children: array[32, Node[T]] + + PersistentVector[T] = object + root: Node[T] + tail: seq[T] + count: int +``` + +- `conj`, `assoc`, `dissoc` — O(log₃₂ n) +- `nth`, `get` — O(log₃₂ n) +- `count` — O(1) + +### 4. Nim Interop (вместо Java Interop) + +Тъй като нямаме JVM, interop-ът е с Nim: + +```clojure +;; Извикване на Nim функция +(nim/strutils.join ", " ["a" "b" "c"]) + +;; Достъп до Nim тип +(nim/times.DateTime) + +;; FFI към C библиотека +(nim/ffi.import "math.h" :functions [["sin" :double [:double]]]) +``` + +### 5. Симулация на Clojure Runtime + +| Clojure JVM | Нашата Имплементация | +|---|---| +| `Var` | Глобални Nim променливи с atomic CAS | +| `Namespace` | Nim модул + хеш таблица със символи | +| `Atom` | `ref` тип с compare-and-swap | +| `Agent` | Channel + Worker thread | +| `Ref/STM` | Software transactional memory в Nim | +| `Multimethod` | Диспач таблица + `isa?` проверка | +| `Protocol` | Nim concepts + dispatch таблица | + +## Фази с Индустриален Фокус + +### Phase 1: Компилатор (не REPL!) +Фокус: AOT компилация на прости Clojure файлове към Nim. + +### Phase 2: Core Библиотека +Фокус: Пренасяне на `clojure.core` функции. Тестове с реални Clojure тестове. + +### Phase 3: Nim Interop + FFI +Фокус: Извикване на Nim/C библиотеки. Това е нашето "убийствено" предимство. + +### Phase 4: REPL (Self-hosted) +Фокус: REPL като ClojureScript (компилира в паметта). + +### Phase 5: Инструменти +Фокус: nREPL съвместимост, LSP, пакетен мениджър (Nimble интеграция). + +## Изисквания за Съвместимост + +- `clojure.core` namespace трябва да е наличен +- `defn`, `let`, `if`, `do`, `recur`, `loop` — точна семантика +- `->`, `->>`, `cond->` — макроси +- Persistent Vector, Map, Set — стандартни операции +- Keywords (`:name`) и Symbols (`'sym`) — interned + +## Разлики от JVM Clojure (приемливи) + +| JVM Clojure | Clojure/Nim | +|---|---| +| Java interop | Nim interop | +| JVM threads | Nim threads (no GIL!) | +| Dynamic classloading | AOT компилация | +| `gen-class` | `gen-nim` (генериране на Nim типове) | +| Reflection | Compile-time метаданни | +| Big ecosystem | Достъп до Nim + C ecosystem | diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6709ac1 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: build test clean run-example + +NIMCACHE = nimcache + +build: + nim c -o:cljnim src/cljnim.nim + +test: + nim c -r tests/test_reader.nim + +run-example: build + ./cljnim run examples/hello.clj + +clean: + rm -f cljnim + rm -rf $(NIMCACHE) + find . -name "*_generated.nim" -delete + find . -name "*_generated" -delete diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd5b544 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Clojure/Nim + +> Реализация на Clojure върху Nim-lang + +## Какво е това? + +Clojure/Nim е проект, целящ да предостави Clojure език и runtime върху Nim. Вдъхновен е от ClojureScript и Babashka, но вместо JavaScript или JVM, целта е компилация към Nim код и използване на Nim runtime. + +## Защо? + +- **Единен AST**: Nim предлага мощна макро система и AST манипулация +- **Производителност**: Компилиран към C, Nim дава C-подобна скорост с Lisp-удобство +- **Размер**: Минимален runtime footprint в сравнение с JVM +- **Метапрограмиране**: Съчетание на Clojure макроси и Nim макроси + +## Архитектура + +``` +Clojure Изходен Код (.clj) + ↓ + Clojure Reader (Clojure → Clojure Data) + ↓ + Макро Разширение (Clojure Macros) + ↓ + AST Транслатор (Clojure AST → Nim AST) + ↓ + Nim Компилатор + ↓ + C Код → Машинен Код +``` + +## Статус + +> ⚠️ **Ранен етап** — Проектът е във фаза на планиране и базова имплементация. + +Виж [ROADMAP.md](ROADMAP.md) за текущия прогрес. + +## Бърз Старт + +```bash +# Клониране +git clone +cd clojure-nim + +# Изграждане +make build + +# REPL +./cljnim + +# Изпълнение на файл +./cljnim run examples/hello.clj +``` + +## Примери + +```clojure +;; hello.clj +(ns hello) + +(println "Здравей, Nim свят!") + +(defn факториел [n] + (if (<= n 1) + 1 + (* n (факториел (- n 1))))) + +(println "5! =" (факториел 5)) +``` + +```clojure +;; Работа със структури от данни +(def данни + {:име "Иван" + :възраст 30 + :езици ["Clojure" "Nim" "C"]}) + +(println (get-in данни [:езици 0])) +;; => "Clojure" +``` + +## Структура на Проекта + +``` +├── src/ +│ ├── reader/ # Clojure reader (EDN парсер) +│ ├── analyzer/ # Semantic анализатор +│ ├── macros/ # Core macros (defn, let, if, и т.н.) +│ ├── emitter/ # Генератор на Nim код +│ ├── runtime/ # Clojure runtime в Nim +│ └── repl/ # Интерактивна обвивка +├── lib/ +│ └── core.clj # Clojure core библиотека +├── tests/ # Тестове +├── examples/ # Примери +├── Makefile +└── README.md +``` + +## Изисквания + +- Nim >= 2.0 +- GCC или Clang +- make + +## Лиценз + +MIT License — виж [LICENSE](LICENSE). + +## Благодарности + +- [Clojure](https://clojure.org/) — Rich Hickey и екипът +- [Nim](https://nim-lang.org/) — Andreas Rumpf и общността +- Вдъхновено от ClojureScript, Babashka и Clojerl diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..54c52a9 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,169 @@ +# Пътна Карта: Clojure/Nim + +## Фаза 0: Инфраструктура и Планиране ✅ + +- [x] Определяне на цели и архитектура +- [x] Избор на подход (изходен Nim код vs директен C) +- [x] Създаване на структура на проекта +- [ ] CI/CD pipeline +- [ ] Система за тестване + +## Фаза 1: Reader (Clojure → Clojure Data Structures) + +**Цел:** Правилно парсиране на Clojure синтаксис към Clojure структури от данни. + +- [ ] Символи (`sym`, `foo/bar`) +- [ ] Ключови думи (`:key`, `:ns/key`) +- [ ] Числа (integers, floats, ratios, big ints) +- [ ] Низове с escape sequences +- [ ] Коментари (`;`) +- [ ] Колекции: + - [ ] Списъци `(...)` + - [ ] Вектори `[...]` + - [ ] Maps `{...}` + - [ ] Sets `#{...}` +- [ ] Цитиране (`'`, `` ` ``, `~`, `~@`, `#`) +- [ ] Четене на dispatch macros (`#'`, `#_`, `#{}`, etc.) +- [ ] Reader conditionals (`#?()`) +- [ ] Метаданни (`^`) + +## Фаза 2: Runtime Структури от Данни + +**Цел:** Имплементиране на Clojure структурите от данни в Nim. + +- [ ] **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** + +## Фаза 3: Макро Система и AST + +**Цел:** Clojure макроси, които работят върху Clojure структури. + +- [ ] **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` + +## Фаза 4: Nim Code Emitter + +**Цел:** Транслиране на Clojure AST към Nim код. + +- [ ] **Symbol мапинг** (Clojure → Nim naming) +- [ ] **Type inference** (основно) +- [ ] **Emitter за специални форми**: + - [ ] Функции (`proc` в Nim) + - [ ] Променливи (`var` / `let` / `const`) + - [ ] Условни изрази (`if` / `case`) + - [ ] Цикли + - [ ] Изключения (`try` / `except`) +- [ ] **Interop**: + - [ ] Извикване на Nim функции от Clojure + - [ ] Извикване на C библиотеки + - [ ] FFI механизъм +- [ ] **Type hints** поддръжка + +## Фаза 5: Core Библиотека + +**Цел:** Имплементация на Clojure core функции. + +### 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` + +### 5.2 Функции за Низове +- [ ] `str`, `pr-str`, `println`, `prn` +- [ ] `subs`, `clojure.string/` namespace + +### 5.3 Функции за IO +- [ ] `slurp`, `spit` +- [ ] `read-line` + +### 5.4 Функции за Мета +- [ ] `meta`, `with-meta`, `vary-meta` +- [ ] `type`, `instance?` + +## Фаза 6: REPL и Инструменти + +**Цел:** Интерактивна среда за разработка. + +- [ ] **REPL цикъл** (Read-Eval-Print-Loop) +- [ ] **Hot reloading** на код +- [ ] **Error reporting** с Clojure стек тракове +- [ ] **Auto-completion** +- [ ] **Doc strings** (`doc` функция) +- [ ] **Source** (`source` функция) +- [ ] **AOT компилация** + +## Фаза 7: Многонишковост и STM (по-късно) + +**Цел:** Clojure's конкурентност модел. + +- [ ] **Atoms** (CAS) +- [ ] **Agents** +- [ ] **Refs + Software Transactional Memory** +- [ ] **Futures** / **Promises** +- [ ] **core.async** (channels, go blocks) — голяма задача + +## Фаза 8: Оптимизации и Полиране + +- [ ] **Protocol имплементация** (като Clojure protocols) +- [ ] **Multimethods** +- [ ] **Records и Types** +- [ ] **Бързо стартиране** (малък runtime) +- [ ] **Малък размер на бинарния файл** +- [ ] **Подобрена производителност** на persistent структурите +- [ ] **Tree shaking** / Dead code elimination + +## Бъдещи Идеи + +- **GraalVM нативен образ** алтернатива (Clojure/Nim като lighter alternative) +- **Babashka-подобен скриптов инструмент** +- **Nim библиотеки достъпни директно** от Clojure +- **ClojureScript съвместимост** (споделен код) +- **LSP/Language Server** за Clojure/Nim +- **nREPL съвместимост** + +--- + +## Текущ Фокус + +Следваща стъпка: **Фаза 1 — Reader** + +Ще започнем с имплементация на базов Clojure reader, който може да парсира: +1. Числа и низове +2. Символи и keywords +3. Специални знаци (`'`, `` ` ``, `~`, `@`) +4. Списъци и вектори + +Първият milestone е работещ reader, който може да обработи `(+ 1 2)` и да върне правилна Clojure data structure. diff --git a/cljnim b/cljnim new file mode 100755 index 0000000..91a9982 Binary files /dev/null and b/cljnim differ diff --git a/cljnim.nimble b/cljnim.nimble new file mode 100644 index 0000000..ffef1bf --- /dev/null +++ b/cljnim.nimble @@ -0,0 +1,10 @@ +# Package +version = "0.1.0" +author = "Clojure/Nim Team" +description = "Clojure dialect compiling to Nim" +license = "MIT" +srcDir = "src" +bin = @["cljnim"] + +# Dependencies +requires "nim >= 2.0.0" diff --git a/examples/hello.clj b/examples/hello.clj new file mode 100644 index 0000000..49c9283 --- /dev/null +++ b/examples/hello.clj @@ -0,0 +1,3 @@ +; First Clojure/Nim program +(println "Hello, Nim world!") +(println (+ 1 2 3)) diff --git a/examples/math.clj b/examples/math.clj new file mode 100644 index 0000000..dc1d422 --- /dev/null +++ b/examples/math.clj @@ -0,0 +1,12 @@ +; Math example +(defn square [x] + (* x x)) + +(defn factorial [n] + (if (= n 0) + 1 + (* n (factorial (- n 1))))) + +(let [a 5] + (println (square a)) + (println (factorial a))) diff --git a/src/cljnim.nim b/src/cljnim.nim new file mode 100644 index 0000000..5785063 --- /dev/null +++ b/src/cljnim.nim @@ -0,0 +1,82 @@ +# Clojure/Nim Compiler +import os, strutils, osproc +import reader, emitter, types + +proc compileFile*(inputPath: string, outputPath: string) = + let source = readFile(inputPath) + let forms = reader.readAll(source) + + if forms.len == 0: + stderr.writeLine("Error: No forms found in " & inputPath) + quit(1) + + let nimCode = emitter.emitProgram(forms) + writeFile(outputPath, nimCode) + echo "Generated: ", outputPath + +proc runFile*(inputPath: string) = + let tmpDir = getTempDir() + let baseName = inputPath.splitFile.name + let nimPath = tmpDir / baseName & "_generated.nim" + let binPath = tmpDir / baseName & "_generated" + + compileFile(inputPath, nimPath) + + # Compile generated Nim code + let compileCmd = "nim c -o:" & binPath & " " & nimPath + echo "Compiling: ", compileCmd + let compileResult = execCmd(compileCmd) + if compileResult != 0: + stderr.writeLine("Compilation failed") + quit(1) + + # Run + echo "Running: ", binPath + let runResult = execCmd(binPath) + if runResult != 0: + stderr.writeLine("Execution failed") + quit(1) + +proc main() = + let args = commandLineParams() + + if args.len == 0: + echo "Clojure/Nim Compiler" + echo "Usage:" + echo " cljnim compile [output.nim] Compile to Nim" + echo " cljnim run Compile and run" + echo " cljnim read Parse and print AST" + quit(0) + + let cmd = args[0] + + case cmd + of "compile": + if args.len < 2: + stderr.writeLine("Error: Missing input file") + quit(1) + let inputPath = args[1] + let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim") + compileFile(inputPath, outputPath) + + of "run": + if args.len < 2: + stderr.writeLine("Error: Missing input file") + quit(1) + runFile(args[1]) + + of "read": + if args.len < 2: + stderr.writeLine("Error: Missing input file") + quit(1) + let source = readFile(args[1]) + let forms = reader.readAll(source) + for form in forms: + echo $form + + else: + stderr.writeLine("Unknown command: " & cmd) + quit(1) + +when isMainModule: + main() diff --git a/src/emitter.nim b/src/emitter.nim new file mode 100644 index 0000000..54bb39e --- /dev/null +++ b/src/emitter.nim @@ -0,0 +1,272 @@ +# Clojure → Nim Emitter +import strutils, sequtils +import types + +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('_') + of '\'': result.add("_QUOTE") + else: result.add(c) + +proc emitExpr*(v: CljVal, indent: int = 0): 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 emitSpecialForm(items: seq[CljVal], indent: int): string = + let spaces = " ".repeat(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 + + 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.. 0: + return spaces & "proc " & procName & "(" & typedParams.join(", ") & "): int =\n" & bodyCode + else: + return spaces & "proc " & procName & "(): int =\n" & bodyCode + + of "fn": + if items.len < 3: + raise newException(EmitterError, "fn requires params and body") + let params = items[1] + if params.kind != ckVector: + raise newException(EmitterError, "fn params must be a vector") + var paramNames: seq[string] = @[] + for p in params.items: + if p.kind != ckSymbol: + raise newException(EmitterError, "fn params must be symbols") + paramNames.add(mangleName(p.symName) & ": auto") + 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 + + of "let": + if items.len < 3: + raise newException(EmitterError, "let requires bindings and body") + let bindings = items[1] + if bindings.kind != ckVector: + raise newException(EmitterError, "let bindings must be a vector") + 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: + 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) + let body = items[2..^1] + for j, b in body: + let bcode = emitExpr(b, indent + 1).strip() + if j == body.len - 1: + lines.add(" ".repeat(indent + 1) & bcode) + else: + if bcode.startsWith("echo ") or bcode.startsWith("discard "): + lines.add(" ".repeat(indent + 1) & bcode) + else: + lines.add(" ".repeat(indent + 1) & "discard " & bcode) + return spaces & 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 thenCode = emitExpr(items[2], indent + 1) + var result = spaces & "if " & condCode & ":\n" & " ".repeat(indent + 1) & thenCode + if items.len == 4: + let elseCode = emitExpr(items[3], indent + 1) + result.add("\n" & spaces & "else:\n" & " ".repeat(indent + 1) & elseCode) + return result + + of "do": + if items.len < 2: + return spaces & "discard" + return emitBlock(items[1..^1], indent) + + 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 & "\")" + of ckList: + var parts: seq[string] = @[] + for item in quoted.items: + parts.add(emitExpr(item, indent)) + return spaces & "cljList(@[" & parts.join(", ") & "])" + of ckVector: + var parts: seq[string] = @[] + for item in quoted.items: + parts.add(emitExpr(item, indent)) + return spaces & "cljVector(@[" & parts.join(", ") & "])" + else: + return emitExpr(quoted, indent) + + else: + # Function call + var args: seq[string] = @[] + for i in 1.. 0 and + form.items[0].kind == ckSymbol and + form.items[0].symName in ["def", "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:"): + mainForms.add(code) + elif i == forms.len - 1: + # Last form - echo its result + mainForms.add("echo " & code) + else: + mainForms.add("discard " & code) + + var lines = headerLines + lines.add(defs) + + if mainForms.len > 0: + lines.add("") + lines.add("when isMainModule:") + for form in mainForms: + lines.add(" " & form) + + return lines.join("\n") & "\n" diff --git a/src/reader.nim b/src/reader.nim new file mode 100644 index 0000000..bb8096f --- /dev/null +++ b/src/reader.nim @@ -0,0 +1,187 @@ +# Clojure Reader (EDN subset) +import strutils, sequtils +import types + +type + ReaderError* = object of CatchableError + +proc skipWhitespaceAndComments(s: string, i: var int) = + while i < s.len: + let c = s[i] + if c in Whitespace: + inc i + elif c == ';': + while i < s.len and s[i] != '\n': + inc i + else: + break + +proc isSymChar(c: char): bool = + c in Letters or c in Digits or c in {'+', '-', '*', '/', '_', '?', '!', '=', '<', '>', '.', '\'', '#', '%', '&'} + +proc readStringTok(s: string, i: var int): string = + inc i # skip opening quote + var resultStr = "" + while i < s.len and s[i] != '"': + if s[i] == '\\' and i + 1 < s.len: + inc i + case s[i] + of 'n': resultStr.add('\n') + of 't': resultStr.add('\t') + of 'r': resultStr.add('\r') + of '\\': resultStr.add('\\') + of '"': resultStr.add('"') + else: resultStr.add(s[i]) + else: + resultStr.add(s[i]) + inc i + if i >= s.len: + raise newException(ReaderError, "Unterminated string") + inc i # skip closing quote + return resultStr + +proc readNumberOrSym(s: string, i: var int): string = + var start = i + # handle negative sign or standalone operators + if s[i] == '-' or s[i] == '+': + if i + 1 < s.len and s[i+1] in Digits: + inc i + while i < s.len and s[i] in Digits: + inc i + if i < s.len and s[i] == '.': + inc i + while i < s.len and s[i] in Digits: + inc i + return s[start.. startIdx: + if isFloat: + return cljFloat(parseFloat(tok)) + else: + return cljInt(parseInt(tok).int64) + + # symbol + return cljSymbol(tok) + +proc readForm(s: string, i: var int): CljVal + +proc readList(s: string, i: var int): CljVal = + inc i # skip '(' + var items: seq[CljVal] = @[] + while true: + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "Unterminated list") + if s[i] == ')': + inc i + break + items.add(readForm(s, i)) + return cljList(items) + +proc readVector(s: string, i: var int): CljVal = + inc i # skip '[' + var items: seq[CljVal] = @[] + while true: + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "Unterminated vector") + if s[i] == ']': + inc i + break + items.add(readForm(s, i)) + return cljVector(items) + +proc readForm(s: string, i: var int): CljVal = + skipWhitespaceAndComments(s, i) + if i >= s.len: + raise newException(ReaderError, "Unexpected end of input") + + let c = s[i] + case c + of '(': + return readList(s, i) + of '[': + return readVector(s, i) + of '"': + return cljString(readStringTok(s, i)) + of '\'': + inc i # skip quote + let form = readForm(s, i) + return cljList(@[cljSymbol("quote"), form]) + else: + return readAtom(s, i) + +proc readOne*(s: string, i: var int): CljVal = + skipWhitespaceAndComments(s, i) + if i >= s.len: + return nil + return readForm(s, i) + +proc read*(s: string): CljVal = + var i = 0 + let result = readForm(s, i) + skipWhitespaceAndComments(s, i) + if i < s.len: + raise newException(ReaderError, "Extra input after form: " & s[i..^1]) + return result + +proc readAll*(s: string): seq[CljVal] = + var i = 0 + var forms: seq[CljVal] = @[] + while true: + skipWhitespaceAndComments(s, i) + if i >= s.len: + break + forms.add(readForm(s, i)) + return forms diff --git a/src/types.nim b/src/types.nim new file mode 100644 index 0000000..c60df3c --- /dev/null +++ b/src/types.nim @@ -0,0 +1,49 @@ +# Clojure Value Types (Nim Runtime) +import sequtils, strutils + +type + CljKind* = enum + ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol, + ckList, ckVector, ckMap + + CljVal* = ref object + case kind*: CljKind + of ckNil: discard + of ckBool: boolVal*: bool + of ckInt: intVal*: int64 + of ckFloat: floatVal*: float64 + of ckString: strVal*: string + of ckKeyword: kwName*: string + of ckSymbol: symName*: string + of ckList, ckVector: items*: seq[CljVal] + of ckMap: pairs*: seq[(CljVal, 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 cljFloat*(v: float64): CljVal = CljVal(kind: ckFloat, floatVal: v) +proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v) +proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v) +proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v) +proc cljList*(items: seq[CljVal]): CljVal = CljVal(kind: ckList, items: items) +proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, items: items) +proc cljMap*(pairs: seq[(CljVal, CljVal)]): CljVal = CljVal(kind: ckMap, pairs: pairs) + +# String representation (for debugging) +proc `$`*(v: CljVal): string = + case v.kind + of ckNil: "nil" + of ckBool: $v.boolVal + of ckInt: $v.intVal + of ckFloat: $v.floatVal + of ckString: "\"" & v.strVal & "\"" + of ckKeyword: ":" & v.kwName + of ckSymbol: v.symName + of ckList: "(" & v.items.mapIt($it).join(" ") & ")" + of ckVector: "[" & v.items.mapIt($it).join(" ") & "]" + of ckMap: + var parts: seq[string] = @[] + for (k, v2) in v.pairs: + parts.add($k & " " & $v2) + "{" & parts.join(", ") & "}"