Collection ops carry the receiver's metadata

conj/assoc/dissoc/disj/pop/into and empty now thread the receiver's
metadata onto the result, matching Clojure (each op constructs a new
collection with meta() carried forward; coll.empty() is
EMPTY.withMeta(meta())). The metadata side-table is now weak so meta on
intermediate collections is reclaimed with them, and empty-list-t carries
an (unused) field so a metadata-bearing () is a distinct identity from the
shared singleton instead of leaking meta onto every ().

Unblocks metadata-driven walks (aero/integrant): (into (empty form) ...)
now preserves a vector/map/set's metadata, so a postwalk whose outer fn
reads (meta x) sees it.
This commit is contained in:
Yogthos 2026-06-24 13:46:58 -04:00
parent 5b55c7f7b0
commit 4ae3d3116e
7 changed files with 484 additions and 457 deletions

View file

@ -103,17 +103,18 @@
(defn reverse [coll] (reduce conj (list) coll))
;; An empty coll of the same category; sorted colls keep their comparator (the
;; An empty coll of the same category, carrying the receiver's metadata (Clojure's
;; .empty() does EMPTY.withMeta(meta())). Sorted colls keep their comparator (the
;; value's own :empty op). Strings and scalars are nil, as in Clojure; a lazy
;; seq empties to ().
(defn empty [coll]
(cond
(nil? coll) nil
(sorted? coll) ((get (jolt.host/ref-get coll :ops) :empty) coll)
(map? coll) {}
(set? coll) #{}
(vector? coll) []
(coll? coll) ()
(map? coll) (with-meta {} (meta coll))
(set? coll) (with-meta #{} (meta coll))
(vector? coll) (with-meta [] (meta coll))
(coll? coll) (with-meta () (meta coll))
:else nil))
(defn assoc-in [m [k & ks] v]