From 2885650ef6d88af4906b4063f9ded65ed4340f91 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 09:18:35 -0400 Subject: [PATCH] fix: transducers/reduce short-circuit over infinite seqs via reduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .beads/issues.jsonl | 2 +- src/jolt/core.janet | 69 +++++++++++++++++++++----------- test/spec/transducers-spec.janet | 23 +++++++++++ 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f101550..9a0b7f7 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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} diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 56dc573..14c8f59 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -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] diff --git a/test/spec/transducers-spec.janet b/test/spec/transducers-spec.janet index b470a63..727581f 100644 --- a/test/spec/transducers-spec.janet +++ b/test/spec/transducers-spec.janet @@ -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])"])