From 9a9de08047278c4e1a14097273b8f585ef353611 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 8 Jun 2026 18:50:36 -0400 Subject: [PATCH] =?UTF-8?q?core:=20fix=20Option-A=20suite=20regressions=20?= =?UTF-8?q?=E2=80=94=20nil=20lazy=20elements=20+=20non-seqable=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running clojure-test-suite surfaced an Option A regression (3971 -> 3957), isolated to two root causes, both around lazy seqs: 1. nil first element wrongly read as end-of-seq. core-empty?, core-seq, and core-reverse tested a lazy seq's emptiness with (nil? (ls-first coll)) — but a lazy element may legitimately be nil. With Option A's lazy `drop`, the `case` macro's (empty? (drop 2 clauses)) hit a nil-first lazy seq at the `nil` case-constant and collapsed the rest of the case (incl :default) to nil — breaking 14 case.cljc assertions. Now they realize one cell (seq-done?-style) instead of trusting ls-first. 2. lazy transformer over a non-seqable silently yielded empty. The eager path threw (realize-for-iteration on a char/number errors); Option A's lazy-from returned nil, so (first (remove nil? \a)) gave nil where Clojure throws. lazy-from now rejects non-seqable scalars (number/boolean/keyword/char/symbol) with "Don't know how to create ISeq from: …". Result: suite 3971 -> 3981 pass (net gain), clean files 45 -> 66 (Option A makes seq?/vector? match Clojure across many cross-dialect files). Baseline raised. Gate: conformance 258x3 (+5 regression guards), lazy-infinite 44/44, suite 3981/66, fixpoint, self-host, specs+unit green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/jolt/core.janet | 31 ++++++++++++++----- .../integration/clojure-test-suite-test.janet | 10 ++++-- test/integration/conformance-test.janet | 9 ++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 1c20803..4301997 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -196,7 +196,11 @@ (if (phm? coll) (= 0 (coll :cnt)) (if (pvec? coll) (= 0 (pv-count coll)) (if (plist? coll) (pl-empty? coll) - (if (lazy-seq? coll) (nil? (ls-first coll)) + # Cell-based, NOT (nil? (ls-first)): a lazy-seq whose first element is + # legitimately nil (e.g. a `nil` case-constant) is non-empty. + (if (lazy-seq? coll) + (let [cell (realize-ls coll)] + (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))) (if (struct? coll) (= 0 (length (keys coll))) (= 0 (length coll)))))))))) @@ -661,7 +665,10 @@ (core-sorted-map? coll) (let [e (sorted-map-entries coll)] (if (empty? e) nil (tuple ;e))) (core-sorted-set? coll) (let [i (coll :items)] (if (empty? i) nil (tuple ;i))) (or (nil? coll) (and (or (tuple? coll) (array? coll)) (= 0 (length coll)))) nil - (lazy-seq? coll) (if (nil? (ls-first coll)) nil coll) + # Cell-based emptiness, NOT (nil? (ls-first)): a lazy-seq whose first element + # is legitimately nil is non-empty, so (seq (cons nil ...)) must not be nil. + (lazy-seq? coll) (let [cell (realize-ls coll)] + (if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil coll)) (pvec? coll) (if (= 0 (pv-count coll)) nil (tuple ;(pv->array coll))) (plist? coll) (if (pl-empty? coll) nil (tuple ;(pl->array coll))) (buffer? coll) (if (= 0 (length coll)) nil (let [a @[]] (each x coll (array/push a x)) (tuple ;a))) @@ -988,9 +995,16 @@ [coll] (if (nil? coll) nil (if (lazy-seq? coll) coll - (let [cell (coll->cells coll)] - (if (nil? cell) nil - (make-lazy-seq (fn [] cell))))))) + (do + # Reject non-seqable scalars (number/boolean/keyword, and tagged structs + # like char/symbol) so a lazy transformer over bad input throws when + # realized — matching Clojure — instead of silently yielding empty. + (when (or (number? coll) (boolean? coll) (keyword? coll) + (and (struct? coll) (not (nil? (get coll :jolt/type))))) + (error (string "Don't know how to create ISeq from: " (type coll)))) + (let [cell (coll->cells coll)] + (if (nil? cell) nil + (make-lazy-seq (fn [] cell)))))))) (defn core-map [f & colls] (def f (as-fn f)) @@ -1245,9 +1259,10 @@ (do (var result @[]) (var cur coll) - (while (not (nil? (ls-first cur))) - (array/push result (ls-first cur)) - (set cur (ls-rest cur))) + # seq-done?, not (nil? (ls-first)): a nil element must not end the walk. + (while (not (seq-done? cur)) + (array/push result (core-first cur)) + (set cur (core-rest cur))) (var reversed @[]) (var i (dec (length result))) (while (>= i 0) diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index 6c448c3..22ab2bf 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -32,9 +32,15 @@ # which several suite tests assert. Runs read 3927 consistently, occasionally 3926 # when a timeout-prone test (of the 9 that can time out) doesn't finish; floor at # the consistent-minus-one 3926. -(def baseline-pass 3971) +# Raised 3971 -> 3981 with Option A full laziness (jolt-fng): transformers return +# lazy seqs, lazy interleave stops timing out, and the lazy-seq nil-element + +# non-seqable-input fixes (case/seq/reverse/empty? over nil-first lazy seqs; +# lazy-from throws on non-seqable like Clojure) recovered + extended the suite. +# clean files 45 -> 66 (Option A makes seq?/vector? results match Clojure across +# many cross-dialect files). Stable across runs. +(def baseline-pass 3981) # A file is "clean" when it ran with zero failures AND zero errors. -(def baseline-clean-files 45) +(def baseline-clean-files 66) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s; # this only fires on infinite-sequence hangs. (def per-file-timeout 6) diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index 8427c65..81fa206 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -157,6 +157,15 @@ ["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]]])))"] + # nil/collection case-constants past the point where Option A's lazy `drop` + # made the case macro's (empty? (drop 2 cls)) hit a nil-first lazy seq. + ["case nil + default" "[:nilr :def]" "(let [f (fn [x] (case x 1 :one nil :nilr :def))] [(f nil) (f 9)])"] + ["case collection consts" "[:v :m :s]" "(let [f (fn [x] (case x [1 2] :v {:a 1} :m #{3} :s :def))] [(f [1 2]) (f {:a 1}) (f #{3})])"] + # a lazy seq whose first element is nil is non-empty (seq/empty?/reverse) + ["seq of nil-first" "true" "(boolean (seq (cons nil (list 1))))"] + ["reverse nil elem" "[2 nil 1]" "(vec (reverse (list 1 nil 2)))"] + # lazy transformer over a non-seqable scalar throws (matches Clojure) + ["map non-seqable throws" "true" "(try (doall (map inc 5)) false (catch Throwable _ true))"] ["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))"]