diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 041abe6..9b87ad3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,7 @@ test: - nim c -r tests/test_reader.nim - nim c -r tests/test_emitter.nim - nim c -r tests/test_pvec.nim + - nim c -r tests/test_pmap.nim - ./cljnim compile examples/hello.clj /tmp/hello.nim - nim c --path:lib -r /tmp/hello.nim - ./cljnim compile examples/math.clj /tmp/math.nim diff --git a/AI_AGENT.md b/AI_AGENT.md new file mode 100644 index 0000000..47f1c4c --- /dev/null +++ b/AI_AGENT.md @@ -0,0 +1,163 @@ +# AI Agent Onboarding Guide + +> If you are an AI reading this: welcome. This project is designed to be worked on by AI agents. Everything you need is in this repo. No human required. + +## What is this project? + +**Clojure/Nim** is a Clojure dialect that compiles to Nim, then to C, then to native binaries. + +Current state: **Working compiler + REPL + macro system + file/git ops + Nim interop.** + +## Quick Start (for AI agents) + +```bash +# 1. Build +make build + +# 2. Run tests +make test + +# 3. Check everything works +make check + +# 4. Try the REPL +./cljnim repl +user> (+ 1 2) +=> 3 +``` + +## How to add a feature + +### Step 1: Pick a task from TASKS.md +```bash +cat TASKS.md +``` +Each task has: +- **Files to modify** — exact Nim/Clojure files +- **Acceptance criteria** — how to verify it works +- **Complexity** — Small / Medium / Large + +### Step 2: Implement + +Most tasks follow this pattern: + +1. **If adding a Clojure core function** (e.g. `range`, `str`): + - Add the Nim implementation in `lib/cljnim_runtime.nim` + - Add the name mapping in `src/emitter.nim` → `runtimeName()` + - Add a test in `tests/test_emitter.nim` + - Add an example in `examples/` + +2. **If adding emitter special form support** (e.g. `loop`/`recur`): + - Modify `src/emitter.nim` → `emitSpecialForm()` + - Add test in `tests/test_emitter.nim` + +3. **If adding a macro** (e.g. `lazy-seq`): + - Add macro implementation in `src/macros.nim` → `initBuiltinMacros()` + - Add test + +### Step 3: Test your change + +```bash +# Build after any change +make build + +# Run all tests +make test + +# Run a specific example to verify +./cljnim run examples/hello.clj +./cljnim run examples/your_new_example.clj +``` + +### Step 4: Commit + +```bash +# From Clojure code: +(git/commit "Your commit message") +(git/push) +``` + +Or from shell: +```bash +git add -A +git commit -m "Your commit message" +git push +``` + +## Project Structure + +``` +├── src/ +│ ├── cljnim.nim # CLI entry point — commands: compile, run, read, repl +│ ├── reader.nim # Clojure → AST (EDN parser) +│ ├── emitter.nim # AST → Nim code generator (~960 lines) +│ ├── macros.nim # Macro expansion engine (~470 lines) +│ ├── repl.nim # Human + JSON REPL +│ ├── types.nim # AST node types +│ ├── core.nim # Core arithmetic/runtime helpers +│ └── runtime.nim # Additional runtime +├── lib/ +│ └── cljnim_runtime.nim # Clojure runtime in Nim (~1100 lines) +│ # This is where Clojure functions live +├── examples/ +│ ├── hello.clj # Basic hello world +│ ├── math.clj # Functions + recursion +│ ├── core.clj # map/filter/reduce +│ ├── macros.clj # defmacro, ->, ->> +│ ├── interop.clj # Nim interop demo +│ ├── ffi.clj # C FFI demo +│ └── ai_tools.clj # File + Git operations +├── tests/ +│ ├── test_reader.nim +│ └── test_emitter.nim +├── docs/ # Full bilingual documentation (EN + BG) +├── Makefile +└── cljnim.nimble +``` + +## Key Files for AI Agents + +| If you want to... | Modify this file | +|---|---| +| Add a Clojure function | `lib/cljnim_runtime.nim` + `src/emitter.nim` | +| Add a special form | `src/emitter.nim` | +| Add a macro | `src/macros.nim` | +| Add REPL command | `src/repl.nim` | +| Fix a reader bug | `src/reader.nim` | +| Add file/git op | `lib/cljnim_runtime.nim` + `src/emitter.nim` | + +## How the compiler works (in 5 steps) + +1. **Reader** (`src/reader.nim`): Text → `CljVal` AST +2. **Macros** (`src/macros.nim`): Expands macros on `CljVal` AST +3. **Emitter** (`src/emitter.nim`): `CljVal` AST → Nim source code +4. **Nim compiler**: Nim → C +5. **C compiler**: C → binary + +## Testing Strategy + +Every change MUST be tested with: +1. `make test` — runs Nim unit tests +2. `make check` — runs examples +3. Manual REPL test if relevant: `./cljnim repl --json` + +## Common Mistakes + +1. **Forgetting `initBuiltinMacros()`**: If you add a macro but it doesn't expand, check that `initBuiltinMacros()` is called before compilation. It's called in `src/cljnim.nim` and `src/repl.nim`. + +2. **Not adding to `runtimeName`**: If you add a runtime function in `lib/cljnim_runtime.nim` but Clojure can't find it, add the name mapping in `src/emitter.nim` → `runtimeName()`. + +3. **String escaping**: Clojure strings with `\n` or quotes may break Nim code generation. The emitter needs to escape strings properly. + +## Communication Protocol + +When you finish a task, update: +1. `TASKS.md` — mark the task as done +2. `docs/ROADMAP.md` — update if phase completed +3. Commit with descriptive message + +## Questions? + +Read `docs/ARCHITECTURE.md` for deep technical details. +Read `docs/API.md` for REPL protocol reference. +Read `docs/GUIDE.md` for user-facing features. diff --git a/Makefile b/Makefile index 171a81f..c8af3d0 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ build: test: nim c -r tests/test_reader.nim nim c -r tests/test_emitter.nim + nim c -r tests/test_pvec.nim + nim c -r tests/test_pmap.nim test-reader: nim c -r tests/test_reader.nim diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..98a882f --- /dev/null +++ b/TASKS.md @@ -0,0 +1,112 @@ +# Task Board for AI Agents + +> Pick a task, implement it, test it, commit it. Each task is self-contained. + +## Legend +- 🔴 Large — 1+ week +- 🟡 Medium — 2-5 days +- 🟢 Small — few hours +- ⬜ Not started / 🔄 In progress / ✅ Done + +--- + +## Phase 4: AI-Native Tooling (Complete) + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T4.1 | JSON REPL mode | ✅ | 🟡 | `src/repl.nim` | `./cljnim repl --json` works, structured I/O | +| T4.2 | Batch evaluation | ✅ | 🟢 | `src/repl.nim` | `{"op":"eval-batch","forms":["(defn f[x]x)","(f 42)"]}` works | +| T4.3 | File operations | ✅ | 🟡 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `(file/read)`, `(file/write)`, `(file/ls)` work | +| T4.4 | Git operations | ✅ | 🟡 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `(git/status)`, `(git/commit)`, `(git/push)` work | +| T4.5 | nREPL protocol | ⬜ | 🟡 | `src/repl.nim` | REPL speaks nREPL over TCP | +| T4.6 | Tool-call format | ⬜ | 🟢 | `src/repl.nim` | Accept `{"tool":"cljnim/eval","args":{...}}` | + +--- + +## Phase 5: Persistent Data Structures + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T5.1 | HAMT Vector — core structure | ✅ | 🔴 | New: `lib/cljnim_pvec.nim`, `lib/cljnim_runtime.nim` | `PersistentVector` with 32-way trie, structural sharing. `nth` in O(log₃₂ n). 14 tests. | +| T5.2 | HAMT Vector — integration | ✅ | 🔴 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `ckVector` uses HAMT. All 51 `vecData` usages migrated. | +| T5.3 | HAMT Map | ✅ | 🔴 | New: `lib/cljnim_pmap.nim`, `lib/cljnim_runtime.nim` | Persistent Hash Map with HAMT. `assoc`, `dissoc`, `get` in O(log₃₂ n). 16 tests. | +| T5.4 | Persistent Set | ✅ | 🟡 | `lib/cljnim_runtime.nim` | Set backed by Persistent Hash Map. `conj`, `disj`, `contains?`, `get`, `count`. Reader emits `(set [items])`. | +| T5.5 | Transients | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | `transient`, `persistent!`, `conj!`, `assoc!` for batch mutations. | + +**Why HAMT matters**: Current Vector is a Nim `seq` — every `conj` copies the entire array (O(n)). Real Clojure uses Hash Array Mapped Trie for O(log₃₂ n) updates with structural sharing. + +**Starting point for T5.1**: Read `lib/cljnim_vector.nim` (if exists) or research Clojure's `PersistentVector.java`. Key concepts: 32-way branching, path copying, tail optimization. + +--- + +## Phase 6: Clojure Core Library + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T6.1 | `str` — string concatenation | ⬜ | 🟢 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `(str "a" 1 true)` → `"a1true"`. Test in `tests/test_emitter.nim`. | +| T6.2 | `pr-str` — readable representation | ⬜ | 🟢 | `lib/cljnim_runtime.nim` | `(pr-str [1 2 3])` → `"[1 2 3]"`. Escapes strings properly. | +| T6.3 | `slurp` — read file to string | ⬜ | 🟢 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `(slurp "file.txt")` returns content. Already have `file/read` — just alias/wrap. | +| T6.4 | `spit` — write string to file | ⬜ | 🟢 | `lib/cljnim_runtime.nim`, `src/emitter.nim` | `(spit "file.txt" "content")` writes file. | +| T6.5 | `read-line` — read from stdin | ⬜ | 🟢 | `lib/cljnim_runtime.nim` | `(read-line)` reads one line from stdin. | +| T6.6 | `range` — lazy number sequence | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | `(range 10)`, `(range 1 10)`, `(range 1 10 2)`. Returns lazy seq. | +| T6.7 | `repeat`, `cycle`, `iterate` | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | Infinite lazy sequences. | +| T6.8 | `take`, `drop` — seq slicing | ⬜ | 🟢 | `lib/cljnim_runtime.nim` | `(take 5 (range 100))`, `(drop 5 [1 2 3 4 5])`. | +| T6.9 | `partition`, `interleave` | ⬜ | 🟢 | `lib/cljnim_runtime.nim` | `(partition 2 [1 2 3 4])`, `(interleave [1 2] [3 4])`. | +| T6.10 | `meta`, `with-meta`, `vary-meta` | ⬜ | 🟡 | `lib/cljnim_runtime.nim`, `src/types.nim` | Metadata support on vars, functions, collections. | +| T6.11 | `type`, `instance?` | ⬜ | 🟢 | `lib/cljnim_runtime.nim` | `(type 42)` → `:int`. `(instance? :int 42)` → true. | + +--- + +## Phase 7: Project Compilation + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T7.1 | `ns` declaration parsing | ⬜ | 🟡 | `src/reader.nim`, `src/emitter.nim` | `(ns my.app (:require [other.lib :as lib]))` parses and compiles. | +| T7.2 | Multi-file compilation | ⬜ | 🔴 | `src/cljnim.nim`, `src/emitter.nim` | `./cljnim compile project.clj` finds all required files and compiles them together. | +| T7.3 | Module caching | ⬜ | 🟡 | `src/cljnim.nim` | Compiled `.nim` files cached in `nimcache/`. Rebuild only changed files. | +| T7.4 | Dependency resolution | ⬜ | 🔴 | New: `src/deps.nim` | Read `deps.edn` or `project.clj` format. Download deps from Git. | + +--- + +## Phase 8: Self-Hosted REPL + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T8.1 | In-memory compilation | ⬜ | 🔴 | `src/repl.nim`, `src/emitter.nim` | REPL compiles to Nim AST in memory, no temp files. | +| T8.2 | Fast REPL startup | ⬜ | 🟡 | `src/repl.nim` | REPL starts in < 100ms. Pre-compiled runtime loaded once. | +| T8.3 | Hot code reloading | ⬜ | 🟡 | `src/repl.nim` | Redefine a function, all callers use new version immediately. | + +--- + +## Phase 9: Concurrency + +| ID | Task | Status | Complexity | Files | Acceptance Criteria | +|---|---|---|---|---|---| +| T9.1 | Atoms (CAS) | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | `(def a (atom 0))`, `(swap! a inc)`, `(reset! a 42)`, `(deref a)`. | +| T9.2 | Agents | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | `(def ag (agent 0))`, `(send ag inc)`. Async execution. | +| T9.3 | core.async channels | ⬜ | 🔴 | New: `lib/cljnim_async.nim` | `(chan)`, `(>! ch val)`, `(>` threading macro with nested `map`/`reduce` requires proper macro expansion context - REPL definitions are re-compiled from scratch on each evaluation (slow but correct) -- No true persistent data structures yet (runtime uses Nim seq/tables) diff --git a/lib/cljnim_pmap.nim b/lib/cljnim_pmap.nim new file mode 100644 index 0000000..1b9daee --- /dev/null +++ b/lib/cljnim_pmap.nim @@ -0,0 +1,269 @@ +# Persistent Map — Hash Array Mapped Trie (HAMT) +# Clojure-style 32-way branching with structural sharing +# Takes pre-computed hashes and equality functions from callers + +import hashes + +const + PMAP_SHIFT* = 5 + PMAP_BRANCH* = 32 + PMAP_MASK* = 31 + PMAP_MAX_SHIFT* = 25 # 6 levels (25,20,15,10,5,0) = 30 bits of hash + +type + PMapNode*[K, V] = ref object + children*: seq[PMapNode[K, V]] + keys*: seq[K] + vals*: seq[V] + hashes*: seq[Hash] # stored hashes for leaf nodes, parallel to keys + + PersistentMap*[K, V] = object + root*: PMapNode[K, V] + count*: int + +proc newPMapLeaf*[K, V](keys: seq[K] = @[], vals: seq[V] = @[], hashes: seq[Hash] = @[]): PMapNode[K, V] = + PMapNode[K, V](keys: keys, vals: vals, hashes: hashes) + +proc newPMapInternal*[K, V](): PMapNode[K, V] = + PMapNode[K, V](children: @[]) + +proc pmapCopyNode*[K, V](n: PMapNode[K, V]): PMapNode[K, V] = + if n.isNil: return nil + if n.children.len > 0: + result = newPMapInternal[K, V]() + result.children = n.children + else: + result = newPMapLeaf[K, V](n.keys, n.vals, n.hashes) + +proc newPersistentMap*[K, V](): PersistentMap[K, V] = + PersistentMap[K, V](root: nil, count: 0) + +proc pmapGet*[K, V](m: PersistentMap[K, V], key: K, default: V, kh: Hash, eq: proc(a, b: K): bool): V = + if m.root.isNil: return default + var node: PMapNode[K, V] = m.root + var shift = PMAP_MAX_SHIFT + var h = kh + + while shift >= 0: + if node.children.len > 0: + let idx = (h shr shift) and PMAP_MASK + if idx >= node.children.len or node.children[idx].isNil: + return default + node = node.children[idx] + shift -= PMAP_SHIFT + continue + + for i in 0..= 0: + if node.children.len > 0: + let idx = (h shr shift) and PMAP_MASK + if idx >= node.children.len or node.children[idx].isNil: + return false + node = node.children[idx] + shift -= PMAP_SHIFT + continue + + for i in 0.. 0: + result = pmapCopyNode(node) + if idx >= result.children.len: + let oldLen = result.children.len + result.children.setLen(idx + 1) + for i in oldLen..= result.children.len: + let oldLen = result.children.len + result.children.setLen(eidx + 1) + for i in oldLen.. result.children.len: + let oldLen = result.children.len + result.children.setLen(ns) + for i in oldLen.. 0: + result = pmapCopyNode(node) + if idx < result.children.len and not result.children[idx].isNil: + result.children[idx] = doDissoc(result.children[idx], shift - PMAP_SHIFT) + return + + # Leaf node + result = newPMapLeaf[K, V]() + for i in 0.. 0: + let node = stack.pop() + if node.isNil: continue + if node.children.len > 0: + for i in countdown(node.children.len - 1, 0): + if not node.children[i].isNil: + stack.add(node.children[i]) + else: + result.add(node.keys) + +proc pmapVals*[K, V](m: PersistentMap[K, V]): seq[V] = + result = @[] + if m.root.isNil: return + var stack: seq[PMapNode[K, V]] = @[m.root] + while stack.len > 0: + let node = stack.pop() + if node.isNil: continue + if node.children.len > 0: + for i in countdown(node.children.len - 1, 0): + if not node.children[i].isNil: + stack.add(node.children[i]) + else: + result.add(node.vals) + +proc pmapEntries*[K, V](m: PersistentMap[K, V]): seq[(K, V)] = + result = @[] + if m.root.isNil: return + var stack: seq[PMapNode[K, V]] = @[m.root] + while stack.len > 0: + let node = stack.pop() + if node.isNil: continue + if node.children.len > 0: + for i in countdown(node.children.len - 1, 0): + if not node.children[i].isNil: + stack.add(node.children[i]) + else: + for i in 0.. 0: + let node = stack.pop() + if node.isNil: continue + if node.children.len > 0: + for i in countdown(node.children.len - 1, 0): + if not node.children[i].isNil: + stack.add(node.children[i]) + else: + for i in 0.." of ckAtom: "(atom " & cljRepr(v.atomVal) & ")" @@ -331,14 +360,15 @@ proc cljOdd*(v: CljVal): CljVal = # ---- Collection operations ---- -proc cljCount*(v: CljVal): int = - if v.isNil: return 0 +proc cljCount*(v: CljVal): CljVal = + if v.isNil: return cljInt(0) case v.kind - of ckList: v.listItems.len - of ckVector: v.vecData.count - of ckMap: v.mapKeys.len - of ckString: v.strVal.len - else: 0 + of ckList: cljInt(v.listItems.len) + of ckVector: cljInt(v.vecData.count) + of ckMap: cljInt(v.mapData.count) + of ckSet: cljInt(v.setData.count) + of ckString: cljInt(v.strVal.len) + else: cljInt(0) proc cljFirst*(v: CljVal): CljVal = if v.isNil: return cljNil() @@ -405,6 +435,8 @@ proc cljConj*(coll: CljVal, item: CljVal): CljVal = var newItems = toSeq(coll.vecData) newItems.add(item) cljVector(newItems) + of ckSet: + CljVal(kind: ckSet, setData: pmapAssoc(coll.setData, item, true, hash(item), cljEq)) else: cljList(@[item]) @@ -423,6 +455,11 @@ proc cljCons*(item: CljVal, coll: CljVal): CljVal = else: cljList(@[item]) +proc cljDisj*(s: CljVal, item: CljVal): CljVal = + if s.isNil or s.kind != ckSet: + return cljSet(@[]) + CljVal(kind: ckSet, setData: pmapDissoc(s.setData, item, hash(item), cljEq)) + proc cljSeq*(v: CljVal): CljVal = if v.isNil: return cljNil() case v.kind @@ -439,6 +476,9 @@ proc cljSeq*(v: CljVal): CljVal = for c in v.strVal: chars.add(cljString($c)) cljList(chars) + of ckSet: + if v.setData.count == 0: cljNil() + else: cljList(pmapKeys(v.setData)) else: cljNil() proc cljVec*(v: CljVal): CljVal = @@ -449,7 +489,7 @@ proc cljVec*(v: CljVal): CljVal = else: cljVector(@[v]) proc cljEmpty*(v: CljVal): CljVal = - cljBool(cljCount(v) == 0) + cljBool(cljCount(v).intVal == 0) proc cljConcat*(args: seq[CljVal]): CljVal = var items: seq[CljVal] = @[] @@ -556,29 +596,23 @@ proc cljPartition*(n: int, coll: CljVal): CljVal = proc cljFrequencies*(coll: CljVal): CljVal = if coll.isNil: return cljMap(@[], @[]) - var keys: seq[CljVal] = @[] - var vals: seq[CljVal] = @[] + var m = newPersistentMap[CljVal, CljVal]() var items: seq[CljVal] case coll.kind of ckList: items = coll.listItems of ckVector: items = toSeq(coll.vecData) else: return cljMap(@[], @[]) for item in items: - var found = false - for j in 0.. 3: raise newException(EmitterError, "range requires 0, 1, or 2 arguments") diff --git a/tests/test_pmap.nim b/tests/test_pmap.nim new file mode 100644 index 0000000..6d2bdfd --- /dev/null +++ b/tests/test_pmap.nim @@ -0,0 +1,153 @@ +import unittest, hashes +import ../lib/cljnim_pmap + +proc intEq(a, b: int): bool = a == b +proc strEq(a, b: string): bool = a == b + +suite "Persistent Map - Basic": + test "empty map": + var m = newPersistentMap[int, int]() + check m.count == 0 + check pmapGet(m, 42, -1, hash(42), intEq) == -1 + + test "single entry": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + check m.count == 1 + check pmapGet(m, 1, "default", hash(1), intEq) == "one" + check pmapGet(m, 2, "default", hash(2), intEq) == "default" + + test "few entries": + var m = newPersistentMap[int, int]() + for i in 0..<5: + m = pmapAssoc(m, i, i * 10, hash(i), intEq) + check m.count == 5 + check pmapGet(m, 0, -1, hash(0), intEq) == 0 + check pmapGet(m, 4, -1, hash(4), intEq) == 40 + check pmapContains(m, 3, hash(3), intEq) == true + check pmapContains(m, 99, hash(99), intEq) == false + +suite "Persistent Map - assoc": + test "assoc overwrites existing": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + m = pmapAssoc(m, 1, "uno", hash(1), intEq) + check pmapGet(m, 1, "", hash(1), intEq) == "uno" + check m.count == 1 + + test "assoc preserves old map": + var m1 = newPersistentMap[int, int]() + m1 = pmapAssoc(m1, 1, 10, hash(1), intEq) + var m2 = pmapAssoc(m1, 2, 20, hash(2), intEq) + check m1.count == 1 + check m2.count == 2 + check pmapGet(m1, 2, -1, hash(2), intEq) == -1 + check pmapGet(m2, 1, -1, hash(1), intEq) == 10 + + test "assoc many entries (100)": + var m = newPersistentMap[int, int]() + for i in 0..<100: + m = pmapAssoc(m, i, i * i, hash(i), intEq) + check m.count == 100 + check pmapGet(m, 0, -1, hash(0), intEq) == 0 + check pmapGet(m, 50, -1, hash(50), intEq) == 2500 + check pmapGet(m, 99, -1, hash(99), intEq) == 9801 + for i in 0..<100: + check pmapGet(m, i, -1, hash(i), intEq) == i * i + + test "assoc many entries (500)": + var m = newPersistentMap[int, int]() + for i in 0..<500: + m = pmapAssoc(m, i, i * 2, hash(i), intEq) + check m.count == 500 + check pmapGet(m, 0, -1, hash(0), intEq) == 0 + check pmapGet(m, 250, -1, hash(250), intEq) == 500 + check pmapGet(m, 499, -1, hash(499), intEq) == 998 + +suite "Persistent Map - dissoc": + test "dissoc existing key": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + m = pmapAssoc(m, 2, "two", hash(2), intEq) + m = pmapAssoc(m, 3, "three", hash(3), intEq) + m = pmapDissoc(m, 2, hash(2), intEq) + check m.count == 2 + check pmapContains(m, 1, hash(1), intEq) == true + check pmapContains(m, 2, hash(2), intEq) == false + check pmapContains(m, 3, hash(3), intEq) == true + + test "dissoc non-existing key": + var m = newPersistentMap[int, int]() + m = pmapAssoc(m, 1, 10, hash(1), intEq) + m = pmapDissoc(m, 99, hash(99), intEq) + check m.count == 1 + check pmapContains(m, 1, hash(1), intEq) == true + + test "dissoc until empty": + var m = newPersistentMap[int, int]() + m = pmapAssoc(m, 1, 10, hash(1), intEq) + m = pmapDissoc(m, 1, hash(1), intEq) + check m.count == 0 + check m.root.isNil + +suite "Persistent Map - keys/values": + test "keys": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + m = pmapAssoc(m, 2, "two", hash(2), intEq) + let k = pmapKeys(m) + check k.len == 2 + check 1 in k + check 2 in k + + test "vals": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + m = pmapAssoc(m, 2, "two", hash(2), intEq) + let v = pmapVals(m) + check v.len == 2 + check "one" in v + check "two" in v + + test "entries": + var m = newPersistentMap[int, string]() + m = pmapAssoc(m, 1, "one", hash(1), intEq) + m = pmapAssoc(m, 2, "two", hash(2), intEq) + let e = pmapEntries(m) + check e.len == 2 + +suite "Persistent Map - items iterator": + test "iterate empty": + var m = newPersistentMap[int, int]() + var count = 0 + for (k, v) in pmapItems(m): + count += 1 + check count == 0 + + test "iterate non-empty": + var m = newPersistentMap[int, int]() + for i in 0..<10: + m = pmapAssoc(m, i, i * 10, hash(i), intEq) + var count = 0 + var sum = 0 + for (k, v) in pmapItems(m): + count += 1 + sum += v + check count == 10 + check sum == 450 + +suite "Persistent Map - string keys": + test "string keys": + var m = newPersistentMap[string, int]() + m = pmapAssoc(m, "hello", 1, hash("hello"), strEq) + m = pmapAssoc(m, "world", 2, hash("world"), strEq) + check pmapGet(m, "hello", -1, hash("hello"), strEq) == 1 + check pmapGet(m, "world", -1, hash("world"), strEq) == 2 + check pmapGet(m, "missing", -1, hash("missing"), strEq) == -1 + +suite "Persistent Map - build from pairs": + test "pmapBuild": + var pairs: seq[(int, string)] = @[(1, "one"), (2, "two"), (3, "three")] + var m = pmapBuild(pairs, intEq) + check m.count == 3 + check pmapGet(m, 2, "", hash(2), intEq) == "two"