From db08cecde845d56926af68e80fa83d0d2633e80f Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 17:31:16 -0400 Subject: [PATCH] perf: var-deref fast path; drop the memoized-getter indirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/jolt/backend.janet | 14 +++++++------- src/jolt/types.janet | 30 ++++++++++++++++++------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index a43c1b5..6625856 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -14,12 +14,9 @@ (use ./evaluator) (import ./reader :as r) -# Var late-binding: deref/set through the cell via a memoized closure so compiled -# code sees redefinition (Janet early-binds plain symbols). Same scheme as the -# bootstrap compiler. -(defn- var-getter [cell] - (or (get cell :jolt/getter) - (let [g (fn [] (var-get cell))] (put cell :jolt/getter g) g))) +# Var late-binding: reads go through `(var-get cell)` with the cell embedded as a +# constant, so compiled code sees redefinition (Janet early-binds plain symbols) +# — var-get reads the cell's root live. Writes go through a memoized setter. (defn- var-setter [cell] (or (get cell :jolt/setter) (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))] (if (direct-var? ctx cell) (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))] :do (emit-seq ctx node) :loop (emit-loop ctx node) diff --git a/src/jolt/types.janet b/src/jolt/types.janet index b6d9823..ae56e4e 100644 --- a/src/jolt/types.janet +++ b/src/jolt/types.janet @@ -129,19 +129,25 @@ "Deref the var. If the var is dynamic and has a thread-local binding, return that. Otherwise return the root binding." [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)) - (var result nil) - (var i (dec (length bs))) - (while (>= i 0) - (let [frame (in bs i) - val (get frame v)] - (if (not (nil? val)) - (do - (set result (if (var? val) (var-get val) val)) - (set i -1)) - (-- i)))) - (if (not (nil? result)) result (v :root))) + (if (= 0 (length bs)) + (v :root) + # walk binding stack top-down for this var + (do + (var result nil) + (var i (dec (length bs))) + (while (>= i 0) + (let [frame (in bs i) + val (get frame v)] + (if (not (nil? val)) + (do + (set result (if (var? val) (var-get val) val)) + (set i -1)) + (-- i)))) + (if (not (nil? result)) result (v :root))))) (defn var-set "Set a var's value. If the var has a thread-local binding on the stack, update