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
This commit is contained in:
2026-05-08 17:58:00 +03:00
parent dbf05f4e96
commit d763e25638
7 changed files with 493 additions and 61 deletions
+247
View File
@@ -0,0 +1,247 @@
# Persistent Vector — Hash Array Mapped Trie
# Clojure-style 32-way branching with structural sharing
type
PVecNode*[T] = ref object
isLeaf*: bool
# Using seq instead of array for flexibility during building
children*: seq[PVecNode[T]] # internal nodes
values*: seq[T] # leaf nodes (max 32)
PersistentVector*[T] = object
root*: PVecNode[T]
tail*: seq[T]
count*: int
shift*: int # tree depth * 5 bits
const
BRANCHING_BITS = 5
BRANCHING_FACTOR = 1 shl BRANCHING_BITS # 32
BRANCHING_MASK = BRANCHING_FACTOR - 1 # 0x1F
# ---- Node helpers ----
proc newLeafNode[T](vals: seq[T] = @[]): PVecNode[T] =
PVecNode[T](isLeaf: true, values: vals)
proc newInternalNode[T](children: seq[PVecNode[T]] = @[]): PVecNode[T] =
PVecNode[T](isLeaf: false, children: children)
proc copyNode[T](n: PVecNode[T]): PVecNode[T] =
if n.isNil: return nil
if n.isLeaf:
newLeafNode[T](n.values)
else:
newInternalNode[T](n.children)
# ---- Debug ----
proc `$`*[T](v: PersistentVector[T]): string =
result = "PersistentVector(count=" & $v.count & ", shift=" & $v.shift & ", tail=["
for i in 0..<v.tail.len:
if i > 0: result.add(", ")
result.add($v.tail[i])
result.add("])")
# ---- nth: Get element at index ----
proc pvecNth*[T](v: PersistentVector[T], index: int): T =
if index < 0 or index >= v.count:
raise newException(IndexDefect, "Index out of bounds: " & $index & " (count: " & $v.count & ")")
# Check tail first
let tailOffset = v.count - v.tail.len
if index >= tailOffset:
return v.tail[index - tailOffset]
# Walk the trie
var node = v.root
var level = v.shift
while level > 0:
let childIdx = (index shr level) and BRANCHING_MASK
if childIdx >= node.children.len:
raise newException(IndexDefect, "Corrupt vector: child index " & $childIdx & " at level " & $level)
node = node.children[childIdx]
if node.isNil:
raise newException(IndexDefect, "Corrupt vector: nil node at level " & $level)
level -= BRANCHING_BITS
let leafIdx = index and BRANCHING_MASK
if leafIdx >= node.values.len:
raise newException(IndexDefect, "Corrupt vector: leaf index " & $leafIdx)
return node.values[leafIdx]
# ---- conj: Append element ----
proc pushLeaf[T](node: PVecNode[T], index: int, shift: int, val: seq[T]): PVecNode[T] =
# Push a full leaf (seq of up to 32 values) into the tree at position 'index'
result = copyNode(node)
if shift == 0:
# Should not happen — we always push into internal nodes
result = newLeafNode[T](val)
else:
let childIdx = (index shr shift) and BRANCHING_MASK
if childIdx >= result.children.len:
# Need to grow children array
let oldLen = result.children.len
result.children.setLen(childIdx + 1)
for i in oldLen..<childIdx:
result.children[i] = nil
var child = result.children[childIdx]
if child.isNil:
if shift == BRANCHING_BITS:
child = newLeafNode[T](val)
else:
child = pushLeaf(newInternalNode[T](), index, shift - BRANCHING_BITS, val)
else:
child = pushLeaf(child, index, shift - BRANCHING_BITS, val)
result.children[childIdx] = child
proc newPath[T](shift: int, leaf: PVecNode[T]): PVecNode[T] =
# Create a path of empty internal nodes from shift down to leaf level
if shift == 0:
return leaf
var node = newInternalNode[T](@[leaf])
var level = BRANCHING_BITS
while level < shift:
node = newInternalNode[T](@[node])
level += BRANCHING_BITS
return node
proc pvecConj*[T](v: PersistentVector[T], val: T): PersistentVector[T] =
result = v
result.count += 1
# Case 1: Room in tail
if v.tail.len < BRANCHING_FACTOR:
result.tail.add(val)
return
# Case 2: Tail is full — promote it into tree
let tailOffset = v.count - v.tail.len
let oldTail = v.tail
result.tail = @[val] # New tail with just the new element
if v.root.isNil:
# First tail promotion — root becomes the old tail
result.root = newLeafNode[T](oldTail)
result.shift = 0
return
# Check if tree needs to grow deeper
# A tree at shift S can hold up to 32^(S/5 + 1) leaves
# If tailOffset >> shift has bits beyond shift, we need a new root
let needsNewRoot = (tailOffset shr v.shift) > 0
if needsNewRoot:
# Create new root pointing to old root
let newRoot = newInternalNode[T](@[v.root])
result.root = pushLeaf(newRoot, tailOffset, v.shift + BRANCHING_BITS, oldTail)
result.shift += BRANCHING_BITS
else:
result.root = pushLeaf(v.root, tailOffset, v.shift, oldTail)
# ---- assoc: Set element at index ----
proc doAssoc[T](node: PVecNode[T], index: int, shift: int, val: T): PVecNode[T] =
result = copyNode(node)
if shift == 0:
# Leaf level
let leafIdx = index and BRANCHING_MASK
if leafIdx >= result.values.len:
raise newException(IndexDefect, "assoc leaf index out of bounds: " & $leafIdx)
result.values[leafIdx] = val
else:
let childIdx = (index shr shift) and BRANCHING_MASK
if childIdx >= result.children.len or result.children[childIdx].isNil:
raise newException(IndexDefect, "assoc: nil child at index " & $childIdx)
result.children[childIdx] = doAssoc(result.children[childIdx], index, shift - BRANCHING_BITS, val)
proc pvecAssoc*[T](v: PersistentVector[T], index: int, val: T): PersistentVector[T] =
if index < 0 or index >= v.count:
raise newException(IndexDefect, "Index out of bounds: " & $index)
result = v
let tailOffset = v.count - v.tail.len
if index >= tailOffset:
# In tail — copy-on-write
result.tail[index - tailOffset] = val
return
# In tree — path copy
result.root = doAssoc(v.root, index, v.shift, val)
# ---- pop: Remove last element ----
proc pvecPop*[T](v: PersistentVector[T]): PersistentVector[T] =
if v.count == 0:
raise newException(IndexDefect, "Can't pop empty vector")
result = v
result.count -= 1
if v.tail.len > 1:
result.tail.setLen(v.tail.len - 1)
return
if v.tail.len == 1:
# Tail had exactly 1 element. Pull previous leaf from tree into tail.
if v.count == 1:
# Now empty
result = PersistentVector[T]()
return
let tailOffset = v.count - v.tail.len - BRANCHING_FACTOR
if tailOffset < 0:
# Only tail existed
result.tail = @[]
return
# Walk to the leaf that becomes the new tail
var node = v.root
var level = v.shift
while level > 0:
let childIdx = (tailOffset shr level) and BRANCHING_MASK
if childIdx < node.children.len:
node = node.children[childIdx]
else:
node = nil
break
level -= BRANCHING_BITS
if not node.isNil and node.isLeaf:
result.tail = node.values
else:
result.tail = @[]
# If tree is now empty, clear it
if result.count <= BRANCHING_FACTOR:
result.root = nil
result.shift = 0
return
# tail was empty (shouldn't happen with correct invariants)
result.tail = @[]
# ---- Builders ----
proc newPersistentVector*[T](items: seq[T] = @[]): PersistentVector[T] =
for item in items:
result = pvecConj(result, item)
proc toSeq*[T](v: PersistentVector[T]): seq[T] =
result = newSeq[T](v.count)
for i in 0..<v.count:
result[i] = pvecNth(v, i)
iterator items*[T](v: PersistentVector[T]): T =
for i in 0..<v.count:
yield pvecNth(v, i)
# Compatibility helpers (seq-like interface)
proc len*[T](v: PersistentVector[T]): int = v.count
proc `[]`*[T](v: PersistentVector[T], idx: int): T = pvecNth(v, idx)
proc `[]`*[T](v: PersistentVector[T], idx: BackwardsIndex): T = pvecNth(v, v.count - int(idx))
+56 -54
View File
@@ -1,3 +1,4 @@
import cljnim_pvec
import strutils, sequtils, hashes, algorithm, os, osproc
type
@@ -16,7 +17,7 @@ type
of ckKeyword: kwName*: string
of ckSymbol: symName*: string
of ckList: listItems*: seq[CljVal]
of ckVector: vecData*: seq[CljVal]
of ckVector: vecData*: PersistentVector[CljVal]
of ckMap:
mapKeys*: seq[CljVal]
mapVals*: seq[CljVal]
@@ -34,7 +35,7 @@ proc cljString*(v: string): CljVal = CljVal(kind: ckString, strVal: v)
proc cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: v)
proc cljSymbol*(v: string): CljVal = CljVal(kind: ckSymbol, symName: v)
proc cljList*(items: seq[CljVal]): CljVal = CljVal(kind: ckList, listItems: items)
proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, vecData: items)
proc cljVector*(items: seq[CljVal]): CljVal = CljVal(kind: ckVector, vecData: newPersistentVector(items))
proc cljFn*(p: proc(args: seq[CljVal]): CljVal): CljVal = CljVal(kind: ckFn, fnProc: p)
proc cljMap*(keys, vals: seq[CljVal]): CljVal =
@@ -77,7 +78,7 @@ proc cljRepr*(v: CljVal): string =
of ckKeyword: ":" & v.kwName
of ckSymbol: v.symName
of ckList: "(" & v.listItems.mapIt(cljRepr(it)).join(" ") & ")"
of ckVector: "[" & v.vecData.mapIt(cljRepr(it)).join(" ") & "]"
of ckVector: "[" & toSeq(v.vecData).mapIt(cljRepr(it)).join(" ") & "]"
of ckMap:
var parts: seq[string] = @[]
for i in 0..<v.mapKeys.len:
@@ -334,7 +335,7 @@ proc cljCount*(v: CljVal): int =
if v.isNil: return 0
case v.kind
of ckList: v.listItems.len
of ckVector: v.vecData.len
of ckVector: v.vecData.count
of ckMap: v.mapKeys.len
of ckString: v.strVal.len
else: 0
@@ -346,8 +347,8 @@ proc cljFirst*(v: CljVal): CljVal =
if v.listItems.len == 0: cljNil()
else: v.listItems[0]
of ckVector:
if v.vecData.len == 0: cljNil()
else: v.vecData[0]
if v.vecData.count == 0: cljNil()
else: pvecNth(v.vecData, 0)
else: cljNil()
proc cljRest*(v: CljVal): CljVal =
@@ -357,8 +358,8 @@ proc cljRest*(v: CljVal): CljVal =
if v.listItems.len <= 1: cljList(@[])
else: cljList(v.listItems[1..^1])
of ckVector:
if v.vecData.len <= 1: cljList(@[])
else: cljList(v.vecData[1..^1])
if v.vecData.count <= 1: cljList(@[])
else: cljList(toSeq(v.vecData)[1..^1])
else: cljList(@[])
proc cljNext*(v: CljVal): CljVal =
@@ -374,20 +375,21 @@ proc cljLast*(v: CljVal): CljVal =
if v.listItems.len == 0: cljNil()
else: v.listItems[^1]
of ckVector:
if v.vecData.len == 0: cljNil()
else: v.vecData[^1]
if v.vecData.count == 0: cljNil()
else: pvecNth(v.vecData, v.vecData.count - 1)
else: cljNil()
proc cljNth*(v: CljVal, n: int): CljVal =
proc cljNth*(v: CljVal, n: CljVal): CljVal =
let idx = n.intVal
case v.kind
of ckList:
if n < 0 or n >= v.listItems.len:
if idx < 0 or idx >= v.listItems.len:
raise newException(IndexDefect, "nth: index out of range")
v.listItems[n]
v.listItems[idx]
of ckVector:
if n < 0 or n >= v.vecData.len:
if idx < 0 or idx >= v.vecData.count:
raise newException(IndexDefect, "nth: index out of range")
v.vecData[n]
pvecNth(v.vecData, idx)
else:
raise newException(CatchableError, "nth requires a collection")
@@ -400,7 +402,7 @@ proc cljConj*(coll: CljVal, item: CljVal): CljVal =
newItems.add(coll.listItems)
cljList(newItems)
of ckVector:
var newItems = coll.vecData
var newItems = toSeq(coll.vecData)
newItems.add(item)
cljVector(newItems)
else:
@@ -416,7 +418,7 @@ proc cljCons*(item: CljVal, coll: CljVal): CljVal =
cljList(newItems)
of ckVector:
var newItems = @[item]
newItems.add(coll.vecData)
newItems.add(toSeq(coll.vecData))
cljList(newItems)
else:
cljList(@[item])
@@ -428,8 +430,8 @@ proc cljSeq*(v: CljVal): CljVal =
if v.listItems.len == 0: cljNil()
else: v
of ckVector:
if v.vecData.len == 0: cljNil()
else: cljList(v.vecData)
if v.vecData.count == 0: cljNil()
else: cljList(toSeq(v.vecData))
of ckString:
if v.strVal.len == 0: cljNil()
else:
@@ -455,7 +457,7 @@ proc cljConcat*(args: seq[CljVal]): CljVal =
if not a.isNil:
case a.kind
of ckList: items.add(a.listItems)
of ckVector: items.add(a.vecData)
of ckVector: items.add(toSeq(a.vecData))
else: discard
cljList(items)
@@ -463,14 +465,14 @@ proc cljTake*(n: int, coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
case coll.kind
of ckList: cljList(coll.listItems[0..<min(n, coll.listItems.len)])
of ckVector: cljList(coll.vecData[0..<min(n, coll.vecData.len)])
of ckVector: cljList(toSeq(coll.vecData)[0..<min(n, coll.vecData.count)])
else: cljList(@[])
proc cljDrop*(n: int, coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
case coll.kind
of ckList: cljList(coll.listItems[min(n, coll.listItems.len)..^1])
of ckVector: cljList(coll.vecData[min(n, coll.vecData.len)..^1])
of ckVector: cljList(toSeq(coll.vecData)[min(n, coll.vecData.count)..^1])
else: cljList(@[])
proc cljReverse*(coll: CljVal): CljVal =
@@ -481,7 +483,7 @@ proc cljReverse*(coll: CljVal): CljVal =
items.reverse()
cljList(items)
of ckVector:
var items = coll.vecData
var items = toSeq(coll.vecData)
items.reverse()
cljList(items)
else: cljList(@[])
@@ -497,7 +499,7 @@ proc cljSort*(coll: CljVal): CljVal =
return 0)
cljList(items)
of ckVector:
var items = coll.vecData
var items = toSeq(coll.vecData)
items.sort(proc(a, b: CljVal): int =
if a.kind == ckInt and b.kind == ckInt:
return cmp(a.intVal, b.intVal)
@@ -515,7 +517,7 @@ proc cljDistinct*(coll: CljVal): CljVal =
seen.add(item)
cljList(seen)
of ckVector:
for item in coll.vecData:
for item in coll.vecData.items:
if item notin seen:
seen.add(item)
cljList(seen)
@@ -531,7 +533,7 @@ proc cljFlatten*(coll: CljVal): CljVal =
for item in v.listItems:
flattenHelper(item)
of ckVector:
for item in v.vecData:
for item in v.vecData.items:
flattenHelper(item)
else:
result.add(v)
@@ -543,7 +545,7 @@ proc cljPartition*(n: int, coll: CljVal): CljVal =
var items: seq[CljVal]
case coll.kind
of ckList: items = coll.listItems
of ckVector: items = coll.vecData
of ckVector: items = toSeq(coll.vecData)
else: return cljList(@[])
var parts: seq[CljVal] = @[]
var i = 0
@@ -559,7 +561,7 @@ proc cljFrequencies*(coll: CljVal): CljVal =
var items: seq[CljVal]
case coll.kind
of ckList: items = coll.listItems
of ckVector: items = coll.vecData
of ckVector: items = toSeq(coll.vecData)
else: return cljMap(@[], @[])
for item in items:
var found = false
@@ -580,7 +582,7 @@ proc cljGroupBy*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
var items: seq[CljVal]
case coll.kind
of ckList: items = coll.listItems
of ckVector: items = coll.vecData
of ckVector: items = toSeq(coll.vecData)
else: return cljMap(@[], @[])
for item in items:
let key = f(@[item])
@@ -708,7 +710,7 @@ proc cljMap*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
case coll.kind
of ckList: cljList(cljMapSeq(f, coll.listItems))
of ckVector: cljList(cljMapSeq(f, coll.vecData))
of ckVector: cljList(cljMapSeq(f, toSeq(coll.vecData)))
else: cljList(@[])
proc cljMap*(f: CljVal, coll: CljVal): CljVal =
@@ -719,7 +721,7 @@ proc cljFilter*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
if coll.isNil: return cljList(@[])
case coll.kind
of ckList: cljList(cljFilterSeq(f, coll.listItems))
of ckVector: cljList(cljFilterSeq(f, coll.vecData))
of ckVector: cljList(cljFilterSeq(f, toSeq(coll.vecData)))
else: cljList(@[])
proc cljFilter*(f: CljVal, coll: CljVal): CljVal =
@@ -730,7 +732,7 @@ proc cljReduce*(f: proc(args: seq[CljVal]): CljVal, init: CljVal, coll: CljVal):
if coll.isNil: return init
case coll.kind
of ckList: cljReduceSeq(f, init, coll.listItems)
of ckVector: cljReduceSeq(f, init, coll.vecData)
of ckVector: cljReduceSeq(f, init, toSeq(coll.vecData))
else: init
proc cljReduce*(f: CljVal, init: CljVal, coll: CljVal): CljVal =
@@ -741,7 +743,7 @@ proc cljMapv*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal =
if coll.isNil: return cljVector(@[])
case coll.kind
of ckList: cljVector(cljMapSeq(f, coll.listItems))
of ckVector: cljVector(cljMapSeq(f, coll.vecData))
of ckVector: cljVector(cljMapSeq(f, toSeq(coll.vecData)))
else: cljVector(@[])
proc cljMapv*(f: CljVal, coll: CljVal): CljVal =
@@ -754,7 +756,7 @@ proc cljSome*(f: CljVal, coll: CljVal): CljVal =
var items: seq[CljVal]
case coll.kind
of ckList: items = coll.listItems
of ckVector: items = coll.vecData
of ckVector: items = toSeq(coll.vecData)
else: return cljNil()
for item in items:
let r = fn(@[item])
@@ -768,7 +770,7 @@ proc cljEvery*(f: CljVal, coll: CljVal): CljVal =
var items: seq[CljVal]
case coll.kind
of ckList: items = coll.listItems
of ckVector: items = coll.vecData
of ckVector: items = toSeq(coll.vecData)
else: return cljBool(true)
for item in items:
let r = fn(@[item])
@@ -782,7 +784,7 @@ proc cljApply*(f: CljVal, args: CljVal): CljVal =
if not args.isNil:
case args.kind
of ckList: argSeq = args.listItems
of ckVector: argSeq = args.vecData
of ckVector: argSeq = toSeq(args.vecData)
else: argSeq = @[args]
f.fnProc(argSeq)
@@ -844,12 +846,12 @@ proc cljStrJoin*(args: seq[CljVal]): CljVal =
if args.len == 1:
case args[0].kind
of ckList: return cljString(args[0].listItems.mapIt(cljStr(it)).join(""))
of ckVector: return cljString(args[0].vecData.mapIt(cljStr(it)).join(""))
of ckVector: return cljString(toSeq(args[0].vecData).mapIt(cljStr(it)).join(""))
else: return cljString(cljStr(args[0]))
let sep = cljStr(args[0])
case args[1].kind
of ckList: cljString(args[1].listItems.mapIt(cljStr(it)).join(sep))
of ckVector: cljString(args[1].vecData.mapIt(cljStr(it)).join(sep))
of ckVector: cljString(toSeq(args[1].vecData).mapIt(cljStr(it)).join(sep))
else: cljString(cljStr(args[1]))
proc cljStrSplit*(s: CljVal, sep: CljVal): CljVal =
@@ -966,12 +968,12 @@ proc cljInto*(to: CljVal, src: CljVal): CljVal =
of ckVector:
case src.kind
of ckList:
var items = to.vecData
var items = toSeq(to.vecData)
items.add(src.listItems)
cljVector(items)
of ckVector:
var items = to.vecData
items.add(src.vecData)
var items = toSeq(to.vecData)
items.add(toSeq(src.vecData))
cljVector(items)
else: to
of ckList:
@@ -982,7 +984,7 @@ proc cljInto*(to: CljVal, src: CljVal): CljVal =
cljList(items)
of ckVector:
var items = to.listItems
items.add(src.vecData)
items.add(toSeq(src.vecData))
cljList(items)
else: to
of ckMap:
@@ -1003,32 +1005,32 @@ proc cljInto*(to: CljVal, src: CljVal): CljVal =
elif src.kind == ckVector:
var keys = to.mapKeys
var vals = to.mapVals
for pair in src.vecData:
if pair.kind == ckVector and pair.vecData.len == 2:
for pair in src.vecData.items:
if pair.kind == ckVector and pair.vecData.count == 2:
var found = false
for j in 0..<keys.len:
if keys[j] == pair.vecData[0]:
vals[j] = pair.vecData[1]
if keys[j] == pvecNth(pair.vecData, 0):
vals[j] = pvecNth(pair.vecData, 1)
found = true
break
if not found:
keys.add(pair.vecData[0])
vals.add(pair.vecData[1])
keys.add(pvecNth(pair.vecData, 0))
vals.add(pvecNth(pair.vecData, 1))
cljMap(keys, vals)
elif src.kind == ckVector:
var keys = to.mapKeys
var vals = to.mapVals
for pair in src.vecData:
if pair.kind == ckVector and pair.vecData.len == 2:
for pair in src.vecData.items:
if pair.kind == ckVector and pair.vecData.count == 2:
var found = false
for j in 0..<keys.len:
if keys[j] == pair.vecData[0]:
vals[j] = pair.vecData[1]
if keys[j] == pvecNth(pair.vecData, 0):
vals[j] = pvecNth(pair.vecData, 1)
found = true
break
if not found:
keys.add(pair.vecData[0])
vals.add(pair.vecData[1])
keys.add(pvecNth(pair.vecData, 0))
vals.add(pvecNth(pair.vecData, 1))
cljMap(keys, vals)
else: to
else: to