perf: var-deref fast path; drop the memoized-getter indirection

var-get short-circuits to the root when no dynamic bindings are active (the
common case — the binding stack is only non-empty inside a `binding` block),
skipping the stack walk on every global deref.

The compiled indirect deref now emits (var-get (quote cell)) instead of routing
through a per-var memoized getter closure — one fewer call and no per-var closure
alloc. The cell is quoted so it embeds by reference: a bare table in argument
position is re-evaluated as a constructor, which deep-copies the cell (and any
atom in its root) on every call — that silently broke swap!/atoms/self-ref
lazy-seqs until the quote. Conformance 218/218 all three modes; battery 3916.
This commit is contained in:
Yogthos 2026-06-06 17:31:16 -04:00
parent a3cc035782
commit db08cecde8
2 changed files with 25 additions and 19 deletions

View file

@ -14,12 +14,9 @@
(use ./evaluator) (use ./evaluator)
(import ./reader :as r) (import ./reader :as r)
# Var late-binding: deref/set through the cell via a memoized closure so compiled # Var late-binding: reads go through `(var-get cell)` with the cell embedded as a
# code sees redefinition (Janet early-binds plain symbols). Same scheme as the # constant, so compiled code sees redefinition (Janet early-binds plain symbols)
# bootstrap compiler. # — var-get reads the cell's root live. Writes go through a memoized setter.
(defn- var-getter [cell]
(or (get cell :jolt/getter)
(let [g (fn [] (var-get cell))] (put cell :jolt/getter g) g)))
(defn- var-setter [cell] (defn- var-setter [cell]
(or (get cell :jolt/setter) (or (get cell :jolt/setter)
(let [s (fn [v] (bind-root cell v) cell)] (put cell :jolt/setter s) s))) (let [s (fn [v] (bind-root cell v) cell)] (put cell :jolt/setter s) s)))
@ -215,7 +212,10 @@
:var (let [cell (cell-for ctx (node :ns) (node :name))] :var (let [cell (cell-for ctx (node :ns) (node :name))]
(if (direct-var? ctx cell) (if (direct-var? ctx cell)
(cell :root) # direct link: embed the fn value (cell :root) # direct link: embed the fn value
(tuple (var-getter cell)))) # indirect: live, redefinable # Indirect: live deref. Quote the cell so it's embedded by
# reference (a bare table in arg position would be re-evaluated as
# a constructor — deep-copying it, and any atom in :root, each call).
(tuple var-get (tuple 'quote cell))))
:if ['if (emit ctx (node :test)) (emit ctx (node :then)) (emit ctx (node :else))] :if ['if (emit ctx (node :test)) (emit ctx (node :then)) (emit ctx (node :else))]
:do (emit-seq ctx node) :do (emit-seq ctx node)
:loop (emit-loop ctx node) :loop (emit-loop ctx node)

View file

@ -129,19 +129,25 @@
"Deref the var. If the var is dynamic and has a thread-local binding, return that. "Deref the var. If the var is dynamic and has a thread-local binding, return that.
Otherwise return the root binding." Otherwise return the root binding."
[v] [v]
# walk binding stack top-down for this var # Fast path: no dynamic bindings are active (the common case — the stack is
# only non-empty inside a `binding` block), so the value is just the root. This
# is the hot path for every global deref; skip building the walk otherwise.
(def bs (cur-binding-stack)) (def bs (cur-binding-stack))
(var result nil) (if (= 0 (length bs))
(var i (dec (length bs))) (v :root)
(while (>= i 0) # walk binding stack top-down for this var
(let [frame (in bs i) (do
val (get frame v)] (var result nil)
(if (not (nil? val)) (var i (dec (length bs)))
(do (while (>= i 0)
(set result (if (var? val) (var-get val) val)) (let [frame (in bs i)
(set i -1)) val (get frame v)]
(-- i)))) (if (not (nil? val))
(if (not (nil? result)) result (v :root))) (do
(set result (if (var? val) (var-get val) val))
(set i -1))
(-- i))))
(if (not (nil? result)) result (v :root)))))
(defn var-set (defn var-set
"Set a var's value. If the var has a thread-local binding on the stack, update "Set a var's value. If the var has a thread-local binding on the stack, update