Phase 5: HAMT Persistent Map + Persistent Set, 86 tests pass

- T5.3: HAMT Persistent Map (lib/cljnim_pmap.nim)
  O(log32 n) assoc/dissoc/get with structural sharing
  16 unit tests, ckMap migrated from seq to HAMT

- T5.4: Persistent Set backed by HAMT map
  ckSet with conj/disj/contains?/get, set literal #{}

- Fix: cljContains/cljCount return CljVal (not bool/int)
- Fix: equality in generics via explicit cljEq parameter
- Updated roadmap, task board, CI/Makefile
This commit is contained in:
2026-05-08 19:13:09 +03:00
parent d763e25638
commit 6dfc2a9308
9 changed files with 864 additions and 165 deletions
+1
View File
@@ -11,6 +11,7 @@ test:
- nim c -r tests/test_reader.nim - nim c -r tests/test_reader.nim
- nim c -r tests/test_emitter.nim - nim c -r tests/test_emitter.nim
- nim c -r tests/test_pvec.nim - nim c -r tests/test_pvec.nim
- nim c -r tests/test_pmap.nim
- ./cljnim compile examples/hello.clj /tmp/hello.nim - ./cljnim compile examples/hello.clj /tmp/hello.nim
- nim c --path:lib -r /tmp/hello.nim - nim c --path:lib -r /tmp/hello.nim
- ./cljnim compile examples/math.clj /tmp/math.nim - ./cljnim compile examples/math.clj /tmp/math.nim
+163
View File
@@ -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.
+2
View File
@@ -8,6 +8,8 @@ build:
test: test:
nim c -r tests/test_reader.nim nim c -r tests/test_reader.nim
nim c -r tests/test_emitter.nim nim c -r tests/test_emitter.nim
nim c -r tests/test_pvec.nim
nim c -r tests/test_pmap.nim
test-reader: test-reader:
nim c -r tests/test_reader.nim nim c -r tests/test_reader.nim
+112
View File
@@ -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)`, `(<! ch)`, `(go ...)`. Simplified version. |
---
## Quick Wins (do these first!)
These tasks are **small, well-defined, and high impact**:
1. **T6.1 `str`** ✅ — Done. `cljStrConcat` in runtime, `str` mapping in emitter.
2. **T6.3 `slurp`** — Alias of existing `cljFileRead`. 1 line of work.
3. **T6.4 `spit`** — Alias of existing `cljFileWrite`. 1 line of work.
4. **T6.8 `take`, `drop`** ✅ — Done. `cljTake`, `cljDrop` in runtime, emitter mappings.
5. **T6.11 `type`, `instance?`** ✅ — Done. `cljType` in runtime, `type` mapping in emitter.
6. **T4.6 Tool-call format** — Wrap REPL input parser to accept `{"tool":...}`.
---
## How to claim a task
1. Read this file
2. Pick an unclaimed task (marked ⬜)
3. Implement it
4. Run `make test && make check`
5. Update this file: change ⬜ to ✅
6. Commit: `(git/commit "T6.1: Add str function")`
7. Push: `(git/push)`
+5 -6
View File
@@ -46,19 +46,19 @@
## Phase 5: Persistent Data Structures ## Phase 5: Persistent Data Structures
- [x] Persistent Vector (Hash Array Mapped Trie, 32-way branching) - [x] Persistent Vector (Hash Array Mapped Trie, 32-way branching)
- [ ] Persistent Map (HAMT) - [x] Persistent Map (HAMT)`pmapAssoc`, `pmapDissoc`, `pmapGet` in O(log₃₂ n)
- [ ] Persistent Set - [x] Persistent Set — backed by HAMT map, `conj`/`disj`/`contains?`/`get`
- [x] `conj`, `assoc`, `dissoc`, `get`, `get-in` - [x] `conj`, `assoc`, `dissoc`, `get`, `get-in`
- [x] `nth`, `first`, `rest`, `last`, `count` on persistent collections - [x] `nth`, `first`, `rest`, `last`, `count` on persistent collections
- [ ] Transients for batch mutations - [ ] Transients for batch mutations
## Phase 6: Clojure Core Library ## Phase 6: Clojure Core Library
- [ ] `range`, `repeat`, `cycle`, `iterate` - [ ] `range`, `repeat`, `cycle`, `iterate`
- [ ] `take`, `drop`, `partition`, `interleave`, `concat` - [x] `take`, `drop`, `partition`, `interleave`, `concat`
- [ ] `str`, `pr-str`, `println`, `prn` - [x] `str`, `pr-str`, `println`, `prn`
- [ ] `slurp`, `spit`, `read-line` - [ ] `slurp`, `spit`, `read-line`
- [ ] `meta`, `with-meta`, `vary-meta` - [ ] `meta`, `with-meta`, `vary-meta`
- [ ] `type`, `instance?`, `satisfies?` - [x] `type`, `instance?`, `satisfies?`
## Phase 7: Project Compilation ## Phase 7: Project Compilation
- [ ] Compile entire projects (not just single files) - [ ] Compile entire projects (not just single files)
@@ -79,4 +79,3 @@
## Known Issues ## Known Issues
- `->>` threading macro with nested `map`/`reduce` requires proper macro expansion context - `->>` threading macro with nested `map`/`reduce` requires proper macro expansion context
- REPL definitions are re-compiled from scratch on each evaluation (slow but correct) - REPL definitions are re-compiled from scratch on each evaluation (slow but correct)
- No true persistent data structures yet (runtime uses Nim seq/tables)
+269
View File
@@ -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..<node.keys.len:
if eq(node.keys[i], key):
return node.vals[i]
return default
return default
proc pmapContains*[K, V](m: PersistentMap[K, V], key: K, kh: Hash, eq: proc(a, b: K): bool): bool =
if m.root.isNil: return false
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 false
node = node.children[idx]
shift -= PMAP_SHIFT
continue
for i in 0..<node.keys.len:
if eq(node.keys[i], key):
return true
return false
return false
proc pmapAssoc*[K, V](m: PersistentMap[K, V], key: K, val: V, kh: Hash, eq: proc(a, b: K): bool): PersistentMap[K, V] =
let h = kh
var added = false
proc doAssoc(node: PMapNode[K, V], shift: int): PMapNode[K, V] =
let idx = (h shr shift) and PMAP_MASK
if node.isNil:
added = true
return newPMapLeaf[K, V](@[key], @[val], @[kh])
if node.children.len > 0:
result = pmapCopyNode(node)
if idx >= result.children.len:
let oldLen = result.children.len
result.children.setLen(idx + 1)
for i in oldLen..<idx:
result.children[i] = nil
let childNode = if idx < node.children.len: node.children[idx] else: nil
result.children[idx] = doAssoc(childNode, shift - PMAP_SHIFT)
return
# Leaf node
if shift == 0:
result = newPMapLeaf[K, V](node.keys, node.vals, node.hashes)
for i in 0..<result.keys.len:
if eq(result.keys[i], key):
result.vals[i] = val
return
result.keys.add(key)
result.vals.add(val)
result.hashes.add(kh)
added = true
return
# Leaf at non-zero shift — promote to internal
let existingKeys = node.keys
let existingHashes = node.hashes
result = newPMapInternal[K, V]()
# Place all existing entries into the new internal node
for ki in 0..<existingKeys.len:
let ek = existingKeys[ki]
let ev = node.vals[ki]
let eh = existingHashes[ki]
let eidx = (eh shr shift) and PMAP_MASK
if eidx >= result.children.len:
let oldLen = result.children.len
result.children.setLen(eidx + 1)
for i in oldLen..<eidx:
result.children[i] = nil
if result.children[eidx].isNil:
result.children[eidx] = newPMapLeaf[K, V](@[ek], @[ev], @[eh])
else:
result.children[eidx].keys.add(ek)
result.children[eidx].vals.add(ev)
result.children[eidx].hashes.add(eh)
# Now insert the new key
let ns = max(idx + 1, result.children.len)
if ns > result.children.len:
let oldLen = result.children.len
result.children.setLen(ns)
for i in oldLen..<ns:
result.children[i] = nil
if result.children[idx].isNil:
result.children[idx] = newPMapLeaf[K, V](@[key], @[val], @[kh])
added = true
else:
result.children[idx] = doAssoc(result.children[idx], shift - PMAP_SHIFT)
if m.root.isNil:
result.root = newPMapLeaf[K, V](@[key], @[val], @[kh])
result.count = 1
else:
result.root = doAssoc(m.root, PMAP_MAX_SHIFT)
result.count = m.count
if added:
result.count += 1
proc pmapDissoc*[K, V](m: PersistentMap[K, V], key: K, kh: Hash, eq: proc(a, b: K): bool): PersistentMap[K, V] =
if m.root.isNil or m.count == 0: return m
var removed = false
let h = kh
proc doDissoc(node: PMapNode[K, V], shift: int): PMapNode[K, V] =
let idx = (h shr shift) and PMAP_MASK
if node.isNil:
return nil
if node.children.len > 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..<node.keys.len:
if eq(node.keys[i], key):
removed = true
else:
result.keys.add(node.keys[i])
result.vals.add(node.vals[i])
result.hashes.add(node.hashes[i])
if result.keys.len == 0:
return nil
result = m
result.root = doDissoc(m.root, PMAP_MAX_SHIFT)
if removed:
result.count -= 1
if result.count == 0:
result.root = nil
proc pmapKeys*[K, V](m: PersistentMap[K, V]): seq[K] =
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.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..<node.keys.len:
result.add((node.keys[i], node.vals[i]))
proc pmapBuild*[K, V](pairs: seq[(K, V)], eq: proc(a, b: K): bool): PersistentMap[K, V] =
for (k, v) in pairs:
result = pmapAssoc(result, k, v, hash(k), eq)
iterator pmapItems*[K, V](m: PersistentMap[K, V]): (K, V) =
if not m.root.isNil:
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..<node.keys.len:
yield (node.keys[i], node.vals[i])
proc `$`*[K, V](m: PersistentMap[K, V]): string =
var parts: seq[string] = @[]
for (k, v) in pmapItems(m):
parts.add($k & " " & $v)
"{" & parts.join(", ") & "}"
+140 -159
View File
@@ -1,10 +1,11 @@
import cljnim_pvec import cljnim_pvec
import cljnim_pmap
import strutils, sequtils, hashes, algorithm, os, osproc import strutils, sequtils, hashes, algorithm, os, osproc
type type
CljKind* = enum CljKind* = enum
ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol, ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol,
ckList, ckVector, ckMap, ckFn, ckAtom ckList, ckVector, ckMap, ckSet, ckFn, ckAtom
CljVal* = ref CljValObj CljVal* = ref CljValObj
CljValObj = object CljValObj = object
@@ -18,36 +19,25 @@ type
of ckSymbol: symName*: string of ckSymbol: symName*: string
of ckList: listItems*: seq[CljVal] of ckList: listItems*: seq[CljVal]
of ckVector: vecData*: PersistentVector[CljVal] of ckVector: vecData*: PersistentVector[CljVal]
of ckMap: of ckMap: mapData*: PersistentMap[CljVal, CljVal]
mapKeys*: seq[CljVal] of ckSet: setData*: PersistentMap[CljVal, bool]
mapVals*: seq[CljVal]
of ckFn: fnProc*: proc(args: seq[CljVal]): CljVal of ckFn: fnProc*: proc(args: seq[CljVal]): CljVal
of ckAtom: atomVal*: CljVal of ckAtom: atomVal*: CljVal
# ---- Constructors ---- # ---- Hashing ----
proc cljNil*(): CljVal = CljVal(kind: ckNil) proc hash*(v: CljVal): Hash =
proc cljBool*(v: bool): CljVal = CljVal(kind: ckBool, boolVal: v) case v.kind
proc cljInt*(v: int64): CljVal = CljVal(kind: ckInt, intVal: v) of ckNil: result = hash(0)
proc cljInt*(v: int): CljVal = CljVal(kind: ckInt, intVal: v.int64) of ckBool: result = hash(v.boolVal)
proc cljFloat*(v: float64): CljVal = CljVal(kind: ckFloat, floatVal: v) of ckInt: result = hash(v.intVal)
proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v) of ckFloat: result = hash(v.floatVal)
proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v) of ckString: result = hash(v.strVal)
proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v) of ckKeyword: result = hash(v.kwName)
proc cljList*(items: seq[CljVal]): CljVal = CljVal(kind: ckList, listItems: items) of ckSymbol: result = hash(v.symName)
proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, vecData: newPersistentVector(items)) of ckFn: result = hash(cast[uint](unsafeAddr v.fnProc))
proc cljFn*(p: proc(args: seq[CljVal]): CljVal): CljVal = CljVal(kind: ckFn, fnProc: p) of ckAtom: result = hash(cast[uint](unsafeAddr v.atomVal))
of ckList, ckVector, ckMap, ckSet: result = hash(0)
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 ---- # ---- Equality ----
@@ -65,6 +55,40 @@ proc `==`*(a, b: CljVal): bool =
of ckSymbol: a.symName == b.symName of ckSymbol: a.symName == b.symName
else: false else: false
proc cljEq*(a, b: CljVal): bool = a == b
# ---- 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: newPersistentVector(items))
proc cljFn*(p: proc(args: seq[CljVal]): CljVal): CljVal = CljVal(kind: ckFn, fnProc: p)
proc cljMap*(keys, vals: seq[CljVal]): CljVal =
var m = newPersistentMap[CljVal, CljVal]()
for i in 0..<keys.len:
m = pmapAssoc(m, keys[i], vals[i], hash(keys[i]), cljEq)
CljVal(kind: ckMap, mapData: m)
proc cljMapFromPairs*(pairs: seq[(CljVal, CljVal)]): CljVal =
var m = newPersistentMap[CljVal, CljVal]()
for (k, v) in pairs:
m = pmapAssoc(m, k, v, hash(k), cljEq)
CljVal(kind: ckMap, mapData: m)
proc cljSet*(items: seq[CljVal]): CljVal =
var s = newPersistentMap[CljVal, bool]()
for item in items:
s = pmapAssoc(s, item, true, hash(item), cljEq)
CljVal(kind: ckSet, setData: s)
# ---- Display ---- # ---- Display ----
proc cljRepr*(v: CljVal): string = proc cljRepr*(v: CljVal): string =
@@ -81,9 +105,14 @@ proc cljRepr*(v: CljVal): string =
of ckVector: "[" & toSeq(v.vecData).mapIt(cljRepr(it)).join(" ") & "]" of ckVector: "[" & toSeq(v.vecData).mapIt(cljRepr(it)).join(" ") & "]"
of ckMap: of ckMap:
var parts: seq[string] = @[] var parts: seq[string] = @[]
for i in 0..<v.mapKeys.len: for (k, v) in pmapItems(v.mapData):
parts.add(cljRepr(v.mapKeys[i]) & " " & cljRepr(v.mapVals[i])) parts.add(cljRepr(k) & " " & cljRepr(v))
"{" & parts.join(", ") & "}" "{" & parts.join(", ") & "}"
of ckSet:
var parts: seq[string] = @[]
for (k, v) in pmapItems(v.setData):
parts.add(cljRepr(k))
"#{" & parts.join(" ") & "}"
of ckFn: "#<fn>" of ckFn: "#<fn>"
of ckAtom: "(atom " & cljRepr(v.atomVal) & ")" of ckAtom: "(atom " & cljRepr(v.atomVal) & ")"
@@ -331,14 +360,15 @@ proc cljOdd*(v: CljVal): CljVal =
# ---- Collection operations ---- # ---- Collection operations ----
proc cljCount*(v: CljVal): int = proc cljCount*(v: CljVal): CljVal =
if v.isNil: return 0 if v.isNil: return cljInt(0)
case v.kind case v.kind
of ckList: v.listItems.len of ckList: cljInt(v.listItems.len)
of ckVector: v.vecData.count of ckVector: cljInt(v.vecData.count)
of ckMap: v.mapKeys.len of ckMap: cljInt(v.mapData.count)
of ckString: v.strVal.len of ckSet: cljInt(v.setData.count)
else: 0 of ckString: cljInt(v.strVal.len)
else: cljInt(0)
proc cljFirst*(v: CljVal): CljVal = proc cljFirst*(v: CljVal): CljVal =
if v.isNil: return cljNil() if v.isNil: return cljNil()
@@ -405,6 +435,8 @@ proc cljConj*(coll: CljVal, item: CljVal): CljVal =
var newItems = toSeq(coll.vecData) var newItems = toSeq(coll.vecData)
newItems.add(item) newItems.add(item)
cljVector(newItems) cljVector(newItems)
of ckSet:
CljVal(kind: ckSet, setData: pmapAssoc(coll.setData, item, true, hash(item), cljEq))
else: else:
cljList(@[item]) cljList(@[item])
@@ -423,6 +455,11 @@ proc cljCons*(item: CljVal, coll: CljVal): CljVal =
else: else:
cljList(@[item]) 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 = proc cljSeq*(v: CljVal): CljVal =
if v.isNil: return cljNil() if v.isNil: return cljNil()
case v.kind case v.kind
@@ -439,6 +476,9 @@ proc cljSeq*(v: CljVal): CljVal =
for c in v.strVal: for c in v.strVal:
chars.add(cljString($c)) chars.add(cljString($c))
cljList(chars) cljList(chars)
of ckSet:
if v.setData.count == 0: cljNil()
else: cljList(pmapKeys(v.setData))
else: cljNil() else: cljNil()
proc cljVec*(v: CljVal): CljVal = proc cljVec*(v: CljVal): CljVal =
@@ -449,7 +489,7 @@ proc cljVec*(v: CljVal): CljVal =
else: cljVector(@[v]) else: cljVector(@[v])
proc cljEmpty*(v: CljVal): CljVal = proc cljEmpty*(v: CljVal): CljVal =
cljBool(cljCount(v) == 0) cljBool(cljCount(v).intVal == 0)
proc cljConcat*(args: seq[CljVal]): CljVal = proc cljConcat*(args: seq[CljVal]): CljVal =
var items: seq[CljVal] = @[] var items: seq[CljVal] = @[]
@@ -556,29 +596,23 @@ proc cljPartition*(n: int, coll: CljVal): CljVal =
proc cljFrequencies*(coll: CljVal): CljVal = proc cljFrequencies*(coll: CljVal): CljVal =
if coll.isNil: return cljMap(@[], @[]) if coll.isNil: return cljMap(@[], @[])
var keys: seq[CljVal] = @[] var m = newPersistentMap[CljVal, CljVal]()
var vals: seq[CljVal] = @[]
var items: seq[CljVal] var items: seq[CljVal]
case coll.kind case coll.kind
of ckList: items = coll.listItems of ckList: items = coll.listItems
of ckVector: items = toSeq(coll.vecData) of ckVector: items = toSeq(coll.vecData)
else: return cljMap(@[], @[]) else: return cljMap(@[], @[])
for item in items: for item in items:
var found = false let current = pmapGet(m, item, cljNil(), hash(item), cljEq)
for j in 0..<keys.len: if cljIsNil(current):
if keys[j] == item: m = pmapAssoc(m, item, cljInt(1), hash(item), cljEq)
vals[j] = cljInt(vals[j].intVal + 1) else:
found = true m = pmapAssoc(m, item, cljInt(current.intVal + 1), hash(item), cljEq)
break CljVal(kind: ckMap, mapData: m)
if not found:
keys.add(item)
vals.add(cljInt(1))
cljMap(keys, vals)
proc cljGroupBy*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal = proc cljGroupBy*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
if coll.isNil: return cljMap(@[], @[]) if coll.isNil: return cljMap(@[], @[])
var keys: seq[CljVal] = @[] var m = newPersistentMap[CljVal, CljVal]()
var vals: seq[CljVal] = @[]
var items: seq[CljVal] var items: seq[CljVal]
case coll.kind case coll.kind
of ckList: items = coll.listItems of ckList: items = coll.listItems
@@ -586,21 +620,14 @@ proc cljGroupBy*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
else: return cljMap(@[], @[]) else: return cljMap(@[], @[])
for item in items: for item in items:
let key = f(@[item]) let key = f(@[item])
var found = false let existing = pmapGet(m, key, cljNil(), hash(key), cljEq)
for j in 0..<keys.len: if cljIsNil(existing):
if keys[j] == key: m = pmapAssoc(m, key, cljList(@[item]), hash(key), cljEq)
case vals[j].kind else:
of ckList: var newItems = existing.listItems
var newItems = vals[j].listItems newItems.add(item)
newItems.add(item) m = pmapAssoc(m, key, cljList(newItems), hash(key), cljEq)
vals[j] = cljList(newItems) CljVal(kind: ckMap, mapData: m)
else: discard
found = true
break
if not found:
keys.add(key)
vals.add(cljList(@[item]))
cljMap(keys, vals)
proc cljGroupBy*(f: CljVal, coll: CljVal): CljVal = proc cljGroupBy*(f: CljVal, coll: CljVal): CljVal =
if f.kind == ckFn: cljGroupBy(f.fnProc, coll) if f.kind == ckFn: cljGroupBy(f.fnProc, coll)
@@ -609,83 +636,63 @@ proc cljGroupBy*(f: CljVal, coll: CljVal): CljVal =
# ---- Map operations ---- # ---- Map operations ----
proc cljGet*(m: CljVal, key: CljVal): CljVal = proc cljGet*(m: CljVal, key: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return cljNil() if m.isNil: return cljNil()
for i in 0..<m.mapKeys.len: case m.kind
if m.mapKeys[i] == key: of ckMap: pmapGet(m.mapData, key, cljNil(), hash(key), cljEq)
return m.mapVals[i] of ckSet:
cljNil() if pmapContains(m.setData, key, hash(key), cljEq): key
else: cljNil()
else: cljNil()
proc cljGetDefault*(m: CljVal, key: CljVal, default: CljVal): CljVal = proc cljGetDefault*(m: CljVal, key: CljVal, default: CljVal): CljVal =
let v = cljGet(m, key) if m.isNil: return default
if cljIsNil(v): default case m.kind
else: v of ckMap: pmapGet(m.mapData, key, default, hash(key), cljEq)
of ckSet:
if pmapContains(m.setData, key, hash(key), cljEq): key
else: default
else: default
proc cljAssoc*(m: CljVal, key: CljVal, val: CljVal): CljVal = proc cljAssoc*(m: CljVal, key: CljVal, val: CljVal): CljVal =
if m.isNil or m.kind != ckMap: if m.isNil or m.kind != ckMap:
return cljMap(@[key], @[val]) return CljVal(kind: ckMap, mapData: pmapAssoc(newPersistentMap[CljVal, CljVal](), key, val, hash(key), cljEq))
var newKeys = m.mapKeys CljVal(kind: ckMap, mapData: pmapAssoc(m.mapData, key, val, hash(key), cljEq))
var newVals = m.mapVals
for i in 0..<newKeys.len:
if newKeys[i] == key:
newVals[i] = val
return cljMap(newKeys, newVals)
newKeys.add(key)
newVals.add(val)
cljMap(newKeys, newVals)
proc cljDissoc*(m: CljVal, key: CljVal): CljVal = proc cljDissoc*(m: CljVal, key: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return cljMap(@[], @[]) if m.isNil or m.kind != ckMap: return cljMap(@[], @[])
var newKeys: seq[CljVal] = @[] CljVal(kind: ckMap, mapData: pmapDissoc(m.mapData, key, hash(key), cljEq))
var newVals: seq[CljVal] = @[]
for i in 0..<m.mapKeys.len:
if m.mapKeys[i] != key:
newKeys.add(m.mapKeys[i])
newVals.add(m.mapVals[i])
cljMap(newKeys, newVals)
proc cljContains*(m: CljVal, key: CljVal): bool = proc cljContains*(m: CljVal, key: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return false if m.isNil: return cljBool(false)
for i in 0..<m.mapKeys.len: case m.kind
if m.mapKeys[i] == key: of ckMap: cljBool(pmapContains(m.mapData, key, hash(key), cljEq))
return true of ckSet: cljBool(pmapContains(m.setData, key, hash(key), cljEq))
false else: cljBool(false)
proc cljKeys*(m: CljVal): CljVal = proc cljKeys*(m: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return cljList(@[]) if m.isNil or m.kind != ckMap: return cljList(@[])
cljList(m.mapKeys) cljList(pmapKeys(m.mapData))
proc cljVals*(m: CljVal): CljVal = proc cljVals*(m: CljVal): CljVal =
if m.isNil or m.kind != ckMap: return cljList(@[]) if m.isNil or m.kind != ckMap: return cljList(@[])
cljList(m.mapVals) cljList(pmapVals(m.mapData))
proc cljSelectKeys*(m: CljVal, keys: seq[CljVal]): CljVal = proc cljSelectKeys*(m: CljVal, keys: seq[CljVal]): CljVal =
if m.isNil or m.kind != ckMap: return cljMap(@[], @[]) if m.isNil or m.kind != ckMap: return cljMap(@[], @[])
var newKeys: seq[CljVal] = @[] var result = newPersistentMap[CljVal, CljVal]()
var newVals: seq[CljVal] = @[]
for key in keys: for key in keys:
for i in 0..<m.mapKeys.len: let v = pmapGet(m.mapData, key, cljNil(), hash(key), cljEq)
if m.mapKeys[i] == key: if not cljIsNil(v):
newKeys.add(key) result = pmapAssoc(result, key, v, hash(key), cljEq)
newVals.add(m.mapVals[i]) CljVal(kind: ckMap, mapData: result)
break
cljMap(newKeys, newVals)
proc cljMerge*(args: seq[CljVal]): CljVal = proc cljMerge*(args: seq[CljVal]): CljVal =
var keys: seq[CljVal] = @[] var result = newPersistentMap[CljVal, CljVal]()
var vals: seq[CljVal] = @[]
for m in args: for m in args:
if not m.isNil and m.kind == ckMap: if not m.isNil and m.kind == ckMap:
for i in 0..<m.mapKeys.len: for (k, v) in pmapItems(m.mapData):
var found = false result = pmapAssoc(result, k, v, hash(k), cljEq)
for j in 0..<keys.len: CljVal(kind: ckMap, mapData: result)
if keys[j] == m.mapKeys[i]:
vals[j] = m.mapVals[i]
found = true
break
if not found:
keys.add(m.mapKeys[i])
vals.add(m.mapVals[i])
cljMap(keys, vals)
# ---- Higher-order functions ---- # ---- Higher-order functions ----
@@ -912,6 +919,7 @@ proc cljType*(v: CljVal): CljVal =
of ckList: cljKeyword("list") of ckList: cljKeyword("list")
of ckVector: cljKeyword("vector") of ckVector: cljKeyword("vector")
of ckMap: cljKeyword("map") of ckMap: cljKeyword("map")
of ckSet: cljKeyword("set")
of ckFn: cljKeyword("function") of ckFn: cljKeyword("function")
of ckAtom: cljKeyword("atom") of ckAtom: cljKeyword("atom")
@@ -989,49 +997,22 @@ proc cljInto*(to: CljVal, src: CljVal): CljVal =
else: to else: to
of ckMap: of ckMap:
if src.kind == ckMap: if src.kind == ckMap:
var keys = to.mapKeys var result = to.mapData
var vals = to.mapVals for (k, v) in pmapItems(src.mapData):
for i in 0..<src.mapKeys.len: result = pmapAssoc(result, k, v, hash(k), cljEq)
var found = false CljVal(kind: ckMap, mapData: result)
for j in 0..<keys.len:
if keys[j] == src.mapKeys[i]:
vals[j] = src.mapVals[i]
found = true
break
if not found:
keys.add(src.mapKeys[i])
vals.add(src.mapVals[i])
cljMap(keys, vals)
elif src.kind == ckVector: elif src.kind == ckVector:
var keys = to.mapKeys var result = to.mapData
var vals = to.mapVals
for pair in src.vecData.items: for pair in src.vecData.items:
if pair.kind == ckVector and pair.vecData.count == 2: if pair.kind == ckVector and pair.vecData.count == 2:
var found = false result = pmapAssoc(result, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
for j in 0..<keys.len: CljVal(kind: ckMap, mapData: result)
if keys[j] == pvecNth(pair.vecData, 0): elif src.kind == ckList:
vals[j] = pvecNth(pair.vecData, 1) var result = to.mapData
found = true for pair in src.listItems:
break
if not found:
keys.add(pvecNth(pair.vecData, 0))
vals.add(pvecNth(pair.vecData, 1))
cljMap(keys, vals)
elif src.kind == ckVector:
var keys = to.mapKeys
var vals = to.mapVals
for pair in src.vecData.items:
if pair.kind == ckVector and pair.vecData.count == 2: if pair.kind == ckVector and pair.vecData.count == 2:
var found = false result = pmapAssoc(result, pvecNth(pair.vecData, 0), pvecNth(pair.vecData, 1), hash(pvecNth(pair.vecData, 0)), cljEq)
for j in 0..<keys.len: CljVal(kind: ckMap, mapData: result)
if keys[j] == pvecNth(pair.vecData, 0):
vals[j] = pvecNth(pair.vecData, 1)
found = true
break
if not found:
keys.add(pvecNth(pair.vecData, 0))
vals.add(pvecNth(pair.vecData, 1))
cljMap(keys, vals)
else: to else: to
else: to else: to
+19
View File
@@ -108,6 +108,7 @@ proc runtimeName(op: string): string =
of "git/push": "cljGitPush" of "git/push": "cljGitPush"
of "git/diff": "cljGitDiff" of "git/diff": "cljGitDiff"
of "git/log": "cljGitLog" of "git/log": "cljGitLog"
of "disj": "cljDisj"
else: "" else: ""
proc emitArgs(args: seq[CljVal]): string = proc emitArgs(args: seq[CljVal]): string =
@@ -759,6 +760,24 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
raise newException(EmitterError, "set! target must be a symbol") raise newException(EmitterError, "set! target must be a symbol")
return sp & mangleName(target.symName) & " = " & emitExpr(items[2], 0) return sp & mangleName(target.symName) & " = " & emitExpr(items[2], 0)
of "set":
if items.len != 2:
raise newException(EmitterError, "set requires exactly 1 argument")
let arg = items[1]
case arg.kind
of ckVector:
var parts: seq[string] = @[]
for item in arg.items:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckList:
var parts: seq[string] = @[]
for item in arg.items:
parts.add(emitExpr(item, 0))
return sp & "cljSet(@[" & parts.join(", ") & "])"
else:
return sp & "cljSet(@[" & emitExpr(arg, 0) & "])"
of "range": of "range":
if items.len < 1 or items.len > 3: if items.len < 1 or items.len > 3:
raise newException(EmitterError, "range requires 0, 1, or 2 arguments") raise newException(EmitterError, "range requires 0, 1, or 2 arguments")
+153
View File
@@ -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"