jolt/jolt-core/jolt/passes.clj
Dmitri Sotnikov 6772e28eae
Specialize record field reads in method bodies; fix with-meta on symbols (#141)
A protocol method reads its fields through the generic guarded keyword lookup
because the method's `this` param is untyped. defrecord now hints `this` with
the record type, the per-form inference seeds ^Record-hinted params (the
:fn branch previously typed all params :any — only the whole-program path
seeded phints), and run-passes feeds the inference the record shapes. So a
hinted param's field reads bare-index instead of going through the :jolt/type
tag guard.

This needed a with-meta fix: (with-meta sym ..) returned a proto'd table, so
symbol? was false and the macro-attached hint broke fn destructuring. Symbols
now carry metadata in-place in their struct (matching how the reader attaches
^hint), keeping symbol? true, as in Clojure.

Modest on dispatch (~3-5%): the field read is a small fraction of a dispatch;
the machinery (record-tag + protocol lookup + wrapper) dominates, which is the
inline-cache target (jolt-ez5h). But it's a correctness fix and lets any
^Record-hinted code — not just methods — drop the field-read guard per-form,
not only under whole-program.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-16 15:14:59 +00:00

50 lines
3 KiB
Clojure

(ns jolt.passes
"IR optimization passes (nanopass-lite, jolt-2om) + the inference/checking
driver. Façade over three weakly-coupled namespaces, loaded with the compiler:
jolt.passes.fold — const-fold (always-on) + the shared const-shape predicate.
jolt.passes.inline — inline + flatten-lets + scalar-replace (direct-link only).
jolt.passes.types — collection-type inference + success-type checking
(RFC 0006) + the inter-procedural driver API (jolt-767).
run-passes (below) is the single entry the back end applies to every analyzed
form. The driver/checker fns the back end looks up by name (check-form,
infer-body, reinfer-def, set-rtenv!, take-diags!, …) are re-exported here via
:refer, so jolt.passes stays the only namespace the back end imports.
Portable Clojure: kernel-tier fns + seed primitives only."
(:require [jolt.host :refer [inline-enabled? record-shapes]]
[jolt.passes.fold :refer [const-fold]]
[jolt.passes.inline :refer [inline-node flatten-lets scalar-replace dirty set-rec-shapes!]]
[jolt.passes.types :refer [run-inference
check-form infer-body reinfer-def phint-seed
set-rtenv! set-vtypes! join-types
set-record-shapes! set-map-shapes! set-protocol-methods!
reset-escapes! collected-escapes
set-check-mode! take-diags!]]))
(defn run-passes
"All passes, in order. The back end applies this to every analyzed form. When
inlining is enabled for the unit (user code under direct-linking, jolt-87f),
run inline + flatten + scalar-replace + const-fold to a capped fixpoint —
inlining exposes map literals to lookups, scalar-replace collapses them, which
may expose more — then a collection-type inference pass (jolt-99x, optionally
also emitting success diagnostics) that auto-drops the lookup guard where the
type is proven. Otherwise (core + bootstrap) just const-fold, as before."
[node ctx]
(if (inline-enabled? ctx)
(let [_ (set-rec-shapes! (record-shapes ctx)) ;; record ctor fold (jolt-15jq)
;; resolve ^Record param hints (incl. defrecord/extend-type method
;; `this`) to bare field reads per-form, not only under whole-program
;; (jolt-3ko). Same shapes the inline pass uses.
_ (set-record-shapes! (record-shapes ctx))
opt (loop [i 0 n (const-fold node)]
(reset! dirty false)
(let [n2 (const-fold (scalar-replace (flatten-lets (inline-node n ctx))))]
(if (and @dirty (< i 8))
(recur (inc i) n2)
n2)))]
;; a final const-fold after inference propagates any predicate folded to a
;; constant (jolt-wcw), collapsing the `if` it gates to the taken branch.
(const-fold (run-inference opt)))
(const-fold node)))