diff --git a/src/jolt/api.janet b/src/jolt/api.janet index 98713e8..78580d7 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -55,44 +55,12 @@ (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"))) - -# Hybrid compile path: try to compile the form, fall back to the interpreter for -# anything the compiler can't (yet) handle. Only the compile step is guarded — -# runtime errors in compiled code propagate, so we never double-eval side effects -# or hide real errors behind a fallback. The interpreter handles every form, so -# the result is always correct; compilation is a transparent speedup. -(defn- try-compile-eval [ctx form] - (let [compiled (protect (compile-ast form ctx))] - (if (compiled 0) - (eval-compiled (compiled 1) ctx) - (eval-form ctx @{} form)))) - (defn eval-one - "Evaluate a single already-parsed form, routing to the compiler when the - context has :compile? enabled (stateful forms always interpret; forms the - compiler can't handle fall back to the interpreter)." + "Evaluate a single already-parsed form. Routing (compile when :compile? is set, + stateful forms interpret, interpreter fallback for forms the compiler can't + handle) lives in loader/eval-toplevel so load-ns and eval-one stay in sync." [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) - (try-compile-eval ctx form))) - (if (or (and (struct? form) (= :symbol (form :jolt/type))) (tuple? form)) - (try-compile-eval ctx form) - (eval-form ctx @{} form))) - (eval-form ctx @{} form))) + (eval-toplevel ctx form)) (defn eval-string "Evaluate a Clojure source string in a Jolt context. diff --git a/src/jolt/loader.janet b/src/jolt/loader.janet index b2197a3..da48ce5 100644 --- a/src/jolt/loader.janet +++ b/src/jolt/loader.janet @@ -6,6 +6,47 @@ (use ./compiler) (use ./evaluator) +# Stateful / context-modifying forms always interpret: they mutate the context +# (namespaces, macros, types, multimethods, dynamic vars, …) in ways the compiler +# doesn't model. Kept here so the compile/interpret routing lives in one place, +# used by both load-ns and the public eval-one. +(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- form-head-name [form] + (when (array? form) + (let [ff (first form)] + (when (and (struct? ff) (= :symbol (ff :jolt/type))) (ff :name))))) + +(defn eval-toplevel + "Evaluate one top-level form for ctx, honoring :compile?. Stateful forms always + interpret; otherwise the form is compiled and run, falling back to the + interpreter when the compiler can't handle it. Only the compile step is guarded + — runtime errors in compiled code propagate (no double-eval, no hidden errors)." + [ctx form] + (defn try-compile [] + (let [compiled (protect (compile-ast form ctx))] + (if (compiled 0) + (eval-compiled (compiled 1) ctx) + (eval-form ctx @{} form)))) + (if (get (ctx :env) :compile?) + (if (array? form) + # A call/list: compile it unless its head is a stateful special form. + (let [hn (form-head-name form)] + (if (and hn (stateful-head? hn)) + (eval-form ctx @{} form) + (try-compile))) + # A bare symbol or vector literal compiles; anything else interprets. + (if (or (and (struct? form) (= :symbol (form :jolt/type))) (tuple? form)) + (try-compile) + (eval-form ctx @{} form))) + (eval-form ctx @{} form))) + (defn load-ns "Load a Clojure namespace from a .clj file. When ctx has :compile? enabled, forms are compiled to Janet source, @@ -41,23 +82,17 @@ (when (nil? ns-name) (error (string "No ns form found in " filepath))) - (if compile? - (do - # Compile each form and eval as Janet - (var cached (get cache ns-name)) - (when (nil? cached) - (set cached @[]) - (put cache ns-name cached)) - - (each form forms - (array/push cached form) - (compile-and-eval form ctx)) - ns-name) - # Interpreter path - (do - (each form forms - (eval-form ctx @{} form)) - ns-name)))) + # Per-form routing (compile-or-interpret, stateful forms interpret) is shared + # with eval-one via eval-toplevel. When compiling, also record the forms so a + # namespace can be inspected / re-emitted. + (when compile? + (var cached (get cache ns-name)) + (when (nil? cached) + (set cached @[]) + (put cache ns-name cached)) + (each form forms (array/push cached form))) + (each form forms (eval-toplevel ctx form)) + ns-name)) (defn compiled? "Check if a namespace has been compiled and cached."