prototype: shape-record representation for vec3 maps (jolt-t34, JOLT_SHAPE)

Validated prototype of the hidden-class object-model change. A vec3-shaped
{:r :g :b} map literal is represented as a cheap Janet tuple [shape vb vg vr]
instead of a struct (~2x cheaper to construct); a lookup on a value the
inference PROVES is the shape reads by bare index with no runtime check.

Result on the ray tracer (direct-link): 12.3s -> 10.7s (~13% faster), with
byte-identical pixel output. The shape value flows transparently through
hit-info/ray/material containers and the colors vector; core-get handles it
(inline check, no fn call) so an unspecialized access is still correct.

Key lessons baked in: the lookup MUST compile to a bare index (a runtime
shape check, even inlined, taxed every field read and made it 2.5-3.4x
SLOWER) — so the inference gained a :shape hint (struct type with keys
exactly {:r :g :b}) that the back end turns into (in m idx). The descriptor
is quoted when embedded (its keys are a parens tuple Janet would otherwise
try to CALL).

All behind JOLT_SHAPE (off by default). Gate green, suite 4718, default-path
bench even. Scoped to the one shape; NOT yet sound in general (assumes every
vec3-shaped value is a shape-rec, true under the flag for the ray tracer) nor
fully transparent (only core-get + the inlined lookup; jolt-call/equality/
print/keys not yet covered). Those are the next steps toward a real feature.
This commit is contained in:
Yogthos 2026-06-13 16:25:07 -04:00
parent 50d8b13ca3
commit d33fb85041
3 changed files with 93 additions and 9 deletions

View file

@ -806,6 +806,17 @@
;; k, if known, else :any.
(defn- struct-safe? [t] (struct-type? t))
(defn- field-type [t k] (if (struct-type? t) (get (sfields t) k :any) :any))
;; vec3 shape detection (jolt-t34, prototype): a struct type whose key set is
;; exactly {:r :g :b}. The back end represents such maps as shape tuples, so a
;; lookup on a value PROVEN to be this shape can read by bare index with no
;; runtime check. Scoped to the one shape for the prototype.
(defn- vec3-shape? [t]
(and (struct-type? t)
(let [fs (sfields t)]
(and (get fs :r) (get fs :g) (get fs :b) (= 3 (count (keys fs)))))))
;; the structural hint for a subject: :shape when provably the vec3 shape (bare
;; indexed read), else :struct when raw-get-safe.
(defn- struct-hint [t] (if (vec3-shape? t) :shape :struct))
;; tag a node (any expression, not just a :local) so the back end can specialize
;; a lookup whose SUBJECT is that node — this is what makes nested access work:
;; (:direction ray) is tagged struct, so (:r (:direction ray)) drops its guard.
@ -929,7 +940,7 @@
(let [t (get tenv (get node :name))]
[(if t t :any)
(cond
(struct-safe? t) (assoc node :hint :struct)
(struct-safe? t) (assoc node :hint (struct-hint t))
(vec-type? t) (assoc node :hint :vector)
:else node)])
(= op :map)
@ -987,7 +998,7 @@
(and (= :const (get fnode :op)) (keyword? (get fnode :val)) (>= n 1) (<= n 2))
(let [mr (infer (nth args 0) tenv)
mt (nth mr 0)
msub (if (struct-safe? mt) (mark-hint (nth mr 1) :struct) (nth mr 1))
msub (if (struct-safe? mt) (mark-hint (nth mr 1) (struct-hint mt)) (nth mr 1))
ft (field-type mt (get fnode :val))
dr (when (= n 2) (infer (nth args 1) tenv))]
[(if dr (join ft (nth dr 0)) ft)
@ -998,7 +1009,7 @@
(>= n 2) (= :const (get (nth args 1) :op)) (keyword? (get (nth args 1) :val)))
(let [mr (infer (nth args 0) tenv)
mt (nth mr 0)
msub (if (struct-safe? mt) (mark-hint (nth mr 1) :struct) (nth mr 1))
msub (if (struct-safe? mt) (mark-hint (nth mr 1) (struct-hint mt)) (nth mr 1))
kr (infer (nth args 1) tenv)
ft (field-type mt (get (nth args 1) :val))
dr (when (= n 3) (infer (nth args 2) tenv))]

View file

@ -355,13 +355,15 @@
# - ^:struct / ^Record hinted subject: skip the guard, bare get (~20 vs ~36ns).
# - hinted + JOLT_CHECK_HINTS: keep the guard but THROW on the tagged arm, so a
# lying hint surfaces a clear error (dev aid; off by default, no perf cost).
# vec3 shape layout: descriptor at 0, then sorted keys [:b :g :r] at 1,2,3 (jolt-t34)
(def- vec3-shape-idx {:b 1 :g 2 :r 3})
(defn- emit-kw-lookup [subj-node m-expr k d-expr]
# the subject is a struct (raw-get-safe) when hinted so — by an explicit
# ^:struct/^Record hint on a local, OR by inference tagging ANY subject
# expression it proved to be a struct (jolt-d6u/RFC 0005), which is what lets
# nested access like (:r (:direction ray)) drop its guard.
(def hinted (and subj-node (= :struct (subj-node :hint))))
(def checked (and hinted (os/getenv "JOLT_CHECK_HINTS")))
(def hinted (and subj-node (or (= :struct (subj-node :hint)) (= :shape (subj-node :hint)))))
(def checked (and (= :struct (subj-node :hint)) (os/getenv "JOLT_CHECK_HINTS")))
(def m (if (symbol? m-expr) m-expr (jsym)))
(def wrap (fn [body] (if (symbol? m-expr) body ['let [m m-expr] body])))
(def err (when checked
@ -369,15 +371,24 @@
k " " (subj-node :name) ") — value carries :jolt/type "
"(a phm/sorted/transient/lazy-seq), not the plain "
"struct/record the ^:struct/^Record hint asserts")]))
(def sidx (and (os/getenv "JOLT_SHAPE") (get vec3-shape-idx k)))
# subject PROVEN to be the vec3 shape (jolt-t34): read by bare index, no check.
(def shaped (and sidx (= :shape (subj-node :hint))))
# otherwise, when JOLT_SHAPE is on and the key is a vec3 field but the shape
# isn't proven, runtime-check (tuple? + struct? of elem 0, inline opcodes).
(defn get-or-shape [getexpr]
(cond shaped ['in m sidx]
sidx ['if ['and ['tuple? m] ['struct? ['in m 0]]] ['in m sidx] getexpr]
getexpr))
(if (nil? d-expr)
(let [fast ['get m k]]
(let [fast (get-or-shape ['get m k])]
(wrap (cond
checked ['if ['get m :jolt/type] err fast]
hinted fast
['if ['get m :jolt/type] (tuple core-get m k) fast])))
(let [d (if (symbol? d-expr) d-expr (jsym))
v (jsym)
fast ['let [v ['get m k]] ['if ['nil? v] d v]]
fast ['let [v (get-or-shape ['get m k])] ['if ['nil? v] d v]]
body (cond
checked ['if ['get m :jolt/type] err fast]
hinted fast
@ -470,6 +481,29 @@
(unless (and (= :const (k :op))
(or (keyword? kv) (string? kv) (number? kv) (boolean? kv)))
(set fast false)))
# Shape-record fast path (jolt-t34, JOLT_SHAPE): a vec3-shaped {:r :g :b} map
# literal becomes a shape tuple (≈2x cheaper than a struct). Scoped to this
# one shape for the prototype so other maps (hit-info/ray/material) stay
# structs and assoc on them is unaffected. Values are emitted in the shape's
# canonical (sorted-key) order; element 0 is the compile-time shape descriptor.
(def shape-keys
(when (and fast (os/getenv "JOLT_SHAPE"))
(def ks @[])
(each pair pairs (array/push ks ((norm-node (in (vview pair) 0)) :val)))
(def srt (sorted ks))
(when (deep= srt @[:b :g :r]) srt)))
(if shape-keys
(do
(def desc (shape-for shape-keys))
(def by-key @{})
(each pair pairs
(def p (vview pair))
(put by-key ((norm-node (in p 0)) :val) (emit ctx (in p 1))))
# quote the descriptor: it is a struct constant, and its keys are a parens
# tuple — unquoted in code position Janet would try to CALL it ((:b :g))
(def out @['tuple ['quote desc]])
(each k shape-keys (array/push out (get by-key k)))
(tuple/slice out))
(if fast
(do
(def binds @[])
@ -499,7 +533,7 @@
(def p (vview pair))
(array/push args (emit ctx (in p 0)))
(array/push args (emit ctx (in p 1))))
(tuple/slice args))))
(tuple/slice args)))))
# A set literal: build (make-phs e1 e2 …) so each element is evaluated at runtime
# then the persistent set is constructed — mirrors compiler.janet's emit-set-expr.

View file

@ -560,9 +560,48 @@
(do (var result @{}) (each k (keys m) (var in-ks false) (each k2 ks (if (deep= k k2) (do (set in-ks true) (break)))) (if (not in-ks) (put result k (m k))))
(if (struct? m) (table/to-struct result) result))))
# --- shape records (hidden classes, jolt-t34) -------------------------------
# A "shape record" is a cheap fixed-layout representation for a map literal
# whose keys are a known compile-time set (e.g. a vec3 {:r :g :b}). It is a
# Janet tuple [SHAPE v0 v1 ...] where SHAPE is an interned descriptor struct
# {:jolt/shape KEYS :idx {k->pos}}. Construction is a tuple (≈2x cheaper than a
# struct), const-keyword lookup compiles to an index, and general map ops below
# recognize it via shape-rec? and treat it as a map — so it is transparent
# wherever it flows. Created only by the backend when JOLT_SHAPE is on and the
# inference proves the shape; the runtime support here is always present so a
# shape value is handled correctly regardless.
(def- shape-cache @{}) # sorted-keys-tuple -> interned shape descriptor
(defn shape-for
"Interned shape descriptor for an ordered key vector (keys in layout order)."
[keyv]
(def kk (tuple ;keyv))
(or (get shape-cache kk)
(let [idx @{}]
(var i 0) (each k keyv (put idx k i) (++ i))
(def desc (struct :jolt/shape kk :idx (table/to-struct idx)))
(put shape-cache kk desc)
desc)))
(defn shape-rec? [x]
(and (tuple? x) (> (length x) 0)
(struct? (in x 0)) (not (nil? (in (in x 0) :jolt/shape)))))
(defn shape-keys [rec] ((in rec 0) :jolt/shape))
(defn shape-get [rec k default]
(def pos (get ((in rec 0) :idx) k))
(if (nil? pos) default (in rec (+ pos 1))))
(defn shape-assoc [rec k v]
# assoc on a known key keeps the layout; a new key falls back to a struct
(def desc (in rec 0))
(def pos (get (desc :idx) k))
(if (nil? pos)
(let [t @{}] (each kk (desc :jolt/shape) (put t kk (shape-get rec kk nil))) (put t k v) (table/to-struct t))
(let [a (array ;rec)] (put a (+ pos 1) v) (tuple ;a))))
(defn core-get [m k &opt default]
(default default nil)
(if (nil? m) default
# inline the shape check (no fn call) so non-shape gets pay only a tuple? test
(if (and (tuple? m) (> (length m) 0) (struct? (in m 0)) (not (nil? (in (in m 0) :jolt/shape))))
(shape-get m k default)
(if (core-sorted? m) ((sorted-op m :get) m k default)
(if (core-transient? m)
(case (m :kind)
@ -583,7 +622,7 @@
# (get "a:b" 1) was nil.
(if (and (or (string? m) (buffer? m)) (number? k) (>= k 0) (< k (length m)))
(make-char (in m k))
default))))))))))
default)))))))))))
# Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's
# jolt-invoke). Handles real functions plus Clojure IFn collections.