perf: structural type inference (RFC 0005) — nested access typed, hint-free
Replace the ad-hoc inference lattice (a flat :struct-map tag plus {:vec ELEM})
with one recursive structural type: {:struct {field -> T}}, {:vec T}, {:set T},
scalar tags, and :any. A keyword lookup now returns its field's type, so nested
access like (:r (:direction ray)) is typed end to end and drops its guard. join
is field-wise and element-wise with a depth cap of 4 so the inter-procedural
fixpoint still terminates.
The back end honors a struct hint on any subject node, not just locals, so an
inferred field type on a nested lookup specializes. The orchestrator's fixpoint
joins through the portable join-types so compound types no longer collapse to
:any.
Ray tracer goes 12.8s to 11.0s with no hints, matching the explicit ^:struct
version (10.9s). Render checksum unchanged (1915337), full gate green,
conformance x3 modes pass.
jolt-5uj
This commit is contained in:
parent
e7473f38cf
commit
9bc7b27245
4 changed files with 127 additions and 36 deletions
|
|
@ -716,27 +716,72 @@
|
|||
;; dynamic guard in place. Sound by construction: a concrete type is assigned
|
||||
;; only when proven, so a wrong bare get is impossible.
|
||||
;;
|
||||
;; Lattice values: :struct-map (raw-get-safe), :phm-map, :set, :truthy (a
|
||||
;; provably non-nil/non-false scalar), :any (top), and a PARAMETRIC vector type
|
||||
;; {:vec ELEM} (jolt-d6u, Phase 3) carrying its element type so a reduce/map
|
||||
;; closure over it can type its element param. {:vec ELEM} is a small struct, so
|
||||
;; it compares by value on both the Clojure and the Janet (orchestrator) side.
|
||||
;; Recursive STRUCTURAL types (RFC 0005). A type mirrors the data tree:
|
||||
;; compound: {:struct {field -> T}} (raw-get-safe map, field types)
|
||||
;; {:vec T} (vector of T)
|
||||
;; {:set T} (set of T)
|
||||
;; scalar: :num :str :kw :truthy (all provably non-nil/non-false)
|
||||
;; :phm (persistent hash map; NOT raw-get-safe)
|
||||
;; :any (top), nil (bottom, identity for join).
|
||||
;; Compound types are small jolt maps, so they compare by value on both the
|
||||
;; Clojure and the Janet (orchestrator) side. struct/vec/set use distinct keys so
|
||||
;; a type is recognised by which key it carries.
|
||||
;; (get t :KEY) is nil for a keyword type and the child for a compound, so a
|
||||
;; compound is detected by some? — no map?/contains? needed.
|
||||
(defn- velem [t] (get t :vec))
|
||||
(defn- selem [t] (get t :set))
|
||||
(defn- sfields [t] (get t :struct))
|
||||
(defn- vec-type? [t] (some? (velem t)))
|
||||
(defn- set-type? [t] (some? (selem t)))
|
||||
(defn- struct-type? [t] (some? (sfields t)))
|
||||
(defn- mk-vec [t] {:vec (if t t :any)})
|
||||
(defn- join [a b]
|
||||
(defn- mk-set [t] {:set (if t t :any)})
|
||||
(defn- mk-struct [fs] {:struct fs})
|
||||
(declare join-t)
|
||||
(defn- merge-fields
|
||||
"Per-field join of two field maps (a key in only one side joins with :any)."
|
||||
[fa fb]
|
||||
(let [m1 (reduce (fn [m k] (assoc m k (join-t (get fa k :any) (get fb k :any)))) {} (keys fa))]
|
||||
(reduce (fn [m k] (if (get m k) m (assoc m k (join-t (get fa k :any) (get fb k :any))))) m1 (keys fb))))
|
||||
(defn- join-t [a b]
|
||||
(cond
|
||||
(= a b) a
|
||||
(and (vec-type? a) (vec-type? b)) (mk-vec (join (velem a) (velem b)))
|
||||
(nil? a) b
|
||||
(nil? b) a
|
||||
(and (struct-type? a) (struct-type? b)) (mk-struct (merge-fields (sfields a) (sfields b)))
|
||||
(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)))
|
||||
:else :any))
|
||||
(defn- struct-safe? [t] (= t :struct-map))
|
||||
;; a value whose type guarantees it is neither nil nor false — the back end only
|
||||
;; builds a struct (vs a phm) when every value is truthy, so a map literal is a
|
||||
;; struct only when all its values have a truthy type. Collections are non-nil.
|
||||
(defn- join [a b] (join-t a b))
|
||||
;; depth cap (RFC 0005): truncate a type below depth d to :any, so recursive data
|
||||
;; can't make an infinite type and the inter-procedural fixpoint stays finite.
|
||||
(def ^:private type-depth 4)
|
||||
(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))))
|
||||
(vec-type? t) (mk-vec (cap (velem t) (dec d)))
|
||||
(set-type? t) (mk-set (cap (selem t) (dec d)))
|
||||
:else t))
|
||||
;; raw-get-safe (a Janet struct / record): a struct type. The field type of key
|
||||
;; 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))
|
||||
;; 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.
|
||||
(defn- mark-hint [node h] (assoc node :hint h))
|
||||
;; a value provably neither nil nor false — the back end only builds a struct
|
||||
;; (vs a phm) when every value is non-nil/non-false, so a map literal is a struct
|
||||
;; only when all its values have such a type. Collections are non-nil.
|
||||
(defn- truthy-type? [t]
|
||||
(or (= t :truthy) (= t :struct-map) (= t :phm-map) (= t :set) (vec-type? t)))
|
||||
(or (= t :num) (= t :str) (= t :kw) (= t :truthy) (= t :phm)
|
||||
(struct-type? t) (vec-type? t) (set-type? t)))
|
||||
|
||||
(def ^:private truthy-ret-fns
|
||||
;; core fns whose result is a number (so it is non-nil/non-false and, for the
|
||||
;; success-type checker, provably numeric).
|
||||
(def ^:private num-ret-fns
|
||||
#{"+" "-" "*" "/" "inc" "dec" "mod" "rem" "quot" "min" "max" "abs"
|
||||
"bit-and" "bit-or" "bit-xor" "count"})
|
||||
(def ^:private vector-ret-fns #{"vec" "vector" "mapv" "filterv" "subvec"})
|
||||
|
|
@ -767,11 +812,11 @@
|
|||
(= op :var) (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
|
||||
(contains? truthy-ret-fns nm) :truthy
|
||||
(contains? num-ret-fns nm) :num
|
||||
(contains? vector-ret-fns nm) (mk-vec :any)
|
||||
:else :any))))
|
||||
(= op :host) (let [nm (get fnode :name)]
|
||||
(cond (contains? truthy-ret-fns nm) :truthy
|
||||
(cond (contains? num-ret-fns nm) :num
|
||||
(contains? vector-ret-fns nm) (mk-vec :any)
|
||||
:else :any))
|
||||
:else :any)))
|
||||
|
|
@ -814,7 +859,13 @@
|
|||
(let [op (get node :op)]
|
||||
(cond
|
||||
(= op :const)
|
||||
[(let [v (get node :val)] (if (or (nil? v) (= false v)) :any :truthy)) node]
|
||||
[(let [v (get node :val)]
|
||||
(cond (number? v) :num
|
||||
(string? v) :str
|
||||
(keyword? v) :kw
|
||||
(or (nil? v) (= false v)) :any ; nil/false are not struct-eligible
|
||||
:else :truthy)) ; true, char, ... -> non-nil
|
||||
node]
|
||||
(= op :local)
|
||||
(let [t (get tenv (get node :name))]
|
||||
[(if t t :any)
|
||||
|
|
@ -823,23 +874,29 @@
|
|||
(vec-type? t) (assoc node :hint :vector)
|
||||
:else node)])
|
||||
(= op :map)
|
||||
(let [res (mapv (fn [pr]
|
||||
(let [pairs (get node :pairs)
|
||||
res (mapv (fn [pr]
|
||||
(let [kr (infer (nth pr 0) tenv)
|
||||
vr (infer (nth pr 1) tenv)]
|
||||
[(nth kr 1) (nth vr 1) (nth vr 0)]))
|
||||
(get node :pairs))
|
||||
t (if (and (> (count res) 0)
|
||||
(every? (fn [pr] (scalar-const? (nth pr 0))) (get node :pairs))
|
||||
(every? (fn [r] (truthy-type? (nth r 2))) res))
|
||||
:struct-map :any)]
|
||||
[(nth kr 1) (nth vr 1) (nth vr 0) (get (nth pr 0) :val)]))
|
||||
pairs)
|
||||
struct? (and (> (count res) 0)
|
||||
(every? (fn [pr] (scalar-const? (nth pr 0))) pairs)
|
||||
(every? (fn [r] (truthy-type? (nth r 2))) res))
|
||||
t (if struct?
|
||||
(cap (mk-struct (reduce (fn [m r] (assoc m (nth r 3) (nth r 2))) {} res)) type-depth)
|
||||
:any)]
|
||||
[t (assoc node :pairs (mapv (fn [r] [(nth r 0) (nth r 1)]) res))])
|
||||
(= op :vector)
|
||||
(let [irs (mapv (fn [x] (infer x tenv)) (get node :items))
|
||||
ets (mapv (fn [r] (nth r 0)) irs)
|
||||
el (if (empty? ets) :any (reduce join (first ets) (rest ets)))]
|
||||
[(mk-vec el) (assoc node :items (mapv (fn [r] (nth r 1)) irs))])
|
||||
[(cap (mk-vec el) type-depth) (assoc node :items (mapv (fn [r] (nth r 1)) irs))])
|
||||
(= op :set)
|
||||
[:set (assoc node :items (mapv (fn [x] (nth (infer x tenv) 1)) (get node :items)))]
|
||||
(let [irs (mapv (fn [x] (infer x tenv)) (get node :items))
|
||||
ets (mapv (fn [r] (nth r 0)) irs)
|
||||
el (if (empty? ets) :any (reduce join (first ets) (rest ets)))]
|
||||
[(cap (mk-set el) type-depth) (assoc node :items (mapv (fn [r] (nth r 1)) irs))])
|
||||
(= op :if)
|
||||
(let [tr (infer (get node :test) tenv)
|
||||
thn (infer (get node :then) tenv)
|
||||
|
|
@ -865,6 +922,29 @@
|
|||
args (get node :args)
|
||||
n (count args)]
|
||||
(cond
|
||||
;; (:k m) / (:k m default): the result is m's field type, and if m is a
|
||||
;; struct the subject is tagged so the back end drops the guard — this
|
||||
;; types nested access end to end (RFC 0005).
|
||||
(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))
|
||||
ft (field-type mt (get fnode :val))
|
||||
dr (when (= n 2) (infer (nth args 1) tenv))]
|
||||
[(if dr (join ft (nth dr 0)) ft)
|
||||
(assoc node :args (if dr [msub (nth dr 1)] [msub]))])
|
||||
;; (get m :k [default]): same, when the key is a constant keyword.
|
||||
(and (or (and (= :var (get fnode :op)) (= "clojure.core" (get fnode :ns)) (= "get" (get fnode :name)))
|
||||
(and (= :host (get fnode :op)) (= "get" (get fnode :name))))
|
||||
(>= 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))
|
||||
kr (infer (nth args 1) tenv)
|
||||
ft (field-type mt (get (nth args 1) :val))
|
||||
dr (when (= n 3) (infer (nth args 2) tenv))]
|
||||
[(if dr (join ft (nth dr 0)) ft)
|
||||
(assoc node :args (if dr [msub (nth kr 1) (nth dr 1)] [msub (nth kr 1)]))])
|
||||
;; reduce over a typed vector with a fn-literal (jolt-d6u): seed the
|
||||
;; closure's accumulator (param 0) to the init type and its element
|
||||
;; (param 1) to the vector's element type, so its body — and any calls
|
||||
|
|
@ -910,7 +990,7 @@
|
|||
(when iscall-var
|
||||
(swap! calls-box conj [(var-key fnode) (mapv (fn [r] (nth r 0)) ares)]))
|
||||
[(cond
|
||||
(= cn "range") (mk-vec :truthy)
|
||||
(= cn "range") (mk-vec :num)
|
||||
;; element-returning fn over a typed vector -> the element type
|
||||
(and cn (contains? elem-fns cn) (> n 0))
|
||||
(let [a0 (nth (nth ares 0) 0)] (if (vec-type? a0) (velem a0) :any))
|
||||
|
|
@ -965,6 +1045,11 @@
|
|||
(non-nil), def vars carry their inferred init type (jolt-d6u)."
|
||||
[m] (reset! vtype-box m))
|
||||
|
||||
(defn join-types
|
||||
"Public structural join (lub), used by the orchestrator's fixpoint so param/
|
||||
return types join field-wise/element-wise instead of collapsing to :any."
|
||||
[a b] (join-t a b))
|
||||
|
||||
(defn reset-escapes! [] (reset! escapes-box #{}))
|
||||
(defn collected-escapes [] (vec @escapes-box))
|
||||
|
||||
|
|
|
|||
|
|
@ -342,7 +342,11 @@
|
|||
# - 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).
|
||||
(defn- emit-kw-lookup [subj-node m-expr k d-expr]
|
||||
(def hinted (and subj-node (= :local (subj-node :op)) (= :struct (subj-node :hint))))
|
||||
# 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 m (if (symbol? m-expr) m-expr (jsym)))
|
||||
(def wrap (fn [body] (if (symbol? m-expr) body ['let [m m-expr] body])))
|
||||
|
|
@ -799,13 +803,14 @@
|
|||
(def pns (ctx-find-ns ctx "jolt.passes"))
|
||||
(def f-set-rtenv (and pns (ns-find pns "set-rtenv!")))
|
||||
(def f-set-vtypes (and pns (ns-find pns "set-vtypes!")))
|
||||
(def f-join (and pns (ns-find pns "join-types")))
|
||||
(def f-infer-body (and pns (ns-find pns "infer-body")))
|
||||
(def f-reinfer (and pns (ns-find pns "reinfer-def")))
|
||||
(def f-reset-esc (and pns (ns-find pns "reset-escapes!")))
|
||||
(def f-get-esc (and pns (ns-find pns "collected-escapes")))
|
||||
(def ns (ctx-find-ns ctx ns-name))
|
||||
(def report @{})
|
||||
(when (and ns f-set-rtenv f-infer-body f-reinfer f-reset-esc f-get-esc)
|
||||
(when (and ns f-set-rtenv f-set-vtypes f-join f-infer-body f-reinfer f-reset-esc f-get-esc)
|
||||
# gather single-fixed-arity fns AND non-fn defs that stashed a :def IR
|
||||
(def fns @[])
|
||||
(def defs @[])
|
||||
|
|
@ -865,7 +870,7 @@
|
|||
(def callee (get by-key (in cv 0)))
|
||||
(def ats (vview (in cv 1)))
|
||||
(def lim (min (length ats) (callee :np)))
|
||||
(for i 0 lim (put npa i (itype-join (in npa i) (in ats i)))))))
|
||||
(for i 0 lim (put npa i ((var-get f-join) (in npa i) (in ats i)))))))
|
||||
# commit + detect change
|
||||
(set changed false)
|
||||
(def nrt @{})
|
||||
|
|
|
|||
|
|
@ -29,11 +29,12 @@
|
|||
(def report (backend/infer-unit! ctx "p1"))
|
||||
|
||||
# --- the fixpoint computed the right param types -----------------------------
|
||||
# rd's param v flows from mk's struct-map result (mk inlines to a struct literal
|
||||
# in drv) and stays struct across the recursive self-call -> :struct-map
|
||||
(assert (= :struct-map (in (get report "p1/rd") 0)) (string "rd param v: " (in (get report "p1/rd") 0)))
|
||||
# rd's param v flows from mk's struct result (mk inlines to a struct literal in
|
||||
# drv) and stays struct across the recursive self-call -> a {:struct ...} type
|
||||
(defn struct-type? [t] (truthy? (get t :struct)))
|
||||
(assert (struct-type? (in (get report "p1/rd") 0)) (string "rd param v: " (in (get report "p1/rd") 0)))
|
||||
# esc escaped (passed to mapv) -> param stays unknown (:any / nil), NOT struct
|
||||
(assert (not= :struct-map (in (get report "p1/esc") 0)) "escaping fn param not inferred struct")
|
||||
(assert (not (struct-type? (in (get report "p1/esc") 0))) "escaping fn param not inferred struct")
|
||||
|
||||
# --- the seeded re-inference drops the guard for a struct param --------------
|
||||
# (on a FRESH analysis, since infer-unit! re-stashes the already-specialized body)
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
(def rd-def (backend/analyze-form ctx (reader/parse-string "(defn rdx [v n] (if (< n 1) (:r v) (rdx v (dec n))))")))
|
||||
(defn guards-seeded [ptmap]
|
||||
(length (string/find-all ":jolt/type" (string/format "%p" (backend/emit-ir ctx ((types/var-get reinfer) rd-def ptmap))))))
|
||||
(assert (= 0 (guards-seeded @{"v" :struct-map})) "struct param -> bare lookup")
|
||||
(assert (= 0 (guards-seeded @{"v" {:struct {}}})) "struct param -> bare lookup")
|
||||
(assert (= 1 (guards-seeded @{})) "no param type -> guard kept")
|
||||
|
||||
# --- correctness: recompiled unit still computes the same --------------------
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@
|
|||
|
||||
# a reduce closure's element param gets the vector's element type
|
||||
(def red "(defn f [coll] (reduce (fn [acc h] (+ acc (:r h))) 0 coll))")
|
||||
(assert (= 0 (guards red @{"coll" {:vec :struct-map}})) "reduce element typed -> bare lookup in closure")
|
||||
(assert (= 0 (guards red @{"coll" {:vec {:struct {}}}})) "reduce element typed -> bare lookup in closure")
|
||||
(assert (= 1 (guards red @{"coll" {:vec :any}})) "reduce over vector of unknown -> guard kept")
|
||||
(assert (= 1 (guards red @{})) "untyped coll -> guard kept")
|
||||
|
||||
# mapv over a vector-of-structs types the closure element too
|
||||
(def mp "(defn g [coll] (mapv (fn [h] (:r h)) coll))")
|
||||
(assert (= 0 (guards mp @{"coll" {:vec :struct-map}})) "mapv element typed -> bare lookup")
|
||||
(assert (= 0 (guards mp @{"coll" {:vec {:struct {}}}})) "mapv element typed -> bare lookup")
|
||||
(assert (= 1 (guards mp @{"coll" {:vec :any}})) "mapv over unknown element -> guard")
|
||||
|
||||
# element type is DERIVED, not just seeded: a vector literal of structs, reduced
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue