Collection fns: JVM-faithful return types + laziness (#219)

A type-aware audit (~190 collection expressions vs reference Clojure) found four
divergences the corpus missed — value-equality (= [0 1] '(0 1)) hides type and
laziness differences. Fixed, with type-predicate + over-infinite corpus rows that
pin them.

- partition-all [n coll] built vector chunks; JVM chunks are seqs. (The [n step
  coll] arity was already correct, as is the partition-all transducer, whose
  chunks are vectors in JVM too.) Now builds seq chunks.
- replace always returned a vector (mapv) and was eager; JVM is type-preserving —
  a vector maps to a vector, any other seqable to a lazy seq.
- sequence eagerly realized its source (into-xform), so (first (sequence (map inc)
  (range))) hung. Rewrote as a transformer iterator: pull one input at a time,
  buffer the step outputs, emit lazily, run the completion to flush a stateful
  xform. eduction builds on it (lazy, no longer an eager vector).
- mapcat and (apply concat coll-of-colls) hung over an infinite source because
  jolt-apply seq->lists the trailing arg and mapcat seq->lists the map result.
  Added lazy-concat-seq (lazily flatten a seq of colls); mapcat uses it directly,
  and apply special-cases concat (its result is lazy) to route through it.

Docs: a cross-cutting return-type + laziness contract in docs/spec/09-core-library;
SPEC.md notes that = masks type/laziness so they need predicate / over-infinite
rows. EBNF is reader syntax only — unaffected.

Seed change (partition-all/replace/eduction are clojure.core overlay) -> re-mint;
selfhost holds. make test + shakesmoke + buildsmoke green, 0 new divergences.

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-26 03:01:36 +00:00 committed by GitHub
parent 8180c85393
commit f3084f8043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 810 additions and 679 deletions

View file

@ -1899,6 +1899,20 @@
{:suite "seq / map filter reduce" :label "mapcat three colls" :expected "[1 2 3]" :actual "(mapcat vector [1] [2] [3])"}
{:suite "seq / map filter reduce" :label "mapcat empty coll" :expected "[]" :actual "(mapcat vector [] [1 2] [3 4])"}
{:suite "seq / map filter reduce" :label "mapcat seqs" :expected "[1 2 3 4]" :actual "(mapcat identity [[1 2] [3 4]])"}
{:suite "seq / lazy over infinite" :label "mapcat is lazy over an infinite source" :expected "[1 2 1 2]" :actual "(take 4 (mapcat identity (repeat [1 2])))"}
{:suite "seq / lazy over infinite" :label "mapcat single coll lazy" :expected "[0 1 2]" :actual "(take 3 (mapcat vector (range)))"}
{:suite "seq / lazy over infinite" :label "apply concat lazy over infinite" :expected "[1 1 1]" :actual "(take 3 (apply concat (repeat [1])))"}
{:suite "seq / lazy over infinite" :label "apply concat fixed + infinite tail" :expected "[9 0 0 0 0]" :actual "(take 5 (apply concat [9] (repeat [0])))"}
{:suite "seq / lazy over infinite" :label "sequence is lazy over an infinite source" :expected "[1 2 3]" :actual "(take 3 (sequence (map inc) (range)))"}
{:suite "seq / lazy over infinite" :label "sequence first of infinite" :expected "2" :actual "(first (sequence (map inc) (range 1 100000000000)))"}
{:suite "seq / lazy over infinite" :label "sequence stateful xform lazy" :expected "[[0 1] [2 3]]" :actual "(take 2 (sequence (partition-all 2) (range)))"}
{:suite "seq / lazy over infinite" :label "sequence value + completion flush" :expected "[[0 1] [2 3] [4]]" :actual "(sequence (partition-all 2) (range 5))"}
{:suite "seq / lazy over infinite" :label "sequence empty stays a seq" :expected "true" :actual "(seq? (sequence (filter even?) [1 3 5]))"}
{:suite "seq / lazy over infinite" :label "sequence empty prints as empty" :expected "()" :actual "(sequence (filter even?) [1 3 5])"}
{:suite "seq / lazy over infinite" :label "eduction is lazy over infinite" :expected "1" :actual "(first (eduction (map inc) (range)))"}
{:suite "seq / lazy over infinite" :label "eduction reduces" :expected "9" :actual "(reduce + (eduction (filter odd?) [1 2 3 4 5]))"}
{:suite "seq / lazy over infinite" :label "eduction into" :expected "[2 3 4]" :actual "(into [] (eduction (map inc) [1 2 3]))"}
{:suite "seq / lazy over infinite" :label "eduction multiple xforms compose left-to-right" :expected "[2 4]" :actual "(into [] (eduction (filter odd?) (map inc) [1 2 3 4]))"}
{:suite "seq / map filter reduce" :label "keep" :expected "[1 3]" :actual "(keep (fn [x] (if (odd? x) x nil)) [1 2 3 4])"}
{:suite "seq / map filter reduce" :label "some truthy" :expected "true" :actual "(some even? [1 2 3])"}
{:suite "seq / map filter reduce" :label "some nil" :expected "nil" :actual "(some even? [1 3 5])"}
@ -1914,6 +1928,11 @@
{:suite "seq / take drop slice" :label "take-nth" :expected "[1 3 5]" :actual "(take-nth 2 [1 2 3 4 5])"}
{:suite "seq / take drop slice" :label "partition" :expected "[[1 2] [3 4]]" :actual "(partition 2 [1 2 3 4 5])"}
{:suite "seq / take drop slice" :label "partition-all" :expected "[[1 2] [3]]" :actual "(partition-all 2 [1 2 3])"}
{:suite "seq / take drop slice" :label "partition elems are seqs" :expected "true" :actual "(every? seq? (partition 2 [1 2 3 4]))"}
{:suite "seq / take drop slice" :label "partition-all elems are seqs not vectors" :expected "true" :actual "(every? seq? (partition-all 2 (range 5)))"}
{:suite "seq / take drop slice" :label "partition-all elem is not a vector" :expected "false" :actual "(vector? (first (partition-all 2 [1 2 3])))"}
{:suite "seq / take drop slice" :label "partition-all [n step coll] elems are seqs" :expected "true" :actual "(every? seq? (partition-all 2 2 [1 2 3 4]))"}
{:suite "seq / take drop slice" :label "partition-by elems are seqs" :expected "true" :actual "(every? seq? (partition-by odd? [1 3 2 4]))"}
{:suite "seq / take drop slice" :label "split-at" :expected "[[1 2] [3 4]]" :actual "(split-at 2 [1 2 3 4])"}
{:suite "seq / transform" :label "reverse" :expected "[3 2 1]" :actual "(reverse [1 2 3])"}
{:suite "seq / transform" :label "sort" :expected "[1 2 3]" :actual "(sort [3 1 2])"}
@ -2026,6 +2045,9 @@
{:suite "seq / overlay-migrated fns" :label "distinct? single" :expected "true" :actual "(distinct? 5)"}
{:suite "seq / overlay-migrated fns" :label "replace maps elements" :expected "[:a 2 :c 2]" :actual "(replace {1 :a 3 :c} [1 2 3 2])"}
{:suite "seq / overlay-migrated fns" :label "replace preserves nil val" :expected "[1 nil 3]" :actual "(replace {2 nil} [1 2 3])"}
{:suite "seq / overlay-migrated fns" :label "replace on a vector stays a vector" :expected "true" :actual "(vector? (replace {1 :a} [1 2 1]))"}
{:suite "seq / overlay-migrated fns" :label "replace on a seq returns a seq" :expected "true" :actual "(seq? (replace {1 :a} (list 1 2 1)))"}
{:suite "seq / overlay-migrated fns" :label "replace on a seq is lazy" :expected "(0 :a 2)" :actual "(take 3 (replace {1 :a} (range)))"}
{:suite "seq / overlay-migrated fns" :label "take-last" :expected "[3 4]" :actual "(take-last 2 [1 2 3 4])"}
{:suite "seq / overlay-migrated fns" :label "take-last empty -> nil" :expected "nil" :actual "(take-last 2 [])"}
{:suite "seq / overlay-migrated fns" :label "take-last n>len" :expected "[1 2]" :actual "(take-last 9 [1 2])"}