From 70711b448914922e6b411213cb6ccab76db6985c Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 01:33:47 -0400 Subject: [PATCH] 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. --- src/jolt/compiler.janet | 70 +++++++++++++----------- src/jolt/types.janet | 5 +- test/integration/compile-mode-test.janet | 13 ++++- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/jolt/compiler.janet b/src/jolt/compiler.janet index 3a3babe..ddc8685 100644 --- a/src/jolt/compiler.janet +++ b/src/jolt/compiler.janet @@ -370,7 +370,13 @@ {:op :local :name name} (if (and (not (special-form? 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) (let [first-form (first form) @@ -435,9 +441,13 @@ :else (if (> (length form) 3) (analyze-form (in form 3) bindings ctx) {:op :const :val nil})} - "def" {:op :def - :name (in form 1) - :init (analyze-form (in form 2) bindings ctx)} + "def" (let [name-sym (in form 1) + nm (if (struct? name-sym) (name-sym :name) (string name-sym)) + # 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) body-bindings (do (var bb @{}) @@ -709,6 +719,7 @@ (match (ast :op) :const (emit-const-str (ast :val) buf) :symbol (emit-symbol-str (ast :name) buf) + :var (emit-symbol-str (ast :name) buf) :local (emit-local-str (ast :name) buf) :core-symbol (emit-core-symbol-str (ast :janet-name) buf) :qualified-symbol (emit-qualified-symbol-str (ast :ns) (ast :name) buf) @@ -768,6 +779,19 @@ (defn- emit-def-expr [name-sym 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] (def param-syms @[]) (each p params @@ -828,7 +852,7 @@ # 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). (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 false)) (def f (emit-expr f-ast)) @@ -854,12 +878,14 @@ (match (ast :op) :const (emit-const-expr (ast :val)) :symbol (emit-symbol-expr (ast :name)) + :var (emit-var-expr (ast :var)) :local (emit-local-expr (ast :name)) :core-symbol (emit-core-symbol-expr (ast :janet-name)) :qualified-symbol (emit-qualified-symbol-expr (ast :ns) (ast :name)) :do (emit-do-expr (ast :statements) (ast :ret)) :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)) :let (emit-let-expr (ast :binding-pairs) (ast :body)) :throw (emit-throw-expr (ast :val)) @@ -893,30 +919,10 @@ (emit-expr (analyze-form form @{} ctx))) (defn compile-and-eval - "Compile a Clojure form and evaluate it as Janet, in the context's persistent - Janet env (so compiled def/defn bindings resolve across forms). For def/defn - forms, also interns the result in the Jolt namespace so the interpreter can - resolve it later." + "Compile a Clojure form and evaluate it as Janet. Globals resolve through Jolt + var cells (see analyze-form/:var), so compiled def/defn results are visible to + the interpreter (the cell is the namespace var), recursion self-references the + cell, and redefinition is seen by compiled callers — no separate interning or + named-fn rewrite needed." [form ctx] - (def env (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) + (eval (compile-ast form ctx) (ctx-janet-env ctx))) diff --git a/src/jolt/types.janet b/src/jolt/types.janet index 41ab0fe..7e07e20 100644 --- a/src/jolt/types.janet +++ b/src/jolt/types.janet @@ -297,7 +297,10 @@ (when (not (nil? val)) (bind-root existing val)) 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) v)))) diff --git a/test/integration/compile-mode-test.janet b/test/integration/compile-mode-test.janet index b83aa75..df9ae1f 100644 --- a/test/integration/compile-mode-test.janet +++ b/test/integration/compile-mode-test.janet @@ -100,10 +100,19 @@ (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")) -# 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})] (eval-string a "(def secret 7)") (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!")