Reader attaches collection metadata to data, not a with-meta form

The reader lowered ^meta on a vector/map/set literal to a runtime
(with-meta form meta) list, so read-string/edn of data with metadata
returned the form and lost the metadata. Attach it to the value instead,
as Clojure does; the analyzer re-emits (with-meta coll meta) for a
meta-carrying collection literal in code, so a literal still carries its
metadata at runtime and ^Type/^long arglist hints (consumed by
analyze-arity directly) are unaffected.

Also: pr honors *print-meta*, and clojure.walk/clojure.edn re-attach
metadata to the collections they rebuild (matches Clojure; a
metadata-driven config lib like aero relies on it).
This commit is contained in:
Yogthos 2026-06-24 12:04:36 -04:00
parent 091d2c4b62
commit 473d1002b7
8 changed files with 763 additions and 716 deletions

View file

@ -86,6 +86,10 @@
(define (hc-sym-meta x)
(let ((m (symbol-t-meta x)))
(if (and m (not (jolt-nil? m)) (not (null? m))) m jolt-nil)))
;; Metadata the reader attached to a collection literal (vec/map/set/list), or
;; jolt-nil. The analyzer re-emits a runtime (with-meta ..) for a meta-carrying
;; vector/map/set so the value keeps its metadata.
(define (hc-coll-meta x) (jolt-meta x))
;; list items -> jolt vector (pvec); the analyzer mapv's over the result.
(define (hc-elements x)
@ -327,6 +331,7 @@
(def-var! "jolt.host" "form-sym-name" hc-sym-name)
(def-var! "jolt.host" "form-sym-ns" hc-sym-ns)
(def-var! "jolt.host" "form-sym-meta" hc-sym-meta)
(def-var! "jolt.host" "form-coll-meta" hc-coll-meta)
(def-var! "jolt.host" "form-list?" hc-list?)
(def-var! "jolt.host" "form-vec?" hc-vec?)
(def-var! "jolt.host" "form-map?" hc-map?)

View file

@ -67,12 +67,31 @@
(if (jolt-nil? s) (reverse acc)
(loop (jolt-seq (seq-more s)) (cons (jolt-pr-readable (seq-first s)) acc))))) ")"))
(else (jolt-pr-str x))))
(define (jolt-pr-readable x)
(define (jolt-pr-readable-dispatch x)
(let loop ((as jolt-pr-readable-arms))
(cond ((null? as) (jolt-pr-readable-base x))
(((caar as) x) ((cdar as) x))
(else (loop (cdr as))))))
;; *print-meta* support. The var is def'd after this file loads, so capture its
;; cell lazily; jolt-var-get (patched by dyn-binding.ss) honors a `binding`.
(define pr-meta-cell #f)
(define (pr-print-meta?)
(unless pr-meta-cell (set! pr-meta-cell (jolt-var "clojure.core" "*print-meta*")))
(jolt-truthy? (jolt-var-get pr-meta-cell)))
;; The metadata to print before x, or jolt-nil. A var prints as #'ns/name (its
;; {:ns :name} is derived, not user metadata) and a procedure is opaque — skip both.
(define (pr-user-meta x)
(if (or (var-cell? x) (procedure? x)) jolt-nil (jolt-meta x)))
(define (jolt-pr-readable x)
(if (pr-print-meta?)
(let ((m (pr-user-meta x)))
(if (jolt-nil? m)
(jolt-pr-readable-dispatch x)
(string-append "^" (jolt-pr-readable-dispatch m) " " (jolt-pr-readable-dispatch x))))
(jolt-pr-readable-dispatch x)))
;; __pr-str1: render ONE value readably (the overlay's pr-str joins these).
(define (jolt-pr-str1 x) (jolt-pr-readable x))

View file

@ -290,20 +290,22 @@
((symbol-t? target)
(make-symbol-t (symbol-t-ns target) (symbol-t-name target)
(rdr-merge-meta (symbol-t-meta target) meta)))
;; A LIST form is code: ^Type (expr) is a compile-time hint on the FORM, not a
;; runtime operation. Attach the metadata to the form itself (so a quoted list
;; keeps it and the analyzer can read :tag) — never lower to a runtime
;; (with-meta (expr) meta), which would mis-apply the hint to the call's RESULT
;; and throw when that result is a string/number (e.g. ^String (to-str x)).
;; Matches Clojure: a tag hint on an evaluated form is discarded at runtime.
((cseq? target) (jolt-with-meta target meta))
((empty-list-t? target) target)
;; A vector/map/set LITERAL: Clojure attaches the metadata to the runtime value
;; ((meta ^{:tag :int} [1 2]) / ^:foo {}), so lower to a runtime (with-meta form
;; meta). Use the BARE `with-meta` symbol (ns #f) — the fn/defn macros unwrap a
;; (with-meta <arglist-vec> _) return-hint by matching the unqualified head, so a
;; qualified clojure.core/with-meta would slip past them (^bytes [b]).
(else (jolt-list (jolt-symbol #f "with-meta") target meta))))
;; Lists/vectors/maps/sets attach metadata to the value itself, as Clojure's
;; reader does. Reading DATA (read-string, edn) then preserves it. A list form
;; is code: ^Type (expr) is a compile-time hint on the FORM, read off the form
;; for :tag and discarded at runtime (a hint on an evaluated form is dropped).
;; A vector/map/set LITERAL keeps it as a runtime value: the analyzer re-emits a
;; (with-meta form meta) for a meta-carrying collection literal in code, so
;; (meta ^{:tag :int} [1 2]) / ^:foo {} still works.
(else
(let ((c (jolt-with-meta target meta)))
;; jolt-with-meta copies a pmap, giving it a fresh identity the rdr-map-order
;; side-table (source key order for left-to-right map-literal eval) loses —
;; carry the order entry over to the copy.
(let ((order (and (pmap? target) (hashtable-ref rdr-map-order target #f))))
(when order (hashtable-set! rdr-map-order c order)))
c))))
;; --- # dispatch -------------------------------------------------------------
;; #(...) anonymous fn shorthand: % -> p1, %N -> pN, %& -> rest. The

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long