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

@ -7,7 +7,10 @@
;;
;; Loaded after records.ss (jrec) + collections/seq/values (the ctors it copies).
(define meta-table (make-eq-hashtable))
;; Weak so a collection's metadata is reclaimed with the collection — collection
;; ops (conj/assoc/into) carry meta forward onto fresh values, so a strong table
;; would retain every meta-bearing intermediate.
(define meta-table (make-weak-eq-hashtable))
(define (jolt-meta x)
(cond
@ -31,7 +34,9 @@
((pmap? x) (make-pmap (pmap-root x) (pmap-cnt x)))
((pset? x) (make-pset (pset-m x)))
((jrec? x) (make-jrec (jrec-tag x) (jrec-pairs x)))
(else x))) ; cseq / empty-list / procedure
;; () is a shared singleton — a fresh instance keeps meta off every other ().
((empty-list-t? x) (fresh-empty-list))
(else x))) ; cseq / procedure
(define (jolt-with-meta x m)
(cond
@ -45,6 +50,20 @@
(def-var! "clojure.core" "meta" jolt-meta)
(def-var! "clojure.core" "with-meta" jolt-with-meta)
;; Carry SRC's collection metadata onto DST (a freshly-built collection of the
;; same kind), as Clojure's ops do — each new collection threads its receiver's
;; meta() forward. Returns DST. The size check is the fast path: programs that
;; never attach collection metadata pay one O(1) check per op, no lookup.
(define (meta-carry src dst)
(if (fx=? 0 (hashtable-size meta-table))
dst
(let ((m (hashtable-ref meta-table src #f)))
(if m
;; never attach to the shared () singleton — use a fresh instance
(let ((d (if (empty-list-t? dst) (fresh-empty-list) dst)))
(hashtable-set! meta-table d m) d)
dst))))
;; (type x) — Clojure's (or (:type (meta x)) (class x)). With no JVM classes the
;; "class" is a host taxonomy: a record yields its ns-qualified class-name SYMBOL
;; (user.TyR), everything else a keyword (:number/:vector/:seq/…).