jolt/test/unit
Dmitri Sotnikov 91e246c682
Persistent hash map: HAMT instead of O(n) copy-on-write (jolt-684u) (#136)
* Add benchmark suite for alloc/dispatch/collection workloads (jolt-1r86)

The ray tracer is float-compute-bound (devirt, alloc removal, type-proving all
measured flat on it), so it can't validate the optimization passes. Add a small
cross-language suite (AWFY + CLBG style, portable Clojure) isolating the axes it
misses:

  binary-trees  allocation / GC pressure (escaping short-lived records)
  dispatch      megamorphic protocol dispatch (~1M dispatches/s; WP can't devirt)
  collections   persistent map/vector churn

bench/run.sh runs them; bench/README.md maps each to the pass it exercises.

collections immediately surfaced jolt-684u: the persistent hash map is O(n) per
assoc (flat copy-on-write bucket array, not a HAMT) — n=4000 assocs take 50s.
Invisible to the ray tracer (no maps).

* Persistent hash map: HAMT instead of O(n) copy-on-write (jolt-684u)

The map was a flat bucket array whose assoc copied the whole array every insert
(O(n)/assoc, O(n^2) to build). Compounding it, small maps are Janet structs that
only promoted to phm for collection keys — never for size — so a scalar-key map
stayed an O(n)-copy struct forever. Building a 4000-entry map took ~50s.

Two fixes, following ClojureScript's design:

- phm.janet is now a HAMT (hash array mapped trie): BitmapIndexedNode /
  ArrayNode / HashCollisionNode, 32-way, 5 hash bits per level, structural
  sharing — assoc/dissoc/get are O(log32 n). Translated from cljs.core, adapted
  to Janet's 32-bit bit-ops (the hash is carried unsigned, the level index is
  extracted with arithmetic, and bits are tested with band against 1<<i since
  brushift rejects negative bitmaps). The public phm-* API and the value shape
  (:jolt/type :jolt/phm, :cnt) are unchanged; transients are a separate rep and
  untouched.

- core_coll promotes a struct map to a phm past 8 entries (not only for
  collection keys), mirroring cljs PersistentArrayMap -> PersistentHashMap, so
  incremental building isn't O(n^2).

20000 raw assocs: 7.1s -> 0.105s. The collections benchmark: 16.7s -> 0.2s.
Correctness covered by test/unit/phm-hamt-test.janet (oracle vs a Janet table,
nil keys, dissoc, a real hash-collision pair, and a sub-linear-assoc guard);
full gate green.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-16 05:01:22 +00:00
..
compile-fallback-test.janet core: AOT context image — init-cached recovers the bootstrap cost across processes 2026-06-10 13:57:37 -04:00
config-test.janet Refactor phase 4: lift run-mode into config.janet + fix the cache-key footgun 2026-06-15 03:15:24 -04:00
eval-test.janet core: AOT context image — init-cached recovers the bootstrap cost across processes 2026-06-10 13:57:37 -04:00
evaluator-test.janet core: AOT context image — init-cached recovers the bootstrap cost across processes 2026-06-10 13:57:37 -04:00
interop-test.janet core: AOT context image — init-cached recovers the bootstrap cost across processes 2026-06-10 13:57:37 -04:00
lazy-seq-test.janet core: AOT context image — init-cached recovers the bootstrap cost across processes 2026-06-10 13:57:37 -04:00
macro-test.janet test: restructure into unit / integration / spec layers + shared harness 2026-06-05 00:00:16 -04:00
persistent-map-test.janet phm: grow the bucket array past load factor 2 (map-read 4.5x recovery) 2026-06-10 17:06:03 -04:00
phm-hamt-test.janet Persistent hash map: HAMT instead of O(n) copy-on-write (jolt-684u) (#136) 2026-06-16 05:01:22 +00:00
reader-test.janet Protocol/interop fixes to run metosin/malli (jolt-ltwk) (#105) 2026-06-15 03:20:33 +00:00
seed-overlay-registry-test.janet Refactor phase 5d/5e: seed↔overlay registry + rep↔API boundary docs (jolt-bvek) 2026-06-15 04:22:05 -04:00
types-test.janet Compiler research (#10) 2026-06-09 07:30:25 +08:00