feat(compile): Phase 1 — wire shared persistent env (compile mode now works across forms)

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.
This commit is contained in:
Yogthos 2026-06-05 17:50:22 -04:00
parent b6e66e53d2
commit f74af5dbc5
3 changed files with 90 additions and 40 deletions

View file

@ -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

View file

@ -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)

View file

@ -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!")