fix: transducers/reduce short-circuit over infinite seqs via reduced

transduce-reduce and core-reduce both called realize-for-iteration, fully
realizing the coll before the reduce loop — so an infinite lazy seq hung before
the reduced short-circuit could fire. (into [] (take 5) (range)) etc. never
terminated.

Add reduce-with-reduced, which steps a lazy seq one cell at a time
(realize-ls/ls cell protocol), checking reduced? after each element so a take/
take-while transducer (or any reducing fn returning reduced) terminates over an
infinite seq. Route transduce-reduce and both core-reduce arities through it;
2-arg reduce now seeds from the first element and reduces the rest lazily.

spec: transducers/short-circuit + reduce/honors-reduced (13 cases).
clojure-test-suite timeouts 7->6, pass 1683->1688. jpm test green.

Fixes jolt-kxb.
This commit is contained in:
Yogthos 2026-06-05 09:18:35 -04:00
parent ae2899ba61
commit 2885650ef6
3 changed files with 69 additions and 25 deletions

View file

@ -5,7 +5,7 @@
{"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T18:09:15Z","closed_at":"2026-06-04T18:09:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T18:06:02Z","closed_at":"2026-06-04T18:06:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"open","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:18Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:09:59Z","started_at":"2026-06-05T13:09:59Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-js6","title":"Port clojure-test-suite as integration battery","description":"Build a minimal clojure.test + portability shim so Jolt can load the external ~/src/clojure-test-suite (240 per-fn .cljc files in clojure.test format). Add a baseline-guarded integration runner following the jank-conformance pattern (skip if absent).","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T12:22:01Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:04Z","started_at":"2026-06-05T12:22:14Z","closed_at":"2026-06-05T13:05:04Z","close_reason":"Ported clojure-test-suite as baseline-guarded integration battery; surfaced+fixed 2 evaluator macro bugs","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-i3g","title":"Clear final 2 SCI bootstrap forms (Var/IVar collision + clojure.repl/template)","description":"namespaces.cljc forms 126/127 fail: (126) lang_stubs IVar collides with real vars.cljc IVar so unbind dispatch misses on sci.lang.Var; (127) clojure.repl + clojure.template namespaces missing. Fix: trim bootstrap stubs to let real modules own canonical protocols/types; port clojure.repl + clojure.template.","status":"closed","priority":2,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-06-05T03:21:40Z","created_by":"Yogthos","updated_at":"2026-06-05T03:35:43Z","closed_at":"2026-06-05T03:35:43Z","close_reason":"Done — SCI bootstrap now loads 422/422 with 0 failures. Fixed deftype protocol-method registration, with-meta on functions, select-keys over vectors, require :as+:refer, defmethod-on-fn; added clojure.set/join and host-module stubs. test-load-sci asserts 0 failures.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-ns4","title":"Persistent collections + build-time mutable/immutable toggle","description":"Add JOLT_MUTABLE build flag (config.janet). Fix list-mutation bug (conj mutated original). Round 2: structural-sharing persistent vector (32-way trie) hooked into all core ops. Round 3: persistent lists / HAMT maps.","notes":"R1 (build flag + list mutation fix) and R2 (structural-sharing persistent vectors + mutable toggle) done \u0026 pushed (1eb2843). Immutable: conformance 206/206, features 71/71, jank 119. R3 (persistent linked lists) pending user decision: lists are immutable Janet arrays — O(1) sharing cons, O(n) conj.","status":"closed","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-04T22:15:39Z","created_by":"Yogthos","updated_at":"2026-06-04T23:20:46Z","closed_at":"2026-06-04T23:20:46Z","close_reason":"Done across 3 rounds (pushed a0c9696): build-time mutable/immutable toggle, structural-sharing persistent vectors (32-way trie), and persistent O(1)-prepend linked lists. Plus bit-clear/get-method and a batch of missing core fns. conformance 206/206, features 78/78, jank 120.","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -620,15 +620,38 @@
(var i -1)
(fn [& a] (case (length a) 0 (rf) 1 (rf (a 0)) (do (++ i) (rf (a 0) (f i (a 1))))))))
(defn- reduce-with-reduced
"Reduce coll with reducing fn rf and seed init, honoring `reduced`. Steps lazy
seqs one cell at a time so a reducing fn that returns `reduced` (e.g. the
`take`/`take-while` transducers) can short-circuit over an INFINITE seq instead
of realizing it eagerly. Returns the final (unwrapped) accumulator."
[rf init coll]
(var acc init)
(if (lazy-seq? coll)
(do
(var cur coll) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do
(set acc (rf acc (in cell 0)))
(if (core-reduced? acc)
(do (set acc (acc :val)) (set go false))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (make-lazy-seq rt))))))))))
(do
(var stop false)
(each x (if (set? coll) (phs-seq coll) (realize-for-iteration coll))
(when (not stop)
(set acc (rf acc x))
(when (core-reduced? acc) (set acc (acc :val)) (set stop true))))))
acc)
(defn- transduce-reduce
"Reduce coll with reducing fn rf and seed init, honoring `reduced`."
[rf init coll]
(var acc init) (var stop false)
(each x (if (set? coll) (phs-seq coll) (realize-for-iteration coll))
(when (not stop)
(set acc (rf acc x))
(when (core-reduced? acc) (set acc (acc :val)) (set stop true))))
acc)
(reduce-with-reduced rf init coll))
(defn core-transduce
"(transduce xform f coll) or (transduce xform f init coll)."
@ -755,24 +778,22 @@
(def core-reduce
(fn [& args]
(case (length args)
2 (let [f (args 0) coll (args 1)
coll (if (set? coll) (phs-seq coll) (realize-for-iteration coll))]
(if (= 0 (length coll))
(f)
(do
(var acc (coll 0))
(var i 1)
(while (< i (length coll))
(set acc (f acc (coll i)))
(if (core-reduced? acc) (do (set acc (acc :val)) (set i (length coll))) (++ i)))
acc)))
3 (let [f (args 0) val (args 1) coll (args 2)
coll (if (set? coll) (phs-seq coll) (realize-for-iteration coll))]
(var acc val) (var i 0)
(while (< i (length coll))
(set acc (f acc (in coll i)))
(if (core-reduced? acc) (do (set acc (acc :val)) (set i (length coll))) (++ i)))
acc)
# 2-arg: seed is the first element; reduce over the rest. Lazy seqs are
# stepped incrementally (via reduce-with-reduced) so `reduced` can
# short-circuit an infinite seq rather than realizing it.
2 (let [f (args 0) coll (args 1)]
(if (lazy-seq? coll)
(let [cell (realize-ls coll)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(f)
(let [rt (in cell 1)]
(if (nil? rt) (in cell 0)
(reduce-with-reduced f (in cell 0) (make-lazy-seq rt))))))
(let [c (if (set? coll) (phs-seq coll) (realize-for-iteration coll))]
(if (= 0 (length c)) (f)
(reduce-with-reduced f (in c 0) (array/slice c 1))))))
3 (let [f (args 0) val (args 1) coll (args 2)]
(reduce-with-reduced f val coll))
(error "Wrong number of args passed to: reduce"))))
(defn core-take [n & rest]

View file

@ -27,3 +27,26 @@
["sequence xform" "[2 3 4]" "(sequence (map inc) [1 2 3])"]
["eduction" "[2 3 4]" "(into [] (eduction (map inc) [1 2 3]))"]
["completing" "9" "(transduce (map inc) (completing +) 0 [1 2 3])"])
# A `take`/`take-while` transducer returns `reduced`, which must short-circuit
# the reduction so transducing over an INFINITE seq terminates rather than
# realizing it eagerly.
(defspec "transducers / short-circuit over infinite seqs"
["into take (range)" "[0 1 2 3 4]" "(into [] (take 5) (range))"]
["transduce take (range)" "3" "(transduce (take 3) + 0 (range))"]
["sequence take (range)" "[0 1 2 3 4]" "(sequence (take 5) (range))"]
["take-while over (range)" "[0 1 2]" "(into [] (take-while (fn [x] (< x 3))) (range))"]
["comp take over (range)" "[1 3 5]" "(into [] (comp (filter odd?) (take 3)) (range))"]
["into take iterate" "[0 1 2 3 4]" "(into [] (take 5) (iterate inc 0))"])
# `reduce` itself honors `reduced`, so a reducing fn that returns `reduced`
# terminates even over an infinite seq.
(defspec "reduce / honors reduced"
["reduced short-circuits inf" "105"
"(reduce (fn [a x] (if (> a 100) (reduced a) (+ a x))) 0 (range))"]
["reduce take inf" "10" "(reduce + (take 5 (range)))"]
["reduce no-init first elem" "6" "(reduce + [1 2 3])"]
["reduce no-init single" "42" "(reduce + [42])"]
["reduce empty calls f" "0" "(reduce + [])"]
["reduce with-init" "16" "(reduce + 10 [1 2 3])"]
["reduce reduced immediate" ":x" "(reduce (fn [a x] (reduced :x)) :init [1 2 3])"])