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:
parent
5e7aea70da
commit
074eb2da41
4 changed files with 127 additions and 116 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
(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)]
|
||||
(or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))))
|
||||
(or (nil? c) (= 0 (length 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.
|
||||
# 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 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))))
|
||||
(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."
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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])"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue