From 1ef3c8db39362c58f368580904d9d0a357d79455 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 4 Jun 2026 12:54:41 -0400 Subject: [PATCH] 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). --- src/jolt/core.janet | 45 +++++++++++++++++---------------------------- src/jolt/phm.janet | 5 +++-- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 4fcfb0d..3f7be35 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -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) @[] diff --git a/src/jolt/phm.janet b/src/jolt/phm.janet index 8a802d9..bc4e4a2 100644 --- a/src/jolt/phm.janet +++ b/src/jolt/phm.janet @@ -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)]