Persistent vectors (32-way trie) and HAMT maps/sets
Vectors and maps were copy-on-write seqs: every assoc/conj rebuilt the whole collection, and set conj re-scanned for duplicates on top of that, so building an n-element set was O(n^3). Replace both with the real structures: - PVec: Clojure's PersistentVector — a 32-way trie with a tail buffer. conj/assoc/nth are O(log32 n) and copy a handful of 32-wide nodes. - PMap: a HAMT, used for both maps and sets (a set maps each key to itself). Collisions fall back to a linear bucket once the 32-bit hash runs out of index bits. - hashValue, consistent with equals: ints and floats that compare equal hash alike, lists and vectors hash alike, sets and maps hash by xor. Maps and sets keep insertion order — each entry carries an `ord` stamp and iteration sorts by it — so printing, keys and vals stay deterministic the way Clojure's small array-maps are. Value grows `xs`/`vec`/`m` fields in place of `items`/`pairs`; `items`, `pairs` and a new `count` are procs over the new representation, so the compiler and the parts of core that only read collections are unchanged. n = 8000, -d:release: vector conj 489ms -> 29ms, map assoc 448ms -> 64ms, set conj 526607ms -> 102ms, map get 3031ms -> 50ms. fib(30) is unchanged. Also: a justfile, examples/persistent.clj as a regression test with recorded output, examples/persistent-bench.clj behind `just bench`, and a fix for hyphenated .clj filenames (the emitted Nim module name must be an identifier, so it is now sanitised independently of the binary name). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86373e5 parent: 980e0ef modified
.gitignore +2 -1 | @@ -1,3 +1,4 @@ | ||
| 1 | 1 | bin/ |
| 2 | 2 | nimcache/ |
| 3 | -*.o | |
| 3 | +.clj-kondo/.cache/ | |
| 4 | +.lsp/ | |
| @@ -1,3 +1,4 @@ | |||
| 1 | bin/ | 1 | bin/ |
| 2 | nimcache/ | 2 | nimcache/ |
| 3 | -*.o | 3 | +.clj-kondo/.cache/ |
| 4 | +.lsp/ | ||
modified
README.md +30 -4 | @@ -15,13 +15,16 @@ nim c --hints:off -o:bin/clonim src/clonim.nim # build the compiler | ||
| 15 | 15 | ./bin/clonim emit examples/tour.clj # show the generated Nim |
| 16 | 16 | ``` |
| 17 | 17 | |
| 18 | +There is a `justfile` too: `just build`, `just test`, `just bench`, | |
| 19 | +`just run <file>`, `just emit <file>`, `just accept` (re-record expectations). | |
| 20 | + | |
| 18 | 21 | ## Pipeline |
| 19 | 22 | |
| 20 | 23 | | stage | file | what it does | |
| 21 | 24 | |---|---|---| |
| 22 | 25 | | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure | |
| 23 | 26 | | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements | |
| 24 | -| runtime | `src/runtime.nim` | the `Value` tagged union, equality, printing, var cells, `call` | | |
| 27 | +| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call` | | |
| 25 | 28 | | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures | |
| 26 | 29 | | driver | `src/clonim.nim` | shells out to `nim c`, times each phase | |
| 27 | 30 | |
| @@ -41,6 +44,28 @@ resolved in the program prelude, so a call site is a pointer deref rather than a | ||
| 41 | 44 | hash lookup, while `def` can still rebind it later. Worth ~20% on call-heavy |
| 42 | 45 | code. |
| 43 | 46 | |
| 47 | +**Collections share structure.** Vectors are 32-way tries with a tail buffer | |
| 48 | +(Clojure's `PersistentVector`); maps and sets are HAMTs. An `assoc` copies a | |
| 49 | +handful of 32-wide nodes and points at the rest of the old value, so it is | |
| 50 | +O(log₃₂ n) and the original stays valid — which is what makes the persistent | |
| 51 | +part of persistent data structures real rather than a spelling of "copy". | |
| 52 | + | |
| 53 | +Maps and sets also keep insertion order: each entry carries an `ord` stamp and | |
| 54 | +iteration sorts by it, so printing, `keys` and `vals` are deterministic the way | |
| 55 | +Clojure's small array-maps are, without giving up hashed lookup. | |
| 56 | + | |
| 57 | +`examples/persistent-bench.clj` (`just bench`), n = 8000, `-d:release`: | |
| 58 | + | |
| 59 | +| operation | copy-on-write `seq` | trie / HAMT | | |
| 60 | +|---|---:|---:| | |
| 61 | +| 8000 × `conj` onto a vector | 489 ms | 29 ms | | |
| 62 | +| 8000 × `assoc` onto a map | 448 ms | 64 ms | | |
| 63 | +| 8000 × `conj` onto a set | 526 607 ms | 102 ms | | |
| 64 | +| 8000 × `get` from a map | 3031 ms | 50 ms | | |
| 65 | + | |
| 66 | +The set column is the honest shape of the old representation: `conj` rebuilt the | |
| 67 | +whole set and re-scanned it for duplicates, so building one was O(n³). | |
| 68 | + | |
| 44 | 69 | ## What works |
| 45 | 70 | |
| 46 | 71 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) |
| @@ -50,7 +75,7 @@ code. | ||
| 50 | 75 | Destructuring: sequential `[a b & rest]` and associative `{:keys [x y]}` in `let`. |
| 51 | 76 | |
| 52 | 77 | Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set — |
| 53 | -with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | |
| 78 | +persistent, with structural equality, hashing, and Clojure-shaped printing. Atoms, closures, `comp`, | |
| 54 | 79 | `partial`, `juxt`, the usual seq library, `clojure.string/*`. |
| 55 | 80 | |
| 56 | 81 | ## What doesn't (yet) |
| @@ -59,8 +84,9 @@ with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | ||
| 59 | 84 | macros need the compiler to be able to *evaluate* code at compile time — |
| 60 | 85 | the honest fix is to bootstrap clonim in itself, or embed an interpreter. |
| 61 | 86 | - **Laziness.** `map`/`filter`/`range` are eager. Infinite seqs will hang. |
| 62 | -- **Persistent data structures.** Vectors and maps are copy-on-write `seq`s, so | |
| 63 | - `assoc` is O(n), not O(log₃₂ n). This is the first thing to replace. | |
| 87 | +- **Laziness for the seq library.** Most of `core` still materialises a | |
| 88 | + `seq[Value]` on the way in and out, so even with persistent vectors, `map` | |
| 89 | + over a big collection allocates twice. | |
| 64 | 90 | - Protocols/records, namespaces (`ns` is parsed and ignored), refs/agents, |
| 65 | 91 | `#()` literals, syntax-quote, transducers, Nim interop. |
| 66 | 92 | |
| @@ -15,13 +15,16 @@ nim c --hints:off -o:bin/clonim src/clonim.nim # build the compiler | |||
| 15 | ./bin/clonim emit examples/tour.clj # show the generated Nim | 15 | ./bin/clonim emit examples/tour.clj # show the generated Nim |
| 16 | ``` | 16 | ``` |
| 17 | 17 | ||
| 18 | +There is a `justfile` too: `just build`, `just test`, `just bench`, | ||
| 19 | +`just run <file>`, `just emit <file>`, `just accept` (re-record expectations). | ||
| 20 | + | ||
| 18 | ## Pipeline | 21 | ## Pipeline |
| 19 | 22 | ||
| 20 | | stage | file | what it does | | 23 | | stage | file | what it does | |
| 21 | |---|---|---| | 24 | |---|---|---| |
| 22 | | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure | | 25 | | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure | |
| 23 | | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements | | 26 | | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements | |
| 24 | -| runtime | `src/runtime.nim` | the `Value` tagged union, equality, printing, var cells, `call` | | 27 | +| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call` | |
| 25 | | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures | | 28 | | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures | |
| 26 | | driver | `src/clonim.nim` | shells out to `nim c`, times each phase | | 29 | | driver | `src/clonim.nim` | shells out to `nim c`, times each phase | |
| 27 | 30 | ||
| @@ -41,6 +44,28 @@ resolved in the program prelude, so a call site is a pointer deref rather than a | |||
| 41 | hash lookup, while `def` can still rebind it later. Worth ~20% on call-heavy | 44 | hash lookup, while `def` can still rebind it later. Worth ~20% on call-heavy |
| 42 | code. | 45 | code. |
| 43 | 46 | ||
| 47 | +**Collections share structure.** Vectors are 32-way tries with a tail buffer | ||
| 48 | +(Clojure's `PersistentVector`); maps and sets are HAMTs. An `assoc` copies a | ||
| 49 | +handful of 32-wide nodes and points at the rest of the old value, so it is | ||
| 50 | +O(log₃₂ n) and the original stays valid — which is what makes the persistent | ||
| 51 | +part of persistent data structures real rather than a spelling of "copy". | ||
| 52 | + | ||
| 53 | +Maps and sets also keep insertion order: each entry carries an `ord` stamp and | ||
| 54 | +iteration sorts by it, so printing, `keys` and `vals` are deterministic the way | ||
| 55 | +Clojure's small array-maps are, without giving up hashed lookup. | ||
| 56 | + | ||
| 57 | +`examples/persistent-bench.clj` (`just bench`), n = 8000, `-d:release`: | ||
| 58 | + | ||
| 59 | +| operation | copy-on-write `seq` | trie / HAMT | | ||
| 60 | +|---|---:|---:| | ||
| 61 | +| 8000 × `conj` onto a vector | 489 ms | 29 ms | | ||
| 62 | +| 8000 × `assoc` onto a map | 448 ms | 64 ms | | ||
| 63 | +| 8000 × `conj` onto a set | 526 607 ms | 102 ms | | ||
| 64 | +| 8000 × `get` from a map | 3031 ms | 50 ms | | ||
| 65 | + | ||
| 66 | +The set column is the honest shape of the old representation: `conj` rebuilt the | ||
| 67 | +whole set and re-scanned it for duplicates, so building one was O(n³). | ||
| 68 | + | ||
| 44 | ## What works | 69 | ## What works |
| 45 | 70 | ||
| 46 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) | 71 | `def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) |
| @@ -50,7 +75,7 @@ code. | |||
| 50 | Destructuring: sequential `[a b & rest]` and associative `{:keys [x y]}` in `let`. | 75 | Destructuring: sequential `[a b & rest]` and associative `{:keys [x y]}` in `let`. |
| 51 | 76 | ||
| 52 | Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set — | 77 | Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set — |
| 53 | -with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | 78 | +persistent, with structural equality, hashing, and Clojure-shaped printing. Atoms, closures, `comp`, |
| 54 | `partial`, `juxt`, the usual seq library, `clojure.string/*`. | 79 | `partial`, `juxt`, the usual seq library, `clojure.string/*`. |
| 55 | 80 | ||
| 56 | ## What doesn't (yet) | 81 | ## What doesn't (yet) |
| @@ -59,8 +84,9 @@ with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | |||
| 59 | macros need the compiler to be able to *evaluate* code at compile time — | 84 | macros need the compiler to be able to *evaluate* code at compile time — |
| 60 | the honest fix is to bootstrap clonim in itself, or embed an interpreter. | 85 | the honest fix is to bootstrap clonim in itself, or embed an interpreter. |
| 61 | - **Laziness.** `map`/`filter`/`range` are eager. Infinite seqs will hang. | 86 | - **Laziness.** `map`/`filter`/`range` are eager. Infinite seqs will hang. |
| 62 | -- **Persistent data structures.** Vectors and maps are copy-on-write `seq`s, so | 87 | +- **Laziness for the seq library.** Most of `core` still materialises a |
| 63 | - `assoc` is O(n), not O(log₃₂ n). This is the first thing to replace. | 88 | + `seq[Value]` on the way in and out, so even with persistent vectors, `map` |
| 89 | + over a big collection allocates twice. | ||
| 64 | - Protocols/records, namespaces (`ns` is parsed and ignored), refs/agents, | 90 | - Protocols/records, namespaces (`ns` is parsed and ignored), refs/agents, |
| 65 | `#()` literals, syntax-quote, transducers, Nim interop. | 91 | `#()` literals, syntax-quote, transducers, Nim interop. |
| 66 | 92 | ||
added
examples/persistent-bench.clj +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +(def n 20000) | |
| 2 | +(let [t0 (now-ms) | |
| 3 | + v (reduce conj [] (range n)) | |
| 4 | + t1 (now-ms) | |
| 5 | + m (reduce (fn [acc i] (assoc acc i i)) {} (range n)) | |
| 6 | + t2 (now-ms) | |
| 7 | + s (reduce conj #{} (range n)) | |
| 8 | + t3 (now-ms) | |
| 9 | + g (reduce (fn [acc i] (+ acc (get m i))) 0 (range n)) | |
| 10 | + t4 (now-ms)] | |
| 11 | + (println "vector conj x" n ":" (- t1 t0) "ms") | |
| 12 | + (println "map assoc x" n ":" (- t2 t1) "ms") | |
| 13 | + (println "set conj x" n ":" (- t3 t2) "ms") | |
| 14 | + (println "map get x" n ":" (- t4 t3) "ms sum" g) | |
| 15 | + (println "counts" (count v) (count m) (count s))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +(def n 20000) | ||
| 2 | +(let [t0 (now-ms) | ||
| 3 | + v (reduce conj [] (range n)) | ||
| 4 | + t1 (now-ms) | ||
| 5 | + m (reduce (fn [acc i] (assoc acc i i)) {} (range n)) | ||
| 6 | + t2 (now-ms) | ||
| 7 | + s (reduce conj #{} (range n)) | ||
| 8 | + t3 (now-ms) | ||
| 9 | + g (reduce (fn [acc i] (+ acc (get m i))) 0 (range n)) | ||
| 10 | + t4 (now-ms)] | ||
| 11 | + (println "vector conj x" n ":" (- t1 t0) "ms") | ||
| 12 | + (println "map assoc x" n ":" (- t2 t1) "ms") | ||
| 13 | + (println "set conj x" n ":" (- t3 t2) "ms") | ||
| 14 | + (println "map get x" n ":" (- t4 t3) "ms sum" g) | ||
| 15 | + (println "counts" (count v) (count m) (count s))) | ||
added
examples/persistent.clj +37 -0 | new file mode 100644 | ||
| @@ -0,0 +1,37 @@ | ||
| 1 | +;; build a big vector by conj, then read it back | |
| 2 | +(def n 5000) | |
| 3 | +(def v (reduce conj [] (range n))) | |
| 4 | +(println "count" (count v)) | |
| 5 | +(println "nth" (nth v 0) (nth v 33) (nth v 1024) (nth v (dec n))) | |
| 6 | +(println "sum" (reduce + 0 v)) | |
| 7 | +(def v2 (assoc v 1234 :x)) | |
| 8 | +(println "assoc" (nth v2 1234) (nth v 1234) (count v2)) | |
| 9 | +(println "last" (last v) (first v)) | |
| 10 | +(println "eq" (= v (reduce conj [] (range n)))) | |
| 11 | + | |
| 12 | +;; big map | |
| 13 | +(def m (reduce (fn [acc i] (assoc acc i (* i i))) {} (range n))) | |
| 14 | +(println "mcount" (count m) (get m 0) (get m 4999) (get m 5000)) | |
| 15 | +(def m2 (dissoc m 100)) | |
| 16 | +(println "dissoc" (count m2) (get m2 100) (get m 100)) | |
| 17 | +(println "keys-sum" (reduce + 0 (keys m))) | |
| 18 | +(println "vals-sum" (reduce + 0 (vals m))) | |
| 19 | +(println "meq" (= m (reduce (fn [acc i] (assoc acc i (* i i))) {} (range n)))) | |
| 20 | + | |
| 21 | +;; sets | |
| 22 | +(def s (set (range n))) | |
| 23 | +(println "scount" (count s) (contains? s 4999) (contains? s 5000)) | |
| 24 | +(println "sconj" (count (conj s 5000)) (count (conj s 0))) | |
| 25 | + | |
| 26 | +;; ordering + mixed keys | |
| 27 | +(def om {:b 1 :a 2 "c" 3 4 5 [1 2] 6}) | |
| 28 | +(println om) | |
| 29 | +(println (keys om)) | |
| 30 | +(println (assoc om :a 99)) | |
| 31 | +(println (get om [1 2]) (get om 4) (get om "c")) | |
| 32 | +(println (= {:a 1 :b 2} {:b 2 :a 1})) | |
| 33 | +(println (= #{1 2 3} #{3 2 1})) | |
| 34 | +(println (get {1 :int} 1.0) (get {1.0 :flt} 1)) | |
| 35 | +(println (frequencies [1 1 2 3 3 3])) | |
| 36 | +(println (group-by even? (range 10))) | |
| 37 | +(println (update {:a 1} :a inc)) | |
| new file mode 100644 | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | +;; build a big vector by conj, then read it back | ||
| 2 | +(def n 5000) | ||
| 3 | +(def v (reduce conj [] (range n))) | ||
| 4 | +(println "count" (count v)) | ||
| 5 | +(println "nth" (nth v 0) (nth v 33) (nth v 1024) (nth v (dec n))) | ||
| 6 | +(println "sum" (reduce + 0 v)) | ||
| 7 | +(def v2 (assoc v 1234 :x)) | ||
| 8 | +(println "assoc" (nth v2 1234) (nth v 1234) (count v2)) | ||
| 9 | +(println "last" (last v) (first v)) | ||
| 10 | +(println "eq" (= v (reduce conj [] (range n)))) | ||
| 11 | + | ||
| 12 | +;; big map | ||
| 13 | +(def m (reduce (fn [acc i] (assoc acc i (* i i))) {} (range n))) | ||
| 14 | +(println "mcount" (count m) (get m 0) (get m 4999) (get m 5000)) | ||
| 15 | +(def m2 (dissoc m 100)) | ||
| 16 | +(println "dissoc" (count m2) (get m2 100) (get m 100)) | ||
| 17 | +(println "keys-sum" (reduce + 0 (keys m))) | ||
| 18 | +(println "vals-sum" (reduce + 0 (vals m))) | ||
| 19 | +(println "meq" (= m (reduce (fn [acc i] (assoc acc i (* i i))) {} (range n)))) | ||
| 20 | + | ||
| 21 | +;; sets | ||
| 22 | +(def s (set (range n))) | ||
| 23 | +(println "scount" (count s) (contains? s 4999) (contains? s 5000)) | ||
| 24 | +(println "sconj" (count (conj s 5000)) (count (conj s 0))) | ||
| 25 | + | ||
| 26 | +;; ordering + mixed keys | ||
| 27 | +(def om {:b 1 :a 2 "c" 3 4 5 [1 2] 6}) | ||
| 28 | +(println om) | ||
| 29 | +(println (keys om)) | ||
| 30 | +(println (assoc om :a 99)) | ||
| 31 | +(println (get om [1 2]) (get om 4) (get om "c")) | ||
| 32 | +(println (= {:a 1 :b 2} {:b 2 :a 1})) | ||
| 33 | +(println (= #{1 2 3} #{3 2 1})) | ||
| 34 | +(println (get {1 :int} 1.0) (get {1.0 :flt} 1)) | ||
| 35 | +(println (frequencies [1 1 2 3 3 3])) | ||
| 36 | +(println (group-by even? (range 10))) | ||
| 37 | +(println (update {:a 1} :a inc)) | ||
added
justfile +46 -0 | new file mode 100644 | ||
| @@ -0,0 +1,46 @@ | ||
| 1 | +# clonim — a Clojure compiler hosted on Nim | |
| 2 | + | |
| 3 | +bin := "bin/clonim" | |
| 4 | + | |
| 5 | +# Build the compiler | |
| 6 | +build: | |
| 7 | + nim c --hints:off --warnings:off -o:{{bin}} src/clonim.nim | |
| 8 | + | |
| 9 | +# Build with optimisations on (compiler and, via -d:release, the programs it emits) | |
| 10 | +release: | |
| 11 | + nim c -d:release --hints:off --warnings:off -o:{{bin}} src/clonim.nim | |
| 12 | + | |
| 13 | +# Run every example against tests/<name>.expected | |
| 14 | +test: build | |
| 15 | + ./run-tests.sh | |
| 16 | + | |
| 17 | +# Compile and run a .clj file, e.g. `just run examples/tour.clj` | |
| 18 | +run file: build | |
| 19 | + ./{{bin}} run {{file}} | |
| 20 | + | |
| 21 | +# Emit the generated Nim for a .clj file without compiling it | |
| 22 | +emit file: build | |
| 23 | + ./{{bin}} emit {{file}} | |
| 24 | + | |
| 25 | +# Compile a .clj file to a native binary | |
| 26 | +compile file: build | |
| 27 | + ./{{bin}} build {{file}} | |
| 28 | + | |
| 29 | +# Persistent-collection benchmark: conj/assoc/get at scale | |
| 30 | +bench: release | |
| 31 | + ./{{bin}} run examples/persistent-bench.clj -d | |
| 32 | + | |
| 33 | +# Re-record tests/<name>.expected from current output | |
| 34 | +accept: build | |
| 35 | + #!/usr/bin/env bash | |
| 36 | + set -euo pipefail | |
| 37 | + for f in examples/*.clj; do | |
| 38 | + name=$(basename "$f" .clj) | |
| 39 | + [ -f "tests/$name.expected" ] || continue | |
| 40 | + ./{{bin}} run "$f" > "tests/$name.expected" 2>&1 | |
| 41 | + echo "recorded $name" | |
| 42 | + done | |
| 43 | + | |
| 44 | +# Remove build output | |
| 45 | +clean: | |
| 46 | + rm -rf bin nimcache | |
| new file mode 100644 | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | +# clonim — a Clojure compiler hosted on Nim | ||
| 2 | + | ||
| 3 | +bin := "bin/clonim" | ||
| 4 | + | ||
| 5 | +# Build the compiler | ||
| 6 | +build: | ||
| 7 | + nim c --hints:off --warnings:off -o:{{bin}} src/clonim.nim | ||
| 8 | + | ||
| 9 | +# Build with optimisations on (compiler and, via -d:release, the programs it emits) | ||
| 10 | +release: | ||
| 11 | + nim c -d:release --hints:off --warnings:off -o:{{bin}} src/clonim.nim | ||
| 12 | + | ||
| 13 | +# Run every example against tests/<name>.expected | ||
| 14 | +test: build | ||
| 15 | + ./run-tests.sh | ||
| 16 | + | ||
| 17 | +# Compile and run a .clj file, e.g. `just run examples/tour.clj` | ||
| 18 | +run file: build | ||
| 19 | + ./{{bin}} run {{file}} | ||
| 20 | + | ||
| 21 | +# Emit the generated Nim for a .clj file without compiling it | ||
| 22 | +emit file: build | ||
| 23 | + ./{{bin}} emit {{file}} | ||
| 24 | + | ||
| 25 | +# Compile a .clj file to a native binary | ||
| 26 | +compile file: build | ||
| 27 | + ./{{bin}} build {{file}} | ||
| 28 | + | ||
| 29 | +# Persistent-collection benchmark: conj/assoc/get at scale | ||
| 30 | +bench: release | ||
| 31 | + ./{{bin}} run examples/persistent-bench.clj -d | ||
| 32 | + | ||
| 33 | +# Re-record tests/<name>.expected from current output | ||
| 34 | +accept: build | ||
| 35 | + #!/usr/bin/env bash | ||
| 36 | + set -euo pipefail | ||
| 37 | + for f in examples/*.clj; do | ||
| 38 | + name=$(basename "$f" .clj) | ||
| 39 | + [ -f "tests/$name.expected" ] || continue | ||
| 40 | + ./{{bin}} run "$f" > "tests/$name.expected" 2>&1 | ||
| 41 | + echo "recorded $name" | ||
| 42 | + done | ||
| 43 | + | ||
| 44 | +# Remove build output | ||
| 45 | +clean: | ||
| 46 | + rm -rf bin nimcache | ||
modified
src/clonim.nim +9 -3 | @@ -65,14 +65,20 @@ proc main() = | ||
| 65 | 65 | return |
| 66 | 66 | |
| 67 | 67 | let stem = file.splitFile.name |
| 68 | - let work = getTempDir() / ("clonim-" & stem & "-" & $getCurrentProcessId()) | |
| 68 | + # Nim module names must be identifiers, but .clj filenames are usually | |
| 69 | + # hyphenated; the binary keeps the original stem, the module doesn't. | |
| 70 | + var modName = "" | |
| 71 | + for ch in stem: | |
| 72 | + modName.add (if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: ch else: '_') | |
| 73 | + if modName.len == 0 or modName[0] in {'0'..'9'}: modName = "m" & modName | |
| 74 | + let work = getTempDir() / ("clonim-" & modName & "-" & $getCurrentProcessId()) | |
| 69 | 75 | createDir(work) |
| 70 | 76 | defer: removeDir(work) |
| 71 | - let nimFile = work / (stem & ".nim") | |
| 77 | + let nimFile = work / (modName & ".nim") | |
| 72 | 78 | writeFile(nimFile, nimSrc) |
| 73 | 79 | |
| 74 | 80 | if outBin.len == 0: |
| 75 | - outBin = (if cmd == "build": stem else: work / stem) | |
| 81 | + outBin = (if cmd == "build": stem else: work / modName) | |
| 76 | 82 | outBin = outBin.absolutePath |
| 77 | 83 | |
| 78 | 84 | var nimCmd = @["nim", "c", "--hints:off", "--warnings:off", |
| @@ -65,14 +65,20 @@ proc main() = | |||
| 65 | return | 65 | return |
| 66 | 66 | ||
| 67 | let stem = file.splitFile.name | 67 | let stem = file.splitFile.name |
| 68 | - let work = getTempDir() / ("clonim-" & stem & "-" & $getCurrentProcessId()) | 68 | + # Nim module names must be identifiers, but .clj filenames are usually |
| 69 | + # hyphenated; the binary keeps the original stem, the module doesn't. | ||
| 70 | + var modName = "" | ||
| 71 | + for ch in stem: | ||
| 72 | + modName.add (if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: ch else: '_') | ||
| 73 | + if modName.len == 0 or modName[0] in {'0'..'9'}: modName = "m" & modName | ||
| 74 | + let work = getTempDir() / ("clonim-" & modName & "-" & $getCurrentProcessId()) | ||
| 69 | createDir(work) | 75 | createDir(work) |
| 70 | defer: removeDir(work) | 76 | defer: removeDir(work) |
| 71 | - let nimFile = work / (stem & ".nim") | 77 | + let nimFile = work / (modName & ".nim") |
| 72 | writeFile(nimFile, nimSrc) | 78 | writeFile(nimFile, nimSrc) |
| 73 | 79 | ||
| 74 | if outBin.len == 0: | 80 | if outBin.len == 0: |
| 75 | - outBin = (if cmd == "build": stem else: work / stem) | 81 | + outBin = (if cmd == "build": stem else: work / modName) |
| 76 | outBin = outBin.absolutePath | 82 | outBin = outBin.absolutePath |
| 77 | 83 | ||
| 78 | var nimCmd = @["nim", "c", "--hints:off", "--warnings:off", | 84 | var nimCmd = @["nim", "c", "--hints:off", "--warnings:off", |
modified
src/core.nim +49 -67 | @@ -43,18 +43,16 @@ proc cmpChain(args: seq[Value], ok: proc (c: int): bool): Value = | ||
| 43 | 43 | proc getIn(coll, k, dflt: Value): Value = |
| 44 | 44 | if coll.isNil or coll.kind == kNil: return dflt |
| 45 | 45 | case coll.kind |
| 46 | - of kMap: | |
| 47 | - for (kk, vv) in coll.pairs: | |
| 48 | - if equals(kk, k): return vv | |
| 49 | - dflt | |
| 50 | - of kVector, kList: | |
| 46 | + of kMap: mapGet(coll.m, k, dflt) | |
| 47 | + of kSet: mapGet(coll.m, k, dflt) | |
| 48 | + of kVector: | |
| 51 | 49 | if k.kind != kInt: return dflt |
| 52 | 50 | let i = int(k.i) |
| 53 | - if i < 0 or i >= coll.items.len: dflt else: coll.items[i] | |
| 54 | - of kSet: | |
| 55 | - for x in coll.items: | |
| 56 | - if equals(x, k): return x | |
| 57 | - dflt | |
| 51 | + if i < 0 or i >= coll.vec.cnt: dflt else: vecNth(coll.vec, i) | |
| 52 | + of kList: | |
| 53 | + if k.kind != kInt: return dflt | |
| 54 | + let i = int(k.i) | |
| 55 | + if i < 0 or i >= coll.xs.len: dflt else: coll.xs[i] | |
| 58 | 56 | of kStr: |
| 59 | 57 | if k.kind != kInt: return dflt |
| 60 | 58 | let i = int(k.i) |
| @@ -63,39 +61,29 @@ proc getIn(coll, k, dflt: Value): Value = | ||
| 63 | 61 | |
| 64 | 62 | proc assocOne(coll, k, v: Value): Value = |
| 65 | 63 | if coll.isNil or coll.kind == kNil: |
| 66 | - return Value(kind: kMap, pairs: @[(k, v)]) | |
| 64 | + return mkMapOf(mapAssoc(emptyPMap(), k, v)) | |
| 67 | 65 | case coll.kind |
| 68 | - of kMap: | |
| 69 | - var ps = coll.pairs | |
| 70 | - for i in 0 ..< ps.len: | |
| 71 | - if equals(ps[i][0], k): | |
| 72 | - ps[i] = (k, v) | |
| 73 | - return Value(kind: kMap, pairs: ps) | |
| 74 | - ps.add (k, v) | |
| 75 | - Value(kind: kMap, pairs: ps) | |
| 66 | + of kMap: mkMapOf(mapAssoc(coll.m, k, v)) | |
| 76 | 67 | of kVector: |
| 77 | 68 | if k.kind != kInt: err("Vector index must be an integer") |
| 78 | - var xs = coll.items | |
| 79 | - let i = int(k.i) | |
| 80 | - if i == xs.len: xs.add v | |
| 81 | - elif i >= 0 and i < xs.len: xs[i] = v | |
| 82 | - else: err("Index out of bounds: " & $i) | |
| 83 | - mkVector(xs) | |
| 69 | + mkVec(vecAssoc(coll.vec, int(k.i), v)) | |
| 84 | 70 | else: err("assoc not supported on " & prStr(coll)) |
| 85 | 71 | |
| 86 | 72 | proc conjOne(coll, x: Value): Value = |
| 87 | 73 | if coll.isNil or coll.kind == kNil: return mkList(@[x]) |
| 88 | 74 | case coll.kind |
| 89 | - of kVector: mkVector(coll.items & @[x]) | |
| 90 | - of kList: mkList(@[x] & coll.items) | |
| 91 | - of kSet: mkSet(coll.items & @[x]) | |
| 75 | + of kVector: mkVec(vecConj(coll.vec, x)) | |
| 76 | + of kList: mkList(@[x] & coll.xs) | |
| 77 | + of kSet: | |
| 78 | + (if mapContains(coll.m, x): coll else: mkSetOf(mapAssoc(coll.m, x, x))) | |
| 92 | 79 | of kMap: |
| 93 | - if x.kind in {kVector, kList} and x.items.len == 2: | |
| 94 | - assocOne(coll, x.items[0], x.items[1]) | |
| 80 | + let xs = items(x) | |
| 81 | + if x.kind in {kVector, kList} and xs.len == 2: | |
| 82 | + assocOne(coll, xs[0], xs[1]) | |
| 95 | 83 | elif x.kind == kMap: |
| 96 | - var m = coll | |
| 97 | - for (k, v) in x.pairs: m = assocOne(m, k, v) | |
| 98 | - m | |
| 84 | + var m = coll.m | |
| 85 | + for e in mapEntries(x.m): m = mapAssoc(m, e.key, e.val) | |
| 86 | + mkMapOf(m) | |
| 99 | 87 | else: err("conj on map needs a pair") |
| 100 | 88 | else: err("conj not supported on " & prStr(coll)) |
| 101 | 89 | |
| @@ -177,21 +165,14 @@ proc registerCore*() = | ||
| 177 | 165 | def "coll?", proc (a: seq[Value]): Value = |
| 178 | 166 | mkBool(a[0].kind in {kList, kVector, kMap, kSet}) |
| 179 | 167 | def "fn?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kFn) |
| 180 | - def "empty?", proc (a: seq[Value]): Value = mkBool(toSeq(a[0]).len == 0) | |
| 168 | + def "empty?", proc (a: seq[Value]): Value = mkBool(count(a[0]) == 0) | |
| 181 | 169 | def "contains?", proc (a: seq[Value]): Value = |
| 182 | 170 | let c = a[0] |
| 183 | 171 | if c.isNil or c.kind == kNil: return FalseV |
| 184 | 172 | case c.kind |
| 185 | - of kMap: | |
| 186 | - for (k, _) in c.pairs: | |
| 187 | - if equals(k, a[1]): return TrueV | |
| 188 | - FalseV | |
| 189 | - of kSet: | |
| 190 | - for x in c.items: | |
| 191 | - if equals(x, a[1]): return TrueV | |
| 192 | - FalseV | |
| 173 | + of kMap, kSet: mkBool(mapContains(c.m, a[1])) | |
| 193 | 174 | of kVector: |
| 194 | - mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.items.len) | |
| 175 | + mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.vec.cnt) | |
| 195 | 176 | else: FalseV |
| 196 | 177 | |
| 197 | 178 | # ---- strings / IO |
| @@ -253,11 +234,11 @@ proc registerCore*() = | ||
| 253 | 234 | def "list", proc (a: seq[Value]): Value = mkList(a) |
| 254 | 235 | def "vector", proc (a: seq[Value]): Value = mkVector(a) |
| 255 | 236 | def "hash-map", proc (a: seq[Value]): Value = |
| 256 | - var m: Value = Value(kind: kMap, pairs: @[]) | |
| 237 | + var m = emptyPMap() | |
| 257 | 238 | var i = 0 |
| 258 | 239 | while i + 1 < a.len: |
| 259 | - m = assocOne(m, a[i], a[i + 1]); i += 2 | |
| 260 | - m | |
| 240 | + m = mapAssoc(m, a[i], a[i + 1]); i += 2 | |
| 241 | + mkMapOf(m) | |
| 261 | 242 | def "hash-set", proc (a: seq[Value]): Value = mkSet(a) |
| 262 | 243 | def "set", proc (a: seq[Value]): Value = mkSet(toSeq(a[0])) |
| 263 | 244 | def "vec", proc (a: seq[Value]): Value = mkVector(toSeq(a[0])) |
| @@ -266,20 +247,22 @@ proc registerCore*() = | ||
| 266 | 247 | (if s.len == 0: NilV else: mkList(s)) |
| 267 | 248 | def "count", proc (a: seq[Value]): Value = |
| 268 | 249 | if a[0].isNil or a[0].kind == kNil: return mkInt(0) |
| 269 | - if a[0].kind == kStr: return mkInt(a[0].s.len) | |
| 270 | - if a[0].kind == kMap: return mkInt(a[0].pairs.len) | |
| 271 | - mkInt(toSeq(a[0]).len) | |
| 250 | + mkInt(count(a[0])) | |
| 272 | 251 | def "conj", proc (a: seq[Value]): Value = |
| 273 | 252 | result = a[0] |
| 274 | 253 | for i in 1 ..< a.len: result = conjOne(result, a[i]) |
| 275 | 254 | def "cons", proc (a: seq[Value]): Value = mkList(@[a[0]] & toSeq(a[1])) |
| 276 | 255 | def "first", proc (a: seq[Value]): Value = |
| 256 | + if a[0].kind == kVector: | |
| 257 | + return (if a[0].vec.cnt == 0: NilV else: vecNth(a[0].vec, 0)) | |
| 277 | 258 | let s = toSeq(a[0]) |
| 278 | 259 | (if s.len == 0: NilV else: s[0]) |
| 279 | 260 | def "second", proc (a: seq[Value]): Value = |
| 280 | 261 | let s = toSeq(a[0]) |
| 281 | 262 | (if s.len < 2: NilV else: s[1]) |
| 282 | 263 | def "last", proc (a: seq[Value]): Value = |
| 264 | + if a[0].kind == kVector: | |
| 265 | + return (if a[0].vec.cnt == 0: NilV else: vecNth(a[0].vec, a[0].vec.cnt - 1)) | |
| 283 | 266 | let s = toSeq(a[0]) |
| 284 | 267 | (if s.len == 0: NilV else: s[^1]) |
| 285 | 268 | def "rest", proc (a: seq[Value]): Value = |
| @@ -289,8 +272,13 @@ proc registerCore*() = | ||
| 289 | 272 | let s = toSeq(a[0]) |
| 290 | 273 | (if s.len <= 1: NilV else: mkList(s[1 .. ^1])) |
| 291 | 274 | def "nth", proc (a: seq[Value]): Value = |
| 292 | - let s = toSeq(a[0]) | |
| 293 | 275 | let i = int(intOf(a[1])) |
| 276 | + if a[0].kind == kVector: | |
| 277 | + # O(log32 n) straight through the trie, no intermediate seq | |
| 278 | + if i >= 0 and i < a[0].vec.cnt: return vecNth(a[0].vec, i) | |
| 279 | + if a.len > 2: return a[2] | |
| 280 | + err("Index out of bounds: " & $i) | |
| 281 | + let s = toSeq(a[0]) | |
| 294 | 282 | if i >= 0 and i < s.len: s[i] |
| 295 | 283 | elif a.len > 2: a[2] |
| 296 | 284 | else: err("Index out of bounds: " & $i) |
| @@ -307,23 +295,19 @@ proc registerCore*() = | ||
| 307 | 295 | while i + 1 < a.len: |
| 308 | 296 | result = assocOne(result, a[i], a[i + 1]); i += 2 |
| 309 | 297 | def "dissoc", proc (a: seq[Value]): Value = |
| 310 | - var ps = a[0].pairs | |
| 311 | - for i in 1 ..< a.len: | |
| 312 | - var keep: seq[(Value, Value)] = @[] | |
| 313 | - for (k, v) in ps: | |
| 314 | - if not equals(k, a[i]): keep.add (k, v) | |
| 315 | - ps = keep | |
| 316 | - Value(kind: kMap, pairs: ps) | |
| 298 | + var m = a[0].m | |
| 299 | + for i in 1 ..< a.len: m = mapDissoc(m, a[i]) | |
| 300 | + mkMapOf(m) | |
| 317 | 301 | def "update", proc (a: seq[Value]): Value = |
| 318 | 302 | let cur = getIn(a[0], a[1], NilV) |
| 319 | 303 | assocOne(a[0], a[1], call(a[2], @[cur] & a[3 .. ^1])) |
| 320 | 304 | def "keys", proc (a: seq[Value]): Value = |
| 321 | 305 | var r: seq[Value] = @[] |
| 322 | - for (k, _) in a[0].pairs: r.add k | |
| 306 | + for e in mapEntries(a[0].m): r.add e.key | |
| 323 | 307 | (if r.len == 0: NilV else: mkList(r)) |
| 324 | 308 | def "vals", proc (a: seq[Value]): Value = |
| 325 | 309 | var r: seq[Value] = @[] |
| 326 | - for (_, v) in a[0].pairs: r.add v | |
| 310 | + for e in mapEntries(a[0].m): r.add e.val | |
| 327 | 311 | (if r.len == 0: NilV else: mkList(r)) |
| 328 | 312 | def "reverse", proc (a: seq[Value]): Value = |
| 329 | 313 | var s = toSeq(a[0]) |
| @@ -484,18 +468,16 @@ proc registerCore*() = | ||
| 484 | 468 | r.add x |
| 485 | 469 | mkList(r) |
| 486 | 470 | def "group-by", proc (a: seq[Value]): Value = |
| 487 | - var m: Value = Value(kind: kMap, pairs: @[]) | |
| 471 | + var m = emptyPMap() | |
| 488 | 472 | for x in toSeq(a[1]): |
| 489 | 473 | let k = call(a[0], @[x]) |
| 490 | - let cur = getIn(m, k, mkVector(@[])) | |
| 491 | - m = assocOne(m, k, conjOne(cur, x)) | |
| 492 | - m | |
| 474 | + m = mapAssoc(m, k, conjOne(mapGet(m, k, mkVector(@[])), x)) | |
| 475 | + mkMapOf(m) | |
| 493 | 476 | def "frequencies", proc (a: seq[Value]): Value = |
| 494 | - var m: Value = Value(kind: kMap, pairs: @[]) | |
| 477 | + var m = emptyPMap() | |
| 495 | 478 | for x in toSeq(a[0]): |
| 496 | - let cur = getIn(m, x, mkInt(0)) | |
| 497 | - m = assocOne(m, x, mkInt(cur.i + 1)) | |
| 498 | - m | |
| 479 | + m = mapAssoc(m, x, mkInt(mapGet(m, x, mkInt(0)).i + 1)) | |
| 480 | + mkMapOf(m) | |
| 499 | 481 | def "identity", proc (a: seq[Value]): Value = a[0] |
| 500 | 482 | def "comp", proc (a: seq[Value]): Value = |
| 501 | 483 | let fs = a |
| @@ -43,18 +43,16 @@ proc cmpChain(args: seq[Value], ok: proc (c: int): bool): Value = | |||
| 43 | proc getIn(coll, k, dflt: Value): Value = | 43 | proc getIn(coll, k, dflt: Value): Value = |
| 44 | if coll.isNil or coll.kind == kNil: return dflt | 44 | if coll.isNil or coll.kind == kNil: return dflt |
| 45 | case coll.kind | 45 | case coll.kind |
| 46 | - of kMap: | 46 | + of kMap: mapGet(coll.m, k, dflt) |
| 47 | - for (kk, vv) in coll.pairs: | 47 | + of kSet: mapGet(coll.m, k, dflt) |
| 48 | - if equals(kk, k): return vv | 48 | + of kVector: |
| 49 | - dflt | ||
| 50 | - of kVector, kList: | ||
| 51 | if k.kind != kInt: return dflt | 49 | if k.kind != kInt: return dflt |
| 52 | let i = int(k.i) | 50 | let i = int(k.i) |
| 53 | - if i < 0 or i >= coll.items.len: dflt else: coll.items[i] | 51 | + if i < 0 or i >= coll.vec.cnt: dflt else: vecNth(coll.vec, i) |
| 54 | - of kSet: | 52 | + of kList: |
| 55 | - for x in coll.items: | 53 | + if k.kind != kInt: return dflt |
| 56 | - if equals(x, k): return x | 54 | + let i = int(k.i) |
| 57 | - dflt | 55 | + if i < 0 or i >= coll.xs.len: dflt else: coll.xs[i] |
| 58 | of kStr: | 56 | of kStr: |
| 59 | if k.kind != kInt: return dflt | 57 | if k.kind != kInt: return dflt |
| 60 | let i = int(k.i) | 58 | let i = int(k.i) |
| @@ -63,39 +61,29 @@ proc getIn(coll, k, dflt: Value): Value = | |||
| 63 | 61 | ||
| 64 | proc assocOne(coll, k, v: Value): Value = | 62 | proc assocOne(coll, k, v: Value): Value = |
| 65 | if coll.isNil or coll.kind == kNil: | 63 | if coll.isNil or coll.kind == kNil: |
| 66 | - return Value(kind: kMap, pairs: @[(k, v)]) | 64 | + return mkMapOf(mapAssoc(emptyPMap(), k, v)) |
| 67 | case coll.kind | 65 | case coll.kind |
| 68 | - of kMap: | 66 | + of kMap: mkMapOf(mapAssoc(coll.m, k, v)) |
| 69 | - var ps = coll.pairs | ||
| 70 | - for i in 0 ..< ps.len: | ||
| 71 | - if equals(ps[i][0], k): | ||
| 72 | - ps[i] = (k, v) | ||
| 73 | - return Value(kind: kMap, pairs: ps) | ||
| 74 | - ps.add (k, v) | ||
| 75 | - Value(kind: kMap, pairs: ps) | ||
| 76 | of kVector: | 67 | of kVector: |
| 77 | if k.kind != kInt: err("Vector index must be an integer") | 68 | if k.kind != kInt: err("Vector index must be an integer") |
| 78 | - var xs = coll.items | 69 | + mkVec(vecAssoc(coll.vec, int(k.i), v)) |
| 79 | - let i = int(k.i) | ||
| 80 | - if i == xs.len: xs.add v | ||
| 81 | - elif i >= 0 and i < xs.len: xs[i] = v | ||
| 82 | - else: err("Index out of bounds: " & $i) | ||
| 83 | - mkVector(xs) | ||
| 84 | else: err("assoc not supported on " & prStr(coll)) | 70 | else: err("assoc not supported on " & prStr(coll)) |
| 85 | 71 | ||
| 86 | proc conjOne(coll, x: Value): Value = | 72 | proc conjOne(coll, x: Value): Value = |
| 87 | if coll.isNil or coll.kind == kNil: return mkList(@[x]) | 73 | if coll.isNil or coll.kind == kNil: return mkList(@[x]) |
| 88 | case coll.kind | 74 | case coll.kind |
| 89 | - of kVector: mkVector(coll.items & @[x]) | 75 | + of kVector: mkVec(vecConj(coll.vec, x)) |
| 90 | - of kList: mkList(@[x] & coll.items) | 76 | + of kList: mkList(@[x] & coll.xs) |
| 91 | - of kSet: mkSet(coll.items & @[x]) | 77 | + of kSet: |
| 78 | + (if mapContains(coll.m, x): coll else: mkSetOf(mapAssoc(coll.m, x, x))) | ||
| 92 | of kMap: | 79 | of kMap: |
| 93 | - if x.kind in {kVector, kList} and x.items.len == 2: | 80 | + let xs = items(x) |
| 94 | - assocOne(coll, x.items[0], x.items[1]) | 81 | + if x.kind in {kVector, kList} and xs.len == 2: |
| 82 | + assocOne(coll, xs[0], xs[1]) | ||
| 95 | elif x.kind == kMap: | 83 | elif x.kind == kMap: |
| 96 | - var m = coll | 84 | + var m = coll.m |
| 97 | - for (k, v) in x.pairs: m = assocOne(m, k, v) | 85 | + for e in mapEntries(x.m): m = mapAssoc(m, e.key, e.val) |
| 98 | - m | 86 | + mkMapOf(m) |
| 99 | else: err("conj on map needs a pair") | 87 | else: err("conj on map needs a pair") |
| 100 | else: err("conj not supported on " & prStr(coll)) | 88 | else: err("conj not supported on " & prStr(coll)) |
| 101 | 89 | ||
| @@ -177,21 +165,14 @@ proc registerCore*() = | |||
| 177 | def "coll?", proc (a: seq[Value]): Value = | 165 | def "coll?", proc (a: seq[Value]): Value = |
| 178 | mkBool(a[0].kind in {kList, kVector, kMap, kSet}) | 166 | mkBool(a[0].kind in {kList, kVector, kMap, kSet}) |
| 179 | def "fn?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kFn) | 167 | def "fn?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kFn) |
| 180 | - def "empty?", proc (a: seq[Value]): Value = mkBool(toSeq(a[0]).len == 0) | 168 | + def "empty?", proc (a: seq[Value]): Value = mkBool(count(a[0]) == 0) |
| 181 | def "contains?", proc (a: seq[Value]): Value = | 169 | def "contains?", proc (a: seq[Value]): Value = |
| 182 | let c = a[0] | 170 | let c = a[0] |
| 183 | if c.isNil or c.kind == kNil: return FalseV | 171 | if c.isNil or c.kind == kNil: return FalseV |
| 184 | case c.kind | 172 | case c.kind |
| 185 | - of kMap: | 173 | + of kMap, kSet: mkBool(mapContains(c.m, a[1])) |
| 186 | - for (k, _) in c.pairs: | ||
| 187 | - if equals(k, a[1]): return TrueV | ||
| 188 | - FalseV | ||
| 189 | - of kSet: | ||
| 190 | - for x in c.items: | ||
| 191 | - if equals(x, a[1]): return TrueV | ||
| 192 | - FalseV | ||
| 193 | of kVector: | 174 | of kVector: |
| 194 | - mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.items.len) | 175 | + mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.vec.cnt) |
| 195 | else: FalseV | 176 | else: FalseV |
| 196 | 177 | ||
| 197 | # ---- strings / IO | 178 | # ---- strings / IO |
| @@ -253,11 +234,11 @@ proc registerCore*() = | |||
| 253 | def "list", proc (a: seq[Value]): Value = mkList(a) | 234 | def "list", proc (a: seq[Value]): Value = mkList(a) |
| 254 | def "vector", proc (a: seq[Value]): Value = mkVector(a) | 235 | def "vector", proc (a: seq[Value]): Value = mkVector(a) |
| 255 | def "hash-map", proc (a: seq[Value]): Value = | 236 | def "hash-map", proc (a: seq[Value]): Value = |
| 256 | - var m: Value = Value(kind: kMap, pairs: @[]) | 237 | + var m = emptyPMap() |
| 257 | var i = 0 | 238 | var i = 0 |
| 258 | while i + 1 < a.len: | 239 | while i + 1 < a.len: |
| 259 | - m = assocOne(m, a[i], a[i + 1]); i += 2 | 240 | + m = mapAssoc(m, a[i], a[i + 1]); i += 2 |
| 260 | - m | 241 | + mkMapOf(m) |
| 261 | def "hash-set", proc (a: seq[Value]): Value = mkSet(a) | 242 | def "hash-set", proc (a: seq[Value]): Value = mkSet(a) |
| 262 | def "set", proc (a: seq[Value]): Value = mkSet(toSeq(a[0])) | 243 | def "set", proc (a: seq[Value]): Value = mkSet(toSeq(a[0])) |
| 263 | def "vec", proc (a: seq[Value]): Value = mkVector(toSeq(a[0])) | 244 | def "vec", proc (a: seq[Value]): Value = mkVector(toSeq(a[0])) |
| @@ -266,20 +247,22 @@ proc registerCore*() = | |||
| 266 | (if s.len == 0: NilV else: mkList(s)) | 247 | (if s.len == 0: NilV else: mkList(s)) |
| 267 | def "count", proc (a: seq[Value]): Value = | 248 | def "count", proc (a: seq[Value]): Value = |
| 268 | if a[0].isNil or a[0].kind == kNil: return mkInt(0) | 249 | if a[0].isNil or a[0].kind == kNil: return mkInt(0) |
| 269 | - if a[0].kind == kStr: return mkInt(a[0].s.len) | 250 | + mkInt(count(a[0])) |
| 270 | - if a[0].kind == kMap: return mkInt(a[0].pairs.len) | ||
| 271 | - mkInt(toSeq(a[0]).len) | ||
| 272 | def "conj", proc (a: seq[Value]): Value = | 251 | def "conj", proc (a: seq[Value]): Value = |
| 273 | result = a[0] | 252 | result = a[0] |
| 274 | for i in 1 ..< a.len: result = conjOne(result, a[i]) | 253 | for i in 1 ..< a.len: result = conjOne(result, a[i]) |
| 275 | def "cons", proc (a: seq[Value]): Value = mkList(@[a[0]] & toSeq(a[1])) | 254 | def "cons", proc (a: seq[Value]): Value = mkList(@[a[0]] & toSeq(a[1])) |
| 276 | def "first", proc (a: seq[Value]): Value = | 255 | def "first", proc (a: seq[Value]): Value = |
| 256 | + if a[0].kind == kVector: | ||
| 257 | + return (if a[0].vec.cnt == 0: NilV else: vecNth(a[0].vec, 0)) | ||
| 277 | let s = toSeq(a[0]) | 258 | let s = toSeq(a[0]) |
| 278 | (if s.len == 0: NilV else: s[0]) | 259 | (if s.len == 0: NilV else: s[0]) |
| 279 | def "second", proc (a: seq[Value]): Value = | 260 | def "second", proc (a: seq[Value]): Value = |
| 280 | let s = toSeq(a[0]) | 261 | let s = toSeq(a[0]) |
| 281 | (if s.len < 2: NilV else: s[1]) | 262 | (if s.len < 2: NilV else: s[1]) |
| 282 | def "last", proc (a: seq[Value]): Value = | 263 | def "last", proc (a: seq[Value]): Value = |
| 264 | + if a[0].kind == kVector: | ||
| 265 | + return (if a[0].vec.cnt == 0: NilV else: vecNth(a[0].vec, a[0].vec.cnt - 1)) | ||
| 283 | let s = toSeq(a[0]) | 266 | let s = toSeq(a[0]) |
| 284 | (if s.len == 0: NilV else: s[^1]) | 267 | (if s.len == 0: NilV else: s[^1]) |
| 285 | def "rest", proc (a: seq[Value]): Value = | 268 | def "rest", proc (a: seq[Value]): Value = |
| @@ -289,8 +272,13 @@ proc registerCore*() = | |||
| 289 | let s = toSeq(a[0]) | 272 | let s = toSeq(a[0]) |
| 290 | (if s.len <= 1: NilV else: mkList(s[1 .. ^1])) | 273 | (if s.len <= 1: NilV else: mkList(s[1 .. ^1])) |
| 291 | def "nth", proc (a: seq[Value]): Value = | 274 | def "nth", proc (a: seq[Value]): Value = |
| 292 | - let s = toSeq(a[0]) | ||
| 293 | let i = int(intOf(a[1])) | 275 | let i = int(intOf(a[1])) |
| 276 | + if a[0].kind == kVector: | ||
| 277 | + # O(log32 n) straight through the trie, no intermediate seq | ||
| 278 | + if i >= 0 and i < a[0].vec.cnt: return vecNth(a[0].vec, i) | ||
| 279 | + if a.len > 2: return a[2] | ||
| 280 | + err("Index out of bounds: " & $i) | ||
| 281 | + let s = toSeq(a[0]) | ||
| 294 | if i >= 0 and i < s.len: s[i] | 282 | if i >= 0 and i < s.len: s[i] |
| 295 | elif a.len > 2: a[2] | 283 | elif a.len > 2: a[2] |
| 296 | else: err("Index out of bounds: " & $i) | 284 | else: err("Index out of bounds: " & $i) |
| @@ -307,23 +295,19 @@ proc registerCore*() = | |||
| 307 | while i + 1 < a.len: | 295 | while i + 1 < a.len: |
| 308 | result = assocOne(result, a[i], a[i + 1]); i += 2 | 296 | result = assocOne(result, a[i], a[i + 1]); i += 2 |
| 309 | def "dissoc", proc (a: seq[Value]): Value = | 297 | def "dissoc", proc (a: seq[Value]): Value = |
| 310 | - var ps = a[0].pairs | 298 | + var m = a[0].m |
| 311 | - for i in 1 ..< a.len: | 299 | + for i in 1 ..< a.len: m = mapDissoc(m, a[i]) |
| 312 | - var keep: seq[(Value, Value)] = @[] | 300 | + mkMapOf(m) |
| 313 | - for (k, v) in ps: | ||
| 314 | - if not equals(k, a[i]): keep.add (k, v) | ||
| 315 | - ps = keep | ||
| 316 | - Value(kind: kMap, pairs: ps) | ||
| 317 | def "update", proc (a: seq[Value]): Value = | 301 | def "update", proc (a: seq[Value]): Value = |
| 318 | let cur = getIn(a[0], a[1], NilV) | 302 | let cur = getIn(a[0], a[1], NilV) |
| 319 | assocOne(a[0], a[1], call(a[2], @[cur] & a[3 .. ^1])) | 303 | assocOne(a[0], a[1], call(a[2], @[cur] & a[3 .. ^1])) |
| 320 | def "keys", proc (a: seq[Value]): Value = | 304 | def "keys", proc (a: seq[Value]): Value = |
| 321 | var r: seq[Value] = @[] | 305 | var r: seq[Value] = @[] |
| 322 | - for (k, _) in a[0].pairs: r.add k | 306 | + for e in mapEntries(a[0].m): r.add e.key |
| 323 | (if r.len == 0: NilV else: mkList(r)) | 307 | (if r.len == 0: NilV else: mkList(r)) |
| 324 | def "vals", proc (a: seq[Value]): Value = | 308 | def "vals", proc (a: seq[Value]): Value = |
| 325 | var r: seq[Value] = @[] | 309 | var r: seq[Value] = @[] |
| 326 | - for (_, v) in a[0].pairs: r.add v | 310 | + for e in mapEntries(a[0].m): r.add e.val |
| 327 | (if r.len == 0: NilV else: mkList(r)) | 311 | (if r.len == 0: NilV else: mkList(r)) |
| 328 | def "reverse", proc (a: seq[Value]): Value = | 312 | def "reverse", proc (a: seq[Value]): Value = |
| 329 | var s = toSeq(a[0]) | 313 | var s = toSeq(a[0]) |
| @@ -484,18 +468,16 @@ proc registerCore*() = | |||
| 484 | r.add x | 468 | r.add x |
| 485 | mkList(r) | 469 | mkList(r) |
| 486 | def "group-by", proc (a: seq[Value]): Value = | 470 | def "group-by", proc (a: seq[Value]): Value = |
| 487 | - var m: Value = Value(kind: kMap, pairs: @[]) | 471 | + var m = emptyPMap() |
| 488 | for x in toSeq(a[1]): | 472 | for x in toSeq(a[1]): |
| 489 | let k = call(a[0], @[x]) | 473 | let k = call(a[0], @[x]) |
| 490 | - let cur = getIn(m, k, mkVector(@[])) | 474 | + m = mapAssoc(m, k, conjOne(mapGet(m, k, mkVector(@[])), x)) |
| 491 | - m = assocOne(m, k, conjOne(cur, x)) | 475 | + mkMapOf(m) |
| 492 | - m | ||
| 493 | def "frequencies", proc (a: seq[Value]): Value = | 476 | def "frequencies", proc (a: seq[Value]): Value = |
| 494 | - var m: Value = Value(kind: kMap, pairs: @[]) | 477 | + var m = emptyPMap() |
| 495 | for x in toSeq(a[0]): | 478 | for x in toSeq(a[0]): |
| 496 | - let cur = getIn(m, x, mkInt(0)) | 479 | + m = mapAssoc(m, x, mkInt(mapGet(m, x, mkInt(0)).i + 1)) |
| 497 | - m = assocOne(m, x, mkInt(cur.i + 1)) | 480 | + mkMapOf(m) |
| 498 | - m | ||
| 499 | def "identity", proc (a: seq[Value]): Value = a[0] | 481 | def "identity", proc (a: seq[Value]): Value = a[0] |
| 500 | def "comp", proc (a: seq[Value]): Value = | 482 | def "comp", proc (a: seq[Value]): Value = |
| 501 | let fs = a | 483 | let fs = a |
modified
src/reader.nim +1 -1 | @@ -110,7 +110,7 @@ proc readForm(r: var Reader): Value = | ||
| 110 | 110 | var i = 0 |
| 111 | 111 | while i < xs.len: |
| 112 | 112 | ps.add (xs[i], xs[i + 1]); i += 2 |
| 113 | - return Value(kind: kMap, pairs: ps) | |
| 113 | + return mkMap(ps) | |
| 114 | 114 | of ')', ']', '}': |
| 115 | 115 | r.readerErr("Unmatched delimiter: " & c) |
| 116 | 116 | of '"': |
| @@ -110,7 +110,7 @@ proc readForm(r: var Reader): Value = | |||
| 110 | var i = 0 | 110 | var i = 0 |
| 111 | while i < xs.len: | 111 | while i < xs.len: |
| 112 | ps.add (xs[i], xs[i + 1]); i += 2 | 112 | ps.add (xs[i], xs[i + 1]); i += 2 |
| 113 | - return Value(kind: kMap, pairs: ps) | 113 | + return mkMap(ps) |
| 114 | of ')', ']', '}': | 114 | of ')', ']', '}': |
| 115 | r.readerErr("Unmatched delimiter: " & c) | 115 | r.readerErr("Unmatched delimiter: " & c) |
| 116 | of '"': | 116 | of '"': |
modified
src/runtime.nim +393 -46 | @@ -1,11 +1,61 @@ | ||
| 1 | -## clonim runtime — persistent-ish Clojure values for compiled Nim code. | |
| 2 | -import std/[tables, strutils] | |
| 1 | +## clonim runtime — persistent Clojure values for compiled Nim code. | |
| 2 | +## | |
| 3 | +## Vectors are 32-way tries with a tail buffer (Clojure's PersistentVector); | |
| 4 | +## maps and sets are HAMTs. Both share structure on update, so `assoc`/`conj` | |
| 5 | +## are O(log32 n) and copy a handful of 32-element nodes instead of the whole | |
| 6 | +## collection. Maps and sets additionally remember insertion order — every | |
| 7 | +## entry carries an `ord` stamp and iteration sorts by it — so printing and | |
| 8 | +## `keys`/`vals` stay predictable the way Clojure's small array-maps are. | |
| 9 | +import std/[tables, strutils, hashes, bitops, algorithm] | |
| 10 | + | |
| 11 | +const | |
| 12 | + Bits = 5 | |
| 13 | + Width = 1 shl Bits # 32 | |
| 14 | + Mask = Width - 1 | |
| 15 | + MaxShift = 30 # beyond this a HAMT runs out of hash bits | |
| 3 | 16 | |
| 4 | 17 | type |
| 5 | 18 | Kind* = enum |
| 6 | 19 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, |
| 7 | 20 | kList, kVector, kMap, kSet, kFn |
| 8 | 21 | |
| 22 | + VNode* = ref object | |
| 23 | + ## A trie node: leaves hold values, internal nodes hold children. | |
| 24 | + case leaf*: bool | |
| 25 | + of true: vals*: seq[Value] | |
| 26 | + of false: kids*: seq[VNode] | |
| 27 | + | |
| 28 | + PVec* = object | |
| 29 | + cnt*: int ## total element count | |
| 30 | + shift*: int ## bit offset of the root level | |
| 31 | + root*: VNode ## internal node (never nil) | |
| 32 | + tail*: seq[Value] ## up to 32 trailing elements, not yet in the trie | |
| 33 | + | |
| 34 | + MEntry* = object | |
| 35 | + key*, val*: Value | |
| 36 | + ord*: int ## insertion stamp, for stable iteration order | |
| 37 | + | |
| 38 | + MSlotKind* = enum msEntry, msNode | |
| 39 | + MSlot* = object | |
| 40 | + case sk*: MSlotKind | |
| 41 | + of msEntry: e*: MEntry | |
| 42 | + of msNode: node*: MNode | |
| 43 | + | |
| 44 | + MNode* = ref object | |
| 45 | + ## Bitmap-indexed node, or — once the hash bits run out — a linear | |
| 46 | + ## collision bucket. | |
| 47 | + case collision*: bool | |
| 48 | + of false: | |
| 49 | + bitmap*: uint32 | |
| 50 | + slots*: seq[MSlot] | |
| 51 | + of true: | |
| 52 | + kvs*: seq[MEntry] | |
| 53 | + | |
| 54 | + PMap* = object | |
| 55 | + root*: MNode ## nil when empty | |
| 56 | + cnt*: int | |
| 57 | + nextOrd*: int | |
| 58 | + | |
| 9 | 59 | Value* = ref object |
| 10 | 60 | case kind*: Kind |
| 11 | 61 | of kNil: discard |
| @@ -13,8 +63,9 @@ type | ||
| 13 | 63 | of kInt: i*: int64 |
| 14 | 64 | of kFloat: f*: float64 |
| 15 | 65 | of kStr, kKeyword, kSymbol: s*: string |
| 16 | - of kList, kVector, kSet: items*: seq[Value] | |
| 17 | - of kMap: pairs*: seq[(Value, Value)] | |
| 66 | + of kList: xs*: seq[Value] | |
| 67 | + of kVector: vec*: PVec | |
| 68 | + of kMap, kSet: m*: PMap | |
| 18 | 69 | of kFn: |
| 19 | 70 | fn*: proc (args: seq[Value]): Value {.closure.} |
| 20 | 71 | name*: string |
| @@ -25,19 +76,337 @@ let NilV* = Value(kind: kNil) | ||
| 25 | 76 | let TrueV* = Value(kind: kBool, b: true) |
| 26 | 77 | let FalseV* = Value(kind: kBool, b: false) |
| 27 | 78 | |
| 79 | +proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | |
| 80 | + | |
| 81 | +proc equals*(a, b: Value): bool | |
| 82 | +proc hashValue*(v: Value): uint32 | |
| 83 | +proc prStr*(v: Value): string | |
| 84 | + | |
| 85 | +# ------------------------------------------------------- persistent vector | |
| 86 | +let emptyVNode = VNode(leaf: false, kids: @[]) | |
| 87 | + | |
| 88 | +proc emptyPVec*(): PVec = PVec(cnt: 0, shift: Bits, root: emptyVNode, tail: @[]) | |
| 89 | + | |
| 90 | +proc tailOff(v: PVec): int = | |
| 91 | + if v.cnt < Width: 0 else: ((v.cnt - 1) shr Bits) shl Bits | |
| 92 | + | |
| 93 | +proc leafFor(v: PVec, i: int): seq[Value] = | |
| 94 | + if i >= tailOff(v): return v.tail | |
| 95 | + var node = v.root | |
| 96 | + var level = v.shift | |
| 97 | + while level > 0: | |
| 98 | + node = node.kids[(i shr level) and Mask] | |
| 99 | + level -= Bits | |
| 100 | + node.vals | |
| 101 | + | |
| 102 | +proc vecNth*(v: PVec, i: int): Value = | |
| 103 | + if i < 0 or i >= v.cnt: err("Index out of bounds: " & $i) | |
| 104 | + leafFor(v, i)[i and Mask] | |
| 105 | + | |
| 106 | +proc newPath(level: int, node: VNode): VNode = | |
| 107 | + if level == 0: node | |
| 108 | + else: VNode(leaf: false, kids: @[newPath(level - Bits, node)]) | |
| 109 | + | |
| 110 | +proc pushTail(cnt, level: int, parent, tailNode: VNode): VNode = | |
| 111 | + let subIdx = ((cnt - 1) shr level) and Mask | |
| 112 | + var kids = parent.kids | |
| 113 | + let child = | |
| 114 | + if level == Bits: tailNode | |
| 115 | + elif subIdx < kids.len: pushTail(cnt, level - Bits, kids[subIdx], tailNode) | |
| 116 | + else: newPath(level - Bits, tailNode) | |
| 117 | + if subIdx < kids.len: kids[subIdx] = child | |
| 118 | + else: kids.add child | |
| 119 | + VNode(leaf: false, kids: kids) | |
| 120 | + | |
| 121 | +proc vecConj*(v: PVec, x: Value): PVec = | |
| 122 | + if v.tail.len < Width: | |
| 123 | + return PVec(cnt: v.cnt + 1, shift: v.shift, root: v.root, tail: v.tail & x) | |
| 124 | + let tailNode = VNode(leaf: true, vals: v.tail) | |
| 125 | + var shift = v.shift | |
| 126 | + var root: VNode | |
| 127 | + if (v.cnt shr Bits) > (1 shl v.shift): # root overflow: grow a level | |
| 128 | + root = VNode(leaf: false, kids: @[v.root, newPath(v.shift, tailNode)]) | |
| 129 | + shift += Bits | |
| 130 | + else: | |
| 131 | + root = pushTail(v.cnt, v.shift, v.root, tailNode) | |
| 132 | + PVec(cnt: v.cnt + 1, shift: shift, root: root, tail: @[x]) | |
| 133 | + | |
| 134 | +proc doAssoc(level: int, node: VNode, i: int, x: Value): VNode = | |
| 135 | + if level == 0: | |
| 136 | + var vals = node.vals | |
| 137 | + vals[i and Mask] = x | |
| 138 | + VNode(leaf: true, vals: vals) | |
| 139 | + else: | |
| 140 | + var kids = node.kids | |
| 141 | + let sub = (i shr level) and Mask | |
| 142 | + kids[sub] = doAssoc(level - Bits, kids[sub], i, x) | |
| 143 | + VNode(leaf: false, kids: kids) | |
| 144 | + | |
| 145 | +proc vecAssoc*(v: PVec, i: int, x: Value): PVec = | |
| 146 | + if i == v.cnt: return vecConj(v, x) | |
| 147 | + if i < 0 or i > v.cnt: err("Index out of bounds: " & $i) | |
| 148 | + if i >= tailOff(v): | |
| 149 | + var tail = v.tail | |
| 150 | + tail[i - tailOff(v)] = x | |
| 151 | + return PVec(cnt: v.cnt, shift: v.shift, root: v.root, tail: tail) | |
| 152 | + PVec(cnt: v.cnt, shift: v.shift, root: doAssoc(v.shift, v.root, i, x), | |
| 153 | + tail: v.tail) | |
| 154 | + | |
| 155 | +proc vecToSeq*(v: PVec): seq[Value] = | |
| 156 | + result = newSeqOfCap[Value](v.cnt) | |
| 157 | + var i = 0 | |
| 158 | + while i < v.cnt: | |
| 159 | + let leaf = leafFor(v, i) | |
| 160 | + let base = i and not Mask | |
| 161 | + for j in 0 ..< leaf.len: | |
| 162 | + if base + j >= v.cnt: break | |
| 163 | + result.add leaf[j] | |
| 164 | + i = base + leaf.len | |
| 165 | + | |
| 166 | +proc toPVec*(xs: seq[Value]): PVec = | |
| 167 | + result = emptyPVec() | |
| 168 | + for x in xs: result = vecConj(result, x) | |
| 169 | + | |
| 170 | +# ----------------------------------------------------------------- hashing | |
| 171 | +proc mixHash(a, b: uint32): uint32 = | |
| 172 | + ## Boost-style combine; cheap and good enough for trie index bits. | |
| 173 | + a xor (b + 0x9e3779b9'u32 + (a shl 6) + (a shr 2)) | |
| 174 | + | |
| 175 | +# ---------------------------------------------------------- persistent map | |
| 176 | +proc bitPos(h: uint32, shift: int): uint32 = 1'u32 shl ((h shr shift) and Mask) | |
| 177 | +proc slotIdx(bitmap, bit: uint32): int = countSetBits(bitmap and (bit - 1)) | |
| 178 | + | |
| 179 | +proc emptyPMap*(): PMap = PMap(root: nil, cnt: 0, nextOrd: 0) | |
| 180 | + | |
| 181 | +proc mergeEntries(shift: int, h1: uint32, e1: MEntry, | |
| 182 | + h2: uint32, e2: MEntry): MNode = | |
| 183 | + if shift > MaxShift: | |
| 184 | + return MNode(collision: true, kvs: @[e1, e2]) | |
| 185 | + let b1 = bitPos(h1, shift) | |
| 186 | + let b2 = bitPos(h2, shift) | |
| 187 | + if b1 == b2: | |
| 188 | + MNode(collision: false, bitmap: b1, | |
| 189 | + slots: @[MSlot(sk: msNode, | |
| 190 | + node: mergeEntries(shift + Bits, h1, e1, h2, e2))]) | |
| 191 | + elif b1 < b2: | |
| 192 | + MNode(collision: false, bitmap: b1 or b2, | |
| 193 | + slots: @[MSlot(sk: msEntry, e: e1), MSlot(sk: msEntry, e: e2)]) | |
| 194 | + else: | |
| 195 | + MNode(collision: false, bitmap: b1 or b2, | |
| 196 | + slots: @[MSlot(sk: msEntry, e: e2), MSlot(sk: msEntry, e: e1)]) | |
| 197 | + | |
| 198 | +proc nodeAssoc(n: MNode, shift: int, h: uint32, k, v: Value, newOrd: int, | |
| 199 | + added: var bool): MNode = | |
| 200 | + if n.collision: | |
| 201 | + for i in 0 ..< n.kvs.len: | |
| 202 | + if equals(n.kvs[i].key, k): | |
| 203 | + var kvs = n.kvs | |
| 204 | + kvs[i].val = v | |
| 205 | + return MNode(collision: true, kvs: kvs) | |
| 206 | + added = true | |
| 207 | + return MNode(collision: true, | |
| 208 | + kvs: n.kvs & MEntry(key: k, val: v, ord: newOrd)) | |
| 209 | + let bit = bitPos(h, shift) | |
| 210 | + let idx = slotIdx(n.bitmap, bit) | |
| 211 | + var slots = n.slots | |
| 212 | + if (n.bitmap and bit) != 0: | |
| 213 | + case slots[idx].sk | |
| 214 | + of msNode: | |
| 215 | + slots[idx] = MSlot(sk: msNode, | |
| 216 | + node: nodeAssoc(slots[idx].node, shift + Bits, h, k, v, newOrd, added)) | |
| 217 | + of msEntry: | |
| 218 | + let e = slots[idx].e | |
| 219 | + if equals(e.key, k): | |
| 220 | + slots[idx] = MSlot(sk: msEntry, e: MEntry(key: k, val: v, ord: e.ord)) | |
| 221 | + else: | |
| 222 | + added = true | |
| 223 | + slots[idx] = MSlot(sk: msNode, | |
| 224 | + node: mergeEntries(shift + Bits, hashValue(e.key), e, h, | |
| 225 | + MEntry(key: k, val: v, ord: newOrd))) | |
| 226 | + return MNode(collision: false, bitmap: n.bitmap, slots: slots) | |
| 227 | + added = true | |
| 228 | + slots.insert(MSlot(sk: msEntry, e: MEntry(key: k, val: v, ord: newOrd)), idx) | |
| 229 | + MNode(collision: false, bitmap: n.bitmap or bit, slots: slots) | |
| 230 | + | |
| 231 | +proc mapAssoc*(m: PMap, k, v: Value): PMap = | |
| 232 | + let h = hashValue(k) | |
| 233 | + var added = false | |
| 234 | + if m.root.isNil: | |
| 235 | + return PMap(root: MNode(collision: false, bitmap: bitPos(h, 0), | |
| 236 | + slots: @[MSlot(sk: msEntry, | |
| 237 | + e: MEntry(key: k, val: v, ord: 0))]), | |
| 238 | + cnt: 1, nextOrd: 1) | |
| 239 | + let root = nodeAssoc(m.root, 0, h, k, v, m.nextOrd, added) | |
| 240 | + PMap(root: root, cnt: m.cnt + (if added: 1 else: 0), | |
| 241 | + nextOrd: m.nextOrd + (if added: 1 else: 0)) | |
| 242 | + | |
| 243 | +proc nodeFind(n: MNode, shift: int, h: uint32, k: Value, | |
| 244 | + found: var bool): Value = | |
| 245 | + if n.isNil: return NilV | |
| 246 | + if n.collision: | |
| 247 | + for e in n.kvs: | |
| 248 | + if equals(e.key, k): | |
| 249 | + found = true | |
| 250 | + return e.val | |
| 251 | + return NilV | |
| 252 | + let bit = bitPos(h, shift) | |
| 253 | + if (n.bitmap and bit) == 0: return NilV | |
| 254 | + let slot = n.slots[slotIdx(n.bitmap, bit)] | |
| 255 | + case slot.sk | |
| 256 | + of msNode: nodeFind(slot.node, shift + Bits, h, k, found) | |
| 257 | + of msEntry: | |
| 258 | + if equals(slot.e.key, k): | |
| 259 | + found = true | |
| 260 | + slot.e.val | |
| 261 | + else: NilV | |
| 262 | + | |
| 263 | +proc mapGet*(m: PMap, k: Value, dflt: Value): Value = | |
| 264 | + var found = false | |
| 265 | + let v = nodeFind(m.root, 0, hashValue(k), k, found) | |
| 266 | + if found: v else: dflt | |
| 267 | + | |
| 268 | +proc mapContains*(m: PMap, k: Value): bool = | |
| 269 | + var found = false | |
| 270 | + discard nodeFind(m.root, 0, hashValue(k), k, found) | |
| 271 | + found | |
| 272 | + | |
| 273 | +proc nodeDissoc(n: MNode, shift: int, h: uint32, k: Value, | |
| 274 | + removed: var bool): MNode = | |
| 275 | + if n.collision: | |
| 276 | + var kvs: seq[MEntry] = @[] | |
| 277 | + for e in n.kvs: | |
| 278 | + if equals(e.key, k): removed = true | |
| 279 | + else: kvs.add e | |
| 280 | + return (if kvs.len == 0: nil else: MNode(collision: true, kvs: kvs)) | |
| 281 | + let bit = bitPos(h, shift) | |
| 282 | + if (n.bitmap and bit) == 0: return n | |
| 283 | + let idx = slotIdx(n.bitmap, bit) | |
| 284 | + var slots = n.slots | |
| 285 | + case slots[idx].sk | |
| 286 | + of msNode: | |
| 287 | + let child = nodeDissoc(slots[idx].node, shift + Bits, h, k, removed) | |
| 288 | + if child.isNil: | |
| 289 | + slots.delete(idx) | |
| 290 | + return (if slots.len == 0: nil | |
| 291 | + else: MNode(collision: false, bitmap: n.bitmap and not bit, | |
| 292 | + slots: slots)) | |
| 293 | + slots[idx] = MSlot(sk: msNode, node: child) | |
| 294 | + MNode(collision: false, bitmap: n.bitmap, slots: slots) | |
| 295 | + of msEntry: | |
| 296 | + if not equals(slots[idx].e.key, k): return n | |
| 297 | + removed = true | |
| 298 | + slots.delete(idx) | |
| 299 | + if slots.len == 0: nil | |
| 300 | + else: MNode(collision: false, bitmap: n.bitmap and not bit, slots: slots) | |
| 301 | + | |
| 302 | +proc mapDissoc*(m: PMap, k: Value): PMap = | |
| 303 | + if m.root.isNil: return m | |
| 304 | + var removed = false | |
| 305 | + let root = nodeDissoc(m.root, 0, hashValue(k), k, removed) | |
| 306 | + if not removed: return m | |
| 307 | + PMap(root: root, cnt: m.cnt - 1, nextOrd: m.nextOrd) | |
| 308 | + | |
| 309 | +proc collect(n: MNode, acc: var seq[MEntry]) = | |
| 310 | + if n.isNil: return | |
| 311 | + if n.collision: | |
| 312 | + for e in n.kvs: acc.add e | |
| 313 | + return | |
| 314 | + for slot in n.slots: | |
| 315 | + case slot.sk | |
| 316 | + of msEntry: acc.add slot.e | |
| 317 | + of msNode: collect(slot.node, acc) | |
| 318 | + | |
| 319 | +proc mapEntries*(m: PMap): seq[MEntry] = | |
| 320 | + ## Entries in insertion order. | |
| 321 | + result = newSeqOfCap[MEntry](m.cnt) | |
| 322 | + collect(m.root, result) | |
| 323 | + result.sort(proc (a, b: MEntry): int = cmp(a.ord, b.ord)) | |
| 324 | + | |
| 325 | +proc hashValue*(v: Value): uint32 = | |
| 326 | + if v.isNil: return 0 | |
| 327 | + case v.kind | |
| 328 | + of kNil: 0'u32 | |
| 329 | + of kBool: (if v.b: 0x9e3779b9'u32 else: 0x85ebca6b'u32) | |
| 330 | + of kInt: uint32(hash(v.i)) | |
| 331 | + of kFloat: | |
| 332 | + # ints and floats compare equal across kinds, so they must hash alike | |
| 333 | + if v.f == float64(int64(v.f)): uint32(hash(int64(v.f))) | |
| 334 | + else: uint32(hash(v.f)) | |
| 335 | + of kStr: mixHash(1'u32, uint32(hash(v.s))) | |
| 336 | + of kKeyword: mixHash(2'u32, uint32(hash(v.s))) | |
| 337 | + of kSymbol: mixHash(3'u32, uint32(hash(v.s))) | |
| 338 | + of kList, kVector: | |
| 339 | + # lists and vectors are `=` when their elements are, so they hash alike | |
| 340 | + var h = 7'u32 | |
| 341 | + for x in (if v.kind == kList: v.xs else: vecToSeq(v.vec)): | |
| 342 | + h = mixHash(h, hashValue(x)) | |
| 343 | + h | |
| 344 | + of kSet: | |
| 345 | + var h = 0'u32 # xor: independent of iteration order | |
| 346 | + for e in mapEntries(v.m): h = h xor hashValue(e.key) | |
| 347 | + h | |
| 348 | + of kMap: | |
| 349 | + var h = 0'u32 | |
| 350 | + for e in mapEntries(v.m): | |
| 351 | + h = h xor mixHash(hashValue(e.key), hashValue(e.val)) | |
| 352 | + h | |
| 353 | + of kFn: uint32(hash(cast[int](cast[pointer](v)))) | |
| 354 | + | |
| 355 | +# ------------------------------------------------------------ constructors | |
| 28 | 356 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) |
| 29 | 357 | proc mkInt*(x: int64): Value = Value(kind: kInt, i: x) |
| 30 | 358 | proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x) |
| 31 | 359 | proc mkStr*(x: string): Value = Value(kind: kStr, s: x) |
| 32 | 360 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x) |
| 33 | 361 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x) |
| 34 | -proc mkList*(xs: seq[Value]): Value = Value(kind: kList, items: xs) | |
| 35 | -proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, items: xs) | |
| 36 | -proc mkSet*(xs: seq[Value]): Value | |
| 362 | +proc mkList*(xs: seq[Value]): Value = Value(kind: kList, xs: xs) | |
| 363 | +proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, vec: toPVec(xs)) | |
| 364 | +proc mkVec*(v: PVec): Value = Value(kind: kVector, vec: v) | |
| 365 | +proc mkMapOf*(m: PMap): Value = Value(kind: kMap, m: m) | |
| 366 | +proc mkSetOf*(m: PMap): Value = Value(kind: kSet, m: m) | |
| 367 | + | |
| 368 | +proc mkMap*(ps: seq[(Value, Value)]): Value = | |
| 369 | + var m = emptyPMap() | |
| 370 | + for (k, v) in ps: m = mapAssoc(m, k, v) | |
| 371 | + Value(kind: kMap, m: m) | |
| 372 | + | |
| 373 | +proc mkSet*(xs: seq[Value]): Value = | |
| 374 | + var m = emptyPMap() | |
| 375 | + for x in xs: | |
| 376 | + if not mapContains(m, x): m = mapAssoc(m, x, x) | |
| 377 | + Value(kind: kSet, m: m) | |
| 378 | + | |
| 37 | 379 | proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = |
| 38 | 380 | Value(kind: kFn, fn: f, name: name) |
| 39 | 381 | |
| 40 | -proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | |
| 382 | +# --------------------------------------------------------------- accessors | |
| 383 | +proc items*(v: Value): seq[Value] = | |
| 384 | + ## Elements of any sequential value, in order. O(n) — prefer `count`/`nth` | |
| 385 | + ## when you only need one element. | |
| 386 | + if v.isNil: return @[] | |
| 387 | + case v.kind | |
| 388 | + of kList: v.xs | |
| 389 | + of kVector: vecToSeq(v.vec) | |
| 390 | + of kSet: | |
| 391 | + var r = newSeqOfCap[Value](v.m.cnt) | |
| 392 | + for e in mapEntries(v.m): r.add e.key | |
| 393 | + r | |
| 394 | + else: @[] | |
| 395 | + | |
| 396 | +proc pairs*(v: Value): seq[(Value, Value)] = | |
| 397 | + if v.isNil or v.kind != kMap: return @[] | |
| 398 | + result = newSeqOfCap[(Value, Value)](v.m.cnt) | |
| 399 | + for e in mapEntries(v.m): result.add (e.key, e.val) | |
| 400 | + | |
| 401 | +proc count*(v: Value): int = | |
| 402 | + if v.isNil: return 0 | |
| 403 | + case v.kind | |
| 404 | + of kNil: 0 | |
| 405 | + of kList: v.xs.len | |
| 406 | + of kVector: v.vec.cnt | |
| 407 | + of kMap, kSet: v.m.cnt | |
| 408 | + of kStr: v.s.len | |
| 409 | + else: err("Don't know how to count: " & prStr(v)) | |
| 41 | 410 | |
| 42 | 411 | proc truthy*(v: Value): bool = |
| 43 | 412 | if v == nil: return false |
| @@ -54,9 +423,11 @@ proc equals*(a, b: Value): bool = | ||
| 54 | 423 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) |
| 55 | 424 | # lists and vectors are sequentially equal in Clojure |
| 56 | 425 | if a.kind in {kList, kVector} and b.kind in {kList, kVector}: |
| 57 | - if a.items.len != b.items.len: return false | |
| 58 | - for i in 0 ..< a.items.len: | |
| 59 | - if not equals(a.items[i], b.items[i]): return false | |
| 426 | + if count(a) != count(b): return false | |
| 427 | + let xs = items(a) | |
| 428 | + let ys = items(b) | |
| 429 | + for i in 0 ..< xs.len: | |
| 430 | + if not equals(xs[i], ys[i]): return false | |
| 60 | 431 | return true |
| 61 | 432 | if a.kind != b.kind: return false |
| 62 | 433 | case a.kind |
| @@ -66,35 +437,18 @@ proc equals*(a, b: Value): bool = | ||
| 66 | 437 | of kFloat: a.f == b.f |
| 67 | 438 | of kStr, kKeyword, kSymbol: a.s == b.s |
| 68 | 439 | of kSet: |
| 69 | - if a.items.len != b.items.len: return false | |
| 70 | - for x in a.items: | |
| 71 | - var found = false | |
| 72 | - for y in b.items: | |
| 73 | - if equals(x, y): found = true; break | |
| 74 | - if not found: return false | |
| 440 | + if a.m.cnt != b.m.cnt: return false | |
| 441 | + for e in mapEntries(a.m): | |
| 442 | + if not mapContains(b.m, e.key): return false | |
| 75 | 443 | true |
| 76 | 444 | of kMap: |
| 77 | - if a.pairs.len != b.pairs.len: return false | |
| 78 | - for (k, v) in a.pairs: | |
| 79 | - var found = false | |
| 80 | - for (k2, v2) in b.pairs: | |
| 81 | - if equals(k, k2): | |
| 82 | - if not equals(v, v2): return false | |
| 83 | - found = true; break | |
| 84 | - if not found: return false | |
| 445 | + if a.m.cnt != b.m.cnt: return false | |
| 446 | + let missing = Value(kind: kKeyword, s: "%clonim-missing") | |
| 447 | + for e in mapEntries(a.m): | |
| 448 | + if not equals(e.val, mapGet(b.m, e.key, missing)): return false | |
| 85 | 449 | true |
| 86 | 450 | of kFn: a == b |
| 87 | 451 | of kList, kVector: false # handled above |
| 88 | - | |
| 89 | -proc mkSet*(xs: seq[Value]): Value = | |
| 90 | - var acc: seq[Value] = @[] | |
| 91 | - for x in xs: | |
| 92 | - var dup = false | |
| 93 | - for y in acc: | |
| 94 | - if equals(x, y): dup = true; break | |
| 95 | - if not dup: acc.add x | |
| 96 | - Value(kind: kSet, items: acc) | |
| 97 | - | |
| 98 | 452 | # ---------------------------------------------------------------- printing |
| 99 | 453 | proc escapeStr(s: string): string = |
| 100 | 454 | result = "\"" |
| @@ -184,19 +538,13 @@ proc call*(f: Value, args: seq[Value]): Value = | ||
| 184 | 538 | if args.len == 0: err("Wrong number of args to keyword") |
| 185 | 539 | let m = args[0] |
| 186 | 540 | if m.isNil or m.kind != kMap: return NilV |
| 187 | - for (k, v) in m.pairs: | |
| 188 | - if equals(k, f): return v | |
| 189 | - (if args.len > 1: args[1] else: NilV) | |
| 541 | + mapGet(m.m, f, (if args.len > 1: args[1] else: NilV)) | |
| 190 | 542 | of kMap: |
| 191 | 543 | if args.len == 0: err("Wrong number of args to map") |
| 192 | - for (k, v) in f.pairs: | |
| 193 | - if equals(k, args[0]): return v | |
| 194 | - (if args.len > 1: args[1] else: NilV) | |
| 544 | + mapGet(f.m, args[0], (if args.len > 1: args[1] else: NilV)) | |
| 195 | 545 | of kVector: |
| 196 | 546 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") |
| 197 | - let i = int(args[0].i) | |
| 198 | - if i < 0 or i >= f.items.len: err("Index out of bounds: " & $i) | |
| 199 | - f.items[i] | |
| 547 | + vecNth(f.vec, int(args[0].i)) | |
| 200 | 548 | else: err("Can't call value of kind " & $f.kind & ": " & prStr(f)) |
| 201 | 549 | |
| 202 | 550 | proc argAt*(args: seq[Value], i: int): Value = |
| @@ -222,9 +570,8 @@ proc toSeq*(v: Value): seq[Value] = | ||
| 222 | 570 | r |
| 223 | 571 | of kMap: |
| 224 | 572 | var r: seq[Value] = @[] |
| 225 | - for (k, val) in v.pairs: r.add mkVector(@[k, val]) | |
| 573 | + for e in mapEntries(v.m): r.add mkVector(@[e.key, e.val]) | |
| 226 | 574 | r |
| 227 | 575 | else: err("Don't know how to create seq from: " & prStr(v)) |
| 228 | 576 | |
| 229 | -proc mkMap*(ps: seq[(Value, Value)]): Value = Value(kind: kMap, pairs: ps) | |
| 230 | 577 | let emptyArgs*: seq[Value] = @[] |
| @@ -1,11 +1,61 @@ | |||
| 1 | -## clonim runtime — persistent-ish Clojure values for compiled Nim code. | 1 | +## clonim runtime — persistent Clojure values for compiled Nim code. |
| 2 | -import std/[tables, strutils] | 2 | +## |
| 3 | +## Vectors are 32-way tries with a tail buffer (Clojure's PersistentVector); | ||
| 4 | +## maps and sets are HAMTs. Both share structure on update, so `assoc`/`conj` | ||
| 5 | +## are O(log32 n) and copy a handful of 32-element nodes instead of the whole | ||
| 6 | +## collection. Maps and sets additionally remember insertion order — every | ||
| 7 | +## entry carries an `ord` stamp and iteration sorts by it — so printing and | ||
| 8 | +## `keys`/`vals` stay predictable the way Clojure's small array-maps are. | ||
| 9 | +import std/[tables, strutils, hashes, bitops, algorithm] | ||
| 10 | + | ||
| 11 | +const | ||
| 12 | + Bits = 5 | ||
| 13 | + Width = 1 shl Bits # 32 | ||
| 14 | + Mask = Width - 1 | ||
| 15 | + MaxShift = 30 # beyond this a HAMT runs out of hash bits | ||
| 3 | 16 | ||
| 4 | type | 17 | type |
| 5 | Kind* = enum | 18 | Kind* = enum |
| 6 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | 19 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, |
| 7 | kList, kVector, kMap, kSet, kFn | 20 | kList, kVector, kMap, kSet, kFn |
| 8 | 21 | ||
| 22 | + VNode* = ref object | ||
| 23 | + ## A trie node: leaves hold values, internal nodes hold children. | ||
| 24 | + case leaf*: bool | ||
| 25 | + of true: vals*: seq[Value] | ||
| 26 | + of false: kids*: seq[VNode] | ||
| 27 | + | ||
| 28 | + PVec* = object | ||
| 29 | + cnt*: int ## total element count | ||
| 30 | + shift*: int ## bit offset of the root level | ||
| 31 | + root*: VNode ## internal node (never nil) | ||
| 32 | + tail*: seq[Value] ## up to 32 trailing elements, not yet in the trie | ||
| 33 | + | ||
| 34 | + MEntry* = object | ||
| 35 | + key*, val*: Value | ||
| 36 | + ord*: int ## insertion stamp, for stable iteration order | ||
| 37 | + | ||
| 38 | + MSlotKind* = enum msEntry, msNode | ||
| 39 | + MSlot* = object | ||
| 40 | + case sk*: MSlotKind | ||
| 41 | + of msEntry: e*: MEntry | ||
| 42 | + of msNode: node*: MNode | ||
| 43 | + | ||
| 44 | + MNode* = ref object | ||
| 45 | + ## Bitmap-indexed node, or — once the hash bits run out — a linear | ||
| 46 | + ## collision bucket. | ||
| 47 | + case collision*: bool | ||
| 48 | + of false: | ||
| 49 | + bitmap*: uint32 | ||
| 50 | + slots*: seq[MSlot] | ||
| 51 | + of true: | ||
| 52 | + kvs*: seq[MEntry] | ||
| 53 | + | ||
| 54 | + PMap* = object | ||
| 55 | + root*: MNode ## nil when empty | ||
| 56 | + cnt*: int | ||
| 57 | + nextOrd*: int | ||
| 58 | + | ||
| 9 | Value* = ref object | 59 | Value* = ref object |
| 10 | case kind*: Kind | 60 | case kind*: Kind |
| 11 | of kNil: discard | 61 | of kNil: discard |
| @@ -13,8 +63,9 @@ type | |||
| 13 | of kInt: i*: int64 | 63 | of kInt: i*: int64 |
| 14 | of kFloat: f*: float64 | 64 | of kFloat: f*: float64 |
| 15 | of kStr, kKeyword, kSymbol: s*: string | 65 | of kStr, kKeyword, kSymbol: s*: string |
| 16 | - of kList, kVector, kSet: items*: seq[Value] | 66 | + of kList: xs*: seq[Value] |
| 17 | - of kMap: pairs*: seq[(Value, Value)] | 67 | + of kVector: vec*: PVec |
| 68 | + of kMap, kSet: m*: PMap | ||
| 18 | of kFn: | 69 | of kFn: |
| 19 | fn*: proc (args: seq[Value]): Value {.closure.} | 70 | fn*: proc (args: seq[Value]): Value {.closure.} |
| 20 | name*: string | 71 | name*: string |
| @@ -25,19 +76,337 @@ let NilV* = Value(kind: kNil) | |||
| 25 | let TrueV* = Value(kind: kBool, b: true) | 76 | let TrueV* = Value(kind: kBool, b: true) |
| 26 | let FalseV* = Value(kind: kBool, b: false) | 77 | let FalseV* = Value(kind: kBool, b: false) |
| 27 | 78 | ||
| 79 | +proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | ||
| 80 | + | ||
| 81 | +proc equals*(a, b: Value): bool | ||
| 82 | +proc hashValue*(v: Value): uint32 | ||
| 83 | +proc prStr*(v: Value): string | ||
| 84 | + | ||
| 85 | +# ------------------------------------------------------- persistent vector | ||
| 86 | +let emptyVNode = VNode(leaf: false, kids: @[]) | ||
| 87 | + | ||
| 88 | +proc emptyPVec*(): PVec = PVec(cnt: 0, shift: Bits, root: emptyVNode, tail: @[]) | ||
| 89 | + | ||
| 90 | +proc tailOff(v: PVec): int = | ||
| 91 | + if v.cnt < Width: 0 else: ((v.cnt - 1) shr Bits) shl Bits | ||
| 92 | + | ||
| 93 | +proc leafFor(v: PVec, i: int): seq[Value] = | ||
| 94 | + if i >= tailOff(v): return v.tail | ||
| 95 | + var node = v.root | ||
| 96 | + var level = v.shift | ||
| 97 | + while level > 0: | ||
| 98 | + node = node.kids[(i shr level) and Mask] | ||
| 99 | + level -= Bits | ||
| 100 | + node.vals | ||
| 101 | + | ||
| 102 | +proc vecNth*(v: PVec, i: int): Value = | ||
| 103 | + if i < 0 or i >= v.cnt: err("Index out of bounds: " & $i) | ||
| 104 | + leafFor(v, i)[i and Mask] | ||
| 105 | + | ||
| 106 | +proc newPath(level: int, node: VNode): VNode = | ||
| 107 | + if level == 0: node | ||
| 108 | + else: VNode(leaf: false, kids: @[newPath(level - Bits, node)]) | ||
| 109 | + | ||
| 110 | +proc pushTail(cnt, level: int, parent, tailNode: VNode): VNode = | ||
| 111 | + let subIdx = ((cnt - 1) shr level) and Mask | ||
| 112 | + var kids = parent.kids | ||
| 113 | + let child = | ||
| 114 | + if level == Bits: tailNode | ||
| 115 | + elif subIdx < kids.len: pushTail(cnt, level - Bits, kids[subIdx], tailNode) | ||
| 116 | + else: newPath(level - Bits, tailNode) | ||
| 117 | + if subIdx < kids.len: kids[subIdx] = child | ||
| 118 | + else: kids.add child | ||
| 119 | + VNode(leaf: false, kids: kids) | ||
| 120 | + | ||
| 121 | +proc vecConj*(v: PVec, x: Value): PVec = | ||
| 122 | + if v.tail.len < Width: | ||
| 123 | + return PVec(cnt: v.cnt + 1, shift: v.shift, root: v.root, tail: v.tail & x) | ||
| 124 | + let tailNode = VNode(leaf: true, vals: v.tail) | ||
| 125 | + var shift = v.shift | ||
| 126 | + var root: VNode | ||
| 127 | + if (v.cnt shr Bits) > (1 shl v.shift): # root overflow: grow a level | ||
| 128 | + root = VNode(leaf: false, kids: @[v.root, newPath(v.shift, tailNode)]) | ||
| 129 | + shift += Bits | ||
| 130 | + else: | ||
| 131 | + root = pushTail(v.cnt, v.shift, v.root, tailNode) | ||
| 132 | + PVec(cnt: v.cnt + 1, shift: shift, root: root, tail: @[x]) | ||
| 133 | + | ||
| 134 | +proc doAssoc(level: int, node: VNode, i: int, x: Value): VNode = | ||
| 135 | + if level == 0: | ||
| 136 | + var vals = node.vals | ||
| 137 | + vals[i and Mask] = x | ||
| 138 | + VNode(leaf: true, vals: vals) | ||
| 139 | + else: | ||
| 140 | + var kids = node.kids | ||
| 141 | + let sub = (i shr level) and Mask | ||
| 142 | + kids[sub] = doAssoc(level - Bits, kids[sub], i, x) | ||
| 143 | + VNode(leaf: false, kids: kids) | ||
| 144 | + | ||
| 145 | +proc vecAssoc*(v: PVec, i: int, x: Value): PVec = | ||
| 146 | + if i == v.cnt: return vecConj(v, x) | ||
| 147 | + if i < 0 or i > v.cnt: err("Index out of bounds: " & $i) | ||
| 148 | + if i >= tailOff(v): | ||
| 149 | + var tail = v.tail | ||
| 150 | + tail[i - tailOff(v)] = x | ||
| 151 | + return PVec(cnt: v.cnt, shift: v.shift, root: v.root, tail: tail) | ||
| 152 | + PVec(cnt: v.cnt, shift: v.shift, root: doAssoc(v.shift, v.root, i, x), | ||
| 153 | + tail: v.tail) | ||
| 154 | + | ||
| 155 | +proc vecToSeq*(v: PVec): seq[Value] = | ||
| 156 | + result = newSeqOfCap[Value](v.cnt) | ||
| 157 | + var i = 0 | ||
| 158 | + while i < v.cnt: | ||
| 159 | + let leaf = leafFor(v, i) | ||
| 160 | + let base = i and not Mask | ||
| 161 | + for j in 0 ..< leaf.len: | ||
| 162 | + if base + j >= v.cnt: break | ||
| 163 | + result.add leaf[j] | ||
| 164 | + i = base + leaf.len | ||
| 165 | + | ||
| 166 | +proc toPVec*(xs: seq[Value]): PVec = | ||
| 167 | + result = emptyPVec() | ||
| 168 | + for x in xs: result = vecConj(result, x) | ||
| 169 | + | ||
| 170 | +# ----------------------------------------------------------------- hashing | ||
| 171 | +proc mixHash(a, b: uint32): uint32 = | ||
| 172 | + ## Boost-style combine; cheap and good enough for trie index bits. | ||
| 173 | + a xor (b + 0x9e3779b9'u32 + (a shl 6) + (a shr 2)) | ||
| 174 | + | ||
| 175 | +# ---------------------------------------------------------- persistent map | ||
| 176 | +proc bitPos(h: uint32, shift: int): uint32 = 1'u32 shl ((h shr shift) and Mask) | ||
| 177 | +proc slotIdx(bitmap, bit: uint32): int = countSetBits(bitmap and (bit - 1)) | ||
| 178 | + | ||
| 179 | +proc emptyPMap*(): PMap = PMap(root: nil, cnt: 0, nextOrd: 0) | ||
| 180 | + | ||
| 181 | +proc mergeEntries(shift: int, h1: uint32, e1: MEntry, | ||
| 182 | + h2: uint32, e2: MEntry): MNode = | ||
| 183 | + if shift > MaxShift: | ||
| 184 | + return MNode(collision: true, kvs: @[e1, e2]) | ||
| 185 | + let b1 = bitPos(h1, shift) | ||
| 186 | + let b2 = bitPos(h2, shift) | ||
| 187 | + if b1 == b2: | ||
| 188 | + MNode(collision: false, bitmap: b1, | ||
| 189 | + slots: @[MSlot(sk: msNode, | ||
| 190 | + node: mergeEntries(shift + Bits, h1, e1, h2, e2))]) | ||
| 191 | + elif b1 < b2: | ||
| 192 | + MNode(collision: false, bitmap: b1 or b2, | ||
| 193 | + slots: @[MSlot(sk: msEntry, e: e1), MSlot(sk: msEntry, e: e2)]) | ||
| 194 | + else: | ||
| 195 | + MNode(collision: false, bitmap: b1 or b2, | ||
| 196 | + slots: @[MSlot(sk: msEntry, e: e2), MSlot(sk: msEntry, e: e1)]) | ||
| 197 | + | ||
| 198 | +proc nodeAssoc(n: MNode, shift: int, h: uint32, k, v: Value, newOrd: int, | ||
| 199 | + added: var bool): MNode = | ||
| 200 | + if n.collision: | ||
| 201 | + for i in 0 ..< n.kvs.len: | ||
| 202 | + if equals(n.kvs[i].key, k): | ||
| 203 | + var kvs = n.kvs | ||
| 204 | + kvs[i].val = v | ||
| 205 | + return MNode(collision: true, kvs: kvs) | ||
| 206 | + added = true | ||
| 207 | + return MNode(collision: true, | ||
| 208 | + kvs: n.kvs & MEntry(key: k, val: v, ord: newOrd)) | ||
| 209 | + let bit = bitPos(h, shift) | ||
| 210 | + let idx = slotIdx(n.bitmap, bit) | ||
| 211 | + var slots = n.slots | ||
| 212 | + if (n.bitmap and bit) != 0: | ||
| 213 | + case slots[idx].sk | ||
| 214 | + of msNode: | ||
| 215 | + slots[idx] = MSlot(sk: msNode, | ||
| 216 | + node: nodeAssoc(slots[idx].node, shift + Bits, h, k, v, newOrd, added)) | ||
| 217 | + of msEntry: | ||
| 218 | + let e = slots[idx].e | ||
| 219 | + if equals(e.key, k): | ||
| 220 | + slots[idx] = MSlot(sk: msEntry, e: MEntry(key: k, val: v, ord: e.ord)) | ||
| 221 | + else: | ||
| 222 | + added = true | ||
| 223 | + slots[idx] = MSlot(sk: msNode, | ||
| 224 | + node: mergeEntries(shift + Bits, hashValue(e.key), e, h, | ||
| 225 | + MEntry(key: k, val: v, ord: newOrd))) | ||
| 226 | + return MNode(collision: false, bitmap: n.bitmap, slots: slots) | ||
| 227 | + added = true | ||
| 228 | + slots.insert(MSlot(sk: msEntry, e: MEntry(key: k, val: v, ord: newOrd)), idx) | ||
| 229 | + MNode(collision: false, bitmap: n.bitmap or bit, slots: slots) | ||
| 230 | + | ||
| 231 | +proc mapAssoc*(m: PMap, k, v: Value): PMap = | ||
| 232 | + let h = hashValue(k) | ||
| 233 | + var added = false | ||
| 234 | + if m.root.isNil: | ||
| 235 | + return PMap(root: MNode(collision: false, bitmap: bitPos(h, 0), | ||
| 236 | + slots: @[MSlot(sk: msEntry, | ||
| 237 | + e: MEntry(key: k, val: v, ord: 0))]), | ||
| 238 | + cnt: 1, nextOrd: 1) | ||
| 239 | + let root = nodeAssoc(m.root, 0, h, k, v, m.nextOrd, added) | ||
| 240 | + PMap(root: root, cnt: m.cnt + (if added: 1 else: 0), | ||
| 241 | + nextOrd: m.nextOrd + (if added: 1 else: 0)) | ||
| 242 | + | ||
| 243 | +proc nodeFind(n: MNode, shift: int, h: uint32, k: Value, | ||
| 244 | + found: var bool): Value = | ||
| 245 | + if n.isNil: return NilV | ||
| 246 | + if n.collision: | ||
| 247 | + for e in n.kvs: | ||
| 248 | + if equals(e.key, k): | ||
| 249 | + found = true | ||
| 250 | + return e.val | ||
| 251 | + return NilV | ||
| 252 | + let bit = bitPos(h, shift) | ||
| 253 | + if (n.bitmap and bit) == 0: return NilV | ||
| 254 | + let slot = n.slots[slotIdx(n.bitmap, bit)] | ||
| 255 | + case slot.sk | ||
| 256 | + of msNode: nodeFind(slot.node, shift + Bits, h, k, found) | ||
| 257 | + of msEntry: | ||
| 258 | + if equals(slot.e.key, k): | ||
| 259 | + found = true | ||
| 260 | + slot.e.val | ||
| 261 | + else: NilV | ||
| 262 | + | ||
| 263 | +proc mapGet*(m: PMap, k: Value, dflt: Value): Value = | ||
| 264 | + var found = false | ||
| 265 | + let v = nodeFind(m.root, 0, hashValue(k), k, found) | ||
| 266 | + if found: v else: dflt | ||
| 267 | + | ||
| 268 | +proc mapContains*(m: PMap, k: Value): bool = | ||
| 269 | + var found = false | ||
| 270 | + discard nodeFind(m.root, 0, hashValue(k), k, found) | ||
| 271 | + found | ||
| 272 | + | ||
| 273 | +proc nodeDissoc(n: MNode, shift: int, h: uint32, k: Value, | ||
| 274 | + removed: var bool): MNode = | ||
| 275 | + if n.collision: | ||
| 276 | + var kvs: seq[MEntry] = @[] | ||
| 277 | + for e in n.kvs: | ||
| 278 | + if equals(e.key, k): removed = true | ||
| 279 | + else: kvs.add e | ||
| 280 | + return (if kvs.len == 0: nil else: MNode(collision: true, kvs: kvs)) | ||
| 281 | + let bit = bitPos(h, shift) | ||
| 282 | + if (n.bitmap and bit) == 0: return n | ||
| 283 | + let idx = slotIdx(n.bitmap, bit) | ||
| 284 | + var slots = n.slots | ||
| 285 | + case slots[idx].sk | ||
| 286 | + of msNode: | ||
| 287 | + let child = nodeDissoc(slots[idx].node, shift + Bits, h, k, removed) | ||
| 288 | + if child.isNil: | ||
| 289 | + slots.delete(idx) | ||
| 290 | + return (if slots.len == 0: nil | ||
| 291 | + else: MNode(collision: false, bitmap: n.bitmap and not bit, | ||
| 292 | + slots: slots)) | ||
| 293 | + slots[idx] = MSlot(sk: msNode, node: child) | ||
| 294 | + MNode(collision: false, bitmap: n.bitmap, slots: slots) | ||
| 295 | + of msEntry: | ||
| 296 | + if not equals(slots[idx].e.key, k): return n | ||
| 297 | + removed = true | ||
| 298 | + slots.delete(idx) | ||
| 299 | + if slots.len == 0: nil | ||
| 300 | + else: MNode(collision: false, bitmap: n.bitmap and not bit, slots: slots) | ||
| 301 | + | ||
| 302 | +proc mapDissoc*(m: PMap, k: Value): PMap = | ||
| 303 | + if m.root.isNil: return m | ||
| 304 | + var removed = false | ||
| 305 | + let root = nodeDissoc(m.root, 0, hashValue(k), k, removed) | ||
| 306 | + if not removed: return m | ||
| 307 | + PMap(root: root, cnt: m.cnt - 1, nextOrd: m.nextOrd) | ||
| 308 | + | ||
| 309 | +proc collect(n: MNode, acc: var seq[MEntry]) = | ||
| 310 | + if n.isNil: return | ||
| 311 | + if n.collision: | ||
| 312 | + for e in n.kvs: acc.add e | ||
| 313 | + return | ||
| 314 | + for slot in n.slots: | ||
| 315 | + case slot.sk | ||
| 316 | + of msEntry: acc.add slot.e | ||
| 317 | + of msNode: collect(slot.node, acc) | ||
| 318 | + | ||
| 319 | +proc mapEntries*(m: PMap): seq[MEntry] = | ||
| 320 | + ## Entries in insertion order. | ||
| 321 | + result = newSeqOfCap[MEntry](m.cnt) | ||
| 322 | + collect(m.root, result) | ||
| 323 | + result.sort(proc (a, b: MEntry): int = cmp(a.ord, b.ord)) | ||
| 324 | + | ||
| 325 | +proc hashValue*(v: Value): uint32 = | ||
| 326 | + if v.isNil: return 0 | ||
| 327 | + case v.kind | ||
| 328 | + of kNil: 0'u32 | ||
| 329 | + of kBool: (if v.b: 0x9e3779b9'u32 else: 0x85ebca6b'u32) | ||
| 330 | + of kInt: uint32(hash(v.i)) | ||
| 331 | + of kFloat: | ||
| 332 | + # ints and floats compare equal across kinds, so they must hash alike | ||
| 333 | + if v.f == float64(int64(v.f)): uint32(hash(int64(v.f))) | ||
| 334 | + else: uint32(hash(v.f)) | ||
| 335 | + of kStr: mixHash(1'u32, uint32(hash(v.s))) | ||
| 336 | + of kKeyword: mixHash(2'u32, uint32(hash(v.s))) | ||
| 337 | + of kSymbol: mixHash(3'u32, uint32(hash(v.s))) | ||
| 338 | + of kList, kVector: | ||
| 339 | + # lists and vectors are `=` when their elements are, so they hash alike | ||
| 340 | + var h = 7'u32 | ||
| 341 | + for x in (if v.kind == kList: v.xs else: vecToSeq(v.vec)): | ||
| 342 | + h = mixHash(h, hashValue(x)) | ||
| 343 | + h | ||
| 344 | + of kSet: | ||
| 345 | + var h = 0'u32 # xor: independent of iteration order | ||
| 346 | + for e in mapEntries(v.m): h = h xor hashValue(e.key) | ||
| 347 | + h | ||
| 348 | + of kMap: | ||
| 349 | + var h = 0'u32 | ||
| 350 | + for e in mapEntries(v.m): | ||
| 351 | + h = h xor mixHash(hashValue(e.key), hashValue(e.val)) | ||
| 352 | + h | ||
| 353 | + of kFn: uint32(hash(cast[int](cast[pointer](v)))) | ||
| 354 | + | ||
| 355 | +# ------------------------------------------------------------ constructors | ||
| 28 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) | 356 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) |
| 29 | proc mkInt*(x: int64): Value = Value(kind: kInt, i: x) | 357 | proc mkInt*(x: int64): Value = Value(kind: kInt, i: x) |
| 30 | proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x) | 358 | proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x) |
| 31 | proc mkStr*(x: string): Value = Value(kind: kStr, s: x) | 359 | proc mkStr*(x: string): Value = Value(kind: kStr, s: x) |
| 32 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x) | 360 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x) |
| 33 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x) | 361 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x) |
| 34 | -proc mkList*(xs: seq[Value]): Value = Value(kind: kList, items: xs) | 362 | +proc mkList*(xs: seq[Value]): Value = Value(kind: kList, xs: xs) |
| 35 | -proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, items: xs) | 363 | +proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, vec: toPVec(xs)) |
| 36 | -proc mkSet*(xs: seq[Value]): Value | 364 | +proc mkVec*(v: PVec): Value = Value(kind: kVector, vec: v) |
| 365 | +proc mkMapOf*(m: PMap): Value = Value(kind: kMap, m: m) | ||
| 366 | +proc mkSetOf*(m: PMap): Value = Value(kind: kSet, m: m) | ||
| 367 | + | ||
| 368 | +proc mkMap*(ps: seq[(Value, Value)]): Value = | ||
| 369 | + var m = emptyPMap() | ||
| 370 | + for (k, v) in ps: m = mapAssoc(m, k, v) | ||
| 371 | + Value(kind: kMap, m: m) | ||
| 372 | + | ||
| 373 | +proc mkSet*(xs: seq[Value]): Value = | ||
| 374 | + var m = emptyPMap() | ||
| 375 | + for x in xs: | ||
| 376 | + if not mapContains(m, x): m = mapAssoc(m, x, x) | ||
| 377 | + Value(kind: kSet, m: m) | ||
| 378 | + | ||
| 37 | proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = | 379 | proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = |
| 38 | Value(kind: kFn, fn: f, name: name) | 380 | Value(kind: kFn, fn: f, name: name) |
| 39 | 381 | ||
| 40 | -proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | 382 | +# --------------------------------------------------------------- accessors |
| 383 | +proc items*(v: Value): seq[Value] = | ||
| 384 | + ## Elements of any sequential value, in order. O(n) — prefer `count`/`nth` | ||
| 385 | + ## when you only need one element. | ||
| 386 | + if v.isNil: return @[] | ||
| 387 | + case v.kind | ||
| 388 | + of kList: v.xs | ||
| 389 | + of kVector: vecToSeq(v.vec) | ||
| 390 | + of kSet: | ||
| 391 | + var r = newSeqOfCap[Value](v.m.cnt) | ||
| 392 | + for e in mapEntries(v.m): r.add e.key | ||
| 393 | + r | ||
| 394 | + else: @[] | ||
| 395 | + | ||
| 396 | +proc pairs*(v: Value): seq[(Value, Value)] = | ||
| 397 | + if v.isNil or v.kind != kMap: return @[] | ||
| 398 | + result = newSeqOfCap[(Value, Value)](v.m.cnt) | ||
| 399 | + for e in mapEntries(v.m): result.add (e.key, e.val) | ||
| 400 | + | ||
| 401 | +proc count*(v: Value): int = | ||
| 402 | + if v.isNil: return 0 | ||
| 403 | + case v.kind | ||
| 404 | + of kNil: 0 | ||
| 405 | + of kList: v.xs.len | ||
| 406 | + of kVector: v.vec.cnt | ||
| 407 | + of kMap, kSet: v.m.cnt | ||
| 408 | + of kStr: v.s.len | ||
| 409 | + else: err("Don't know how to count: " & prStr(v)) | ||
| 41 | 410 | ||
| 42 | proc truthy*(v: Value): bool = | 411 | proc truthy*(v: Value): bool = |
| 43 | if v == nil: return false | 412 | if v == nil: return false |
| @@ -54,9 +423,11 @@ proc equals*(a, b: Value): bool = | |||
| 54 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) | 423 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) |
| 55 | # lists and vectors are sequentially equal in Clojure | 424 | # lists and vectors are sequentially equal in Clojure |
| 56 | if a.kind in {kList, kVector} and b.kind in {kList, kVector}: | 425 | if a.kind in {kList, kVector} and b.kind in {kList, kVector}: |
| 57 | - if a.items.len != b.items.len: return false | 426 | + if count(a) != count(b): return false |
| 58 | - for i in 0 ..< a.items.len: | 427 | + let xs = items(a) |
| 59 | - if not equals(a.items[i], b.items[i]): return false | 428 | + let ys = items(b) |
| 429 | + for i in 0 ..< xs.len: | ||
| 430 | + if not equals(xs[i], ys[i]): return false | ||
| 60 | return true | 431 | return true |
| 61 | if a.kind != b.kind: return false | 432 | if a.kind != b.kind: return false |
| 62 | case a.kind | 433 | case a.kind |
| @@ -66,35 +437,18 @@ proc equals*(a, b: Value): bool = | |||
| 66 | of kFloat: a.f == b.f | 437 | of kFloat: a.f == b.f |
| 67 | of kStr, kKeyword, kSymbol: a.s == b.s | 438 | of kStr, kKeyword, kSymbol: a.s == b.s |
| 68 | of kSet: | 439 | of kSet: |
| 69 | - if a.items.len != b.items.len: return false | 440 | + if a.m.cnt != b.m.cnt: return false |
| 70 | - for x in a.items: | 441 | + for e in mapEntries(a.m): |
| 71 | - var found = false | 442 | + if not mapContains(b.m, e.key): return false |
| 72 | - for y in b.items: | ||
| 73 | - if equals(x, y): found = true; break | ||
| 74 | - if not found: return false | ||
| 75 | true | 443 | true |
| 76 | of kMap: | 444 | of kMap: |
| 77 | - if a.pairs.len != b.pairs.len: return false | 445 | + if a.m.cnt != b.m.cnt: return false |
| 78 | - for (k, v) in a.pairs: | 446 | + let missing = Value(kind: kKeyword, s: "%clonim-missing") |
| 79 | - var found = false | 447 | + for e in mapEntries(a.m): |
| 80 | - for (k2, v2) in b.pairs: | 448 | + if not equals(e.val, mapGet(b.m, e.key, missing)): return false |
| 81 | - if equals(k, k2): | ||
| 82 | - if not equals(v, v2): return false | ||
| 83 | - found = true; break | ||
| 84 | - if not found: return false | ||
| 85 | true | 449 | true |
| 86 | of kFn: a == b | 450 | of kFn: a == b |
| 87 | of kList, kVector: false # handled above | 451 | of kList, kVector: false # handled above |
| 88 | - | ||
| 89 | -proc mkSet*(xs: seq[Value]): Value = | ||
| 90 | - var acc: seq[Value] = @[] | ||
| 91 | - for x in xs: | ||
| 92 | - var dup = false | ||
| 93 | - for y in acc: | ||
| 94 | - if equals(x, y): dup = true; break | ||
| 95 | - if not dup: acc.add x | ||
| 96 | - Value(kind: kSet, items: acc) | ||
| 97 | - | ||
| 98 | # ---------------------------------------------------------------- printing | 452 | # ---------------------------------------------------------------- printing |
| 99 | proc escapeStr(s: string): string = | 453 | proc escapeStr(s: string): string = |
| 100 | result = "\"" | 454 | result = "\"" |
| @@ -184,19 +538,13 @@ proc call*(f: Value, args: seq[Value]): Value = | |||
| 184 | if args.len == 0: err("Wrong number of args to keyword") | 538 | if args.len == 0: err("Wrong number of args to keyword") |
| 185 | let m = args[0] | 539 | let m = args[0] |
| 186 | if m.isNil or m.kind != kMap: return NilV | 540 | if m.isNil or m.kind != kMap: return NilV |
| 187 | - for (k, v) in m.pairs: | 541 | + mapGet(m.m, f, (if args.len > 1: args[1] else: NilV)) |
| 188 | - if equals(k, f): return v | ||
| 189 | - (if args.len > 1: args[1] else: NilV) | ||
| 190 | of kMap: | 542 | of kMap: |
| 191 | if args.len == 0: err("Wrong number of args to map") | 543 | if args.len == 0: err("Wrong number of args to map") |
| 192 | - for (k, v) in f.pairs: | 544 | + mapGet(f.m, args[0], (if args.len > 1: args[1] else: NilV)) |
| 193 | - if equals(k, args[0]): return v | ||
| 194 | - (if args.len > 1: args[1] else: NilV) | ||
| 195 | of kVector: | 545 | of kVector: |
| 196 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") | 546 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") |
| 197 | - let i = int(args[0].i) | 547 | + vecNth(f.vec, int(args[0].i)) |
| 198 | - if i < 0 or i >= f.items.len: err("Index out of bounds: " & $i) | ||
| 199 | - f.items[i] | ||
| 200 | else: err("Can't call value of kind " & $f.kind & ": " & prStr(f)) | 548 | else: err("Can't call value of kind " & $f.kind & ": " & prStr(f)) |
| 201 | 549 | ||
| 202 | proc argAt*(args: seq[Value], i: int): Value = | 550 | proc argAt*(args: seq[Value], i: int): Value = |
| @@ -222,9 +570,8 @@ proc toSeq*(v: Value): seq[Value] = | |||
| 222 | r | 570 | r |
| 223 | of kMap: | 571 | of kMap: |
| 224 | var r: seq[Value] = @[] | 572 | var r: seq[Value] = @[] |
| 225 | - for (k, val) in v.pairs: r.add mkVector(@[k, val]) | 573 | + for e in mapEntries(v.m): r.add mkVector(@[e.key, e.val]) |
| 226 | r | 574 | r |
| 227 | else: err("Don't know how to create seq from: " & prStr(v)) | 575 | else: err("Don't know how to create seq from: " & prStr(v)) |
| 228 | 576 | ||
| 229 | -proc mkMap*(ps: seq[(Value, Value)]): Value = Value(kind: kMap, pairs: ps) | ||
| 230 | let emptyArgs*: seq[Value] = @[] | 577 | let emptyArgs*: seq[Value] = @[] |
added
tests/persistent.expected +23 -0 | new file mode 100644 | ||
| @@ -0,0 +1,23 @@ | ||
| 1 | +count 5000 | |
| 2 | +nth 0 33 1024 4999 | |
| 3 | +sum 12497500 | |
| 4 | +assoc :x 1234 5000 | |
| 5 | +last 4999 0 | |
| 6 | +eq true | |
| 7 | +mcount 5000 0 24990001 nil | |
| 8 | +dissoc 4999 nil 10000 | |
| 9 | +keys-sum 12497500 | |
| 10 | +vals-sum 41654167500 | |
| 11 | +meq true | |
| 12 | +scount 5000 true false | |
| 13 | +sconj 5001 5000 | |
| 14 | +{:b 1, :a 2, c 3, 4 5, [1 2] 6} | |
| 15 | +(:b :a c 4 [1 2]) | |
| 16 | +{:b 1, :a 99, c 3, 4 5, [1 2] 6} | |
| 17 | +6 5 3 | |
| 18 | +true | |
| 19 | +true | |
| 20 | +:int :flt | |
| 21 | +{1 2, 2 1, 3 3} | |
| 22 | +{true [0 2 4 6 8], false [1 3 5 7 9]} | |
| 23 | +{:a 2} | |
| new file mode 100644 | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | +count 5000 | ||
| 2 | +nth 0 33 1024 4999 | ||
| 3 | +sum 12497500 | ||
| 4 | +assoc :x 1234 5000 | ||
| 5 | +last 4999 0 | ||
| 6 | +eq true | ||
| 7 | +mcount 5000 0 24990001 nil | ||
| 8 | +dissoc 4999 nil 10000 | ||
| 9 | +keys-sum 12497500 | ||
| 10 | +vals-sum 41654167500 | ||
| 11 | +meq true | ||
| 12 | +scount 5000 true false | ||
| 13 | +sconj 5001 5000 | ||
| 14 | +{:b 1, :a 2, c 3, 4 5, [1 2] 6} | ||
| 15 | +(:b :a c 4 [1 2]) | ||
| 16 | +{:b 1, :a 99, c 3, 4 5, [1 2] 6} | ||
| 17 | +6 5 3 | ||
| 18 | +true | ||
| 19 | +true | ||
| 20 | +:int :flt | ||
| 21 | +{1 2, 2 1, 3 3} | ||
| 22 | +{true [0 2 4 6 8], false [1 3 5 7 9]} | ||
| 23 | +{:a 2} | ||