Fix seed assoc! to throw on odd args (jolt-ea9k)

The transient assoc! accepted an odd key/val count and silently assigned
nil to the dangling key — non-Clojure, and inconsistent with the seed's
own plain assoc (which throws) and Clojure's assoc!. Make it throw.

Updates the 4 'assoc! odd args' transient spec rows to 3 :throws + 1
even-args positive, and regenerates corpus.edn. The Chez host already
threw on these, so this only realigns the corpus contract.
This commit is contained in:
Yogthos 2026-06-18 03:27:21 -04:00
parent d7420deecb
commit 6cc3dc2c7f
3 changed files with 19 additions and 23 deletions

View file

@ -402,11 +402,13 @@
(error "conj! requires a transient")))))
(defn core-assoc! [t & kvs]
# Unlike assoc, assoc! accepts an ODD number of args — a missing final value
# is taken as nil (so (get kvs (+ i 1)) rather than (in ...), which would
# error on the dangling key).
# assoc! requires an even key/val count, like assoc and Clojure's assoc! — a
# dangling key with no value throws (jolt-ea9k: the former lenient nil-fill
# was non-Clojure and inconsistent with jolt's own plain assoc).
(when (odd? (length kvs))
(error "assoc! expects an even number of key/value arguments"))
(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) (in kvs (+ i 1))) (+= i 2)) t)
(error "assoc! requires a transient")))
(defn core-dissoc! [t & ks]

View file

@ -2348,10 +2348,10 @@
{:suite "transient / invokable lookup" :label "set membership" :expected "2" :actual "((transient #{1 2 3}) 2)"}
{:suite "transient / invokable lookup" :label "set miss default" :expected ":no" :actual "((transient #{1 2 3}) 42 :no)"}
{:suite "transient / invokable lookup" :label "collection key" :expected ":v" :actual "((transient {[1 2] :v}) [1 2])"}
{:suite "transient / assoc! odd args" :label "odd arg key present" :expected "true" :actual "(contains? (persistent! (assoc! (transient {}) :a 1 :b)) :b)"}
{:suite "transient / assoc! odd args" :label "odd arg value is nil" :expected "true" :actual "(nil? (get (persistent! (assoc! (transient {}) :a 1 :b)) :b))"}
{:suite "transient / assoc! odd args" :label "odd arg keeps prior" :expected "1" :actual "(get (persistent! (assoc! (transient {}) :a 1 :b)) :a)"}
{:suite "transient / assoc! odd args" :label "vector odd arg" :expected "[9 nil]" :actual "(persistent! (apply assoc! (transient []) [0 9 1]))"}
{:suite "transient / assoc! odd args throw" :label "map dangling key" :expected :throws :actual "(persistent! (assoc! (transient {}) :a 1 :b))"}
{:suite "transient / assoc! odd args throw" :label "map lone key" :expected :throws :actual "(persistent! (assoc! (transient {}) :a))"}
{:suite "transient / assoc! odd args throw" :label "vector dangling" :expected :throws :actual "(persistent! (apply assoc! (transient []) [0 9 1]))"}
{:suite "transient / assoc! odd args throw" :label "even args still ok" :expected "true" :actual "(= {:a 1, :b 2} (persistent! (assoc! (transient {}) :a 1 :b 2)))"}
{:suite "transient / invalidation" :label "conj! after persistent!" :expected :throws :actual "(let [t (transient [])] (persistent! t) (conj! t 1))"}
{:suite "transient / invalidation" :label "assoc! after persistent!" :expected :throws :actual "(let [t (transient {})] (persistent! t) (assoc! t :a 1))"}
{:suite "transient / invalidation" :label "persistent! twice" :expected :throws :actual "(let [t (transient [])] (persistent! t) (persistent! t))"}

View file

@ -55,21 +55,15 @@
["set miss default" ":no" "((transient #{1 2 3}) 42 :no)"]
["collection key" ":v" "((transient {[1 2] :v}) [1 2])"])
# assoc! (unlike assoc) accepts an odd arg count — a missing final value is nil.
# (A struct literal can't express an explicit nil value, so assert via contains?.)
# KNOWN BUG (jolt-ea9k): this leniency is non-Clojure — Clojure's assoc! throws on
# odd args, as does jolt's own plain assoc. These rows encode the buggy behavior;
# when the seed is fixed to throw, update them to :throws. The Chez host already
# throws (Clojure-correct), so these sit in its crash bucket until then.
(defspec "transient / assoc! odd args"
["odd arg key present" "true"
"(contains? (persistent! (assoc! (transient {}) :a 1 :b)) :b)"]
["odd arg value is nil" "true"
"(nil? (get (persistent! (assoc! (transient {}) :a 1 :b)) :b))"]
["odd arg keeps prior" "1"
"(get (persistent! (assoc! (transient {}) :a 1 :b)) :a)"]
["vector odd arg" "[9 nil]"
"(persistent! (apply assoc! (transient []) [0 9 1]))"])
# assoc! requires an even key/val arg count, like assoc and Clojure's assoc!: a
# dangling key with no value throws (jolt-ea9k fixed the former lenient
# nil-fill, which was non-Clojure and inconsistent with jolt's own plain assoc).
(defspec "transient / assoc! odd args throw"
["map dangling key" :throws "(persistent! (assoc! (transient {}) :a 1 :b))"]
["map lone key" :throws "(persistent! (assoc! (transient {}) :a))"]
["vector dangling" :throws "(persistent! (apply assoc! (transient []) [0 9 1]))"]
["even args still ok" "true"
"(= {:a 1, :b 2} (persistent! (assoc! (transient {}) :a 1 :b 2)))"])
# Using a transient after persistent! (or popping an empty one) throws.
(defspec "transient / invalidation"