Skip to main content

shape_jit/
executor.rs

1//! JIT executor implementing the ProgramExecutor trait
2
3use shape_ast::Program;
4use shape_runtime::engine::{ExecutionType, ProgramExecutor, ShapeEngine};
5use shape_runtime::error::Result;
6use shape_wire::WireValue;
7use std::time::Instant;
8
9/// JIT executor with selective per-function compilation.
10///
11/// JIT-compatible functions are compiled to native code; incompatible functions
12/// (e.g. those using async, pattern matching, or unsupported builtins) are left
13/// as `Interpreted` entries in the mixed function table for VM fallback.
14///
15/// # `--mode jit` semantics (W12-jit-mode-semantics-and-fallthrough, 2026-05-18)
16///
17/// `--mode jit` attempts to JIT-compile the toplevel script and every function
18/// it reaches. If JIT compilation or JIT execution fails for any reason (a
19/// preflight rejection at `compile_program_selective`, a Cranelift codegen
20/// error, a panic in the JIT pipeline, a `RETURN_TAG_NANBOXED` kind-source
21/// gap surface, etc.), the executor falls through to the bytecode interpreter
22/// via `BytecodeExecutor::execute_program` instead of surfacing a hard error
23/// to the CLI. A one-line `[jit-fallback]` diagnostic is emitted on stderr at
24/// `tracing::info` level (default) so the fall-through is observable but does
25/// not silently mask broken programs (the interpreter still re-runs the same
26/// `Program` and surfaces any genuine runtime error from there).
27///
28/// Verbose JIT tracing remains under the `--trace-jit=...` CLI flag (replaces
29/// the legacy `SHAPE_JIT_DEBUG` env-var per closure-wave-F migration). Tier-up
30/// thresholds at T1@100 / T2@10k on hot functions are preserved by the
31/// underlying `compile_program_selective` pipeline — fall-through only fires
32/// when JIT can not handle the program at all.
33pub struct JITExecutor {
34    /// Bytecode executor used for extension loading, module resolution,
35    /// and other pre-compilation setup that the CLI wires through.
36    /// Also the fall-through target when JIT compile/execute fails.
37    pub bytecode_executor: shape_vm::BytecodeExecutor,
38}
39
40impl JITExecutor {
41    pub fn new() -> Self {
42        Self {
43            bytecode_executor: shape_vm::BytecodeExecutor::new(),
44        }
45    }
46}
47
48impl ProgramExecutor for JITExecutor {
49    fn execute_program(
50        &mut self,
51        engine: &mut ShapeEngine,
52        program: &Program,
53    ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
54        use shape_vm::BytecodeCompiler;
55
56        // REPL cross-cell persistence (WS-11): when the engine is a REPL
57        // (`init_repl` enabled persistence), execute the cell on the
58        // bytecode interpreter. Cross-cell `let`/`var` bindings and
59        // `fn`/`type` definitions are round-tripped through the
60        // persistent `ExecutionContext` by `BytecodeExecutor::
61        // execute_program`; the JIT's ahead-of-time `compile_strategy`
62        // path stores top-level module bindings in its own `jit_ctx`
63        // locals which never reach that context, so a JIT-executed cell
64        // would silently drop every binding the next cell needs.
65        //
66        // This is not a fallback or a degradation hatch: a REPL cell is
67        // a one-shot interactive line for which ahead-of-time native
68        // codegen yields no measurable benefit, and the interpreter's
69        // own tiered JIT (T1@100 / T2@10k) still promotes any function
70        // that genuinely runs hot across cells. The `--mode jit` flag
71        // continues to drive AOT compilation for `shape run` scripts;
72        // only the interactive REPL routes through the interpreter, so
73        // cross-cell correctness is identical to `--mode vm`.
74        if engine.repl_persistence() {
75            return self.bytecode_executor.execute_program(engine, program);
76        }
77
78        // Cluster-2 closure-wave-F tracing-crate migration (2026-05-16):
79        // `tracing::enabled!` compiles away under `release_max_level_off`
80        // (the default when the `jit-trace` Cargo feature is OFF), so this
81        // collapses to `false` and the phase-timing accounting below is
82        // dead-code-eliminated by the optimizer. Replaces the legacy
83        // `SHAPE_JIT_PHASE_METRICS` env-var; CLI selector is
84        // `--trace-jit=shape_jit::metrics=info`.
85        let emit_phase_metrics = tracing::enabled!(
86            target: "shape_jit::metrics",
87            tracing::Level::INFO,
88        );
89
90        // Capture source text before getting runtime reference (for error messages)
91        let source_for_compilation = engine.current_source().map(|s| s.to_string());
92
93        // Compile to bytecode first to check JIT compatibility
94        let runtime = engine.get_runtime_mut();
95
96        // Get known module bindings — prefer persistent context, fallback to precompiled names
97        let known_bindings: Vec<String> = if let Some(ctx) = runtime.persistent_context() {
98            let names = ctx.root_scope_binding_names();
99            if names.is_empty() {
100                shape_vm::stdlib::core_binding_names(runtime)
101            } else {
102                names
103            }
104        } else {
105            shape_vm::stdlib::core_binding_names(runtime)
106        };
107
108        // Build module graph and compile via graph pipeline.
109        //
110        // W9: pass `self.bytecode_executor.extensions()` so the graph build
111        // can hybridize native extension modules with their Shape overlay
112        // (e.g. `std::core::remote`'s `pub annotation remote(addr)`). Without
113        // the extensions list, the graph would skip the hybridization probe
114        // and the namespace import path would lose annotation visibility.
115        let extensions = self.bytecode_executor.extensions().to_vec();
116        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
117        let (graph, stdlib_names, prelude_imports) =
118            shape_vm::module_resolution::build_graph_and_stdlib_names(
119                program,
120                &mut loader,
121                &extensions,
122            )
123            .map_err(|e| shape_runtime::error::ShapeError::RuntimeError {
124                message: format!("Module graph construction failed: {}", e),
125                location: None,
126            })?;
127
128        let bytecode_compile_start = Instant::now();
129        let mut compiler = if extensions.is_empty() {
130            BytecodeCompiler::new()
131        } else {
132            BytecodeCompiler::new().with_extensions(extensions.clone())
133        };
134        compiler.stdlib_function_names = stdlib_names;
135        compiler.register_known_bindings(&known_bindings);
136        if let Some(source) = &source_for_compilation {
137            compiler.set_source(source);
138        }
139        let bytecode = compiler
140            .compile_with_graph_and_prelude(program, graph, &prelude_imports)
141            .map_err(|e| shape_runtime::error::ShapeError::RuntimeError {
142                message: format!("Bytecode compilation failed: {}", e),
143                location: None,
144            })?;
145        let bytecode_compile_ms = bytecode_compile_start.elapsed().as_millis();
146
147        // W12-jit-mode-semantics-and-fallthrough (Phase 3d, 2026-05-18):
148        // attempt JIT compile+execute; if either step fails for any reason,
149        // fall through to the bytecode interpreter so `--mode jit` never
150        // silently no-ops on a broken JIT path. The interpreter executes
151        // the same `Program` directly (not the JIT-side bytecode), so
152        // bytecode-graph differences between the two paths do not matter
153        // here. The fall-through is observable via a one-line stderr
154        // `[jit-fallback]` diagnostic at `tracing::info` level (default
155        // visibility); `--trace-jit=shape_jit=debug` promotes the JIT
156        // pipeline's own tracing for root-cause investigation.
157        //
158        // Per supervisor path (3) binding 2026-05-18: "On JIT-compile
159        // failure: fall through to interpreter (NOT silent-no-output);
160        // Diagnostic emitted at info level: `[jit-fallback] function X
161        // failed JIT compile: <reason>; running under interpreter`".
162        // r5c-2-gz-cp2-jit-div: `execute_with_jit` returns a nested result so
163        // a JIT-COMPILE-stage failure (fall through to interpreter) is kept
164        // distinct from a genuine PROGRAM runtime error the JIT executed
165        // soundly (e.g. division by zero). The latter must propagate
166        // directly — re-running it under the interpreter would execute the
167        // program a second time, doubling any side effects (a `print`
168        // before the failing divide would fire twice). Outer `Err` =
169        // compile-stage failure; `Ok(Err(_))` = JIT-executed runtime error.
170        match self.execute_with_jit(engine, &bytecode, bytecode_compile_ms, emit_phase_metrics) {
171            Ok(Ok(result)) => Ok(result),
172            Ok(Err(runtime_err)) => Err(runtime_err),
173            Err(jit_err) => {
174                // Emit the structured fall-through diagnostic. Use `eprintln!`
175                // so the diagnostic is visible even when the user has not
176                // wired up a tracing subscriber (the default `shape run`
177                // CLI invocation has no subscriber installed). Mirror the
178                // event to `tracing::info!` so JSON-tracing consumers and
179                // the `--trace-jit` filter see it too.
180                let reason = jit_err.to_string();
181                let function_name = "main".to_string();
182                eprintln!(
183                    "[jit-fallback] function {function_name} failed JIT compile: \
184                     {reason}; running under interpreter"
185                );
186                tracing::info!(
187                    target: "shape_jit::fallback",
188                    function = %function_name,
189                    reason = %reason,
190                    "jit-fallback: function failed JIT compile, running under interpreter",
191                );
192                self.bytecode_executor.execute_program(engine, program)
193            }
194        }
195    }
196}
197
198impl JITExecutor {
199    /// Run the JIT pipeline for `bytecode`.
200    ///
201    /// r5c-2-gz-cp2-jit-div: the nested result separates two error classes
202    /// the W12 fall-through must treat differently:
203    ///
204    /// - Outer `Err` — a JIT-COMPILE-stage failure (compiler init, selective
205    ///   compile, foreign-fn link, or a `RETURN_TAG_NANBOXED` kind-source
206    ///   gap). The interpreter can run the program; `execute_program` falls
207    ///   through with a `[jit-fallback]` diagnostic.
208    /// - `Ok(Err(_))` — the JIT compiled and EXECUTED the program soundly,
209    ///   but the program itself hit a runtime error the bytecode VM also
210    ///   reports (division by zero). This must propagate directly: a
211    ///   fall-through would re-execute the program under the interpreter and
212    ///   double any side effects already performed by the JIT run.
213    /// - `Ok(Ok(_))` — success.
214    fn execute_with_jit(
215        &self,
216        engine: &mut ShapeEngine,
217        bytecode: &shape_vm::bytecode::BytecodeProgram,
218        bytecode_compile_ms: u128,
219        emit_phase_metrics: bool,
220    ) -> Result<Result<shape_runtime::engine::ProgramExecutorResult>> {
221        use crate::JITConfig;
222        use crate::JITContext;
223        use crate::compiler::JITCompiler;
224
225        // R8 W7 G.5 (v0.3 divergence-elimination, ADR-006 §2.7.14 SURFACE):
226        // Refuse to JIT-compile programs whose V2 typed opcodes lack matching
227        // FrameDescriptor entries. The bytecode interpreter handles such
228        // opcodes by reading their kind from the runtime parallel-kind
229        // track per §2.7.7 — the JIT path consumes FrameDescriptors and
230        // previously emitted native code that silently bypassed the runtime
231        // string-key check in `as_string_key`, returning garbage where the
232        // VM cleanly errored (audit
233        // `docs/cluster-audits/v0.3-r8w6-hashmap-key-kind-audit.md` §4 —
234        // `set::from_array([1,2,3])` ec=0 with `{"Integer": -1407...}` vs
235        // VM ec=1 "HashMap key must be a string"). Returning the outer
236        // `Err` triggers the existing `[jit-fallback]` path in
237        // `JITExecutor::execute_program` (line 173): the program runs
238        // under the bytecode interpreter and reports the same surface as
239        // `--mode vm`. Full V2 type soundness for every JIT-emitted opcode
240        // is v0.4 follow-up (per audit §5 Option B; Option A was infeasible
241        // because smoke s2 currently emits the same `Vec.map::*` unverified
242        // shape and depends on the interpreter handling it cleanly).
243        if let Err(errors) = shape_vm::bytecode::verifier::verify_v2_typed_opcodes(bytecode) {
244            let total = errors.len();
245            let first = errors
246                .first()
247                .map(|e| e.to_string())
248                .unwrap_or_else(|| "<none>".to_string());
249            return Err(shape_runtime::error::ShapeError::RuntimeError {
250                message: format!(
251                    "V2 bytecode verification failed: {} violation(s); first: {}. \
252                     R8 W7 G.5 SURFACE (ADR-006 §2.7.14) — JIT refuses unverified \
253                     V2 typed opcodes; falling through to bytecode interpreter so \
254                     the runtime error surface agrees with `--mode vm`. Tracked via \
255                     docs/cluster-audits/v0.3-r8w6-hashmap-key-kind-audit.md (v0.4 \
256                     / planned: full V2 type soundness for every JIT-emitted opcode)",
257                    total, first,
258                ),
259                location: None,
260            });
261        }
262
263        // R8 W8 Cluster A imported-const ident-eval SURFACE
264        // (v0.3 divergence-elimination per supervisor 2026-05-25 path (i),
265        // ADR-006 §2.7.14): Refuse to JIT-compile programs whose bytecode
266        // was emitted via the Cluster A `compile_expr_identifier`
267        // inlined-at-use intercept for imported `pub const` bindings.
268        // The inlined `PushConst(<value>)` bytecode is correct, but the
269        // JIT direct-identifier-eval lowering of this shape fires
270        // `jit_print_*` FFI with zero-init bits — silent-wrong-output
271        // VM=2 / JIT=0 on `print(IMPORTED_CONST)` bare. Whole-program
272        // deopt to the bytecode interpreter is the binding-compliant
273        // surface-and-stop (the interpreter evaluates the inlined
274        // PushConst correctly). Mirrors R8 W7 G.5 V2-verifier deopt
275        // immediately above + R8 W8 aliased-CoW
276        // `mir_has_prior_move_of_slot` precedent.
277        // Root-cause fix in JIT identifier-eval lowering is v0.4 per
278        // `docs/v0.3-close-summary.md` §5.16 JIT-lowering followup
279        // workstream.
280        if bytecode.has_imported_const_inline {
281            return Err(shape_runtime::error::ShapeError::RuntimeError {
282                message: "R8 W8 Cluster A imported-const ident-eval SURFACE \
283                          (ADR-006 §2.7.14): the program uses imported `pub const` \
284                          identifiers whose values were inlined-at-use as \
285                          `PushConst(<value>)` bytecode by `compile_expr_identifier`. \
286                          The JIT direct-identifier-eval lowering of this shape \
287                          produces silent-wrong-output (zero-init bits at the print \
288                          FFI dispatch); whole-program deopting to the bytecode \
289                          interpreter via this `[jit-fallback]` path preserves \
290                          VM == JIT semantics. Tracked via \
291                          `docs/v0.3-close-summary.md` §5.16 (v0.4 / planned: JIT \
292                          identifier-eval lowering root-cause fix)".to_string(),
293                location: None,
294            });
295        }
296
297        // R8 W9 B1 W17-marshal-return JIT surface-and-stop
298        // (v0.3 divergence-elimination per supervisor 2026-05-25 ruling,
299        // ADR-006 §2.7.14): Refuse to JIT-compile programs whose
300        // bytecode contains direct calls to imported stdlib functions
301        // (callee resolved via `resolve_scoped_module_binding_name` at
302        // `compile_expr_function_call` — see
303        // `crates/shape-vm/src/compiler/expressions/function_calls.rs`).
304        // Such calls flow through `op_call_value` whose VM-side
305        // ModuleFn dispatch arm in
306        // `crates/shape-vm/src/executor/call_convention.rs:999` cleanly
307        // routes through `invoke_module_fn_id_stub` +
308        // `project_typed_return` and surfaces the W17-marshal-return-arms
309        // catch-all at
310        // `crates/shape-vm/src/executor/vm_impl/modules.rs:74` when the
311        // stdlib body returns a `ConcreteReturn` arm without a typed-slot
312        // projection (`Bytes` / `ArrayHeapValue` /
313        // `HashMapStringHeapValue` / etc.).
314        //
315        // The JIT-side `jit_call_value` ModuleFn arm at
316        // `crates/shape-jit/src/ffi/control/mod.rs:704-715` instead
317        // returns `TAG_NULL` silently (`-1407374883553280` NaN-box null
318        // pattern) with only a `tracing::debug!` line — swallowing the
319        // surface and producing silent-wrong-output VM=ec1 SURFACE /
320        // JIT=ec0 garbage on `print(serialize([1.0,2.0,3.0]).len())`.
321        //
322        // Whole-program deopt to the bytecode interpreter is the
323        // binding-compliant surface-and-stop (mirrors R8 W7 G.5
324        // V2-verifier deopt + R8 W8 imported-const-inline deopt
325        // immediately above + R8 W8 aliased-CoW
326        // `mir_has_prior_move_of_slot` precedent). Root-cause fix in
327        // JIT ModuleFn dispatch (`dispatch_module_fn_call` `todo!()` +
328        // §2.7.10/Q11 kinded handler ABI rebuild) is v0.4 per
329        // `docs/v0.3-close-summary.md` §5.16 JIT-lowering followup
330        // workstream — third member of the bundle alongside Cluster A
331        // imported-const-inline + aliased-CoW.
332        if bytecode.has_w17_marshal_residual {
333            return Err(shape_runtime::error::ShapeError::RuntimeError {
334                message: "R8 W9 B1 W17-marshal-return-arms SURFACE (ADR-006 \
335                          §2.7.14): the program contains direct calls to imported \
336                          stdlib functions (callee resolved via \
337                          `resolve_scoped_module_binding_name`). The JIT-side \
338                          `jit_call_value` ModuleFn dispatch arm at \
339                          `ffi/control/mod.rs:704-715` returns TAG_NULL silently, \
340                          swallowing the W17-marshal-return-arms surface that \
341                          VM-side `invoke_module_fn_id_stub` + \
342                          `project_typed_return` would clean-surface on (e.g. \
343                          `state.serialize` returning Array<int>/Bytes hits the \
344                          catch-all at `vm_impl/modules.rs:74`). Whole-program \
345                          deopting to the bytecode interpreter via this \
346                          `[jit-fallback]` path preserves VM == JIT semantics. \
347                          Tracked via `docs/v0.3-close-summary.md` §5.16 (v0.4 / \
348                          planned: JIT ModuleFn dispatch root-cause fix at \
349                          `dispatch_module_fn_call` todo!() + §2.7.10/Q11 kinded \
350                          handler ABI rebuild)".to_string(),
351                location: None,
352            });
353        }
354
355        // R8 W9 B3 Drop-bearing-scope-exit SURFACE (v0.3 divergence-elimination
356        // per supervisor 2026-05-25 G.2 Step 2 ruling, ADR-006 §2.7.14):
357        // Refuse to JIT-compile programs that register a user `impl Drop for T`
358        // impl. The JIT `emit_drop` codegen (`mir_compiler/ownership.rs`)
359        // currently only releases the slot's refcount and nulls the slot —
360        // there is NO user-Drop trait-method dispatch on the JIT path, so
361        // `drop_locals_at_scope_exit` silently elides the user's `Drop::drop`
362        // body, breaking the resource-management.mdx documented RAII
363        // contract (VM prints the user's drop output; JIT prints nothing).
364        // Whole-program deopt to the bytecode interpreter is the binding-
365        // compliant surface-and-stop: the interpreter has a working Drop
366        // dispatch at `executor/trait_object_ops.rs::op_drop_call_impl`
367        // (per audit `docs/cluster-audits/v0.3-r8w9-drop-runtime-audit.md`
368        // §4 — `op_drop_call_impl` looks up `Drop::TypeName::__default__::drop`
369        // in `trait_method_symbols` and dispatches via
370        // `call_function_with_nb_args`).
371        //
372        // Detection: presence of any `Drop::*::*::drop` entry in
373        // `trait_method_symbols` (registered at compile time per
374        // `compiler/statements.rs::register_trait_method_symbol` for every
375        // `impl Drop for T` block — see drop_type_info handling at
376        // `compiler/statements.rs:418`).
377        //
378        // Root-cause fix in JIT Drop codegen (Drop-trait-method dispatch
379        // at `emit_drop` time) is v0.4 per `docs/v0.3-close-summary.md`
380        // §5.16 JIT-lowering followup workstream (joining the
381        // aliased-CoW + imported-const + W17-marshal bundle).
382        let has_user_drop_impl = bytecode
383            .trait_method_symbols
384            .keys()
385            .any(|k| k.starts_with("Drop::"));
386        if has_user_drop_impl {
387            return Err(shape_runtime::error::ShapeError::RuntimeError {
388                message: "R8 W9 B3 Drop-bearing-scope-exit SURFACE \
389                          (ADR-006 §2.7.14): the program registers one or more \
390                          `impl Drop for T` impls. The JIT `emit_drop` codegen \
391                          (mir_compiler/ownership.rs::emit_drop) lacks user-Drop \
392                          trait-method dispatch — it only releases refcounts + \
393                          nulls slots, silently eliding the user's `Drop::drop` \
394                          body and breaking the resource-management.mdx \
395                          documented RAII contract. Whole-program deopting to \
396                          the bytecode interpreter via this `[jit-fallback]` \
397                          path preserves VM == JIT semantics (the interpreter \
398                          dispatches Drop methods through \
399                          `op_drop_call_impl` at trait_object_ops.rs:687). \
400                          Tracked via `docs/v0.3-close-summary.md` §5.16 \
401                          (v0.4 / planned: JIT Drop codegen root-cause fix)"
402                    .to_string(),
403                location: None,
404            });
405        }
406
407        // JIT compile the bytecode
408        let jit_config = JITConfig::default();
409        let mut jit = JITCompiler::new(jit_config).map_err(|e| {
410            shape_runtime::error::ShapeError::RuntimeError {
411                message: format!("JIT compiler initialization failed: {}", e),
412                location: None,
413            }
414        })?;
415
416        // Use selective compilation: JIT-compatible functions get native code,
417        // incompatible ones get Interpreted entries for VM fallback.
418        //
419        // Cluster-2 closure-wave-F tracing-crate migration (2026-05-16):
420        // `tracing::enabled!` collapses to `false` under feature-OFF builds
421        // so the per-instruction enumeration loop is dead-code-eliminated.
422        // Replaces SHAPE_JIT_DEBUG env-var gating.
423        if tracing::enabled!(target: "shape_jit", tracing::Level::DEBUG) {
424            tracing::debug!(
425                target: "shape_jit",
426                instruction_count = bytecode.instructions.len(),
427                function_count = bytecode.functions.len(),
428                "starting compile_program_selective",
429            );
430            for (i, instr) in bytecode.instructions.iter().enumerate() {
431                tracing::debug!(
432                    target: "shape_jit",
433                    idx = i,
434                    opcode = ?instr.opcode,
435                    operand = ?instr.operand,
436                    "instruction",
437                );
438            }
439        }
440        let jit_compile_start = Instant::now();
441        let compile_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
442            jit.compile_program_selective("main", bytecode)
443        }));
444        let jit_compile_ms = jit_compile_start.elapsed().as_millis();
445        let (jit_fn, _mixed_table) = match compile_result {
446            Ok(Ok(result)) => result,
447            Ok(Err(e)) => {
448                return Err(shape_runtime::error::ShapeError::RuntimeError {
449                    message: format!("JIT compilation failed: {}", e),
450                    location: None,
451                });
452            }
453            Err(panic_info) => {
454                let msg = if let Some(s) = panic_info.downcast_ref::<String>() {
455                    s.clone()
456                } else if let Some(s) = panic_info.downcast_ref::<&str>() {
457                    s.to_string()
458                } else {
459                    "unknown panic".to_string()
460                };
461                return Err(shape_runtime::error::ShapeError::RuntimeError {
462                    message: format!("JIT compilation panicked: {}", msg),
463                    location: None,
464                });
465            }
466        };
467
468        let foreign_bridge = {
469            let runtime = engine.get_runtime_mut();
470            crate::foreign_bridge::link_foreign_functions_for_jit(
471                bytecode,
472                runtime.persistent_context(),
473            )
474            .map_err(|e| shape_runtime::error::ShapeError::RuntimeError {
475                message: format!("JIT foreign-function linking failed: {}", e),
476                location: None,
477            })?
478        };
479
480        // Create JIT context and execute
481        let mut jit_ctx = JITContext::default();
482        if let Some(state) = foreign_bridge.as_ref() {
483            jit_ctx.foreign_bridge_ptr = state.as_ref() as *const _ as *const std::ffi::c_void;
484        }
485
486        // Set exec_context_ptr so JIT FFI can access cached data
487        {
488            let runtime = engine.get_runtime_mut();
489            if let Some(ctx) = runtime.persistent_context_mut() {
490                jit_ctx.exec_context_ptr = ctx as *mut _ as *mut std::ffi::c_void;
491            }
492        }
493
494        // Link the JIT function table into the context. jit_call_value uses
495        // this table to resolve callees — unlinked, every closure / function
496        // value dispatch BAILs at the "fn_id out of bounds" check.
497        //
498        // SAFETY: `jit.get_function_table()` borrows from the JITCompiler
499        // which lives for the duration of this block. The ctx does not
500        // outlive `jit` — we execute below and drop `jit` at end of scope.
501        {
502            let table: &[*const u8] = jit.get_function_table();
503            jit_ctx.function_table = table.as_ptr() as *const crate::context::JittedStrategyFn;
504            jit_ctx.function_table_len = table.len();
505        }
506
507        // ADR-006 §2.7.10 / Q11 (Phase 3 cluster-0 Round 20 sub-cluster γ —
508        // W12-jit-trait-impl-method-registry, 2026-05-14): link the JIT
509        // function-name table into the context so `jit_call_method`'s
510        // user-method UFCS dispatch (`try_call_user_method` →
511        // `find_function_by_name("TypeName::method")`) can resolve user-
512        // defined trait/impl methods at runtime.
513        //
514        // The R15 W17-narrow sub-cluster fixed the upstream classification
515        // (`receiver_type_name`) to correctly return the schema's type
516        // name for `Ptr(HeapKind::TypedObject)` receivers. Without the
517        // function-name table linkage here, every UFCS lookup at
518        // `find_function_by_name` returned None (the early-return guard at
519        // `call_method/mod.rs:230` triggered because `function_names_ptr`
520        // was always the `JITContext::default()` null sentinel). That
521        // returned TAG_NULL from `try_call_user_method`, which surfaced as
522        // `None` at the print path (post-R19 C β filter; pre-β SIGSEGV).
523        //
524        // Cluster-0 close criterion for Smoke 3: `t.name()` on
525        // `let t = X{}` returns `"x"` under `--mode jit` matching VM.
526        //
527        // The names slice is built from `bytecode.functions` 1:1 by index
528        // so `function_table[idx]` and `function_names[idx]` describe the
529        // same function — the same invariant `compile_program_selective`
530        // upholds for the function-table itself (see
531        // `compiler/program.rs:800-823`). The `Vec<String>` lives in the
532        // local `function_names_storage` and is dropped after `jit_fn`
533        // executes — same lifetime discipline as the function-table
534        // borrow above and the trampoline VM below.
535        let function_names_storage: Vec<String> =
536            bytecode.functions.iter().map(|f| f.name.clone()).collect();
537        jit_ctx.function_names_ptr = function_names_storage.as_ptr();
538        jit_ctx.function_names_len = function_names_storage.len();
539
540        // Set up the trampoline VM that JIT's `jit_call_value` falls back
541        // to when a callee's function_table slot is null (i.e. the
542        // function was not JIT-compiled, typically because its MIR
543        // lowering bailed). Without this, `dispatch_call_via_trampoline_vm`
544        // short-circuits to TAG_NULL, losing the callee's real result.
545        //
546        // The VM is populated with the **unlinked** bytecode (the exact
547        // same input the JIT compiled from) so function_id lookups agree
548        // between JIT and interpreter. Going through `load_program` with
549        // a `content_addressed` field set would route through the linker,
550        // which topologically sorts function blobs and renumbers them —
551        // breaking JIT↔interpreter function-ID parity. Clear the
552        // content-addressed payload first so `load_program` takes the
553        // direct path.
554        //
555        // The trampoline VM lives for the duration of `jit_fn` execution
556        // and is unset afterwards so a stale pointer does not leak across
557        // threads / subsequent executions.
558        let mut trampoline_bytecode = bytecode.clone();
559        trampoline_bytecode.content_addressed = None;
560        let mut trampoline_vm = shape_vm::VirtualMachine::new(shape_vm::VMConfig::default());
561        trampoline_vm.load_program(trampoline_bytecode);
562        unsafe {
563            crate::ffi::control::set_trampoline_vm(
564                &mut trampoline_vm as *mut shape_vm::VirtualMachine,
565            );
566        }
567
568        // Drop guard: even if `jit_fn` panics, the thread-local
569        // TRAMPOLINE_VM must not keep pointing at a VM that is about to
570        // be freed when the stack unwinds.
571        struct TrampolineGuard;
572        impl Drop for TrampolineGuard {
573            fn drop(&mut self) {
574                crate::ffi::control::unset_trampoline_vm();
575            }
576        }
577        let _trampoline_guard = TrampolineGuard;
578
579        // Execute the JIT-compiled function
580        tracing::debug!(
581            target: "shape_jit",
582            "compilation OK, about to execute",
583        );
584        // W11-jit-new-array (supervisor reopen Step 4): snapshot arc
585        // retain/release counters before/after the JIT-emitted code runs
586        // so the supervisor can verify refcount balance — silent leaks
587        // here are the W-series defection-attractor shape we're refusing.
588        //
589        // Cluster-2 closure-wave-F tracing-crate migration (2026-05-16):
590        // gate the snapshot reads on `tracing::enabled!` so the atomic
591        // loads themselves are dead-code-eliminated under feature-OFF
592        // builds (`release_max_level_off` collapses the macro to `false`).
593        // Replaces SHAPE_JIT_ARC_COUNTERS env-var; CLI selector is
594        // `--trace-jit=shape_jit::arc_counters=info`.
595        //
596        // Cluster-2 closure-wave-E §F string-constant leak measurement
597        // (2026-05-16): STRING_* counters share the same arc_counters
598        // gate (Arc<UnifiedValue> + Arc<String> are both Arc-tier;
599        // single tracing target keeps CLI filter narrow). Take-both
600        // ceremony at Round 1 merge.
601        let arc_counters_enabled = tracing::enabled!(
602            target: "shape_jit::arc_counters",
603            tracing::Level::INFO,
604        );
605        let (retain_before, release_before, frees_before,
606             str_allocs_before, str_retain_before,
607             str_release_before, str_frees_before) = if arc_counters_enabled {
608            (
609                crate::ffi::arc::JIT_ARC_RETAIN_CALLS.load(std::sync::atomic::Ordering::Relaxed),
610                crate::ffi::arc::JIT_ARC_RELEASE_CALLS.load(std::sync::atomic::Ordering::Relaxed),
611                crate::ffi::arc::JIT_ARC_RELEASE_FREES.load(std::sync::atomic::Ordering::Relaxed),
612                crate::ffi::arc::STRING_CONSTANT_ALLOCS.load(std::sync::atomic::Ordering::Relaxed),
613                crate::ffi::arc::STRING_RETAIN_CALLS.load(std::sync::atomic::Ordering::Relaxed),
614                crate::ffi::arc::STRING_RELEASE_CALLS.load(std::sync::atomic::Ordering::Relaxed),
615                crate::ffi::arc::STRING_RELEASE_FREES.load(std::sync::atomic::Ordering::Relaxed),
616            )
617        } else {
618            (0, 0, 0, 0, 0, 0, 0)
619        };
620
621        let jit_exec_start = Instant::now();
622        let signal = unsafe { jit_fn(&mut jit_ctx) };
623        let jit_exec_ms = jit_exec_start.elapsed().as_millis();
624
625        if arc_counters_enabled {
626            let retain_after =
627                crate::ffi::arc::JIT_ARC_RETAIN_CALLS.load(std::sync::atomic::Ordering::Relaxed);
628            let release_after =
629                crate::ffi::arc::JIT_ARC_RELEASE_CALLS.load(std::sync::atomic::Ordering::Relaxed);
630            let frees_after =
631                crate::ffi::arc::JIT_ARC_RELEASE_FREES.load(std::sync::atomic::Ordering::Relaxed);
632            let str_allocs_after =
633                crate::ffi::arc::STRING_CONSTANT_ALLOCS
634                    .load(std::sync::atomic::Ordering::Relaxed);
635            let str_retain_after =
636                crate::ffi::arc::STRING_RETAIN_CALLS
637                    .load(std::sync::atomic::Ordering::Relaxed);
638            let str_release_after =
639                crate::ffi::arc::STRING_RELEASE_CALLS
640                    .load(std::sync::atomic::Ordering::Relaxed);
641            let str_frees_after =
642                crate::ffi::arc::STRING_RELEASE_FREES
643                    .load(std::sync::atomic::Ordering::Relaxed);
644            tracing::info!(
645                target: "shape_jit::arc_counters",
646                retain_calls = retain_after - retain_before,
647                release_calls = release_after - release_before,
648                release_frees = frees_after - frees_before,
649                "shape-jit-arc counter delta",
650            );
651            // cluster-2-cw-E §F measurement output: per-call-site
652            // §2.7.5 String carrier metrics. Leak quantification
653            // shape is `str_allocs - str_frees` = number of
654            // permanently-leaked Arc<String> allocations for this
655            // execution. The "_cum" event is process-wide running
656            // total — surfaces compile-time allocations that happen
657            // before any jit_fn invocation (the dominant source for
658            // `MirConstant::Str` materialization). Migrated to
659            // tracing::info! per cw-F mechanism at Round 1 merge
660            // take-both ceremony (2026-05-16).
661            tracing::info!(
662                target: "shape_jit::arc_counters",
663                str_allocs = str_allocs_after - str_allocs_before,
664                str_retain = str_retain_after - str_retain_before,
665                str_release = str_release_after - str_release_before,
666                str_frees = str_frees_after - str_frees_before,
667                leaked = (str_allocs_after - str_allocs_before)
668                    .saturating_sub(str_frees_after - str_frees_before),
669                "shape-jit-arc-str counter delta",
670            );
671            tracing::info!(
672                target: "shape_jit::arc_counters",
673                str_allocs_total = str_allocs_after,
674                str_retain_total = str_retain_after,
675                str_release_total = str_release_after,
676                str_frees_total = str_frees_after,
677                leaked_total = str_allocs_after.saturating_sub(str_frees_after),
678                "shape-jit-arc-str cumulative",
679            );
680        }
681
682        // Get result from JIT context stack via TypedScalar boundary
683        let raw_result = if jit_ctx.stack_ptr > 0 {
684            jit_ctx.stack[0]
685        } else {
686            crate::ffi::value_ffi::TAG_NULL
687        };
688
689        // Check for errors
690        if signal < 0 {
691            // Recoverable Shape-level runtime errors the bytecode VM handles
692            // cleanly are carved out of the negative-signal space. The JIT
693            // executed soundly up to the error point, so they are returned as
694            // `Ok(Err(_))` and propagated directly by `execute_program` — NOT
695            // an outer `Err`, which would fall through to an interpreter
696            // re-run and double any prior side effects (the double-execution
697            // bug r5c-2-gz-cp2-jit-div found). An unknown negative signal is a
698            // JIT-pipeline failure (outer `Err` -> interpreter fall-through,
699            // preserving W12 behavior).
700            match signal {
701                crate::context::JIT_SIGNAL_DIVISION_BY_ZERO => {
702                    // r5c-2-gz-cp2-jit-div: JIT codegen emits a guarded branch
703                    // returning this signal instead of a `ud2`/`sdiv` trap.
704                    return Ok(Err(shape_runtime::error::ShapeError::RuntimeError {
705                        message: "Division by zero".to_string(),
706                        location: None,
707                    }));
708                }
709                crate::context::JIT_SIGNAL_INDEX_OUT_OF_BOUNDS => {
710                    // WS-3 F1: JIT typed-array codegen emits a guarded branch
711                    // returning this signal on an out-of-bounds element
712                    // access instead of silently fabricating the element-type
713                    // zero (read) / skipping the store (write). Maps to the
714                    // same `Index out of bounds` diagnostic the bytecode VM
715                    // emits for `VMError::IndexOutOfBounds`, so `--mode jit`
716                    // reports the SAME error as `--mode vm`.
717                    return Ok(Err(shape_runtime::error::ShapeError::RuntimeError {
718                        message: "Index out of bounds".to_string(),
719                        location: None,
720                    }));
721                }
722                crate::context::SIGNAL_TRAMPOLINE_ERROR => {
723                    // r5c-2-bz-b-jit-err-surface: a VM-trampoline FFI call
724                    // (`jit_call_method`) surfaced a clean `Err` (e.g.
725                    // `Set.add()` with a non-string key) and the JIT frame was
726                    // abandoned before the placeholder result could reach a
727                    // heap-kinded refcount-retain site. The VM-side message
728                    // was stored in the `JIT_RUNTIME_ERROR` thread-local —
729                    // surface it verbatim so `--mode jit` reports the SAME
730                    // error the interpreter would.
731                    let message =
732                        match crate::ffi::control::take_jit_runtime_error() {
733                            Some(vm_err) => vm_err,
734                            None => format!("JIT execution error (code: {})", signal),
735                        };
736                    return Ok(Err(shape_runtime::error::ShapeError::RuntimeError {
737                        message,
738                        location: None,
739                    }));
740                }
741                _ => {
742                    return Err(shape_runtime::error::ShapeError::RuntimeError {
743                        message: format!("JIT execution error (code: {})", signal),
744                        location: None,
745                    });
746                }
747            }
748        }
749        // Clear any stale trampoline error on the success path so it cannot
750        // leak into a later, unrelated JIT execution on the same thread.
751        let _ = crate::ffi::control::take_jit_runtime_error();
752
753        // v2: check return_type_tag for native-typed return values.
754        // Non-zero tags bypass NaN-box decoding entirely.
755        let wire_value = match jit_ctx.return_type_tag {
756            crate::context::RETURN_TAG_F64 => {
757                WireValue::Number(f64::from_bits(raw_result))
758            }
759            crate::context::RETURN_TAG_I64 => {
760                WireValue::Integer(raw_result as i64)
761            }
762            crate::context::RETURN_TAG_I32 => {
763                WireValue::Integer((raw_result as i32) as i64)
764            }
765            crate::context::RETURN_TAG_BOOL => {
766                WireValue::Bool(raw_result != 0)
767            }
768            crate::context::RETURN_TAG_UNIT => {
769                // W11-jit-new-array: `()`-typed return — the program's
770                // terminal expression produced no value. Map to Null
771                // (matches the VM's `wire_value` for `print(x)` at the
772                // top level).
773                WireValue::Null
774            }
775            _ => {
776                // tag=0 (RETURN_TAG_NANBOXED) or unknown: per ADR-006
777                // §2.7.5 / §2.7.5.1, the JIT-FFI return path must be
778                // kind-stamped at compile time from the call signature
779                // (`FrameDescriptor::return_kind: Option<NativeKind>`).
780                // The pre-strict-typing fallback decoded `tag_bits` from
781                // `raw_result` to recover a kind at runtime — that path
782                // is the W-series defection-attractor (deleted-runtime
783                // tag-bit dispatch + kind-blind classifier) and is
784                // forbidden per CLAUDE.md "Forbidden Patterns".
785                //
786                // The correct §2.7.5 surface stamps `return_kind` from
787                // the JIT-emitted call signature so the typed return
788                // path (RETURN_TAG_F64 / I64 / I32 / BOOL) handles every
789                // case statically. A `RETURN_TAG_NANBOXED` arrival here
790                // is a kind-source gap — surface-and-stop per W10
791                // jit-playbook §5.
792                //
793                // PHASE_2C / SURFACE: stamp `return_type_tag` to a
794                // typed variant from the FrameDescriptor at JIT-emit
795                // time (rvalue path — W10-mir-compiler territory) so
796                // this arm is unreachable in production bytecode.
797                let return_hint = bytecode
798                    .top_level_frame
799                    .as_ref()
800                    .and_then(|fd| fd.return_kind.or_else(|| fd.slots.last().copied()));
801                let _ = return_hint;
802                return Err(shape_runtime::error::ShapeError::RuntimeError {
803                    message: format!(
804                        "JIT-FFI return path: RETURN_TAG_NANBOXED reached the \
805                         host boundary without a stamped NativeKind (raw_bits={:#x}). \
806                         Per ADR-006 §2.7.5 / §2.7.5.1 the return tag must be a \
807                         typed variant; this is a kind-source gap (W10 jit-playbook \
808                         §5 surface-and-stop). See executor.rs:267 comment.",
809                        raw_result
810                    ),
811                    location: None,
812                });
813            }
814        };
815
816        if emit_phase_metrics {
817            let total_ms = bytecode_compile_ms + jit_compile_ms + jit_exec_ms;
818            tracing::info!(
819                target: "shape_jit::metrics",
820                bytecode_compile_ms = bytecode_compile_ms,
821                jit_compile_ms = jit_compile_ms,
822                jit_exec_ms = jit_exec_ms,
823                total_ms = total_ms,
824                "shape-jit-phases timing",
825            );
826        }
827
828        // r5c-2-gz-cp2-jit-div: `Ok(Ok(_))` — JIT compiled and executed
829        // successfully (see `execute_with_jit` nested-result contract).
830        Ok(Ok(shape_runtime::engine::ProgramExecutorResult {
831            wire_value,
832            type_info: None,
833            execution_type: ExecutionType::Script,
834            content_json: None,
835            content_html: None,
836            content_terminal: None,
837        }))
838    }
839
840    // typed_scalar_to_wire and value_word_to_wire removed — both were
841    // kind-blind dispatch paths. The former dispatched on
842    // `ScalarKind::None` to `value_word_to_wire`; the latter decoded
843    // `tag_bits` from a raw u64 to recover a kind. Per ADR-006 §2.7.5
844    // / §2.7.5.1 the JIT-FFI return path stamps a typed `RETURN_TAG_*`
845    // from the JIT-emitted call signature, so the kind-blind fallback
846    // is unreachable in production bytecode (and the surface-and-stop
847    // path on the `_ =>` arm of the `return_type_tag` match documents
848    // any kind-source gap that does land here).
849    //
850    // CLAUDE.md "Forbidden Patterns" forbids `tag_bits` decode in JIT
851    // codegen; the W-series defection-attractor list forbids the
852    // "decode/tag/dispatch helper/bridge/probe" framing these helpers
853    // would need to come back under.
854}