feat(strictness): transient bang ops require a transient; conj! arities

conj!/assoc!/dissoc!/disj!/pop!/persistent! now throw on a non-transient (or
wrong transient kind) instead of falling back to the persistent op, matching
Clojure. conj! keeps its special arities: (conj!) -> (transient []), (conj! coll)
-> coll. conj! onto a transient map accepts a [k v] pair or a map (merge), and
throws on a list/set/seq.

pop_bang/dissoc_bang clean; conj_bang 13/1/22->47/3/1; persistent_bang 9/8->16/1.
clojure-test-suite pass 3781->3824. spec: transient/strictness (10). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 11:09:38 -04:00
parent fb36577ee7
commit a4a48a8520
3 changed files with 43 additions and 14 deletions

View file

@ -3266,7 +3266,16 @@
(case (t :kind) (case (t :kind)
:vector (array/push (t :arr) x) :vector (array/push (t :arr) x)
:set (put (t :tbl) (canon-key x) x) :set (put (t :tbl) (canon-key x) x)
:map (let [k (vnth x 0)] (put (t :tbl) (canon-key k) @[k (vnth x 1)]))) :map (cond
# a [k v] pair (map-entry / 2-vector)
(and (or (pvec? x) (tuple? x) (array? x))
(= 2 (if (pvec? x) (pv-count x) (length x))))
(put (t :tbl) (canon-key (vnth x 0)) @[(vnth x 0) (vnth x 1)])
# a map: merge all its entries
(or (phm? x) (and (struct? x) (nil? (get x :jolt/type))))
(each k (if (phm? x) (keys (phm-to-struct x)) (keys x))
(put (t :tbl) (canon-key k) @[k (if (phm? x) (phm-get x k) (in x k))]))
(error "conj! on a transient map requires a [key value] pair or a map")))
t) t)
(defn- tr-assoc! [t k v] (defn- tr-assoc! [t k v]
@ -3277,10 +3286,16 @@
(error "assoc! expects a transient vector or map")) (error "assoc! expects a transient vector or map"))
t) t)
(defn core-conj! [t & xs] # The bang ops require a transient (Clojure throws otherwise); no lenient
(if (core-transient? t) # fallback to the persistent op.
(do (each x xs (tr-conj! t x)) t) (defn core-conj! [& args]
(apply core-conj t xs))) # lenient fallback for a persistent coll (cond
(= 0 (length args)) (core-transient (make-vec @[])) # (conj!) -> (transient [])
(= 1 (length args)) (first args) # (conj! coll) -> coll, as-is
(let [t (first args) xs (tuple/slice args 1)]
(if (core-transient? t)
(do (each x xs (tr-conj! t x)) t)
(error "conj! requires a transient")))))
(defn core-assoc! [t & kvs] (defn core-assoc! [t & kvs]
# Unlike assoc, assoc! accepts an ODD number of args — a missing final value # Unlike assoc, assoc! accepts an ODD number of args — a missing final value
@ -3288,24 +3303,24 @@
# error on the dangling key). # error on the dangling key).
(if (core-transient? t) (if (core-transient? t)
(do (var i 0) (while (< i (length kvs)) (tr-assoc! t (in kvs i) (get kvs (+ i 1))) (+= i 2)) t) (do (var i 0) (while (< i (length kvs)) (tr-assoc! t (in kvs i) (get kvs (+ i 1))) (+= i 2)) t)
(apply core-assoc t kvs))) (error "assoc! requires a transient")))
(defn core-dissoc! [t & ks] (defn core-dissoc! [t & ks]
(if (core-transient? t) (if (and (core-transient? t) (= :map (t :kind)))
(do (tr-check-active! t) (each k ks (put (t :tbl) (canon-key k) nil)) t) (do (tr-check-active! t) (each k ks (put (t :tbl) (canon-key k) nil)) t)
(apply core-dissoc t ks))) (error "dissoc! requires a transient map")))
(defn core-disj! [t & xs] (defn core-disj! [t & xs]
(if (core-transient? t) (if (and (core-transient? t) (= :set (t :kind)))
(do (tr-check-active! t) (each x xs (put (t :tbl) (canon-key x) nil)) t) (do (tr-check-active! t) (each x xs (put (t :tbl) (canon-key x) nil)) t)
(apply core-disj t xs))) (error "disj! requires a transient set")))
(defn core-pop! [t] (defn core-pop! [t]
(if (core-transient? t) (if (and (core-transient? t) (= :vector (t :kind)))
(do (tr-check-active! t) (do (tr-check-active! t)
(when (= 0 (length (t :arr))) (error "Can't pop empty vector")) (when (= 0 (length (t :arr))) (error "Can't pop empty vector"))
(array/pop (t :arr)) t) (array/pop (t :arr)) t)
(core-pop t))) (error "pop! requires a transient vector")))
(defn core-persistent! [t] (defn core-persistent! [t]
(if (core-transient? t) (if (core-transient? t)
@ -3319,7 +3334,7 @@
# Invalidate: any further bang op (or a second persistent!) now throws. # Invalidate: any further bang op (or a second persistent!) now throws.
(put t :jolt/persistent true) (put t :jolt/persistent true)
result) result)
t)) (error "persistent! requires a transient")))
# Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops. # Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops.
(defn core-unchecked-add [a b] (+ a b)) (defn core-unchecked-add [a b] (+ a b))

View file

@ -18,7 +18,7 @@
# Baseline: assertions Jolt currently passes across the suite. Raise as Jolt # Baseline: assertions Jolt currently passes across the suite. Raise as Jolt
# improves so a regression (previously-passing assertion breaking) is caught. # improves so a regression (previously-passing assertion breaking) is caught.
(def baseline-pass 3770) (def baseline-pass 3820)
# A file is "clean" when it ran with zero failures AND zero errors. # A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 45) (def baseline-clean-files 45)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s; # Per-file wall-clock budget (seconds). Normal files finish in well under 1s;

View file

@ -76,3 +76,17 @@
["persistent! twice" :throws ["persistent! twice" :throws
"(let [t (transient [])] (persistent! t) (persistent! t))"] "(let [t (transient [])] (persistent! t) (persistent! t))"]
["pop! empty" :throws "(pop! (transient []))"]) ["pop! empty" :throws "(pop! (transient []))"])
# The bang ops require an appropriate transient (Clojure throws otherwise);
# conj! has the special 0-/1-arg identity arities.
(defspec "transient / strictness"
["conj! on persistent" :throws "(conj! [1 2] 3)"]
["assoc! on persistent" :throws "(assoc! {:a 1} :b 2)"]
["persistent! on vector" :throws "(persistent! [1 2])"]
["persistent! on nil" :throws "(persistent! nil)"]
["pop! on transient map" :throws "(pop! (transient {:a 1}))"]
["dissoc! on tset" :throws "(dissoc! (transient #{1}) 1)"]
["conj! map bad item" :throws "(conj! (transient {}) (list :a 1))"]
["conj! no args" "[]" "(persistent! (conj!))"]
["conj! identity" "[1 2]" "(conj! [1 2])"]
["conj! map merges map" "{:a 1, :b 2}" "(persistent! (conj! (transient {:a 1}) {:b 2}))"])