core: Phase 5 Option A — map/filter/take/take-while always return a lazy seq

Lazy transformers now return a LazySeq even over a concrete vector, matching
Clojure: (seq? (map inc [1 2 3])) is true, (vector? ...) false. Replaces the
"preserve representation" eager branch (which returned a vector over vector
input) by routing concrete colls through lazy-from + the lazy step machinery.

Flipping these surfaced four boundary bugs, all fixed here:

- cons over a lazy-seq returned a raw cell @[x thunk]; a cons-of-a-cons then
  treated it as a plain 2-array and leaked the rest-thunk as an element (broke
  interleave). cons over lazy now returns a proper LazySeq.
- coll->cells mistook a user vector whose 2nd elem is a function ([first last]
  from juxt) for a cons cell. Cons cells are mutable arrays; user data is
  immutable — route pvec/plist/tuple through immutable tuples and apply the
  [val,fn] cell heuristic only to mutable arrays. Also coerce set/map/string/
  buffer via core-seq.
- ~@ splice over a lazy map result iterated a LazySeq as a Janet table (broke
  lazy-cat / self-ref fib). syntax-quote* now realizes via d-realize before
  splicing; core-sqcat (self-host) already realized.
- core-next did (length r) on a lazy rest (never 0 on a table) and ls-rest
  could return nil → (length nil) crash. core-rest never returns nil; core-next
  uses seq-done? (realizes one cell). seq-done? moved above core-rest.

normalize-pvecs (test helper) realizes lazy-seqs so Janet-= comparisons work.

Gate: conformance 239x3 (interpret/compile/self-host, +10 Option A cases),
lazy-infinite 18/18, fixpoint, self-host, all specs+unit green. (sci-bootstrap
and clojure-test-suite skip — vendored dirs absent in this checkout.)

Remaining for full Option A consistency (jolt-7w4): drop/map-indexed/keep/
keep-indexed/take-nth/interpose/distinct/partition/partition-all still eager
over concrete input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yogthos 2026-06-08 13:47:22 -04:00
parent 5e7aea70da
commit 074eb2da41
4 changed files with 127 additions and 116 deletions

View file

@ -30,6 +30,9 @@
and lists with the same elements are equal."
[x]
(cond
# lazy-seq: realize to a tuple (map/filter/take now return lazy seqs).
(and (table? x) (= (get x :jolt/type) :jolt/lazy-seq))
(tuple ;(map normalize-pvecs (realize-for-iteration x)))
(pvec? x) (tuple ;(map normalize-pvecs (pv->array x)))
(plist? x) (tuple ;(map normalize-pvecs (pl->array x)))
(tuple? x) (tuple ;(map normalize-pvecs x))

View file

@ -600,9 +600,19 @@
(= 0 (length coll)) nil
(in coll 0)))
(defn- seq-done?
"True when cursor c (a lazy-seq or a concrete collection) is exhausted.
Uses cell realization for lazy-seqs so nil elements don't end the seq early."
[c]
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))))
(or (nil? c) (= 0 (length c)))))
(defn core-rest [coll]
(cond
(lazy-seq? coll) (ls-rest coll)
# rest never returns nil — Clojure's rest yields () on an exhausted seq.
(lazy-seq? coll) (let [r (ls-rest coll)] (if (nil? r) @[] r))
(plist? coll) (pl-rest coll)
(pvec? coll) (let [a (pv->array coll)] (if (<= (length a) 1) @[] (array/slice a 1)))
(or (nil? coll) (= 0 (length coll))) @[]
@ -611,14 +621,19 @@
(array/slice coll 1)))
(defn core-next [coll]
# next is rest, but nil when the rest is empty. seq-done? realizes one lazy
# cell so a lazy rest that turns out empty (length on the table won't tell us)
# collapses to nil, matching Clojure.
(let [r (core-rest coll)]
(if (= 0 (length r)) nil r)))
(if (seq-done? r) nil r)))
(defn core-cons [x coll]
"Prepend x onto coll. For concrete collections this is an O(1) persistent cons
node; for lazy-seqs it stays a lazy cell so laziness is preserved."
(cond
(lazy-seq? coll) @[x (fn [] coll)]
# Lazy tail: return a LazySeq (NOT a bare cell), so a cons-of-a-cons stays a
# proper lazy-seq and the rest-thunk never leaks as a plain array element.
(lazy-seq? coll) (make-lazy-seq (fn [] @[x (fn [] coll)]))
(or (nil? coll) (plist? coll) (array? coll) (tuple? coll)) (pl-cons x coll)
# second arg must be seqable (a collection or string); reject scalars
(not (or (core-coll? coll) (string? coll)))
@ -853,14 +868,53 @@
(core-seq a)
(tuple ;(core-transduce a (fn [& x] (case (length x) 0 @[] 1 (x 0) (do (array/push (x 0) (x 1)) (x 0)))) @[] (in rest 0)))))
(defn- seq-done?
"True when cursor c (a lazy-seq or a concrete collection) is exhausted.
Uses cell realization for lazy-seqs so nil elements don't end the seq early."
[c]
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))))
(or (nil? c) (= 0 (length c)))))
(defn coll->cells [c]
"Convert a seqable to a lazy-seq cell chain: nil or [first, rest-thunk].
A cons cell is a MUTABLE array `@[val rest-thunk]` (produced by `cons`/the lazy
transformers); user collections (tuples, pvecs, lists) are immutable. We rely
on that distinction: only a mutable 2-array whose tail is a function is treated
as an already-built cell — a user vector like `[first last]` (tail is the fn
`last`) is data and must NOT be misread as a cell. User data is recursed through
immutable tuples so its tails never reach the cell-detection branch."
(if (nil? c) nil
(if (pvec? c) (coll->cells (tuple ;(pv->array c)))
(if (plist? c) (coll->cells (tuple ;(pl->array c)))
(if (function? c)
(let [r (c)]
(if (and (array? r) (= 2 (length r)) (function? (in r 1)))
r
(coll->cells r)))
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(if (= :jolt/pending cell) nil cell))
(if (tuple? c)
# user sequential data: every element is a value, no cell-detection.
(if (= 0 (length c)) nil
@[(in c 0) (fn [] (coll->cells (tuple/slice c 1)))])
(if (array? c)
# mutable array: a genuine cons cell, or an eager seq result.
(if (= 0 (length c)) nil
(if (and (= 2 (length c)) (function? (in c 1)))
c # already a cell [val, rest-thunk]
@[(in c 0) (fn [] (coll->cells (array/slice c 1)))]))
# Other concrete seqables (set/map/string/buffer): coerce to a tuple
# seq via core-seq, then recurse. (lazy/indexed handled above.)
(if (or (set? c) (phm? c) (buffer? c) (string? c)
(and (struct? c) (nil? (get c :jolt/type))))
(coll->cells (core-seq c))
nil)))))))))
(defn lazy-from
"Coerce any seqable to a uniform lazy view without forcing.
Returns nil if coll is nil or empty, the LazySeq unchanged if already lazy,
or a new LazySeq that walks element by element."
[coll]
(if (nil? coll) nil
(if (lazy-seq? coll) 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))
@ -868,18 +922,14 @@
(td-map f) # transducer arity
(if (= 1 (length colls))
(let [coll (colls 0)]
(if (lazy-seq? coll)
# Lazy input: stay lazy so infinite/self-referential seqs work.
(do
(defn mstep [c]
(fn []
(if (seq-done? c) nil
@[(f (core-first c)) (mstep (core-rest c))])))
(make-lazy-seq (mstep coll)))
# Concrete collection: eager (preserves tuple/array representation).
(let [c (if (set? coll) (phs-seq coll) (realize-for-iteration coll))
result (do (var res @[]) (each x c (array/push res (f x))) res)]
(if (jvec? coll) (make-vec result) result))))
# Option A: always lazy, even over concrete collections (matches Clojure —
# map returns a seq, not a vector).
(do
(defn mstep [c]
(fn []
(if (seq-done? c) nil
@[(f (core-first c)) (mstep (core-rest c))])))
(make-lazy-seq (mstep (lazy-from coll)))))
# Multi-collection: lazy-seq with per-element independent state
(let [init-cs (array/new-filled (length colls) nil)
init-idxs (array/new-filled (length colls) 0)
@ -934,8 +984,7 @@
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-filter pred)
(let [coll (in rest 0)]
(if (lazy-seq? coll)
# lazy input -> lazy output (supports infinite seqs)
# Option A: always lazy (matches Clojure — filter returns a seq).
(do
(defn fstep [c]
(fn []
@ -945,12 +994,7 @@
(if (pred x) (do (set hit @[x (core-rest cur)]) (set found true))
(set cur (core-rest cur)))))
(if found @[(in hit 0) (fstep (in hit 1))] nil)))
(make-lazy-seq (fstep coll)))
(do
(var result @[])
(each x (if (set? coll) (phs-seq coll) (realize-for-iteration coll))
(if (pred x) (array/push result x)))
(if (jvec? coll) (make-vec result) result))))))
(make-lazy-seq (fstep (lazy-from coll)))))))
(defn core-remove [pred & rest]
(def pred (as-fn pred))
@ -981,23 +1025,12 @@
(defn core-take [n & rest]
(if (= 0 (length rest)) (td-take n)
(let [coll (in rest 0)]
(if (lazy-seq? coll)
(do
(var result @[])
(var cur coll)
(var i 0)
(while (and (< i n) (not (nil? (ls-first cur))))
(array/push result (ls-first cur))
(set cur (ls-rest cur))
(++ i))
result)
(let [c (realize-for-iteration coll)]
(var result @[])
(var i 0)
(while (and (< i n) (< i (length c)))
(array/push result (in c i))
(++ i))
(if (jvec? coll) (make-vec result) result))))))
# Option A: lazy take (returns a seq, not a vector, even over a vector).
(defn tstep [c i]
(fn []
(if (or (>= i n) (seq-done? c)) nil
@[(core-first c) (tstep (core-rest c) (+ i 1))])))
(make-lazy-seq (tstep (lazy-from coll) 0)))))
(defn core-drop [n & rest]
(if (= 0 (length rest)) (td-drop n)
@ -1024,18 +1057,13 @@
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-take-while pred)
(let [coll (in rest 0)]
(if (lazy-seq? coll)
(do
(var result @[]) (var cur coll) (var go true)
(while (and go (not (seq-done? cur)))
(let [x (core-first cur)]
(if (pred x) (do (array/push result x) (set cur (core-rest cur)))
(set go false))))
result)
(do
(var result @[])
(each x (realize-for-iteration coll) (if (pred x) (array/push result x) (break)))
(if (jvec? coll) (make-vec result) result))))))
# Option A: lazy take-while.
(defn twstep [c]
(fn []
(if (seq-done? c) nil
(let [x (core-first c)]
(if (pred x) @[x (twstep (core-rest c))] nil)))))
(make-lazy-seq (twstep (lazy-from coll))))))
(defn core-drop-while [pred & rest]
(def pred (as-fn pred))
@ -1058,32 +1086,6 @@
(tuple/slice c start)
(array/slice c start)))))))
(defn coll->cells [c]
"Convert a seqable to lazy-seq cell chain: nil or [first, rest-thunk].
If the value is a function, call it and use the result.
If the result is already a cell (array of [val, function]), return it directly."
(if (nil? c) nil
(if (pvec? c) (coll->cells (pv->array c))
(if (plist? c) (coll->cells (pl->array c))
(if (function? c)
(let [r (c)]
(if (and (indexed? r) (= 2 (length r)) (function? (in r 1)))
r
(coll->cells r)))
(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)))
c # already a cell [val, rest-thunk]
(let [f (in c 0)
rest (if (> (length c) 1)
(if (tuple? c) (tuple/slice c 1) (array/slice c 1))
nil)]
@[f (fn [] (coll->cells rest))])))
nil)))))))
(defn core-concat [& colls]
"Truly lazy concatenation. `step` returns a 0-arg thunk that is only forced
when the consumer asks for the next cell, so nothing in `colls` is realized at
@ -1109,16 +1111,6 @@
(array/insert remaining 0 rest-fn)))]))))))
(make-lazy-seq (step colls)))))
(defn lazy-from
"Coerce any seqable to a uniform lazy view without forcing.
Returns nil if coll is nil or empty, the LazySeq unchanged if already lazy,
or a new LazySeq that walks element by element."
[coll]
(if (nil? coll) nil
(if (lazy-seq? coll) coll
(let [cell (coll->cells coll)]
(if (nil? cell) nil
(make-lazy-seq (fn [] cell)))))))
(defn core-mapcat
"(mapcat f & colls) — map then concat. (mapcat f) returns a transducer."

View file

@ -152,6 +152,25 @@
{:jolt/type :symbol :ns (ctx-current-ns ctx) :name nm}))
form))
(defn- d-realize
"Realize a lazy-seq to an array for positional destructuring / splicing; pass
others (pvec/plist coerced to array, everything else unchanged)."
[val]
(if (pvec? val) (pv->array val)
(if (plist? val) (pl->array val)
(if (lazy-seq? val)
(do
(var items @[]) (var cur val) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do (array/push items (in cell 0))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (make-lazy-seq rt))))))))
items)
val))))
(defn- syntax-quote*
[ctx bindings form &opt gsmap]
(default gsmap @{})
@ -169,7 +188,7 @@
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (if (pvec? sv) (pv->array sv) sv) (array/push result v)))
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ i)) (tuple ;result))
(array? form)
@ -177,7 +196,7 @@
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (if (pvec? sv) (pv->array sv) sv) (array/push result v)))
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ i)) result)
(and (struct? form) (get form :jolt/type)) form
@ -497,24 +516,6 @@
(do (array/push fixed a) (+= i 1)))))
{:fixed (tuple/slice (tuple ;fixed)) :rest rest-pat})
(defn- d-realize
"Realize a lazy-seq to an array for positional destructuring; pass others through."
[val]
(if (pvec? val) (pv->array val)
(if (plist? val) (pl->array val)
(if (lazy-seq? val)
(do
(var items @[]) (var cur val) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do (array/push items (in cell 0))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (make-lazy-seq rt))))))))
items)
val))))
(defn- d-get
"Look up key k in a map-like value (phm/struct/table/nil)."
[m k]

View file

@ -62,6 +62,21 @@
["into map onto map" "{:a 1 :b 2 :c 3}" "(into {:a 1} [[:b 2] [:c 3]])"]
["into list" "(quote (3 2 1))" "(into (list) [1 2 3])"]
### ---- Option A: lazy transformers return seqs, not vectors ----
# map/filter/take/take-while over a concrete vector yield a lazy seq, matching
# Clojure: (seq? (map ...)) is true, (vector? (map ...)) is false.
["map vec is seq" "true" "(seq? (map inc [1 2 3]))"]
["map vec not vector" "false" "(vector? (map inc [1 2 3]))"]
["filter vec is seq" "true" "(seq? (filter odd? [1 2 3]))"]
["take vec is seq" "true" "(seq? (take 2 [1 2 3]))"]
["map over set" "true" "(= #{2 3 4} (set (map inc #{1 2 3})))"]
["filter over map ev" "(quote ([:b 2]))" "(filter (fn [[k v]] (> v 1)) {:a 1 :b 2})"]
# cons of cons over a lazy tail must not leak the rest-thunk
["cons cons lazy" "(quote (1 2 3))" "(cons 1 (cons 2 (lazy-seq (cons 3 nil))))"]
["juxt fns in vec" "[1 3]" "((juxt first last) [1 2 3])"]
["last of lazy take" "5" "(last (take 5 (iterate inc 1)))"]
["next empty lazy" "nil" "(next (take 1 [1]))"]
### ---- HIGH: destructuring ----
["destr nested seq" "[1 2 3]" "(let [[a [b c]] [1 [2 3]]] [a b c])"]
["destr rest+as" "[1 (quote (2 3)) [1 2 3]]" "(let [[a & r :as all] [1 2 3]] [a r all])"]