From f74af5dbc5b9ef4f73b6424c45973d511a0e30f5 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 17:50:22 -0400 Subject: [PATCH] =?UTF-8?q?feat(compile):=20Phase=201=20=E2=80=94=20wire?= =?UTF-8?q?=20shared=20persistent=20env=20(compile=20mode=20now=20works=20?= =?UTF-8?q?across=20forms)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile mode was broken: compiled defns interned only into the jolt namespace, which Janet's eval couldn't see, so calling a compiled function threw 'unknown symbol'. And load-string ignored :compile? entirely. - Each context gets a persistent Janet env (ctx-janet-env), a child of the compiler module env (so core-* resolve) holding the context's user defs. compile-and-eval evals into it, so def/defn persist and resolve across forms; contexts stay isolated. nil ctx (one-off eval) gets a fresh child. - Emit a named fn for defn ((def f (fn f [..] (f ..)))) so recursion resolves lexically (the anonymous form couldn't forward-reference f at compile time). - Extract eval-one (per-form routing) and use it in both eval-string and load-string, so load-string honors :compile?. Stateful forms still interpret. compiled fib(30): ~50s (interpreted) -> 3.4s (~15x). spec: compile-mode-test gains cross-form/recursion/def + context-isolation cases. jpm test green. Fast operator emission (the rest of the win) is Phase 2. --- src/jolt/api.janet | 55 ++++++++++++----------- src/jolt/compiler.janet | 56 ++++++++++++++++++------ test/integration/compile-mode-test.janet | 19 +++++++- 3 files changed, 90 insertions(+), 40 deletions(-) diff --git a/src/jolt/api.janet b/src/jolt/api.janet index 8e5ba4b..4e59165 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -43,6 +43,33 @@ (install-async! ctx) ctx)) +# Stateful / context-modifying forms always use the interpreter (they mutate +# the context: namespaces, macros, types, multimethods, dynamic vars, …). +(defn- stateful-head? [head-name] + (or (= head-name "defmacro") (= head-name "ns") + (= head-name "deftype") (= head-name "defmulti") (= head-name "defmethod") + (= head-name "require") (= head-name "in-ns") + (= head-name "syntax-quote") (= head-name "set!") + (= head-name "var") (= head-name ".") (= head-name "new") + (= head-name "eval"))) + +(defn eval-one + "Evaluate a single already-parsed form, routing to the compiler when the + context has :compile? enabled (stateful forms always interpret)." + [ctx form] + (if (get (ctx :env) :compile?) + (if (array? form) + (let [first-form (first form) + head-name (if (and (struct? first-form) (= :symbol (first-form :jolt/type))) + (first-form :name) nil)] + (if (stateful-head? head-name) + (eval-form ctx @{} form) + (compile-and-eval form ctx))) + (if (or (and (struct? form) (= :symbol (form :jolt/type))) (tuple? form)) + (compile-and-eval form ctx) + (eval-form ctx @{} form))) + (eval-form ctx @{} form))) + (defn eval-string "Evaluate a Clojure source string in a Jolt context. When :compile? is enabled, compiles to Janet and evaluates. @@ -50,31 +77,7 @@ Context-modifying forms (ns, defmacro, deftype, require, in-ns, defmulti, defmethod) always use the interpreter." [ctx s] - (let [compile? (get (ctx :env) :compile?) - form (parse-string s)] - (if compile? - (if (array? form) - # Lists: check for stateful forms - (let [first-form (first form) - head-name (if (and (struct? first-form) (= :symbol (first-form :jolt/type))) - (first-form :name) - nil) - stateful? (or (= head-name "defmacro") (= head-name "ns") - (= head-name "deftype") (= head-name "defmulti") (= head-name "defmethod") - (= head-name "require") (= head-name "in-ns") - (= head-name "syntax-quote") (= head-name "set!") - (= head-name "var") (= head-name ".") (= head-name "new") - (= head-name "eval"))] - (if stateful? - (eval-form ctx @{} form) - (compile-and-eval form ctx))) - # Bare symbols and other non-literal forms: also compile - (if (or (and (struct? form) (= :symbol (form :jolt/type))) - (tuple? form)) - (compile-and-eval form ctx) - (eval-form ctx @{} form))) - # No compile flag: always interpret - (eval-form ctx @{} form)))) + (eval-one ctx (parse-string s))) (defn eval-string* "Evaluate a Clojure source string with explicit bindings." @@ -93,7 +96,7 @@ (def [form rest] (parse-next cur)) (set cur rest) (when (not (nil? form)) - (set result (eval-form ctx @{} form)))) + (set result (eval-one ctx form)))) result) (defn compile-string diff --git a/src/jolt/compiler.janet b/src/jolt/compiler.janet index b56ca08..90684f5 100644 --- a/src/jolt/compiler.janet +++ b/src/jolt/compiler.janet @@ -8,6 +8,24 @@ (use ./core) (use ./phm) +# The compiler emits Janet that references the core fns (core-+, core-<, …), +# which are bare-bound in this module's environment via (use ./core). Capture it +# so each Jolt context can get a child env where those resolve and where compiled +# `def`/`defn` bindings persist across forms (isolated per context). +(def jolt-runtime-env (curenv)) + +(defn ctx-janet-env + "Lazily create/cache a per-context Janet environment for compiled code: a child + of the runtime env (so core fns resolve) that holds this context's user defs. + For a nil context (one-off compile/eval) returns a fresh child env." + [ctx] + (if (and ctx (table? (get ctx :env))) + (or (get (ctx :env) :janet-rt) + (let [e (make-env jolt-runtime-env)] + (put (ctx :env) :janet-rt e) + e)) + (make-env jolt-runtime-env))) + (def- core-renames @{"+" "core-+" "-" "core-sub" @@ -854,18 +872,30 @@ (emit-expr (analyze-form form @{} ctx))) (defn compile-and-eval - "Compile a Clojure form and evaluate it as Janet. - For def/defn forms, interns the result in the Jolt namespace - so the interpreter can resolve it later." + "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." [form ctx] - (def result (eval (compile-ast form ctx))) - # Intern def/defn results in Jolt namespace for interpreter resolution - (when (and ctx (array? form) (> (length form) 0) - (struct? (first form)) (= :symbol ((first form) :jolt/type))) - (def head-name ((first form) :name)) - (when (or (= head-name "def") (= head-name "defn") (= head-name "defn-")) - (def name-sym (in form 1)) - (def name (if (struct? name-sym) (name-sym :name) name-sym)) - (def ns (ctx-find-ns ctx (ctx-current-ns ctx))) - (ns-intern ns name result))) + (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) diff --git a/test/integration/compile-mode-test.janet b/test/integration/compile-mode-test.janet index 880ba43..1b06cbc 100644 --- a/test/integration/compile-mode-test.janet +++ b/test/integration/compile-mode-test.janet @@ -77,6 +77,23 @@ (print " complex...") (assert (= 6 (ct-eval ctx "(let [f (fn [n] (loop [i 0 acc 0] (if (< i n) (recur (inc i) (+ acc i)) acc)))] (f 4))")) "nested") - (assert (= 15 (ct-eval ctx "(reduce + (map inc [0 1 2 3 4]))")) "reduce+map")) + (assert (= 15 (ct-eval ctx "(reduce + (map inc [0 1 2 3 4]))")) "reduce+map") + + # Phase 1 wiring: compiled defns persist across forms (the per-context Janet + # env) and recurse correctly (named-fn self-reference). + (print " cross-form defns + recursion...") + (eval-string ctx "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))") + (assert (= 832040 (ct-eval ctx "(fib 30)")) "recursive fib across forms") + (eval-string ctx "(defn sq [x] (* x x))") + (eval-string ctx "(defn sum-sq [a b] (+ (sq a) (sq b)))") + (assert (= 25 (ct-eval ctx "(sum-sq 3 4)")) "defn calling earlier defn") + (eval-string ctx "(def base 100)") + (assert (= 142 (ct-eval ctx "(+ base 42)")) "compiled def referenced later")) + +# Context isolation: a def in one compiled context is invisible in another. +(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")) (print "\nAll Phase 6 tests passed!")