record field type hints -> per-field value types in inference (jolt-3ko)

A record field can carry a type hint — ^Vec3 (a defined record type) or ^:num —
and the inference now resolves it so reading the field back yields that exact type
instead of :any. A Vec3 stored in a Ray field reads out as Vec3, so the vec ops on
field-read values prove their reads (bare-index). This is Stalin's per-slot type
sets, but DECLARED rather than inferred: the exact shape is known up front.

- deftype captures each field's :tag / :num metadata (was stripped) and passes it
  to make-deftype-ctor; the ctor registers per-field tags, resolving a record-type
  hint to its ctor-key (same-ns) so the inference can look it up directly.
- call-ret-type builds a record's struct type with field types resolved from the
  hints, recursing into nested record types (depth-bounded for self/cyclic types).

Measured: a nested-record read loop (:r (:origin ray)) runs 1.3s with ^Vec3 hints
vs 7.1s without — 5.5x. This is the lever the ray tracer needed (vecs flow through
container fields); records without it read back as :any and stay unproven.
This commit is contained in:
Yogthos 2026-06-14 07:00:29 -04:00
parent 872cea1c37
commit 840f699f54
3 changed files with 52 additions and 10 deletions

View file

@ -286,6 +286,15 @@
;; a seq of field keywords; spliced into a vector LITERAL below ([~@…]) so
;; the analyzer sees a vector form, not a runtime pvec value.
field-kws (map (fn [f] (keyword (name f))) fields)
;; per-field TYPE HINT (jolt-3ko): ^Vec3 origin -> "Vec3" (a record type
;; name), ^:num x -> "num", else nil. Lets the inference know a field's
;; exact type up front, so reading it back carries that type (not :any) —
;; the key to fast nested-record code. Spliced as a vector literal too.
field-tags (map (fn [f] (let [mt (meta f)]
(cond (and mt (:tag mt)) (:tag mt)
(and mt (:num mt)) "num"
:else nil)))
fields)
impl (fn [proto specs]
`(extend-type ~tname ~proto
~@(map (fn [spec]
@ -295,7 +304,7 @@
`(~(first spec) ~argv (let [~@binds] ~@(drop 2 spec)))))
specs)))]
`(do
(def ~tname (make-deftype-ctor (quote ~tname) [~@field-kws]))
(def ~tname (make-deftype-ctor (quote ~tname) [~@field-kws] [~@field-tags]))
(def ~arrow ~tname)
~@(map (fn [g] (impl (first g) (rest g))) (group-by-head body))
~tname)))

View file

@ -884,6 +884,27 @@
;; jolt-41m: protocol-method registry "ns/method" -> [proto method], for
;; devirtualizing a protocol call whose receiver is a known record type.
(def ^:private protocol-methods-box (atom {}))
;; jolt-3ko: build a record's struct TYPE from its registry entry, resolving each
;; field's declared type hint. A field tagged with a record type (its ctor-key)
;; recurses, so a Vec3 stored in a Ray field reads back as Vec3 — not :any —
;; which is what lets nested-record code prove its reads. Depth-bounded so a
;; self/cyclic-referencing record type can't loop.
(declare record-type-from-entry)
(defn- field-type-from-tag [tag depth]
(cond
(or (nil? tag) (<= depth 0)) :any
(= tag "num") :num
:else (let [e (get @record-shapes-box tag)]
(if e (record-type-from-entry e depth) :any))))
(defn- record-type-from-entry [rs depth]
(let [fields (get rs :fields)
tags (get rs :tags)
fmap (reduce (fn [m i]
(assoc m (nth fields i)
(field-type-from-tag (when tags (nth tags i)) (dec depth))))
{} (range (count fields)))]
(assoc (mk-struct fmap) :shape (vec fields) :type (get rs :type))))
;; jolt-t34: whether to shape generic const-key MAP literals (opt-in, JOLT_SHAPE).
;; Records are shaped regardless; maps only when this is on.
(def ^:private map-shapes-box (atom false))
@ -912,11 +933,10 @@
(= op :var) (let [rs (get @record-shapes-box (var-key fnode))]
(if rs
;; record ctor -> struct of declared shape (jolt-t34); :shape
;; is the DECLARED field order the back end indexes by, and
;; :type is the record tag (for devirtualizing protocol calls)
(let [fields (get rs :fields)]
(assoc (mk-struct (reduce (fn [m k] (assoc m k :any)) {} fields))
:shape (vec fields) :type (get rs :type)))
;; is the DECLARED field order the back end indexes by, :type
;; the record tag (devirt), and field types come from the
;; declared hints so nested records stay typed (jolt-3ko)
(record-type-from-entry rs type-depth)
(let [r (get @rtenv-box (var-key fnode))]
(if r r (let [nm (and (= "clojure.core" (get fnode :ns)) (get fnode :name))]
(cond (nil? nm) :any