core: Stage 3 — sorted collections are pure Clojure (canonical port)

sorted-map/sorted-map-by/sorted-set/sorted-set-by/sorted?/sorted-map?/
sorted-set?/subseq/rsubseq now live in their own overlay tier (25-sorted.clj).
A sorted coll is a tagged host table with a comparator-ordered :entries
vector, a 3-way :cmp, and the tier's op implementations ATTACHED to the value
(:ops map): the seed's conj/assoc/get/seq/count/... branches are each a
one-line call through (coll :ops), so the ops travel with the value — correct
across contexts, forks, and AOT images, no module-level hooks to re-wire.
The host surface grows by three minimal value primitives: jolt.host/
tagged-table, ref-put! (already there), and ref-get — a raw field read,
because plain get on a sorted coll IS the comparator lookup and reading
:entries with it recurses.

This fixes a pile of Clojure-correctness gaps the Janet kernel had:
- lookup/membership now go through the COMPARATOR: (contains? (sorted-set 1)
  1.0) was a deep= scan, (conj (sorted-set 1) 1.0) and assoc of a
  comparator-equal key now no-op/replace as in Clojure
- equality is representation-agnostic: (= (sorted-map :a 1) {:a 1}) and
  (= (sorted-set 1 2) #{1 2}) were false
- iteration was broken: (map inc (sorted-set 3 1 2)) errored
  (realize-for-iteration and coll->cells had no sorted branches)
- empty?/empty saw the host wrapper, not the collection: (empty? (sorted-map))
  was false, (empty sc) returned a bare table; it now keeps the comparator
- sorted colls canonicalize as map keys; comparator fns may be boolean
  predicates or 3-way (Clojure's fn->comparator)
- sorted-map throws on odd kv count; conj nil is a no-op

Also fixes jolt-h86 en passant: into-conj had no branch for sets (or sorted
colls) and silently returned the target unchanged — (into #{} [:a :b]) was
#{}. The fallback now folds conj. Regression rows in sets-spec.

sorted-spec grows to 77 rows (comparator-based membership, equality,
empty/rseq/printing, seq-fn interop, subseq/rsubseq on maps). Gate green:
conformance 326x3, suite 4577 (vs 4566 prior — the battery gained rows),
sorted+sets specs, full jpm test, bench at parity with main back-to-back
(4521ms vs 4619ms TOTAL under identical load).
This commit is contained in:
Yogthos 2026-06-10 14:38:09 -04:00
parent d6c5552fda
commit 55a3ebf93f
9 changed files with 400 additions and 143 deletions

View file

@ -1,10 +1,13 @@
# Specification: sorted collections (sorted-map / sorted-set, subseq/rsubseq).
#
# sorted collections are first-class for the core ops (jolt-ti9): get/assoc/dissoc/
# conj/contains?/keys/vals/disj all work and preserve sort order, and a sorted coll
# is callable as a key-lookup fn. STILL TODO: the by-comparator constructors
# (sorted-map-by / sorted-set-by) ignore the supplied comparator (jolt-ti9). (vec
# coerces a seq to a vector so expecteds are vector literals, not quoted lists.)
# Sorted collections are pure Clojure (stage 3, jolt-0lj): the entries live in
# a comparator-ordered vector, all ops are overlay Clojure attached to the
# value, and the Janet seed only dispatches to them. Semantics match Clojure:
# lookup/membership go through the COMPARATOR ((contains? (sorted-set 1) 1.0)
# is true), equality is representation-agnostic ((= (sorted-map :a 1) {:a 1})),
# empty?/empty see the collection (not the host wrapper) and (empty sc) keeps
# the comparator. (vec coerces a seq to a vector so expecteds are vector
# literals, not quoted lists.)
(use ../support/harness)
(defspec "sorted / construction & ordering"
@ -56,4 +59,62 @@
["subseq >=" "[3 4 5]" "(vec (subseq (sorted-set 1 2 3 4 5) >= 3))"]
["subseq <" "[1 2]" "(vec (subseq (sorted-set 1 2 3 4 5) < 3))"]
["subseq range" "[2 3 4]" "(vec (subseq (sorted-set 1 2 3 4 5) > 1 < 5))"]
["rsubseq <=" "[3 2 1]" "(vec (rsubseq (sorted-set 1 2 3 4 5) <= 3))"])
["rsubseq <=" "[3 2 1]" "(vec (rsubseq (sorted-set 1 2 3 4 5) <= 3))"]
["subseq on map" "[[2 :b] [3 :c]]" "(vec (subseq (sorted-map 1 :a 2 :b 3 :c) > 1))"]
["subseq empty result" "nil" "(subseq (sorted-set 1 2) > 5)"]
["rsubseq on map" "[[2 :b] [1 :a]]" "(vec (rsubseq (sorted-map 1 :a 2 :b 3 :c) < 3))"])
(defspec "sorted / predicates"
["sorted-map? true" "true" "(sorted-map? (sorted-map 1 :a))"]
["sorted-map? false" "false" "(sorted-map? {:a 1})"]
["sorted-set? true" "true" "(sorted-set? (sorted-set 1))"]
["sorted-set? false" "false" "(sorted-set? #{1})"]
["map? sorted-map" "true" "(map? (sorted-map 1 :a))"]
["coll? sorted-set" "true" "(coll? (sorted-set 1))"])
(defspec "sorted / lookup + membership use the comparator"
["get cross-numeric" ":a" "(get (sorted-map 1 :a) 1.0)"]
["contains? cross-numeric" "true" "(contains? (sorted-set 1) 1.0)"]
["conj equal elem no-op" "1" "(count (conj (sorted-set 1) 1.0))"]
["assoc equal key replaces" "[[1 :z]]" "(vec (seq (assoc (sorted-map 1 :a) 1.0 :z)))"]
["first sorted-map" "[1 :a]" "(first (sorted-map 2 :b 1 :a))"]
["dissoc missing no-op" "2" "(count (dissoc (sorted-map 1 :a 2 :b) 9))"]
["conj map merges" "3" "(count (conj (sorted-map 1 :a) {2 :b 3 :c}))"]
["conj nil no-op" "1" "(count (conj (sorted-map 1 :a) nil))"]
["into sorted-map" "[[1 :a] [2 :b]]" "(vec (seq (into (sorted-map) [[2 :b] [1 :a]])))"]
["source unchanged" "[1 2]" "(let [s (sorted-set 1 2)] (conj s 9) (vec (seq s)))"]
["sorted-map odd kvs throws" :throws "(sorted-map 1 :a 2)"])
(defspec "sorted / equality is representation-agnostic"
["sorted-map = literal" "true" "(= (sorted-map :a 1 :b 2) {:a 1 :b 2})"]
["literal = sorted-map" "true" "(= {:a 1 :b 2} (sorted-map :a 1 :b 2))"]
["sorted-map = hash-map" "true" "(= (sorted-map :a 1) (hash-map :a 1))"]
["sorted-map != more keys" "false" "(= (sorted-map :a 1) {:a 1 :b 2})"]
["sorted-set = literal" "true" "(= (sorted-set 1 2) #{1 2})"]
["literal = sorted-set" "true" "(= #{1 2} (sorted-set 2 1))"]
["sorted-set != diff" "false" "(= (sorted-set 1 2) #{1 3})"]
["two sorted-maps" "true" "(= (sorted-map 1 :a 2 :b) (sorted-map 2 :b 1 :a))"]
["cmp irrelevant to =" "true" "(= (sorted-map-by > 1 :a 2 :b) (sorted-map 1 :a 2 :b))"]
["sorted-map as map key" ":hit" "(get {(sorted-map :a 1) :hit} {:a 1})"]
["sorted-set as map key" ":hit" "(get {(sorted-set 1 2) :hit} #{2 1})"])
(defspec "sorted / empty + empty? + rseq + printing"
["empty? empty map" "true" "(empty? (sorted-map))"]
["empty? non-empty" "false" "(empty? (sorted-map 1 :a))"]
["empty? empty set" "true" "(empty? (sorted-set))"]
["empty keeps sortedness" "true" "(sorted? (empty (sorted-map 1 :a)))"]
["empty keeps cmp" "[3 1]" "(vec (seq (into (empty (sorted-set-by > 1 2)) [1 3])))"]
["empty set kind" "true" "(sorted-set? (empty (sorted-set 1)))"]
["rseq map" "[[2 :b] [1 :a]]" "(vec (rseq (sorted-map 1 :a 2 :b)))"]
["rseq set" "[3 2 1]" "(vec (rseq (sorted-set 1 2 3)))"]
["pr-str sorted-map" "\"{1 :a, 2 :b}\"" "(pr-str (sorted-map 2 :b 1 :a))"]
["pr-str sorted-set" "\"#{1 2 3}\"" "(pr-str (sorted-set 3 1 2))"])
(defspec "sorted / seq fn interop"
["map over sorted-map" "[1 2 3]" "(vec (map first (sorted-map 2 :b 1 :a 3 :c)))"]
["map over sorted-set" "[2 3 4]" "(vec (map inc (sorted-set 3 1 2)))"]
["filter entries" "[[2 :b]]" "(vec (filter (fn [[k v]] (even? k)) (sorted-map 1 :a 2 :b)))"]
["reduce over set" "6" "(reduce + (sorted-set 1 2 3))"]
["vec of sorted-set" "[1 2 3]" "(vec (sorted-set 3 1 2))"]
["into vec" "[[1 :a] [2 :b]]" "(into [] (sorted-map 2 :b 1 :a))"]
["sorted-map-by 3way cmp" "[3 2 1]" "(vec (keys (sorted-map-by (fn [a b] (- b a)) 1 :a 2 :b 3 :c)))"])