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}