compiler phase 1: var-indirection for redefinable compiled globals

Compiled global references now deref through the Jolt var cell instead of an
early-bound Janet symbol, so redefining a def/defn is visible to already-compiled
callers (Janet early-binds plain symbols). A global ref analyzes to :op :var
(resolving/creating the cell); a def sets that same cell's root. Because Janet
copies table constants but references functions, we embed memoized getter/setter
closures over the cell rather than the cell itself. This also subsumes the old
named-fn recursion rewrite and the separate ns-intern.

ns-intern now stores the namespace *name* (string) in a var, not the ns table, so
var cells aren't cyclic (required for embedding).

fib(30) compiled ~0.5s (the late-binding cost vs phase 2's direct-linked 0.08s;
direct-linking for hot calls is a later optimization). Suite green; cross-form,
recursion, and context isolation preserved; redefinition now works.
This commit is contained in:
Yogthos 2026-06-06 01:33:47 -04:00
parent 614962d535
commit 70711b4489
3 changed files with 53 additions and 35 deletions

View file

@ -370,7 +370,13 @@
{:op :local :name name} {:op :local :name name}
(if (and (not (special-form? name)) (get core-renames name)) (if (and (not (special-form? name)) (get core-renames name))
{:op :core-symbol :name name :janet-name (get core-renames name)} {:op :core-symbol :name name :janet-name (get core-renames name)}
{:op :symbol :name name})))) # A global reference: resolve (or create) the Jolt var cell now and
# compile a deref through it, so redefinition is visible to compiled
# callers (Janet early-binds plain symbols). No ctx -> plain symbol.
(if ctx
{:op :var :name name
:var (ns-intern (ctx-find-ns ctx (ctx-current-ns ctx)) name)}
{:op :symbol :name name})))))
(array? form) (array? form)
(let [first-form (first form) (let [first-form (first form)
@ -435,9 +441,13 @@
:else (if (> (length form) 3) :else (if (> (length form) 3)
(analyze-form (in form 3) bindings ctx) (analyze-form (in form 3) bindings ctx)
{:op :const :val nil})} {:op :const :val nil})}
"def" {:op :def "def" (let [name-sym (in form 1)
:name (in form 1) nm (if (struct? name-sym) (name-sym :name) (string name-sym))
:init (analyze-form (in form 2) bindings ctx)} # Create/find the var cell first so a recursive init body
# self-references the same cell.
cell (when ctx (ns-intern (ctx-find-ns ctx (ctx-current-ns ctx)) nm))]
{:op :def :name name-sym :var cell
:init (analyze-form (in form 2) bindings ctx)})
"fn*" (let [params (in form 1) "fn*" (let [params (in form 1)
body-bindings (do body-bindings (do
(var bb @{}) (var bb @{})
@ -709,6 +719,7 @@
(match (ast :op) (match (ast :op)
:const (emit-const-str (ast :val) buf) :const (emit-const-str (ast :val) buf)
:symbol (emit-symbol-str (ast :name) buf) :symbol (emit-symbol-str (ast :name) buf)
:var (emit-symbol-str (ast :name) buf)
:local (emit-local-str (ast :name) buf) :local (emit-local-str (ast :name) buf)
:core-symbol (emit-core-symbol-str (ast :janet-name) buf) :core-symbol (emit-core-symbol-str (ast :janet-name) buf)
:qualified-symbol (emit-qualified-symbol-str (ast :ns) (ast :name) buf) :qualified-symbol (emit-qualified-symbol-str (ast :ns) (ast :name) buf)
@ -768,6 +779,19 @@
(defn- emit-def-expr [name-sym init] (defn- emit-def-expr [name-sym init]
['def (symbol (name-sym :name)) (emit-expr init)]) ['def (symbol (name-sym :name)) (emit-expr init)])
# Var-indirection: a global reference derefs its cell at call time, and a def
# sets the same cell's root and returns it (Clojure's #'var). Janet COPIES table
# constants when compiling but references functions, so we embed memoized
# getter/setter CLOSURES over the cell (by reference) rather than the cell itself.
(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]
(or (get cell :jolt/setter)
(let [s (fn [v] (bind-root cell v) cell)] (put cell :jolt/setter s) s)))
(defn- emit-var-expr [cell] (tuple (var-getter cell)))
(defn- emit-def-var-expr [cell init] (tuple (var-setter cell) (emit-expr init)))
(defn- emit-fn-expr [params body] (defn- emit-fn-expr [params body]
(def param-syms @[]) (def param-syms @[])
(each p params (each p params
@ -828,7 +852,7 @@
# only when the head is a keyword/collection literal in call position (an IFn # only when the head is a keyword/collection literal in call position (an IFn
# that needs runtime lookup), e.g. (:k m) or ({:a 1} :a). # that needs runtime lookup), e.g. (:k m) or ({:a 1} :a).
(def direct (case (f-ast :op) (def direct (case (f-ast :op)
:core-symbol true :symbol true :local true :core-symbol true :symbol true :var true :local true
:qualified-symbol true :fn true :qualified-symbol true :fn true
false)) false))
(def f (emit-expr f-ast)) (def f (emit-expr f-ast))
@ -854,12 +878,14 @@
(match (ast :op) (match (ast :op)
:const (emit-const-expr (ast :val)) :const (emit-const-expr (ast :val))
:symbol (emit-symbol-expr (ast :name)) :symbol (emit-symbol-expr (ast :name))
:var (emit-var-expr (ast :var))
:local (emit-local-expr (ast :name)) :local (emit-local-expr (ast :name))
:core-symbol (emit-core-symbol-expr (ast :janet-name)) :core-symbol (emit-core-symbol-expr (ast :janet-name))
:qualified-symbol (emit-qualified-symbol-expr (ast :ns) (ast :name)) :qualified-symbol (emit-qualified-symbol-expr (ast :ns) (ast :name))
:do (emit-do-expr (ast :statements) (ast :ret)) :do (emit-do-expr (ast :statements) (ast :ret))
:if (emit-if-expr (ast :test) (ast :then) (ast :else)) :if (emit-if-expr (ast :test) (ast :then) (ast :else))
:def (emit-def-expr (ast :name) (ast :init)) :def (if (ast :var) (emit-def-var-expr (ast :var) (ast :init))
(emit-def-expr (ast :name) (ast :init)))
:fn (emit-fn-expr (ast :params) (ast :body)) :fn (emit-fn-expr (ast :params) (ast :body))
:let (emit-let-expr (ast :binding-pairs) (ast :body)) :let (emit-let-expr (ast :binding-pairs) (ast :body))
:throw (emit-throw-expr (ast :val)) :throw (emit-throw-expr (ast :val))
@ -893,30 +919,10 @@
(emit-expr (analyze-form form @{} ctx))) (emit-expr (analyze-form form @{} ctx)))
(defn compile-and-eval (defn compile-and-eval
"Compile a Clojure form and evaluate it as Janet, in the context's persistent "Compile a Clojure form and evaluate it as Janet. Globals resolve through Jolt
Janet env (so compiled def/defn bindings resolve across forms). For def/defn var cells (see analyze-form/:var), so compiled def/defn results are visible to
forms, also interns the result in the Jolt namespace so the interpreter can the interpreter (the cell is the namespace var), recursion self-references the
resolve it later." cell, and redefinition is seen by compiled callers — no separate interning or
named-fn rewrite needed."
[form ctx] [form ctx]
(def env (ctx-janet-env ctx)) (eval (compile-ast form ctx) (ctx-janet-env ctx)))
(def def-form? (and ctx (array? form) (> (length form) 0)
(struct? (first form)) (= :symbol ((first form) :jolt/type))
(let [h ((first form) :name)] (or (= h "def") (= h "defn") (= h "defn-")))))
(def def-name (when def-form?
(let [name-sym (in form 1)] (if (struct? name-sym) (name-sym :name) name-sym))))
(var compiled (compile-ast form ctx))
# Name the fn after the def so a recursive body self-references lexically
# ((def f (fn [..] (f ..))) -> (def f (fn f [..] (f ..)))); the anonymous form
# can't resolve f at compile time.
(when (and def-name (indexed? compiled) (= 3 (length compiled))
(= 'def (in compiled 0))
(indexed? (in compiled 2)) (= 'fn (in (in compiled 2) 0))
(indexed? (in (in compiled 2) 1))) # 3rd elem is (fn [params] ...)
(let [f (in compiled 2)]
(set compiled [(in compiled 0) (in compiled 1)
[(in f 0) (symbol def-name) ;(tuple/slice f 1)]])))
(def result (eval compiled env))
# Also intern def/defn results in the Jolt namespace for interpreter resolution.
(when def-name
(ns-intern (ctx-find-ns ctx (ctx-current-ns ctx)) def-name result))
result)

View file

@ -297,7 +297,10 @@
(when (not (nil? val)) (when (not (nil? val))
(bind-root existing val)) (bind-root existing val))
existing) existing)
(let [v (make-var sym val {:ns ns :name sym})] # Store the namespace *name*, not the ns table: a back-pointer to the ns
# would make the var cyclic (ns -> mappings -> var -> ns), and the compiler
# embeds var cells as constants, which can't be cyclic.
(let [v (make-var sym val {:ns (get ns :name) :name sym})]
(put mappings sym v) (put mappings sym v)
v)))) v))))

View file

@ -100,10 +100,19 @@
(assert (= 2 (ct-eval ctx "(#{1 2 3} 2)")) "set as fn") (assert (= 2 (ct-eval ctx "(#{1 2 3} 2)")) "set as fn")
(assert (= true (ct-eval ctx "(= [1 2] [1 2])")) "= is value equality, not core-= bypass")) (assert (= true (ct-eval ctx "(= [1 2] [1 2])")) "= is value equality, not core-= bypass"))
# Context isolation: a def in one compiled context is invisible in another. # Context isolation: a def in one compiled context is invisible in another. With
# var-indirection each context has its own var cells, so b's `secret` is a
# distinct, unbound var (nil) rather than a's 7.
(let [a (init {:compile? true}) b (init {:compile? true})] (let [a (init {:compile? true}) b (init {:compile? true})]
(eval-string a "(def secret 7)") (eval-string a "(def secret 7)")
(assert (= 7 (ct-eval a "secret")) "def visible in its own ctx") (assert (= 7 (ct-eval a "secret")) "def visible in its own ctx")
(assert (not ((protect (ct-eval b "secret")) 0)) "def isolated to its ctx")) (assert (nil? (ct-eval b "secret")) "def isolated to its ctx"))
# Redefinition is visible to already-compiled callers (var-indirection).
(let [c (init {:compile? true})]
(eval-string c "(defn g [] 1)")
(eval-string c "(defn calls-g [] (g))")
(eval-string c "(defn g [] 2)")
(assert (= 2 (ct-eval c "(calls-g)")) "compiled caller sees redefined global"))
(print "\nAll Phase 6 tests passed!") (print "\nAll Phase 6 tests passed!")