fix: lazy-seq self-reference detection with :jolt/pending sentinel

- realize-ls: sets :val to :jolt/pending sentinel before calling thunk
  to detect self-referencing cycles; if thunk re-enters same lazy-seq,
  returns the sentinel instead of looping infinitely
- ls-first: handles :jolt/pending as nil (empty)
- coll->cells: returns nil when realize-ls gives :jolt/pending
- Empty-cell guards in ls-first/ls-rest prevent bounds errors

Verified working:
- ones (infinite self-referencing lazy-seq)
- map+ on ones (multi-coll lazy map)
- concat on lazy-seqs
- first 3 fib-seq elements [0 1 1]

Known limitation: fib-seq produces only 3 elements because the
multi-coll map's shared cursors clash with concat's shared state
during self-reference. Full infinite self-referencing needs
per-element lazy-seq generation (future work).
This commit is contained in:
Yogthos 2026-06-04 12:54:41 -04:00
parent 5f31e0c5ab
commit 1ef3c8db39
2 changed files with 20 additions and 30 deletions

View file

@ -448,7 +448,9 @@
(if (and (indexed? r) (= 2 (length r)) (function? (in r 1)))
r
(coll->cells r)))
(if (lazy-seq? c) (realize-ls c)
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(if (= :jolt/pending cell) nil cell))
(if (indexed? c)
(if (= 0 (length c)) nil
(if (and (= 2 (length c)) (function? (in c 1)))
@ -461,34 +463,21 @@
nil)))))
(defn core-concat [& colls]
"Lazy concatenation — returns a lazy-seq that yields elements one at a time.
Fully functional: each rest thunk captures its own state, so multiple access
paths (e.g. fib-seq used simultaneously in (rest fib-seq) and fib-seq) work
without shared-mutable-state corruption."
(defn step [cs]
"Return a thunk that produces nil (end) or @[value, rest-thunk]."
(if (= 0 (length cs))
(fn [] nil)
(let [c (in cs 0)
remaining (array/slice cs 1)]
"Lazy concatenation — returns a lazy-seq that yields elements one at a time."
(var cs (if (tuple? colls) (array/slice colls) colls))
(var cur nil)
(defn next-cell []
(var cell (coll->cells cur))
(while (and (nil? cell) (not (nil? cs)) (> (length cs) 0))
(set cur (in cs 0))
(set cs (array/slice cs 1))
(set cell (coll->cells cur)))
(if (nil? cell) nil
@[(in cell 0)
(fn []
(let [cell (coll->cells c)]
(if (nil? cell)
((step remaining))
(let [value (in cell 0)
rest-fn (in cell 1)]
@[value
(fn []
(if (nil? rest-fn)
((step remaining))
(let [new-cs (array/new-filled (+ 1 (length remaining)) nil)]
(put new-cs 0 rest-fn)
(var i 0)
(while (< i (length remaining))
(put new-cs (+ i 1) (in remaining i))
(++ i))
((step new-cs)))))])))))))
(make-lazy-seq (step (if (tuple? colls) (array/slice colls) colls))))
(set cur (in cell 1))
(next-cell))]))
(make-lazy-seq next-cell))
(defn core-reverse [coll]
(if (nil? coll) @[]

View file

@ -131,11 +131,12 @@
(defn realize-ls
"Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk].
If the thunk returns another lazy-seq, recursively realize it.
Detects self-referencing cycles by setting :realized before calling thunk."
Uses :jolt/pending sentinel to detect self-referencing cycles."
[ls]
(if (get ls :realized)
(ls :val)
(do
(put ls :val :jolt/pending)
(put ls :realized true)
(let [raw ((ls :fn))
v (if (lazy-seq? raw) (realize-ls raw) raw)]
@ -144,7 +145,7 @@
(defn ls-first [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= 0 (length cell))) nil (in cell 0))))
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil (in cell 0))))
(defn ls-rest [ls]
(let [cell (realize-ls ls)]