From 297d92fbb88a5c4872cf56f24b9a4000f1305cca Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 8 Jun 2026 17:47:52 -0400 Subject: [PATCH] =?UTF-8?q?core:=20fix=20jolt-r81=20at=20root=20=E2=80=94?= =?UTF-8?q?=20move=20lazy-seq/lazy-cat=20to=20the=20early=20syntax=20tier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: lazy-seq/lazy-cat were defined in 30-macros, which loads AFTER the seq/coll tiers (10-seq, 20-coll) that use them. In compile mode a tier's forms are compiled as the tier loads, so (lazy-seq …) in those tiers was compiled when lazy-seq was not yet a registered macro — i.e. as a CALL to the macro-as-function, which at runtime returns its own expansion `(make-lazy-seq (fn* [] …))` as data. That leaked form then flowed into ops like `odd?` (partition-by) → type errors, or silently produced wrong structure. Interpret/self-host masked it (expand at call time); the eager fallbacks and the earlier letfn versions masked it by falling back to the interpreter. Fix: define lazy-seq/lazy-cat in 00-syntax (loaded first), exactly as when-let already is for the same reason. They use only seed fns (make-lazy-seq/coll->cells/ concat) + map. With the macro registered early, the seq/coll tiers compile (lazy-seq …) correctly. With the root fixed, interleave/reductions/tree-seq drop their letfn workarounds and use the canonical recursive Clojure forms (top-level / fn-self-name recursion inside lazy-seq), verified leak-free in compile mode with strict probes. Regression guards added: partition-by with odd? (the strict pred that exposed the leak; the prior case used identity which masked it), reductions over an infinite range, tree-seq summed through a strict filter — all ×3 modes. Gate: conformance 249x3, lazy-infinite 40/40, fixpoint, self-host, specs+unit green. Co-Authored-By: Claude Opus 4.8 (1M context) --- jolt-core/clojure/core/00-syntax.clj | 13 ++++++ jolt-core/clojure/core/20-coll.clj | 57 ++++++++++--------------- jolt-core/clojure/core/30-macros.clj | 13 +++--- test/integration/conformance-test.janet | 5 +++ 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/jolt-core/clojure/core/00-syntax.clj b/jolt-core/clojure/core/00-syntax.clj index a549bee..aeb33f2 100644 --- a/jolt-core/clojure/core/00-syntax.clj +++ b/jolt-core/clojure/core/00-syntax.clj @@ -330,3 +330,16 @@ (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (if temp# (let [~form temp#] ~@body) nil)))) + +;; lazy-seq / lazy-cat live here (not 30-macros) because the seq/coll tiers use +;; them and compile-as-they-load: the macro must be registered before those tiers +;; or (lazy-seq …) compiles to a call of the macro-as-function and leaks its +;; expansion at runtime (jolt-r81). They use only seed fns (make-lazy-seq/ +;; coll->cells/concat) + map, all available from the start. +;; lazy-seq defers its body: make-lazy-seq holds a thunk that realizes the body +;; to cells when forced. lazy-cat wraps each coll in a lazy-seq and concats. +(defmacro lazy-seq [& body] + `(make-lazy-seq (fn* [] (coll->cells (do ~@body))))) + +(defmacro lazy-cat [& colls] + `(concat ~@(map (fn [c] `(lazy-seq ~c)) colls))) diff --git a/jolt-core/clojure/core/20-coll.clj b/jolt-core/clojure/core/20-coll.clj index e6e127a..ace99b4 100644 --- a/jolt-core/clojure/core/20-coll.clj +++ b/jolt-core/clojure/core/20-coll.clj @@ -151,9 +151,7 @@ (defn comparator [pred] (fn [a b] (cond (pred a b) -1 (pred b a) 1 :else 0))) -;; Lazy: the running accumulators, one at a time (matches Clojure). Recursion is -;; letfn-bound (NOT top-level self-call) so the lazy-seq body compiles cleanly in -;; the overlay — see jolt-r81. +;; Lazy: the running accumulators, one at a time (matches Clojure). (defn reductions ([f coll] (lazy-seq @@ -162,22 +160,19 @@ (reductions f (first s) (rest s)) (list (f)))))) ([f init coll] - (letfn [(step [acc s] - (cons acc - (lazy-seq - (let [s (seq s)] - (when s - (step (f acc (first s)) (rest s)))))))] - (step init coll)))) + (cons init + (lazy-seq + (when-let [s (seq coll)] + (reductions f (f init (first s)) (rest s))))))) -;; Lazy pre-order DFS (matches Clojure). letfn-bound walk (not (fn walk …)) so it -;; compiles cleanly in the overlay under :compile? — see jolt-r81. +;; Lazy pre-order DFS (matches Clojure): node, then its children's walks spliced +;; via the (now lazy) mapcat. (defn tree-seq [branch? children root] - (letfn [(walk [node] - (lazy-seq - (cons node - (when (branch? node) - (mapcat walk (children node))))))] + (let [walk (fn walk [node] + (lazy-seq + (cons node + (when (branch? node) + (mapcat walk (children node))))))] (walk root))) ;; Canonical flatten via tree-seq: the leaves (non-sequential nodes) in order. @@ -190,28 +185,22 @@ (tree-seq (complement string?) (comp seq :content) root)) ;; Lazy interleave: round-robin one element from each coll until any exhausts. -;; letfn-bound recursion (not top-level self-call) so the lazy-seq body compiles -;; cleanly in the overlay — see jolt-r81. (defn interleave ([] ()) ([c1] (lazy-seq c1)) ([c1 c2] - (letfn [(step [s1 s2] - (lazy-seq - (let [s1 (seq s1) s2 (seq s2)] - (when (and s1 s2) - (cons (first s1) - (cons (first s2) - (step (rest s1) (rest s2))))))))] - (step c1 c2))) + (lazy-seq + (let [s1 (seq c1) s2 (seq c2)] + (when (and s1 s2) + (cons (first s1) + (cons (first s2) + (interleave (rest s1) (rest s2)))))))) ([c1 c2 & cs] - (letfn [(step [ss] - (lazy-seq - (let [ss (map seq ss)] - (when (every? identity ss) - (concat (map first ss) - (step (map rest ss)))))))] - (step (list* c1 c2 cs))))) + (lazy-seq + (let [ss (map seq (list* c1 c2 cs))] + (when (every? identity ss) + (concat (map first ss) + (apply interleave (map rest ss)))))))) ;; No ratio type on Jolt, so rationalize is identity. (defn rationalize [x] x) diff --git a/jolt-core/clojure/core/30-macros.clj b/jolt-core/clojure/core/30-macros.clj index 6e3c900..1379e63 100644 --- a/jolt-core/clojure/core/30-macros.clj +++ b/jolt-core/clojure/core/30-macros.clj @@ -219,11 +219,8 @@ ~@(map (fn [g] (impl (first g) (rest g))) (group-by-head body))))) ;; --- laziness -------------------------------------------------------------- -;; lazy-seq defers its body: make-lazy-seq holds a thunk that, when forced, -;; realizes the body to cells. lazy-cat wraps each coll in a lazy-seq and concats -;; (concat is itself lazy, so no outer wrapping needed). -(defmacro lazy-seq [& body] - `(make-lazy-seq (fn* [] (coll->cells (do ~@body))))) - -(defmacro lazy-cat [& colls] - `(concat ~@(map (fn [c] `(lazy-seq ~c)) colls))) +;; lazy-seq / lazy-cat moved to the 00-syntax tier: the seq/coll tiers (10-seq, +;; 20-coll) use lazy-seq, and in compile mode a tier's forms are compiled as it +;; loads — so the macro must be registered BEFORE those tiers, else (lazy-seq …) +;; compiles as a call to the macro-as-function and leaks its expansion at runtime +;; (jolt-r81). They only need seed fns (make-lazy-seq/coll->cells/concat). diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index 42dc66c..407fc2c 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -152,6 +152,11 @@ ["reductions" "(quote (1 3 6 10))" "(reductions + [1 2 3 4])"] ["reductions init" "(quote (0 1 3 6))" "(reductions + 0 [1 2 3])"] ["dedupe" "(quote (1 2 3 1))" "(dedupe [1 1 2 3 3 1])"] + # partition-by with a strict pred (odd?) — guards jolt-r81: a lazy overlay fn + # whose lazy-seq leaked its expansion in compile mode passed a non-int to odd?. + ["partition-by odd?" "(quote ((1 1) (2) (3 3)))" "(partition-by odd? [1 1 2 3 3])"] + ["reductions inf" "(quote (0 1 3 6))" "(take 4 (reductions + (range)))"] + ["tree-seq strict" "10" "(reduce + 0 (filter (complement coll?) (tree-seq coll? seq [1 [2 [3 4]]])))"] ["keep-indexed" "(quote (:b :d))" "(keep-indexed (fn [i x] (if (odd? i) x)) [:a :b :c :d])"] ["map-indexed" "(quote ([0 :a] [1 :b]))" "(map-indexed (fn [i x] [i x]) [:a :b])"] ["trampoline" ":done" "(do (defn a [n] (if (zero? n) :done (fn [] (a (dec n))))) (trampoline a 5))"]