diff --git a/jolt-core/jolt/passes.clj b/jolt-core/jolt/passes.clj index fc6199b..6214e49 100644 --- a/jolt-core/jolt/passes.clj +++ b/jolt-core/jolt/passes.clj @@ -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))] diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index d7485fb..b9c32c7 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -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. diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 297178f..5095756 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -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.