shape_jit/compiler/program.rs
1//! Program compilation with multiple functions
2
3use cranelift::codegen::ir::FuncRef;
4use cranelift::prelude::*;
5use cranelift_module::{Linkage, Module};
6use std::collections::{BTreeMap, HashMap};
7
8use super::setup::JITCompiler;
9use crate::context::{JittedFn, JittedStrategyFn};
10use crate::mixed_table::{FunctionEntry, MixedFunctionTable};
11use crate::numeric_compiler::compile_numeric_program;
12use shape_vm::bytecode::{BytecodeProgram, OpCode};
13
14#[derive(Default)]
15struct NumericOpcodeStats {
16 typed: usize,
17 generic: usize,
18 typed_breakdown: BTreeMap<String, usize>,
19 generic_breakdown: BTreeMap<String, usize>,
20}
21
22fn bump_breakdown(map: &mut BTreeMap<String, usize>, opcode: OpCode) {
23 let key = format!("{:?}", opcode);
24 *map.entry(key).or_insert(0) += 1;
25}
26
27fn collect_numeric_opcode_stats(program: &BytecodeProgram) -> NumericOpcodeStats {
28 let mut stats = NumericOpcodeStats::default();
29 for instr in &program.instructions {
30 match instr.opcode {
31 // Typed arithmetic opcodes
32 OpCode::AddInt
33 | OpCode::SubInt
34 | OpCode::MulInt
35 | OpCode::DivInt
36 | OpCode::ModInt
37 | OpCode::PowInt
38 | OpCode::AddNumber
39 | OpCode::SubNumber
40 | OpCode::MulNumber
41 | OpCode::DivNumber
42 | OpCode::ModNumber
43 | OpCode::PowNumber
44 // Typed comparisons
45 | OpCode::GtInt
46 | OpCode::LtInt
47 | OpCode::GteInt
48 | OpCode::LteInt
49 | OpCode::GtNumber
50 | OpCode::LtNumber
51 | OpCode::GteNumber
52 | OpCode::LteNumber
53 | OpCode::EqInt
54 | OpCode::EqNumber
55 | OpCode::NeqInt
56 | OpCode::NeqNumber
57 | OpCode::EqString
58 | OpCode::GtString
59 | OpCode::LtString
60 | OpCode::GteString
61 | OpCode::LteString
62 | OpCode::EqDecimal
63 | OpCode::IsNull
64 | OpCode::NegInt
65 | OpCode::NegNumber => {
66 stats.typed += 1;
67 bump_breakdown(&mut stats.typed_breakdown, instr.opcode);
68 }
69 // Generic arithmetic/comparison opcodes (DELETED in Phase 2 — left
70 // here as a no-op arm for future-proofing if a generic class is
71 // re-introduced).
72 _ => {}
73 }
74 }
75 stats
76}
77
78fn maybe_emit_numeric_metrics(program: &BytecodeProgram) {
79 // Cluster-2 closure-wave-F tracing-crate migration (2026-05-16):
80 // `tracing::enabled!` collapses to `false` under feature-OFF builds
81 // (`release_max_level_off`), so the early return executes and the
82 // stat-collection work is skipped exactly as before. Replaces the
83 // legacy `SHAPE_JIT_METRICS` / `SHAPE_JIT_METRICS_DETAIL` env-var
84 // gating; CLI selector is `--trace-jit=shape_jit::metrics=info` (the
85 // `_DETAIL` suffix maps to trace level on the same target).
86 if !tracing::enabled!(target: "shape_jit::metrics", tracing::Level::INFO) {
87 return;
88 }
89 let static_stats = collect_numeric_opcode_stats(program);
90 let static_total = static_stats.typed + static_stats.generic;
91 let static_coverage_pct = if static_total == 0 {
92 100.0
93 } else {
94 (static_stats.typed as f64 * 100.0) / (static_total as f64)
95 };
96 // Report effective coverage conservatively: generic opcodes remain generic
97 // unless the frontend/runtime has concretely emitted typed variants.
98 let effective_typed = static_stats.typed;
99 let effective_generic = static_stats.generic;
100 let effective_coverage_pct = static_coverage_pct;
101 tracing::info!(
102 target: "shape_jit::metrics",
103 typed_numeric_ops = effective_typed,
104 generic_numeric_ops = effective_generic,
105 typed_numeric_coverage_pct = effective_coverage_pct,
106 static_typed_numeric_ops = static_stats.typed,
107 static_generic_numeric_ops = static_stats.generic,
108 static_typed_numeric_coverage_pct = static_coverage_pct,
109 "shape-jit-metrics numeric coverage",
110 );
111 if tracing::enabled!(target: "shape_jit::metrics", tracing::Level::TRACE) {
112 let fmt_breakdown = |breakdown: &BTreeMap<String, usize>| -> String {
113 breakdown
114 .iter()
115 .map(|(name, count)| format!("{}:{}", name, count))
116 .collect::<Vec<_>>()
117 .join(",")
118 };
119 tracing::trace!(
120 target: "shape_jit::metrics",
121 typed_breakdown = %fmt_breakdown(&static_stats.typed_breakdown),
122 generic_breakdown = %fmt_breakdown(&static_stats.generic_breakdown),
123 "shape-jit-metrics-detail breakdown",
124 );
125 }
126}
127
128impl JITCompiler {
129 #[inline(always)]
130 pub fn compile(&mut self, name: &str, program: &BytecodeProgram) -> Result<JittedFn, String> {
131 let mut sig = self.module.make_signature();
132 sig.params.push(AbiParam::new(types::I64));
133 sig.params.push(AbiParam::new(types::I64));
134 sig.params.push(AbiParam::new(types::I64));
135 sig.returns.push(AbiParam::new(types::F64));
136
137 let func_id = self
138 .module
139 .declare_function(name, Linkage::Export, &sig)
140 .map_err(|e| format!("Failed to declare function: {}", e))?;
141
142 let mut ctx = self.module.make_context();
143 ctx.func.signature = sig;
144
145 {
146 let mut builder = FunctionBuilder::new(&mut ctx.func, &mut self.builder_context);
147 let entry_block = builder.create_block();
148 builder.append_block_params_for_function_params(entry_block);
149 builder.switch_to_block(entry_block);
150 builder.seal_block(entry_block);
151
152 let stack_ptr = builder.block_params(entry_block)[0];
153 let constants_ptr = builder.block_params(entry_block)[1];
154
155 let result = compile_numeric_program(&mut builder, program, stack_ptr, constants_ptr)?;
156
157 builder.ins().return_(&[result]);
158 builder.finalize();
159 }
160
161 self.module
162 .define_function(func_id, &mut ctx)
163 .map_err(|e| format!("Failed to define function: {}", e))?;
164
165 self.module.clear_context(&mut ctx);
166 self.module
167 .finalize_definitions()
168 .map_err(|e| format!("Failed to finalize: {}", e))?;
169
170 let code_ptr = self.module.get_finalized_function(func_id);
171 self.compiled_functions.insert(name.to_string(), code_ptr);
172
173 Ok(unsafe { std::mem::transmute(code_ptr) })
174 }
175
176 #[inline(always)]
177 pub fn compile_program(
178 &mut self,
179 name: &str,
180 program: &BytecodeProgram,
181 ) -> Result<JittedStrategyFn, String> {
182 maybe_emit_numeric_metrics(program);
183
184 let mut user_func_arities: HashMap<u16, u16> = HashMap::new();
185 let mut user_func_ids: HashMap<u16, cranelift_module::FuncId> = HashMap::new();
186
187 for (idx, func) in program.functions.iter().enumerate() {
188 let func_name = format!("{}_{}", name, func.name.replace("::", "__"));
189 let mut user_sig = self.module.make_signature();
190 user_sig.params.push(AbiParam::new(types::I64)); // ctx_ptr
191 for _ in 0..func.arity {
192 user_sig.params.push(AbiParam::new(types::I64));
193 }
194 user_sig.returns.push(AbiParam::new(types::I32));
195 let func_id = self
196 .module
197 .declare_function(&func_name, Linkage::Local, &user_sig)
198 .map_err(|e| format!("Failed to pre-declare function {}: {}", func.name, e))?;
199 user_func_ids.insert(idx as u16, func_id);
200 user_func_arities.insert(idx as u16, func.arity);
201 }
202
203 let main_func_id = self.compile_strategy_with_user_funcs(
204 name,
205 program,
206 &user_func_ids,
207 &user_func_arities,
208 )?;
209
210 for (idx, func) in program.functions.iter().enumerate() {
211 let func_name = format!("{}_{}", name, func.name.replace("::", "__"));
212 self.compile_function_with_user_funcs(
213 &func_name,
214 program,
215 idx,
216 &user_func_ids,
217 &user_func_arities,
218 )?;
219 }
220
221 self.module
222 .finalize_definitions()
223 .map_err(|e| format!("Failed to finalize definitions: {:?}", e))?;
224
225 let main_code_ptr = self.module.get_finalized_function(main_func_id);
226 self.compiled_functions
227 .insert(name.to_string(), main_code_ptr);
228
229 self.function_table.clear();
230 for (idx, func) in program.functions.iter().enumerate() {
231 let func_name = format!("{}_{}", name, func.name.replace("::", "__"));
232 if let Some(&func_id) = user_func_ids.get(&(idx as u16)) {
233 let ptr = self.module.get_finalized_function(func_id);
234 while self.function_table.len() <= idx {
235 self.function_table.push(std::ptr::null());
236 }
237 self.function_table[idx] = ptr;
238 self.compiled_functions.insert(func_name, ptr);
239 }
240 }
241
242 Ok(unsafe { std::mem::transmute(main_code_ptr) })
243 }
244
245 fn compile_function_with_user_funcs(
246 &mut self,
247 name: &str,
248 program: &BytecodeProgram,
249 func_idx: usize,
250 user_func_ids: &HashMap<u16, cranelift_module::FuncId>,
251 user_func_arities: &HashMap<u16, u16>,
252 ) -> Result<(), String> {
253 let func = &program.functions[func_idx];
254 let func_id = *user_func_ids
255 .get(&(func_idx as u16))
256 .ok_or_else(|| format!("Function {} not pre-declared", name))?;
257
258 let mut sig = self.module.make_signature();
259 sig.params.push(AbiParam::new(types::I64)); // ctx_ptr
260 // Closures receive captures as leading native args, followed by user params.
261 let effective_arity = func.captures_count + func.arity;
262 for _ in 0..effective_arity {
263 sig.params.push(AbiParam::new(types::I64));
264 }
265 sig.returns.push(AbiParam::new(types::I32));
266
267 let mut ctx = self.module.make_context();
268 ctx.func.signature = sig;
269
270 let mut func_builder_ctx = FunctionBuilderContext::new();
271 {
272 let mut builder = FunctionBuilder::new(&mut ctx.func, &mut func_builder_ctx);
273 let entry_block = builder.create_block();
274 builder.append_block_params_for_function_params(entry_block);
275 builder.switch_to_block(entry_block);
276 builder.seal_block(entry_block);
277
278 let ctx_ptr = builder.block_params(entry_block)[0];
279 let mut user_func_refs: HashMap<u16, FuncRef> = HashMap::new();
280 for (&fn_idx, &fn_id) in user_func_ids {
281 let func_ref = self.module.declare_func_in_func(fn_id, builder.func);
282 user_func_refs.insert(fn_idx, func_ref);
283 }
284
285 let ffi = self.build_ffi_refs(&mut builder)?;
286
287 let func_end = func.entry_point + func.body_length;
288 let sub_instructions = &program.instructions[func.entry_point..func_end];
289 let sub_program = BytecodeProgram {
290 instructions: sub_instructions.to_vec(),
291 constants: program.constants.clone(),
292 strings: program.strings.clone(),
293 // Use empty functions list: the sub_program only contains ONE function's
294 // body, so the original entry points are meaningless in the rebased index
295 // space. This prevents analyze_inline_candidates from using wrong instruction
296 // ranges. Direct calls between functions use user_func_refs instead.
297 functions: Vec::new(),
298 debug_info: Default::default(),
299 data_schema: program.data_schema.clone(),
300 module_binding_names: program.module_binding_names.clone(),
301 top_level_locals_count: program.top_level_locals_count,
302 top_level_local_storage_hints: program
303 .function_local_storage_hints
304 .get(func_idx)
305 .cloned()
306 .unwrap_or_default(),
307 type_schema_registry: program.type_schema_registry.clone(),
308 module_binding_storage_hints: program.module_binding_storage_hints.clone(),
309 function_local_storage_hints: Vec::new(),
310 top_level_frame: None,
311 top_level_local_concrete_types: Vec::new(),
312 function_local_concrete_types: Vec::new(),
313 function_return_concrete_types: Vec::new(),
314 monomorphized_method_call_sites: Default::default(),
315 value_call_return_concrete_types: Default::default(),
316 operator_trait_dispatch_sites: Default::default(),
317 top_level_mir: None,
318 compiled_annotations: program.compiled_annotations.clone(),
319 trait_method_symbols: program.trait_method_symbols.clone(),
320 expanded_function_defs: program.expanded_function_defs.clone(),
321 string_index: Default::default(),
322 foreign_functions: program.foreign_functions.clone(),
323 native_struct_layouts: program.native_struct_layouts.clone(),
324 content_addressed: None,
325 function_blob_hashes: Vec::new(),
326 monomorphization_keys: Vec::new(),
327 closure_function_layouts: program.closure_function_layouts.clone(),
328 trait_vtables: program.trait_vtables.clone(),
329 has_imported_const_inline: program.has_imported_const_inline,
330 has_w17_marshal_residual: program.has_w17_marshal_residual,
331 };
332
333 // MirToIR is the ONLY JIT compilation path (Phase 4: BytecodeToIR removed).
334 // All functions must have valid MIR data. If not, report the error.
335 let mir_data = func.mir_data.as_ref().ok_or_else(|| {
336 format!("MirToIR: function '{}' has no MIR data (bytecode-only functions are no longer supported)", func.name)
337 })?;
338 let preflight = crate::mir_compiler::preflight(mir_data);
339 if !preflight.can_compile {
340 return Err(format!(
341 "MirToIR: function '{}' failed preflight: {}",
342 func.name,
343 preflight.blockers.join("; ")
344 ));
345 }
346
347 {
348 let slot_kinds: Vec<Option<shape_vm::type_tracking::NativeKind>> = func
349 .frame_descriptor
350 .as_ref()
351 .map(|fd| fd.slots.iter().copied().map(Some).collect())
352 .unwrap_or_default();
353 // ADR-006 §2.7.5 conduit: thread the bytecode compiler's
354 // proven per-MIR-slot `ConcreteType` for THIS user function
355 // into MirToIR (W12-jit-aggregate-non-array close,
356 // 2026-05-12). The producer
357 // (`infer_top_level_concrete_types_from_mir`) was already
358 // landed for top-level code by Round 3; its body is generic
359 // over any MirFunction, and Round 5B extends the populate
360 // site to per-user-function MIR via
361 // `BytecodeProgram.function_local_concrete_types`. The
362 // top-level conduit's user-visible benefit (Smoke 3
363 // `Point{}` literal short-circuit) now extends to user
364 // function bodies (`Ok(v)`/`Err(e)`/`Some(x)` inside
365 // `divide` / `first_positive` / 28 stdlib helpers).
366 //
367 // Empty inner vec (function has no MIR data, or the conduit
368 // couldn't prove a particular slot) → MirToIR's v2 fast
369 // path falls through to the legacy NaN-boxed path / surfaces
370 // honestly per ADR-006 §2.7.5.1 (no Bool-default).
371 let concrete_types: Vec<shape_value::v2::ConcreteType> =
372 program
373 .function_local_concrete_types
374 .get(func_idx)
375 .cloned()
376 .unwrap_or_default();
377 // Build function name → index map for Call terminator resolution.
378 // Use the original program's functions (sub_program has empty functions list).
379 let function_indices: std::collections::HashMap<String, u16> = program
380 .functions
381 .iter()
382 .enumerate()
383 .map(|(i, f)| (f.name.clone(), i as u16))
384 .collect();
385 // Closure-spec Phase H1: thread the per-function
386 // ClosureLayout map into MirToIR so `emit_heap_closure`
387 // can lay out captures at natural-width offsets without
388 // going through the legacy `jit_make_closure` FFI.
389 let closure_function_layouts: std::collections::HashMap<
390 u16,
391 std::sync::Arc<shape_value::v2::closure_layout::ClosureLayout>,
392 > = program
393 .closure_function_layouts
394 .iter()
395 .enumerate()
396 .filter_map(|(i, opt)| opt.as_ref().map(|l| (i as u16, l.clone())))
397 .collect();
398 let mut mir_compiler = crate::mir_compiler::MirToIR::new_with_closure_layouts(
399 &mut builder,
400 ctx_ptr,
401 ffi,
402 mir_data,
403 slot_kinds,
404 concrete_types,
405 &sub_program.strings,
406 entry_block,
407 &function_indices,
408 user_func_refs.clone(),
409 user_func_arities.clone(),
410 closure_function_layouts,
411 );
412 // V3-S6c-jit-method-monomorph-routing (ADR-006 §2.7.5
413 // stamp-at-compile-time; supervisor 2026-05-15 PATH α-prime
414 // RATIFIED): thread the V3-S6b side-table from the ORIGINAL
415 // `program: &BytecodeProgram` (the `sub_program` above
416 // clears it at line ~305 to keep the per-function compile
417 // scope minimal) so the Call-terminator pass can re-route
418 // `MirConstant::Method` sites to direct FuncRef calls.
419 //
420 // Composite key `(call_site_span, caller_function_id)`:
421 // `caller_function_id = Some(func_idx)` matches the
422 // bytecode compiler's `self.current_function` at
423 // specialization time (`expressions/function_calls.rs:3278`).
424 mir_compiler.set_monomorph_routing_context(
425 program.monomorphized_method_call_sites.clone(),
426 Some(func_idx),
427 );
428 // W10 jit-call-method-user-trait-fix (2026-05-17): install
429 // the bytecode compiler's operator-trait-dispatch side-
430 // table so the per-user-function MirToIR consumer can
431 // re-emit `Rvalue::BinaryOp` / `Rvalue::UnaryOp` at
432 // trait-dispatch spans as method-call IR.
433 mir_compiler.set_operator_trait_dispatch_sites(
434 program.operator_trait_dispatch_sites.clone(),
435 );
436 // Bounds-check elision: install the per-function plan
437 // before MIR codegen so `Place::Index` lowering can
438 // bypass the inline bounds check on trusted (arr, iv)
439 // pairs. Default empty plan keeps every access checked.
440 let elision_plan =
441 crate::mir_compiler::bounds_elision::analyze(&mir_data.mir);
442 mir_compiler.set_bounds_elision_plan(elision_plan);
443 // W14.2-E-followup SURFACE-A2 fix (2026-05-19, v0.3-gating
444 // SOUNDNESS BUG per supervisor ratify): pre-populate
445 // `field_byte_offsets` from the program's
446 // `type_schema_registry` so trait-impl method bodies (and
447 // any function that reads typed-object fields without
448 // emitting a local `ObjectStore`) can resolve field byte
449 // offsets at JIT compile time. Without this pre-pass,
450 // `try_resolve_field_byte_offset` returns `None` for impl
451 // bodies and `Place::Field` falls through to
452 // `jit_get_prop`, whose `heap_kind(obj_bits)` predicate
453 // returns `None` under ADR-006 §2.7.5 raw `Box::into_raw`
454 // typed-object carriers — empirically returning `TAG_NULL`
455 // for every `self.field` read (`vm_trait_method_self_field
456 // _access_n0` reproducer's garbage NaN-bits root cause).
457 //
458 // Per ADR-006 §2.7.5 producer-side stamp: schema field
459 // positions are stamped at AST→bytecode-compile time in
460 // the canonical schema registry. The JIT consumes the
461 // stamp through `populate_field_byte_offsets_from_schemas`
462 // — a derived index, not a runtime decode.
463 mir_compiler
464 .populate_field_byte_offsets_from_schemas(&program.type_schema_registry);
465 // Track A.1D.2: flag the leading capture param slots whose
466 // `ClosureLayout` marks them as `OwnedMutable`. `read_place`
467 // / `write_place` then route through the cell pointer bits
468 // stored in those slots, matching the interpreter's
469 // `Load/StoreOwnedMutableCapture` handlers. The lookup is
470 // keyed on this function's own `func_idx`, which doubles as
471 // the closure body's `function_id` when it is a closure.
472 // Non-closure functions hit no entry in the layout map →
473 // the side-table stays empty, preserving pre-A.1D.2
474 // behaviour for ordinary functions.
475 if func.is_closure && func.captures_count > 0 {
476 if let Some(layout) = program
477 .closure_function_layouts
478 .get(func_idx)
479 .and_then(|o| o.as_ref())
480 {
481 mir_compiler.register_owned_mutable_capture_slots(
482 func.captures_count,
483 layout.as_ref(),
484 );
485 }
486 }
487 // Set up blocks and locals, then store function parameters.
488 mir_compiler.create_blocks();
489 mir_compiler.declare_locals();
490
491 // Store function parameters to MIR local variables.
492 // MIR slot layout: [return_slot(0), param0(1), param1(2), ..., locals...]
493 // Entry block params: [ctx_ptr, capture0..N, param0..N]
494 // Use mir.param_slots to map params to their actual MIR slots.
495 let entry_params = mir_compiler.builder.block_params(entry_block).to_vec();
496 let param_slots = &mir_data.mir.param_slots;
497
498 // Initialize ALL locals with type-appropriate defaults.
499 mir_compiler.initialize_locals();
500
501 // Session 1 Commit 3: allocate Arc<SharedCell>s for
502 // every SharedCow local slot (outer-scope `var` bindings
503 // that escape into closures). After this call every
504 // SharedCow slot's Cranelift var holds the raw
505 // `*const SharedCell` pointer bits; subsequent
506 // read_place / write_place route through the lock-gated
507 // pointer-deref lowering, and `emit_drop` on the slot
508 // emits `jit_arc_shared_release` to balance the share.
509 mir_compiler.initialize_shared_local_slots();
510
511 // Store function parameters (including captures) to MIR local variables.
512 // MIR param_slots includes capture slots followed by user param slots.
513 // Entry block params: [ctx_ptr, capture0..N, param0..M]
514 // param_slots aligns 1:1 with captures+params, so native_idx = param_idx + 1.
515 //
516 // R4.2E: callee ABI delivers params as uniform I64 bit-patterns.
517 // When the MIR slot is a native narrow type, reduce I64 → narrow
518 // inline (bitcast for F64, ireduce for I32/I16/I8). No NaN-box
519 // tag stripping — raw bit-patterns only.
520 for (param_idx, &mir_slot) in param_slots.iter().enumerate() {
521 let native_idx = param_idx + 1; // +1 for ctx_ptr
522 if native_idx < entry_params.len() {
523 if let Some(&var) = mir_compiler.locals.get(&mir_slot) {
524 let kind = crate::mir_compiler::types::slot_kind_for_local(
525 &mir_compiler.slot_kinds,
526 mir_slot.0,
527 );
528 let param_val = entry_params[native_idx];
529 let converted = match kind {
530 Some(shape_vm::type_tracking::NativeKind::Float64) => mir_compiler
531 .builder
532 .ins()
533 .bitcast(types::F64, MemFlags::new(), param_val),
534 Some(shape_vm::type_tracking::NativeKind::Int32)
535 | Some(shape_vm::type_tracking::NativeKind::UInt32) => {
536 mir_compiler.builder.ins().ireduce(types::I32, param_val)
537 }
538 Some(shape_vm::type_tracking::NativeKind::Bool)
539 | Some(shape_vm::type_tracking::NativeKind::Int8)
540 | Some(shape_vm::type_tracking::NativeKind::UInt8) => {
541 mir_compiler.builder.ins().ireduce(types::I8, param_val)
542 }
543 Some(shape_vm::type_tracking::NativeKind::Int16)
544 | Some(shape_vm::type_tracking::NativeKind::UInt16) => {
545 mir_compiler.builder.ins().ireduce(types::I16, param_val)
546 }
547 _ => param_val,
548 };
549 mir_compiler.builder.def_var(var, converted);
550 }
551 }
552 }
553 mir_compiler.compile_body()?;
554 tracing::debug!(
555 target: "shape_jit",
556 func_name = %func.name,
557 "jit-mir compiled function via MirToIR",
558 );
559 }
560 builder.finalize();
561 }
562
563 self.module
564 .define_function(func_id, &mut ctx)
565 .map_err(|e| format!("Failed to define function: {:?}", e))?;
566
567 self.module.clear_context(&mut ctx);
568
569 Ok(())
570 }
571
572 /// Compile a single function for Tier 1 whole-function JIT.
573 ///
574 /// This path previously used BytecodeToIR which has been removed.
575 /// Tier 1 JIT is deprecated; use compile_program_selective instead.
576 pub fn compile_single_function(
577 &mut self,
578 _program: &BytecodeProgram,
579 _func_index: usize,
580 _feedback: Option<shape_vm::feedback::FeedbackVector>,
581 ) -> Result<
582 (
583 *const u8,
584 Vec<shape_vm::bytecode::DeoptInfo>,
585 Vec<shape_value::shape_graph::ShapeId>,
586 ),
587 String,
588 > {
589 Err("Tier 1 JIT is deprecated".to_string())
590 }
591
592 /// Compile a function for Tier 2 optimizing JIT with feedback-guided speculation.
593 ///
594 /// This path previously used BytecodeToIR which has been removed.
595 /// Optimizing JIT is deprecated; use compile_program_selective instead.
596 pub fn compile_optimizing_function(
597 &mut self,
598 _program: &BytecodeProgram,
599 _func_index: usize,
600 _feedback: shape_vm::feedback::FeedbackVector,
601 _callee_feedback: &HashMap<u16, shape_vm::feedback::FeedbackVector>,
602 ) -> Result<
603 (
604 *const u8,
605 Vec<shape_vm::bytecode::DeoptInfo>,
606 Vec<shape_value::shape_graph::ShapeId>,
607 ),
608 String,
609 > {
610 Err("Optimizing JIT is deprecated".to_string())
611 }
612
613 /// Selectively compile a program, JIT-compiling compatible functions and
614 /// falling back to interpreter entries for incompatible ones.
615 ///
616 /// Returns a `MixedFunctionTable` mapping each function index to either
617 /// a `Native` pointer (JIT-compiled) or `Interpreted` marker.
618 ///
619 /// The main strategy body is always compiled. Only user-defined functions
620 /// go through per-function preflight.
621 pub fn compile_program_selective(
622 &mut self,
623 name: &str,
624 program: &BytecodeProgram,
625 ) -> Result<(JittedStrategyFn, MixedFunctionTable), String> {
626 use super::accessors::preflight_instructions;
627
628 maybe_emit_numeric_metrics(program);
629
630 // Phase 1: Per-function preflight to classify each function.
631 // A function is JIT-compatible if its bytecode passes instruction
632 // preflight OR it has MIR data that passes MirToIR preflight.
633 // MirToIR is the compilation path — bytecode preflight only gates eligibility.
634 let mut jit_compatible: Vec<bool> = Vec::with_capacity(program.functions.len());
635
636 for (_idx, func) in program.functions.iter().enumerate() {
637 if func.body_length == 0 {
638 jit_compatible.push(false);
639 continue;
640 }
641 let func_end = func.entry_point + func.body_length;
642 let instructions = &program.instructions[func.entry_point..func_end];
643 let report = preflight_instructions(instructions);
644 let bytecode_ok = report.can_jit();
645 let mir_ok = func.mir_data.as_ref().is_some_and(|md| {
646 crate::mir_compiler::preflight(md).can_compile
647 });
648 // Track A.1D / A.1D.2: the A.1B/A.1C.1/A.1C.3 mutable-cell
649 // opcodes carry runtime semantics the MIR layer cannot
650 // reconstruct from its slot-based model — MIR just sees
651 // `LoadLocal` / `StoreLocal`, erasing the pointer-deref
652 // semantics the cell opcodes encode.
653 //
654 // A.1D.2 closes the gap for `LoadOwnedMutableCapture` /
655 // `StoreOwnedMutableCapture` via a JIT-side side-table that
656 // patches `read_place` / `write_place` on flagged capture
657 // slots (see `MirToIR::register_owned_mutable_capture_slots`).
658 // Those two opcodes have been removed from
659 // `vm_only_opcode_reason`, so `bytecode_ok` is now `true`
660 // for functions whose only cell opcodes are OwnedMutable.
661 //
662 // A.1E closed the gap for the closure-body Shared-cell
663 // opcodes (`LoadSharedCapture` / `StoreSharedCapture`) via
664 // the `MirToIR::shared_capture_slots` side-table.
665 //
666 // Session 1 Commit 3 lands the MirToIR infrastructure for
667 // the outer-scope `var` cell lifecycle — the
668 // `MirToIR::shared_local_slots` side-table is populated
669 // from `StoragePlan::slot_classes`, function entry
670 // allocates one `Arc<SharedCell>` per SharedCow slot via
671 // `jit_alloc_shared_cell`, and `read_place`/`write_place`
672 // /`emit_drop` branch to lock-gated access +
673 // `jit_arc_shared_release`. The preflight gate for the
674 // four local opcodes (`AllocSharedLocal` /
675 // `LoadSharedLocal` / `StoreSharedLocal` /
676 // `DropSharedLocal`) REMAINS IN PLACE pending resolution
677 // of the outer-frame cell-identity handshake —
678 // lifting the gate prematurely segfaults the JIT'd
679 // outer frame's interaction with closure dispatch (see
680 // memory note `project_jit_closure_fix.md`).
681 //
682 // Still gated after this commit:
683 // * the four outer-scope `var` local opcodes above;
684 // * the three module-binding opcodes
685 // (`AllocSharedModuleBinding`,
686 // `LoadSharedModuleBinding`,
687 // `StoreSharedModuleBinding`) — per-module side-table,
688 // separate lowering (A.1C.3 follow-up).
689 let _ = mir_ok;
690 jit_compatible.push(bytecode_ok);
691 }
692
693 // Phase 1b: Preflight main code (non-stdlib, non-function-body instructions).
694 // Without this, unsupported builtins in top-level code slip through.
695 {
696 let skip_ranges = Self::compute_skip_ranges(program);
697 let main_instructions: Vec<_> = program
698 .instructions
699 .iter()
700 .enumerate()
701 .filter(|(i, _)| !skip_ranges.iter().any(|(s, e)| *i >= *s && *i < *e))
702 .map(|(_, instr)| instr.clone())
703 .collect();
704 let main_report = preflight_instructions(&main_instructions);
705 if !main_report.can_jit() {
706 return Err(format!(
707 "Main code contains unsupported constructs: {:?}",
708 main_report
709 ));
710 }
711 }
712
713 // v0.3 WS-6: a generic free function specialized on a struct type
714 // argument (`fn id<T>(x: T) -> T` called as `id(P { .. })`) produces
715 // a `<base>::struct_<name>` specialization. The JIT MIR codegen for
716 // a struct value flowing out of such a specialization is currently
717 // unsound — the returned `HeapKind::TypedObject` handle is
718 // mishandled when the result is stored to a slot and a field is
719 // later read, producing a use-after-free. The bytecode VM handles
720 // this case correctly. Per the CLAUDE.md surface-and-stop discipline
721 // (refuse what cannot be lowered soundly rather than emit crashing
722 // native code), surface here so `--mode jit` cleanly falls back to
723 // the interpreter for the whole program. Enum / Option / Result /
724 // Array / HashMap monomorphizations are unaffected — only the
725 // struct-typed free-function specialization is gated. (Generic
726 // struct args were rejected outright at the compile stage before
727 // WS-6, so this is a strict improvement: such programs now run
728 // correctly on the interpreter rather than failing to compile.)
729 if program
730 .functions
731 .iter()
732 .any(|func| func.name.contains("::struct_"))
733 {
734 return Err(
735 "WS-6 surface-and-stop: program uses a generic free function \
736 specialized on a struct type argument; the JIT struct-value \
737 codegen for that specialization is not yet sound — falling \
738 back to the bytecode interpreter"
739 .to_string(),
740 );
741 }
742
743 // Phase 2: Pre-declare ALL functions (both JIT and interpreted) in
744 // Cranelift so that JIT functions can call other JIT functions.
745 // Interpreted functions get declared too (for uniform call tables)
746 // but won't have a body defined - they'll use the trampoline.
747 let mut user_func_arities: HashMap<u16, u16> = HashMap::new();
748 let mut user_func_ids: HashMap<u16, cranelift_module::FuncId> = HashMap::new();
749
750 for (idx, func) in program.functions.iter().enumerate() {
751 if !jit_compatible[idx] {
752 user_func_arities.insert(idx as u16, func.arity);
753 continue;
754 }
755 // Use function index in the name to avoid collisions between
756 // closures with the same auto-generated name but different arities
757 // (e.g., multiple __closure_0 from different stdlib modules).
758 let func_name = format!("{}_f{}_{}", name, idx, func.name.replace("::", "__"));
759 let mut user_sig = self.module.make_signature();
760 user_sig.params.push(AbiParam::new(types::I64)); // ctx_ptr
761 // Closures receive captures as leading native args, followed by user params.
762 let effective_arity = func.captures_count + func.arity;
763 for _ in 0..effective_arity {
764 user_sig.params.push(AbiParam::new(types::I64));
765 }
766 user_sig.returns.push(AbiParam::new(types::I32));
767 let func_id = self
768 .module
769 .declare_function(&func_name, Linkage::Local, &user_sig)
770 .map_err(|e| format!("Failed to pre-declare function {}: {}", func.name, e))?;
771 user_func_ids.insert(idx as u16, func_id);
772 // Store user-visible arity (without captures) for CallValue arg count checks
773 user_func_arities.insert(idx as u16, func.arity);
774 }
775
776 // Phase 3: Compile main strategy body.
777 let main_func_id = self.compile_strategy_with_user_funcs(
778 name,
779 program,
780 &user_func_ids,
781 &user_func_arities,
782 )?;
783
784 // Phase 4: Compile only JIT-compatible function bodies.
785 // Functions that fail to compile are demoted to interpreted fallback.
786 for (idx, func) in program.functions.iter().enumerate() {
787 if jit_compatible[idx] || func.mir_data.is_some() {
788 tracing::debug!(
789 target: "shape_jit",
790 idx,
791 func_name = %func.name,
792 jit_compat = jit_compatible[idx],
793 has_mir = func.mir_data.is_some(),
794 "jit-mir per-function classification",
795 );
796 }
797 if !jit_compatible[idx] {
798 continue;
799 }
800 let func_name = format!("{}_f{}_{}", name, idx, func.name.replace("::", "__"));
801 if func.mir_data.is_some() {
802 tracing::debug!(
803 target: "shape_jit",
804 idx,
805 func_name = %func.name,
806 "jit-mir compiling function",
807 );
808 }
809 if let Err(e) = self.compile_function_with_user_funcs(
810 &func_name,
811 program,
812 idx,
813 &user_func_ids,
814 &user_func_arities,
815 ) {
816 tracing::debug!(
817 target: "shape_jit",
818 func_name = %func.name,
819 error = %e,
820 "jit-mir compile failed",
821 );
822 // Define a stub body so Cranelift doesn't panic on undefined symbol.
823 // The stub returns signal -1 (error), causing the caller to deopt.
824 //
825 // W12-jit-linker-resolve (`docs/cluster-audits/w12-jit-linker-audit.md`):
826 // Cranelift's `iconst` immediate-bounds rule requires the I32
827 // immediate to be the unsigned bit-pattern, not the signed
828 // value. `iconst.i32 -1` is rejected by the verifier because
829 // `-1i64 as u64 = 0xFFFFFFFFFFFFFFFF` exceeds the I32 mask
830 // `u32::MAX = 0xFFFFFFFF`. Pass the two's-complement unsigned
831 // bit pattern instead — see `cranelift-codegen/src/verifier/
832 // mod.rs:1644-1665` for the documented invariant.
833 //
834 // Also: previously the stub `define_function` failure was
835 // silently swallowed via `let _ = ...`, which left the
836 // declared FuncId with no body and caused `finalize_definitions`
837 // to panic with `can't resolve symbol main_f{idx}_{name}` —
838 // the very surface this audit traced. Surface the stub
839 // failure under `SHAPE_JIT_DEBUG=1` so future regressions
840 // don't hide beneath the linker panic.
841 if let Some(&fid) = user_func_ids.get(&(idx as u16)) {
842 let mut stub_sig = self.module.make_signature();
843 stub_sig.params.push(AbiParam::new(types::I64));
844 let effective_arity = func.captures_count + func.arity;
845 for _ in 0..effective_arity {
846 stub_sig.params.push(AbiParam::new(types::I64));
847 }
848 stub_sig.returns.push(AbiParam::new(types::I32));
849 let mut stub_ctx = self.module.make_context();
850 stub_ctx.func.signature = stub_sig;
851 let mut stub_builder_ctx = FunctionBuilderContext::new();
852 {
853 let mut b = FunctionBuilder::new(&mut stub_ctx.func, &mut stub_builder_ctx);
854 let block = b.create_block();
855 b.append_block_params_for_function_params(block);
856 b.switch_to_block(block);
857 b.seal_block(block);
858 // Cranelift I32 iconst convention: pass the unsigned
859 // bit-pattern, not the signed value. `-1i32` is
860 // `0xFFFFFFFF` as a `u32`.
861 let neg = b.ins().iconst(types::I32, (-1i32 as u32) as i64);
862 b.ins().return_(&[neg]);
863 b.finalize();
864 }
865 if let Err(stub_err) = self.module.define_function(fid, &mut stub_ctx) {
866 tracing::debug!(
867 target: "shape_jit",
868 func_name = %func.name,
869 idx,
870 fid = ?fid,
871 error = ?stub_err,
872 "jit-mir stub define_function failed",
873 );
874 // Surface-and-stop: a failed stub leaves the declared
875 // FuncId with no body, which propagates to
876 // `finalize_definitions` as a `can't resolve symbol`
877 // panic. Convert to a structured error here so the
878 // caller sees a typed JIT-compilation failure, not a
879 // panic through `catch_unwind`. The stub itself was
880 // supposed to be a recovery path; if recovery fails,
881 // the whole JIT compilation is unsound.
882 return Err(format!(
883 "JIT stub fallback failed for function '{}' (idx={}): {:?}. \
884 The Cranelift module is in an inconsistent state — \
885 this is a JIT-compiler bug, not a user-code error. \
886 See docs/cluster-audits/w12-jit-linker-audit.md.",
887 func.name, idx, stub_err
888 ));
889 }
890 self.module.clear_context(&mut stub_ctx);
891 }
892 jit_compatible[idx] = false;
893 }
894 }
895
896 self.module
897 .finalize_definitions()
898 .map_err(|e| format!("Failed to finalize definitions: {:?}", e))?;
899
900 let main_code_ptr = self.module.get_finalized_function(main_func_id);
901 self.compiled_functions
902 .insert(name.to_string(), main_code_ptr);
903
904 // Phase 5: Build the MixedFunctionTable.
905 let mut mixed_table = MixedFunctionTable::with_capacity(program.functions.len());
906
907 self.function_table.clear();
908 for (idx, func) in program.functions.iter().enumerate() {
909 if jit_compatible[idx] {
910 if let Some(&func_id) = user_func_ids.get(&(idx as u16)) {
911 let ptr = self.module.get_finalized_function(func_id);
912 while self.function_table.len() <= idx {
913 self.function_table.push(std::ptr::null());
914 }
915 self.function_table[idx] = ptr;
916 let func_name = format!("{}_f{}_{}", name, idx, func.name.replace("::", "__"));
917 self.compiled_functions.insert(func_name, ptr);
918 mixed_table.insert(idx, FunctionEntry::Native(ptr));
919 }
920 } else {
921 while self.function_table.len() <= idx {
922 self.function_table.push(std::ptr::null());
923 }
924 // Leave function_table[idx] as null for interpreted functions.
925 mixed_table.insert(idx, FunctionEntry::Interpreted(idx as u16));
926 }
927 }
928
929 let jit_fn = unsafe { std::mem::transmute(main_code_ptr) };
930 Ok((jit_fn, mixed_table))
931 }
932}