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:
Yogthos 2026-06-26 19:41:02 -04:00
parent 331a41ee26
commit 448611a5df
6 changed files with 57 additions and 12 deletions

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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)