diff --git a/src/jolt/core_coll.janet b/src/jolt/core_coll.janet index dffe766..15f3f03 100644 --- a/src/jolt/core_coll.janet +++ b/src/jolt/core_coll.janet @@ -427,7 +427,12 @@ (do (var result to) (each item items (set result (core-assoc result (vnth item 0) (vnth item 1)))) result) - (pvec? to) (do (var result to) (each x items (set result (pv-conj result x))) result) + # Accumulate into a native array and bulk-build the pvec ONCE (pv-from-indexed), + # instead of an immutable pv-conj per element (each allocating a wrapper + + # copying the tail). This is the transient-style fast path for into-a-vector. + (pvec? to) (let [arr (array/slice (pv->array to))] + (each x items (array/push arr x)) + (make-vec arr)) (array? to) (if mutable? (do (each x items (array/push to x)) to) # vector: append (do (var result (array/slice to)) (each x items (array/insert result 0 x)) result)) # list: prepend diff --git a/src/jolt/pv.janet b/src/jolt/pv.janet index 11e3587..e4f43d4 100644 --- a/src/jolt/pv.janet +++ b/src/jolt/pv.janet @@ -156,9 +156,34 @@ out) (defn pv-from-indexed [xs] - # Build a pvec from any Janet-indexed collection (tuple/array). - (var pv EMPTY) + # Build a pvec from any Janet-indexed collection (tuple/array) by constructing + # the trie BOTTOM-UP in one pass, instead of n incremental pv-conj calls (each + # of which allocated a pv wrapper and copied the tail). The structure produced + # is identical to the incremental one — tail-offset(n) = ((n-1)>>5)<<5 is + # exactly the trie/tail split, so leaf-for/nth/conj/assoc all read it the same. + # This is the bulk path behind vec/mapv/into-a-vector (jolt-5vsp collections). (def n (length xs)) - (var i 0) - (while (< i n) (set pv (pv-conj pv (in xs i))) (++ i)) - pv) + (if (<= n width) + # everything fits in the tail (matches EMPTY's shift) + (make-pv n bits empty-node (tuple/slice xs)) + (let [tail-len (+ 1 (mod (- n 1) width)) + trie-count (- n tail-len) # = tail-offset(n), a multiple of width + tail (tuple/slice xs trie-count n)] + # value leaves: full 32-element tuples over [0, trie-count) + (def leaves @[]) + (var i 0) + (while (< i trie-count) + (array/push leaves (tuple/slice xs i (+ i width))) + (set i (+ i width))) + # group nodes 32-wide, bottom-up, until <=32 remain; that becomes the root + (var level leaves) + (var shift bits) + (while (> (length level) width) + (def parent @[]) + (var j 0) + (while (< j (length level)) + (array/push parent (tuple/slice level j (min (length level) (+ j width)))) + (set j (+ j width))) + (set level parent) + (set shift (+ shift bits))) + (make-pv n shift (tuple/slice level) tail)))) diff --git a/test/spec/vectors-spec.janet b/test/spec/vectors-spec.janet index 90572d8..136752d 100644 --- a/test/spec/vectors-spec.janet +++ b/test/spec/vectors-spec.janet @@ -52,3 +52,18 @@ ["large vector nth" "1500" "(nth (vec (range 2000)) 1500)"] ["large vector count" "2000" "(count (vec (range 2000)))"] ["large conj immutable" "true" "(let [v (vec (range 1000)) w (conj v :end)] (and (= 1000 (count v)) (= 1001 (count w))))"]) + +# vec/into build the pvec trie BOTTOM-UP in one pass (pv-from-indexed, jolt-5vsp +# collections). Exercise the structure transitions: 32 (tail full), 33 (first +# trie leaf), 1024 (root full at shift 5), 1025 (root grows to shift 10) — and +# that a conj/assoc after a bulk build still lands and reads back. +(defspec "vector / bulk build boundaries" + ["count at 1025" "1025" "(count (vec (range 1025)))"] + ["into = vec at 1025" "true" "(= (vec (range 1025)) (into [] (range 1025)))"] + ["nth at leaf boundary" "32" "(nth (vec (range 1025)) 32)"] + ["nth at root boundary" "1024" "(nth (vec (range 1025)) 1024)"] + ["vec=into at 33" "true" "(= (vec (range 33)) (into [] (range 33)))"] + ["conj after bulk 1024" "1025" "(count (conj (vec (range 1024)) :x))"] + ["conj-after reads back" ":x" "(nth (conj (vec (range 1024)) :x) 1024)"] + ["assoc into bulk vec" "9" "(nth (assoc (vec (range 1025)) 1000 9) 1000)"] + ["into onto non-empty" "[0 1 2 0 1]" "(into (vec (range 3)) (range 2))"])