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))) (if (and (indexed? r) (= 2 (length r)) (function? (in r 1)))
r r
(coll->cells 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 (indexed? c)
(if (= 0 (length c)) nil (if (= 0 (length c)) nil
(if (and (= 2 (length c)) (function? (in c 1))) (if (and (= 2 (length c)) (function? (in c 1)))
@ -461,34 +463,21 @@
nil))))) nil)))))
(defn core-concat [& colls] (defn core-concat [& colls]
"Lazy concatenation — returns a lazy-seq that yields elements one at a time. "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 (var cs (if (tuple? colls) (array/slice colls) colls))
paths (e.g. fib-seq used simultaneously in (rest fib-seq) and fib-seq) work (var cur nil)
without shared-mutable-state corruption." (defn next-cell []
(defn step [cs] (var cell (coll->cells cur))
"Return a thunk that produces nil (end) or @[value, rest-thunk]." (while (and (nil? cell) (not (nil? cs)) (> (length cs) 0))
(if (= 0 (length cs)) (set cur (in cs 0))
(fn [] nil) (set cs (array/slice cs 1))
(let [c (in cs 0) (set cell (coll->cells cur)))
remaining (array/slice cs 1)] (if (nil? cell) nil
@[(in cell 0)
(fn [] (fn []
(let [cell (coll->cells c)] (set cur (in cell 1))
(if (nil? cell) (next-cell))]))
((step remaining)) (make-lazy-seq next-cell))
(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))))
(defn core-reverse [coll] (defn core-reverse [coll]
(if (nil? coll) @[] (if (nil? coll) @[]

View file

@ -131,11 +131,12 @@
(defn realize-ls (defn realize-ls
"Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk]. "Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk].
If the thunk returns another lazy-seq, recursively realize it. 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] [ls]
(if (get ls :realized) (if (get ls :realized)
(ls :val) (ls :val)
(do (do
(put ls :val :jolt/pending)
(put ls :realized true) (put ls :realized true)
(let [raw ((ls :fn)) (let [raw ((ls :fn))
v (if (lazy-seq? raw) (realize-ls raw) raw)] v (if (lazy-seq? raw) (realize-ls raw) raw)]
@ -144,7 +145,7 @@
(defn ls-first [ls] (defn ls-first [ls]
(let [cell (realize-ls 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] (defn ls-rest [ls]
(let [cell (realize-ls ls)] (let [cell (realize-ls ls)]