Bare-index field reads for statically-known records (#228)

When the inference types a keyword-lookup receiver as a record — it carries the
field-order :shape and :hint :struct from the whole-program fixpoint — the back
end reads the field by its declared slot via jrec-field-at instead of jolt-get.
That skips the jolt-get case-lambda, the dispatch fn, and the field-key
hashtable lookup, leaving a jrec? check + a static-index vector-ref.

jrec-field-at falls back to jolt-get when the receiver isn't the expected record
(a map downgraded by dissoc, or a value the inference mistyped), so it stays
correct if the static type is wrong. Only the no-default form takes the bare
path (a declared field is always present).

Sound only for non-nil records: a self-recursive param that can be nil (e.g.
binary-trees check-tree, whose untagged child is nil at leaves) types :any and
keeps jolt-get — the whole-program fixpoint demotes it. The target is non-nil
record params, like a Vec3 dot product (~5% there; boxed-flonum arithmetic
dominates the rest, a separate numeric lever).

run-fieldread.ss gate: emitted form uses jrec-field-at at the right slot and
matches jolt-get for each declared field; a non-field key and a default-arg form
keep the generic path. make test / shakesmoke green, 0 new divergences.

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-26 11:29:14 +00:00 committed by GitHub
parent af11aaa7ff
commit de31221573
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 188 additions and 80 deletions

View file

@ -482,6 +482,14 @@
(let [op (case kind :double (dbl-ops nm) :long (lng-ops nm) :bigdec (bd-ops nm))]
(order-args (fn [as] (str "(" op " " (str/join " " as) ")"))))))
;; slot of a declared field key in a record's field-order shape, or nil.
(defn- struct-field-index [shape kw]
(when shape
(loop [i 0]
(cond (>= i (count shape)) nil
(= (nth shape i) kw) i
:else (recur (inc i))))))
(defn- emit-invoke [node]
(let [fnode (:fn node)
arg-nodes (:args node)
@ -519,9 +527,18 @@
(and nop (= 1 (count args)) (cmp1-ops nop)) (str "(begin " (first args) " #t)")
nop (order-args (fn [as] (str "(" nop " " (str/join " " as) ")")))
;; (:k coll [default]) -> (jolt-get coll :k [default]) — the key (fnode) is a
;; const, so only the coll/default args carry order.
;; const, so only the coll/default args carry order. When the inference typed
;; the receiver as a record whose declared fields include :k (it carries the
;; field-order :shape), read the field by its static slot — no field-key
;; lookup, no jolt-get dispatch. Only the no-default form (a declared field is
;; always present, so a default is never taken).
(= kind :keyword)
(order-args (fn [as] (str "(jolt-get " (first as) " " (emit fnode) (defstr as) ")")))
(let [recv (first arg-nodes)
idx (when (and (= :struct (:hint recv)) (= 1 (count arg-nodes)))
(struct-field-index (:shape recv) (:val fnode)))]
(if idx
(order-args (fn [as] (str "(jrec-field-at " (first as) " " idx " " (emit fnode) ")")))
(order-args (fn [as] (str "(jolt-get " (first as) " " (emit fnode) (defstr as) ")")))))
;; (coll k [default]) -> (jolt-get coll k [default]) — coll (fnode) is the
;; callee, evaluated before the key/default args.
(= kind :coll)