fix: transient invokable lookup, assoc! odd args, use-after-persistent! invalidation
Real transient correctness gaps surfaced by the clojure-test-suite: - Transients are now invokable for read-only lookup like their persistent forms: ((transient v) i), ((transient m) k [default]), (:k (transient m)), ((transient s) x). jolt-invoke/coll-lookup gained a transient branch (transient-lookup) that indexes :arr / canon-keyed :tbl. Added phm/canon as a public canonicalizer so collection keys compare by value here too. - assoc! accepts an ODD arg count (a missing final value is nil), unlike assoc — core-assoc! now uses nil-safe get for the value instead of erroring. - Using a transient after persistent! (or a second persistent!, or pop! on an empty transient vector) now throws, via a :jolt/persistent invalidation flag checked by the mutating ops. Catches the classic transient footgun. spec: transients-spec gains invokable-lookup (7), assoc!-odd-args (4), invalidation (4). clojure-test-suite pass 1704->1719. Remaining transient fails are accepted lenient divergences (jolt doesn't throw on bad-shape conj!/assoc!/pop! of wrong-typed args; nil set elements). jpm test green.
This commit is contained in:
parent
e75771084e
commit
09532dac05
5 changed files with 83 additions and 10 deletions
|
|
@ -3129,7 +3129,13 @@
|
|||
(array? coll) @{:jolt/type :jolt/transient :kind :vector :arr (array/slice coll)}
|
||||
(error (string "Don't know how to create a transient from " (type coll)))))
|
||||
|
||||
# A transient is invalidated by persistent!; using it afterwards is a bug.
|
||||
(defn- tr-check-active! [t]
|
||||
(when (get t :jolt/persistent)
|
||||
(error "Transient used after persistent! call")))
|
||||
|
||||
(defn- tr-conj! [t x]
|
||||
(tr-check-active! t)
|
||||
(case (t :kind)
|
||||
:vector (array/push (t :arr) x)
|
||||
:set (put (t :tbl) (canon-key x) x)
|
||||
|
|
@ -3137,6 +3143,7 @@
|
|||
t)
|
||||
|
||||
(defn- tr-assoc! [t k v]
|
||||
(tr-check-active! t)
|
||||
(case (t :kind)
|
||||
:vector (let [a (t :arr)] (if (= k (length a)) (array/push a v) (put a k v)))
|
||||
:map (put (t :tbl) (canon-key k) @[k v])
|
||||
|
|
@ -3149,31 +3156,42 @@
|
|||
(apply core-conj t xs))) # lenient fallback for a persistent coll
|
||||
|
||||
(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).
|
||||
(if (core-transient? t)
|
||||
(do (var i 0) (while (< i (length kvs)) (tr-assoc! t (in kvs i) (in 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)))
|
||||
|
||||
(defn core-dissoc! [t & ks]
|
||||
(if (core-transient? t)
|
||||
(do (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)))
|
||||
|
||||
(defn core-disj! [t & xs]
|
||||
(if (core-transient? t)
|
||||
(do (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)))
|
||||
|
||||
(defn core-pop! [t]
|
||||
(if (core-transient? t)
|
||||
(do (array/pop (t :arr)) t)
|
||||
(do (tr-check-active! t)
|
||||
(when (= 0 (length (t :arr))) (error "Can't pop empty vector"))
|
||||
(array/pop (t :arr)) t)
|
||||
(core-pop t)))
|
||||
|
||||
(defn core-persistent! [t]
|
||||
(if (core-transient? t)
|
||||
(case (t :kind)
|
||||
:vector (make-vec (t :arr))
|
||||
:set (do (var s (make-phs)) (each [_ e] (pairs (t :tbl)) (set s (phs-conj s e))) s)
|
||||
:map (do (var m (make-phm)) (each [_ pair] (pairs (t :tbl)) (set m (phm-assoc m (in pair 0) (in pair 1)))) m))
|
||||
(do
|
||||
(tr-check-active! t)
|
||||
(def result
|
||||
(case (t :kind)
|
||||
:vector (make-vec (t :arr))
|
||||
:set (do (var s (make-phs)) (each [_ e] (pairs (t :tbl)) (set s (phs-conj s e))) s)
|
||||
:map (do (var m (make-phm)) (each [_ pair] (pairs (t :tbl)) (set m (phm-assoc m (in pair 0) (in pair 1)))) m)))
|
||||
# Invalidate: any further bang op (or a second persistent!) now throws.
|
||||
(put t :jolt/persistent true)
|
||||
result)
|
||||
t))
|
||||
|
||||
# Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops.
|
||||
|
|
|
|||
|
|
@ -34,10 +34,27 @@
|
|||
|
||||
(var eval-form nil)
|
||||
|
||||
# A transient is a tagged mutable table @{:jolt/type :jolt/transient :kind ...}.
|
||||
(defn- jolt-transient? [x]
|
||||
(and (table? x) (= :jolt/transient (get x :jolt/type))))
|
||||
|
||||
# Read-only lookup over a transient (vector index / map key / set membership),
|
||||
# mirroring core-get. Map/set backing tables are keyed by the same canon used
|
||||
# by phm, so canonicalize collection keys here too.
|
||||
(defn- transient-lookup [t k default]
|
||||
(case (t :kind)
|
||||
:vector (let [a (t :arr)]
|
||||
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length a)))
|
||||
(in a k) default))
|
||||
:map (let [e (get (t :tbl) (canon k))] (if (nil? e) default (in e 1)))
|
||||
:set (if (nil? (get (t :tbl) (canon k))) default k)
|
||||
default))
|
||||
|
||||
(defn- coll-lookup
|
||||
"Clojure `get` semantics over a jolt collection, used for collection-as-IFn."
|
||||
[coll k default]
|
||||
(cond
|
||||
(jolt-transient? coll) (transient-lookup coll k default)
|
||||
(phm? coll) (phm-get coll k default)
|
||||
(set? coll) (if (phs-contains? coll k) k default)
|
||||
(pvec? coll)
|
||||
|
|
@ -59,6 +76,7 @@
|
|||
[ctx f args]
|
||||
(cond
|
||||
(or (function? f) (cfunction? f)) (apply f args)
|
||||
(jolt-transient? f) (transient-lookup f (get args 0) (get args 1))
|
||||
(keyword? f) (coll-lookup (get args 0) f (get args 1))
|
||||
(and (struct? f) (= :symbol (f :jolt/type)))
|
||||
(coll-lookup (get args 0) f (get args 1))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
(if (and canonicalize-key (or (table? k) (struct? k) (array? k) (tuple? k)))
|
||||
(canonicalize-key k)
|
||||
k))
|
||||
(defn canon
|
||||
"Public canonicalizer: maps a key to its value-hashable form (identity for
|
||||
scalars). Used by callers that index the same canonicalized tables phm uses
|
||||
(e.g. transient maps/sets)."
|
||||
[k] (ck k))
|
||||
(defn- key= [a b] (= (ck a) (ck b)))
|
||||
|
||||
(defn phm-hash-key [k]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue