From 6cc3dc2c7fc1690cd7896968ac14441c0f7adbb4 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 18 Jun 2026 03:27:21 -0400 Subject: [PATCH] Fix seed assoc! to throw on odd args (jolt-ea9k) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/jolt/core_extra.janet | 10 ++++++---- test/chez/corpus.edn | 8 ++++---- test/spec/transients-spec.janet | 24 +++++++++--------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/jolt/core_extra.janet b/src/jolt/core_extra.janet index a6c0ab5..982c6a5 100644 --- a/src/jolt/core_extra.janet +++ b/src/jolt/core_extra.janet @@ -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] diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index 69a4f6a..24078e0 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -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))"} diff --git a/test/spec/transients-spec.janet b/test/spec/transients-spec.janet index e0c6068..26c523f 100644 --- a/test/spec/transients-spec.janet +++ b/test/spec/transients-spec.janet @@ -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"