support type hints; make metadata coherent

The reader expanded ^X form to (with-meta form X), which evaluated the tag (so
^String errored 'Unable to resolve symbol: String') and, as a param, was no
longer a bare symbol (so the arg bound to nil). Now a keyword/symbol/string hint
on a symbol attaches to the symbol's :meta and the symbol stays bare, so type
hints are transparent in params, lets, and bodies. Map metadata still uses a
runtime with-meta form.

meta now returns a symbol's :meta, and def applies the name's metadata
(^:dynamic, ^:private, ^Type tag, ^{:doc}) to the var, so (meta (var x)) is
consistent. Specs in metadata-spec; grammar note in the ebnf.

README notes regex \p{...} as unsupported (separate from this).
This commit is contained in:
Yogthos 2026-06-06 00:36:10 -04:00
parent f749ec3c19
commit 761a2b8f72
8 changed files with 94 additions and 39 deletions

View file

@ -1410,8 +1410,12 @@
(defn core-meta [x]
"Returns the metadata of x, or nil."
(if (var? x) (var-meta x)
(if (table? x) (or (get x :jolt/meta) (get x :meta)) nil)))
(cond
(var? x) (var-meta x)
# symbols carry reader metadata (type hints etc.) in a :meta field
(and (struct? x) (= :symbol (get x :jolt/type))) (get x :meta)
(table? x) (or (get x :jolt/meta) (get x :meta))
nil))
(defn core-every-pred [& preds]
(fn [& xs]

View file

@ -632,10 +632,16 @@
(if (> (length form) 3) (eval-form ctx bindings (in form 3)) nil)))
"def" (let [raw-name (in form 1)
name-sym (unwrap-meta-name raw-name)
# Check for ^:dynamic metadata
dynamic? (and (array? raw-name) (> (length raw-name) 0)
(sym-name? (first raw-name) "with-meta")
(= :dynamic (last raw-name)))
# Metadata on the name: keyword/type-hint metadata rides on the
# symbol (:meta); a ^{:map} reads as a with-meta form we evaluate.
sym-meta (or (and (struct? name-sym) (get name-sym :meta)) {})
wm-meta (if (and (array? raw-name) (> (length raw-name) 0)
(sym-name? (first raw-name) "with-meta"))
(let [mv (protect (eval-form ctx bindings (last raw-name)))]
(if (and (mv 0) (or (table? (mv 1)) (struct? (mv 1)))) (mv 1) {}))
{})
name-meta (merge wm-meta sym-meta)
dynamic? (truthy? (get name-meta :dynamic))
ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)
# Create var first (unbound) so self-referencing defs resolve
@ -644,8 +650,9 @@
has-doc (and (> (length form) 3) (string? (in form 2)))
val (eval-form ctx bindings (in form (if has-doc 3 2)))]
(bind-root v val)
(when has-doc
(put v :meta (merge (or (get v :meta) {}) {:doc (in form 2)})))
(let [extra (if has-doc (merge name-meta {:doc (in form 2)}) name-meta)]
(when (not (empty? extra))
(put v :meta (merge (or (get v :meta) {}) extra))))
(when dynamic?
(put v :dynamic true))
# def returns the var (Clojure semantics); REPL prints #'ns/name

View file

@ -446,11 +446,30 @@
(let [[form final-pos] (read-form s new-pos)]
[(array token-sym form) final-pos]))
(defn- meta-form->map
"Normalize a metadata reader form (Clojure semantics): a symbol or string is a
:tag, a keyword is {kw true}. Returns a metadata table, or nil if it isn't one
of those simple shapes (e.g. a map literal — handled via with-meta instead)."
[meta-form]
(cond
(keyword? meta-form) {meta-form true}
(and (struct? meta-form) (= :symbol (meta-form :jolt/type))) {:tag (meta-form :name)}
(string? meta-form) {:tag meta-form}
nil))
(defn read-meta [s pos]
# pos is at ^
(let [[meta-form new-pos] (read-form s (+ pos 1))
[form new-pos2] (read-form s new-pos)]
[(array (sym "with-meta") form meta-form) new-pos2]))
[form new-pos2] (read-form s new-pos)
m (meta-form->map meta-form)]
(if (and m (struct? form) (= :symbol (form :jolt/type)))
# Attach the metadata to the symbol itself and keep it a bare symbol, so
# type hints (^String x) and ^:dynamic etc. are transparent in every
# position (params, lets, bodies) — the evaluator reads :name and ignores
# :meta. This is what makes type hints "parse and otherwise do nothing".
[(struct ;(kvs form) :meta (merge (or (form :meta) {}) m)) new-pos2]
# Map metadata or non-symbol targets keep the runtime with-meta form.
[(array (sym "with-meta") form meta-form) new-pos2])))
(defn read-until-newline [s pos]
(if (or (>= pos (length s)) (= (s pos) 10))