Files
bara-lang/docs/PHASE5_HAMT.md
T
dimgigov d763e25638 Phase 5: HAMT Persistent Vector + CI fixes
- Add lib/cljnim_pvec.nim: 32-way HAMT Persistent Vector with structural sharing
- Migrate ckVector from seq[CljVal] to PersistentVector[CljVal] in runtime
- Fix recursive pushLeaf bug (nil nodes at depth > 2)
- Fix defn/if return value bug: replace discard with result = in proc bodies
- Fix cljNth to accept CljVal index
- Add len and [] overloads for PersistentVector seq compatibility
- Add tests/test_pvec.nim (14 tests)
- Update .gitlab-ci.yml to nim:2.2.10 and add test_pvec
- Update docs/ROADMAP.md and add PHASE5_HAMT.md
2026-05-08 17:58:00 +03:00

2.9 KiB

Phase 5: HAMT Persistent Vector

Status: COMPLETE

What was implemented

Hash Array Mapped Trie (HAMT) Persistent Vector — 32-way branching trie with path-copying structural sharing, matching Clojure's PersistentVector semantics.

  • lib/cljnim_pvec.nim — Core implementation
    • PersistentVector[T] — root pointer + count + shift + tail
    • pvecConj — O(log₃₂ N) append with structural sharing
    • pvecNth — O(log₃₂ N) random access
    • pvecAssoc — O(log₃₂ N) update with path copying
    • pvecPop — O(log₃₂ N) remove last
    • newPersistentVector — builder from seq[T]
    • toSeq — convert back to seq[T]
    • len, [] overloads for seq-like interface

Integration into Runtime

  • lib/cljnim_runtime.nim: ckVector now stores PersistentVector[CljVal]
  • All 51 vecData references migrated to use pvecNth/toSeq/count
  • cljVector constructor builds from seq[CljVal] via newPersistentVector

Architecture Decisions

  1. Tail optimization: Like Clojure, maintains a 32-element tail buffer for fast append of recent items
  2. Lazy tree growth: Root promotion only happens when tree is full (needsNewRoot check)
  3. Path copying: Every conj/assoc copies only the nodes along the path to the changed leaf
  4. Seq compatibility: Added len and [] overloads so mapIt, join, items iterator work transparently

Bug fixes during implementation

  • Recursive pushLeaf bug: When child.isNil and shift != BRANCHING_BITS, was creating empty internal nodes without recursing to place the leaf. Fixed by calling pushLeaf(newInternalNode(), index, shift-5, val) instead of just newInternalNode().

Tests

  • tests/test_pvec.nim — 14 tests covering:
    • Empty vector, single element, few elements
    • conj one-by-one (100 elements)
    • conj across tail boundary (32)
    • conj across second level (1024)
    • conj large (10000)
    • assoc in tail and tree
    • assoc preserves old vector (structural sharing)
    • pop from tail, across boundary, to empty

Performance Characteristics

Operation Complexity Notes
conj O(log₃₂ N) ~1-3 node copies
nth O(log₃₂ N) ~3-5 pointer hops
assoc O(log₃₂ N) ~1-3 node copies
pop O(log₃₂ N) May pull leaf into tail
toSeq O(N) Full traversal

For N ≤ 1M, depth ≤ 3 (shift ≤ 15), making operations effectively constant-time.

Migration Guide for Other AIs

If you need to work with vectors in cljnim_runtime.nim:

# Instead of:
v.vecData.len        # Use: v.vecData.count
v.vecData[0]         # Use: pvecNth(v.vecData, 0)
v.vecData[1..^1]     # Use: toSeq(v.vecData)[1..^1]
for item in v.vecData:  # Use: for item in v.vecData.items:

mapIt from sequtils works transparently thanks to len + items iterator overloads.