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.