Match Clojure's lazy seq realization model
jolt's seq layer realized one element ahead of Clojure, so a side-effecting lazy seq ran its producer too eagerly. Four changes bring it in line: - rest is Clojure's more(): it returns the tail without realizing it. An unforced tail (vector / string / lazy-seq cell) comes back as a deferred seq, so (rest (iterate f x)) does not call f. next still realizes one. - iterate applies f lazily, inside the tail thunk, so (first (iterate f x)) is x with no call to f (clojure.lang.Iterate parity). - take realizes exactly n: the last element terminates without touching the rest, instead of forcing one more element of the source. - an empty realized lazy seq is still a sequence value, printing "()" not "nil" (a JVM LazySeq is never nil). Also: the map transducer's step fn now takes multiple inputs ([result input & inputs]) so a multi-collection transduce applies f across all of them. Fixes medley's join/window/sequence-padded laziness and multi-input transducer tests (now 293/293). The rest change also fixed a latent overrun in distinct/dedupe over a map's empty tail. iterate is a seed source, re-minted.
This commit is contained in:
parent
331a41ee26
commit
448611a5df
6 changed files with 57 additions and 12 deletions
|
|
@ -65,10 +65,15 @@
|
|||
(set! jolt-nth (case-lambda
|
||||
((coll i) (if (jolt-lazyseq? coll) (%ls-nth (jolt-seq coll) i) (%ls-nth coll i)))
|
||||
((coll i d) (if (jolt-lazyseq? coll) (%ls-nth (jolt-seq coll) i d) (%ls-nth coll i d)))))
|
||||
;; a lazy seq prints as its realized seq — force, then re-dispatch through the printer.
|
||||
(register-pr-str-arm! jolt-lazyseq? (lambda (x) (jolt-pr-str (jolt-seq x))))
|
||||
(register-pr-readable-arm! jolt-lazyseq? (lambda (x) (jolt-pr-readable (jolt-seq x))))
|
||||
(register-str-render! jolt-lazyseq? (lambda (x) (jolt-str-render-one (jolt-seq x))))
|
||||
;; a lazy seq prints as its realized seq — force, then re-dispatch through the
|
||||
;; printer. An empty realized lazy seq is still a sequence, printing "()" (like a
|
||||
;; JVM LazySeq), not "nil" — so (lazy-seq nil) and (rest '(1)) render "()".
|
||||
(register-pr-str-arm! jolt-lazyseq?
|
||||
(lambda (x) (let ((s (jolt-seq x))) (if (jolt-nil? s) "()" (jolt-pr-str s)))))
|
||||
(register-pr-readable-arm! jolt-lazyseq?
|
||||
(lambda (x) (let ((s (jolt-seq x))) (if (jolt-nil? s) "()" (jolt-pr-readable s)))))
|
||||
(register-str-render! jolt-lazyseq?
|
||||
(lambda (x) (let ((s (jolt-seq x))) (if (jolt-nil? s) "()" (jolt-str-render-one s)))))
|
||||
|
||||
;; seq? — a lazy seq IS a seq (predicates.ss's jolt-seq? predates the lazyseq
|
||||
;; record). Unlike the native-op dispatchers above (called via a direct top-level
|
||||
|
|
|
|||
|
|
@ -17,13 +17,16 @@
|
|||
;; call routes through jolt-invoke. A `reduced` step stops the fold — reduce-seq
|
||||
;; (seq.ss) already short-circuits on a jolt-reduced.
|
||||
;; ============================================================================
|
||||
;; The map transducer's step fn supports multiple inputs ([result input & inputs]),
|
||||
;; so a multi-collection sequence/transduce — or medley's sequence-padded, which
|
||||
;; calls (f acc i1 i2 …) — applies f across all of them: (rf result (apply f inputs)).
|
||||
(define (td-map f)
|
||||
(lambda (rf)
|
||||
(lambda a
|
||||
(case (length a)
|
||||
((0) (jolt-invoke rf))
|
||||
((1) (jolt-invoke rf (car a)))
|
||||
(else (jolt-invoke rf (car a) (jolt-invoke f (cadr a))))))))
|
||||
(else (jolt-invoke rf (car a) (apply jolt-invoke f (cdr a))))))))
|
||||
(define (td-filter pred)
|
||||
(lambda (rf)
|
||||
(lambda a
|
||||
|
|
|
|||
|
|
@ -933,7 +933,7 @@
|
|||
(guard (e (#t #f))
|
||||
(def-var! "clojure.core" "repeat" (letrec ((repeat (case-lambda ((x) (let fnrec2319 ((x x)) (jolt-invoke (var-deref "clojure.core" "make-lazy-seq") (lambda () (let fnrec2320 () (jolt-invoke (var-deref "clojure.core" "coll->cells") (jolt-cons x (repeat x)))))))) ((n x) (let fnrec2321 ((n n) (x x)) (jolt-take n (repeat x))))))) repeat)))
|
||||
(guard (e (#t #f))
|
||||
(def-var! "clojure.core" "iterate" (letrec ((iterate (lambda (f x) (let fnrec2322 ((f f) (x x)) (jolt-invoke (var-deref "clojure.core" "make-lazy-seq") (lambda () (let fnrec2323 () (jolt-invoke (var-deref "clojure.core" "coll->cells") (jolt-cons x (iterate f (jolt-invoke f x))))))))))) iterate)))
|
||||
(def-var! "clojure.core" "iterate" (letrec ((iterate (lambda (f x) (let fnrec2322 ((f f) (x x)) (jolt-cons x (jolt-invoke (var-deref "clojure.core" "make-lazy-seq") (lambda () (let fnrec2323 () (jolt-invoke (var-deref "clojure.core" "coll->cells") (iterate f (jolt-invoke f x))))))))))) iterate)))
|
||||
(guard (e (#t #f))
|
||||
(def-var! "clojure.core" "partition-all" (letrec ((partition-all (case-lambda ((n) (let fnrec2324 ((n n)) (lambda (rf) (let fnrec2325 ((rf rf)) (let* ((a (jolt-invoke (var-deref "clojure.core" "volatile!") (jolt-vector)))) (case-lambda (() (let fnrec2326 () (jolt-invoke rf))) ((result) (let fnrec2327 ((result result)) (let* ((result (if (jolt-zero? (jolt-count (jolt-invoke (var-deref "clojure.core" "deref") a))) result (let* ((v (jolt-invoke (var-deref "clojure.core" "deref") a))) (begin (jolt-invoke (var-deref "clojure.core" "vreset!") a (jolt-vector)) (jolt-invoke (var-deref "clojure.core" "unreduced") (jolt-invoke rf result v))))))) (jolt-invoke rf result)))) ((result input) (let fnrec2328 ((result result) (input input)) (begin (jolt-invoke (var-deref "clojure.core" "vswap!") a jolt-conj input) (if (jolt= n (jolt-count (jolt-invoke (var-deref "clojure.core" "deref") a))) (let* ((v (jolt-invoke (var-deref "clojure.core" "deref") a))) (begin (jolt-invoke (var-deref "clojure.core" "vreset!") a (jolt-vector)) (jolt-invoke rf result v))) result)))))))))) ((n coll) (let fnrec2329 ((n n) (coll coll)) (letrec* ((go (letrec ((go (lambda (s) (let fnrec2330 ((s s)) (jolt-invoke (var-deref "clojure.core" "make-lazy-seq") (lambda () (let fnrec2331 () (jolt-invoke (var-deref "clojure.core" "coll->cells") (if (jolt-truthy? (jolt-seq s)) (let* ((i 0) (acc (jolt-list )) (cur s)) (let loop2332 ((i i) (acc acc) (cur cur)) (if (jolt-truthy? (let* ((and__25__auto (< i n))) (if (jolt-truthy? and__25__auto) (jolt-seq cur) and__25__auto))) (let* ((_a$2333 (jolt-inc i)) (_a$2334 (jolt-cons (jolt-first cur) acc)) (_a$2335 (jolt-rest cur))) (loop2332 _a$2333 _a$2334 _a$2335)) (let* ((_a$2336 (jolt-reverse acc)) (_a$2337 (go cur))) (jolt-cons _a$2336 _a$2337))))) jolt-nil))))))))) go))) (jolt-invoke go coll)))) ((n step coll) (let fnrec2338 ((n n) (step step) (coll coll)) (letrec* ((go (letrec ((go (lambda (s) (let fnrec2339 ((s s)) (jolt-invoke (var-deref "clojure.core" "make-lazy-seq") (lambda () (let fnrec2340 () (jolt-invoke (var-deref "clojure.core" "coll->cells") (if (jolt-truthy? (jolt-seq s)) (let* ((_a$2341 (jolt-take n s)) (_a$2342 (go (jolt-invoke (var-deref "clojure.core" "nthrest") s step)))) (jolt-cons _a$2341 _a$2342)) jolt-nil))))))))) go))) (jolt-invoke go coll))))))) partition-all)))
|
||||
(guard (e (#t #f))
|
||||
|
|
|
|||
|
|
@ -85,10 +85,25 @@
|
|||
;; the seq leaf ops the emitter lowers core fns to
|
||||
;; ============================================================================
|
||||
(define (jolt-first x) (let ((s (jolt-seq x))) (if (jolt-nil? s) jolt-nil (seq-first s))))
|
||||
(define (jolt-rest x) ; () when the seq has 0/1 elements (NOT nil)
|
||||
;; rest = Clojure's more(): the tail as a (possibly empty) seq, NOT nil, and
|
||||
;; WITHOUT realizing it. A forced cseq (list / realized chain) hands back its tail
|
||||
;; directly. An UNFORCED tail (vector / string / lazy-seq cell) is returned as a
|
||||
;; deferred seq so (rest s) does not realize the next node — matching Clojure,
|
||||
;; where (rest (iterate f x)) does not call f and a side-effecting lazy seq is
|
||||
;; realized one element at a time. next = (seq (rest s)) still realizes one.
|
||||
;; jolt-make-lazy-seq (lazy-bridge.ss) resolves at call time.
|
||||
(define (jolt-rest x)
|
||||
(let ((s (jolt-seq x)))
|
||||
(if (jolt-nil? s) jolt-empty-list
|
||||
(let ((m (seq-more s))) (if (jolt-nil? m) jolt-empty-list m)))))
|
||||
(cond
|
||||
((jolt-nil? s) jolt-empty-list)
|
||||
((cseq-forced? s) (let ((m (cseq-tail s))) (if (jolt-nil? m) jolt-empty-list m)))
|
||||
;; the lazyseq forces to a seq (cseq | nil); an empty realized lazyseq is
|
||||
;; still a sequence value, printing "()" (see lazy-bridge.ss), so (rest s)
|
||||
;; is never nil even when the tail is empty. jolt-seq coerces seq-more's
|
||||
;; result (which may be jolt-empty-list, e.g. map's tail) back to cseq | nil,
|
||||
;; the contract force-lazyseq relies on — else (seq (rest s)) of an empty
|
||||
;; tail yields a truthy empty-list and walkers (distinct, dedupe) overrun.
|
||||
(else (jolt-make-lazy-seq (lambda () (jolt-seq (seq-more s))))))))
|
||||
(define (jolt-next x) ; nil when the rest is empty
|
||||
;; next = (seq (rest x)): the rest must be RE-SEQ'd so an empty tail collapses to
|
||||
;; nil. seq-more on a lazy seq (e.g. map's) forces to jolt-empty-list, which is
|
||||
|
|
@ -246,11 +261,21 @@
|
|||
((start end) (range-bounded start end 1))
|
||||
((start end step) (range-bounded start end step))))
|
||||
|
||||
;; An empty take result is () (jolt-empty-list), NOT nil — (take 0 coll) and
|
||||
;; (take n []) are empty seqs in Clojure, so (= () (take 0 [:a])) and printing
|
||||
;; "()" hold. jolt-empty-list seqs back to nil, so it also terminates the lazy
|
||||
;; tail when n hits 0 mid-stream (see map-seq).
|
||||
;; The LAST element (n=1) terminates without touching the rest, so (take n s)
|
||||
;; realizes exactly n elements of a side-effecting seq — matching Clojure, where
|
||||
;; (take 0 (rest s)) never seqs coll. Realizing one more, as forcing seq-more at
|
||||
;; the boundary would, over-runs the source by one (medley's sequence-padded).
|
||||
(define (jolt-take n coll)
|
||||
(let ((n (->idx n)))
|
||||
(let loop ((n n) (s (jolt-seq coll)))
|
||||
(if (or (fx<=? n 0) (jolt-nil? s)) jolt-nil
|
||||
(cseq-lazy (seq-first s) (lambda () (loop (fx- n 1) (jolt-seq (seq-more s)))))))))
|
||||
(cond
|
||||
((or (fx<=? n 0) (jolt-nil? s)) jolt-empty-list)
|
||||
((fx=? n 1) (cseq-lazy (seq-first s) (lambda () jolt-empty-list)))
|
||||
(else (cseq-lazy (seq-first s) (lambda () (loop (fx- n 1) (jolt-seq (seq-more s))))))))))
|
||||
(define (jolt-drop n coll)
|
||||
(let loop ((n (->idx n)) (s (jolt-seq coll)))
|
||||
(if (or (fx<=? n 0) (jolt-nil? s)) (if (jolt-nil? s) jolt-empty-list s)
|
||||
|
|
|
|||
|
|
@ -96,8 +96,12 @@
|
|||
([n x] (take n (repeat x))))
|
||||
|
||||
;; --- iterate ---
|
||||
;; f is applied lazily, inside the tail thunk — (first (iterate f x)) is x with no
|
||||
;; call to f, matching clojure.lang.Iterate. Wrapping the whole body in lazy-seq
|
||||
;; instead would force (f x) the moment the head realizes (it is an eager argument
|
||||
;; to cons), realizing one step ahead.
|
||||
(defn iterate [f x]
|
||||
(lazy-seq (cons x (iterate f (f x)))))
|
||||
(cons x (lazy-seq (iterate f (f x)))))
|
||||
|
||||
|
||||
;; --- partition-all --- (transducer + [n coll] + [n step coll])
|
||||
|
|
|
|||
|
|
@ -3261,4 +3261,12 @@
|
|||
{:suite "printer / print-level" :label "scalars print regardless of level" :expected "true" :actual "(= \"[1 2]\" (binding [*print-level* 1] (pr-str [1 2])))"}
|
||||
{:suite "reader / default-data-reader-fn" :label "consulted for an unregistered tag" :expected "true" :actual "(= [(quote foo) 42] (binding [*default-data-reader-fn* (fn [tag v] [tag v])] (read-string \"#foo 42\")))"}
|
||||
{:suite "printer / print-vars" :label "print-length / print-level / default-data-reader-fn default to nil" :expected "[true true true]" :actual "[(nil? *print-length*) (nil? *print-level*) (nil? *default-data-reader-fn*)]"}
|
||||
{:suite "seqs / laziness" :label "an empty lazy seq is () not nil" :expected "true" :actual "(= () (lazy-seq nil))"}
|
||||
{:suite "seqs / laziness" :label "rest of a one-element coll is ()" :expected "true" :actual "(= () (rest [1]))"}
|
||||
{:suite "seqs / laziness" :label "first of iterate does not call f" :expected "true" :actual "(let [a (atom 0)] (first (iterate (fn [x] (swap! a inc) (inc x)) 0)) (= 0 @a))"}
|
||||
{:suite "seqs / laziness" :label "rest of iterate does not realize the next element" :expected "true" :actual "(let [a (atom 0)] (rest (iterate (fn [x] (swap! a inc) (inc x)) 0)) (= 0 @a))"}
|
||||
{:suite "seqs / laziness" :label "take from an infinite iterate" :expected "[0 1 2 3 4]" :actual "(vec (take 5 (iterate inc 0)))"}
|
||||
{:suite "seqs / laziness" :label "distinct over a rest-derived seq does not overrun" :expected "2" :actual "(count (distinct (map inc (rest [10 20 30]))))"}
|
||||
{:suite "transducers / map multi-input" :label "the map transducer applies f across all inputs" :expected "[6]" :actual "(((map +) conj) [] 1 2 3)"}
|
||||
{:suite "transducers / map multi-input" :label "single input is unchanged" :expected "[3]" :actual "(((map +) conj) [] 3)"}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue