From e61a175f917ac99f4a0120c2a6862e68e6a6032c Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 13 Jun 2026 18:30:22 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20completeness=20preservation=20=E2=80=94?= =?UTF-8?q?=20shapes=20survive=20cap=20and=20same-shape=20joins=20(jolt-t3?= =?UTF-8?q?4=20R2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inference dropped the complete :shape whenever it rebuilt a struct type (cap) or joined two (join-t/merge-fields), so a vec3 retrieved from a container or a fn param typed across call sites lost its layout and every field read fell to the slow descriptor path. Two fixes: - cap preserves :shape: capping truncates field VALUES below the depth limit but never the key SET, so the layout is still complete. It also recurses into fields, so a shaped value nested in a container (a vec3 inside a hit-info) keeps its own :shape — which is what lets (:r (:normal hit-info)) bare-index. - join-t preserves :shape when both sides are the SAME complete shape (the merged struct has the same keys); different shapes still drop it. This carries the shape through if-joins and the inter-procedural fixpoint's call-site joins. Result: the ray tracer goes from 22s (R1, correct-but-descriptor-path) to 4.36s — 2.7x FASTER than the 11.7s no-shape baseline, and ~3x the JVM (was 8.5x), with byte-identical output. The compounding of cheaper tuple construction plus bare-index reads across the whole render far exceeds the per-op estimate. Gate green flag-off, suite 4718, default-path bench even, transparency intact. --- jolt-core/jolt/passes.clj | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/jolt-core/jolt/passes.clj b/jolt-core/jolt/passes.clj index 9646e10..6887b82 100644 --- a/jolt-core/jolt/passes.clj +++ b/jolt-core/jolt/passes.clj @@ -782,7 +782,14 @@ (= a b) a (nil? a) b (nil? b) a - (and (struct-type? a) (struct-type? b)) (mk-struct (merge-fields (sfields a) (sfields b))) + (and (struct-type? a) (struct-type? b)) + (let [merged (mk-struct (merge-fields (sfields a) (sfields b)))] + ;; joining two values of the SAME complete shape preserves it — the + ;; merged struct has the same key set (jolt-t34 R2). Different shapes + ;; (or an incomplete side) drop it, as the layout is no longer proven. + (if (and (get a :shape) (= (get a :shape) (get b :shape))) + (assoc merged :shape (get a :shape)) + merged)) (and (vec-type? a) (vec-type? b)) (mk-vec (join-t (velem a) (velem b))) (and (set-type? a) (set-type? b)) (mk-set (join-t (selem a) (selem b))) ;; differing kinds: form a scalar union when both sides reduce to scalars @@ -797,8 +804,14 @@ (defn- cap [t d] (cond (<= d 0) (if (or (struct-type? t) (vec-type? t) (set-type? t)) :any t) - (struct-type? t) (mk-struct (reduce (fn [m k] (assoc m k (cap (get (sfields t) k) (dec d)))) - {} (keys (sfields t)))) + (struct-type? t) + ;; capping truncates VALUES below depth d, but the KEY SET is unchanged, so + ;; a complete :shape survives — keep it so nested/container field reads can + ;; still bare-index (jolt-t34 R2). cap recurses into fields, so a nested + ;; shaped value (a vec3 inside a hit-info) keeps its own :shape too. + (let [capped (mk-struct (reduce (fn [m k] (assoc m k (cap (get (sfields t) k) (dec d)))) + {} (keys (sfields t))))] + (if (get t :shape) (assoc capped :shape (get t :shape)) capped)) (vec-type? t) (mk-vec (cap (velem t) (dec d))) (set-type? t) (mk-set (cap (selem t) (dec d))) :else t))