Skip to main content

shape_jit/
osr_compiler.rs

1//! OSR (On-Stack Replacement) Loop Compilation
2//!
3//! Compiles hot loop bodies to native code via Cranelift IR for mid-execution
4//! transfer from the bytecode interpreter to JIT-compiled code.
5//!
6//! # OSR ABI
7//! `extern "C" fn(ctx_ptr: *mut u8, _unused: *const u8) -> u64`
8//! - Returns 0 on normal loop exit (locals written back to ctx).
9//! - Returns `u64::MAX` on deoptimization (locals partially written back).
10//!
11//! # Escape Analysis / Scalar Replacement
12//! The escape analysis pass (Phase 5) identifies small non-escaping arrays
13//! for scalar replacement in the whole-function JIT compiler. OSR compilation
14//! does NOT support NewArray/GetProp/SetLocalIndex opcodes (they fail the
15//! preflight check in `is_osr_supported_opcode`), so scalar replacement does
16//! not apply to OSR-compiled loop bodies. If OSR support for array opcodes is
17//! added in the future, deopt materialization must reconstruct scalar-replaced
18//! arrays from their SSA variable elements before writing locals back to ctx.
19
20use std::collections::{HashMap, HashSet};
21
22use cranelift::prelude::*;
23use cranelift_module::{Linkage, Module};
24
25use shape_vm::bytecode::{DeoptInfo, Instruction, OpCode, Operand, OsrEntryPoint};
26use shape_vm::type_tracking::{FrameDescriptor, NativeKind};
27
28use crate::loop_analysis::LoopInfo;
29
30/// Result of compiling a loop body for OSR entry.
31#[derive(Debug)]
32pub struct OsrCompilationResult {
33    /// Native code pointer for the compiled loop body.
34    pub native_code: *const u8,
35    /// OSR entry point metadata (live locals, kinds, bytecode IPs).
36    pub entry_point: OsrEntryPoint,
37    /// Deopt info for all guard points within the compiled loop.
38    pub deopt_points: Vec<DeoptInfo>,
39}
40
41// SAFETY: native_code pointer is valid for the lifetime of the JIT compilation
42// and is only used within the VM execution context.
43unsafe impl Send for OsrCompilationResult {}
44
45/// Maximum number of locals the JIT context buffer can hold.
46/// The locals area spans u64 indices 8..264 (256 slots).
47const JIT_LOCALS_CAP: usize = 256;
48
49/// Byte offset where locals begin in the JIT context buffer.
50const LOCALS_BYTE_OFFSET: i32 = 64; // 8 * 8
51
52/// Check whether an opcode is in the supported MVP set for OSR compilation.
53fn is_osr_supported_opcode(opcode: OpCode, operand: &Option<Operand>) -> bool {
54    use shape_vm::bytecode::BuiltinFunction as BF;
55    match opcode {
56        // Stack
57        OpCode::PushConst | OpCode::PushNull | OpCode::Pop | OpCode::Dup | OpCode::Swap => true,
58        // Variables
59        OpCode::LoadLocal
60        | OpCode::LoadLocalTrusted
61        | OpCode::StoreLocal
62        | OpCode::StoreLocalTyped => true,
63        OpCode::LoadModuleBinding
64        | OpCode::StoreModuleBinding
65        | OpCode::StoreModuleBindingTyped => true,
66        // Arithmetic (Int)
67        OpCode::AddInt
68        | OpCode::SubInt
69        | OpCode::MulInt
70        | OpCode::DivInt
71        | OpCode::ModInt
72        | OpCode::PowInt => true,
73        // Arithmetic (Number)
74        OpCode::AddNumber
75        | OpCode::SubNumber
76        | OpCode::MulNumber
77        | OpCode::DivNumber
78        | OpCode::ModNumber
79        | OpCode::PowNumber => true,
80        // Neg
81        OpCode::NegInt | OpCode::NegNumber => true,
82        // Comparison (Int)
83        OpCode::GtInt
84        | OpCode::LtInt
85        | OpCode::GteInt
86        | OpCode::LteInt
87        | OpCode::EqInt
88        | OpCode::NeqInt => true,
89        // Comparison (Number)
90        OpCode::GtNumber
91        | OpCode::LtNumber
92        | OpCode::GteNumber
93        | OpCode::LteNumber
94        | OpCode::EqNumber
95        | OpCode::NeqNumber => true,
96        // Typed comparisons (string/decimal/null)
97        OpCode::EqString | OpCode::GtString | OpCode::LtString | OpCode::GteString
98        | OpCode::LteString | OpCode::EqDecimal | OpCode::IsNull => true,
99        // Logic
100        OpCode::And | OpCode::Or | OpCode::Not => true,
101        // Control
102        OpCode::Jump
103        | OpCode::JumpIfFalse
104        | OpCode::JumpIfFalseTrusted
105        | OpCode::JumpIfTrue
106        | OpCode::LoopStart
107        | OpCode::LoopEnd
108        | OpCode::Break
109        | OpCode::Continue => true,
110        // Coercion / width casts
111        OpCode::IntToNumber | OpCode::NumberToInt | OpCode::CastWidth => true,
112        // Return (mapped to loop exit)
113        OpCode::Return | OpCode::ReturnValue => true,
114        // Misc
115        OpCode::Nop | OpCode::Halt => true,
116        // BuiltinCall: only selected math builtins
117        OpCode::BuiltinCall => {
118            if let Some(Operand::Builtin(bf)) = operand {
119                matches!(
120                    bf,
121                    BF::Abs
122                        | BF::Sqrt
123                        | BF::Min
124                        | BF::Max
125                        | BF::Floor
126                        | BF::Ceil
127                        | BF::Round
128                        | BF::Pow
129                )
130            } else {
131                false
132            }
133        }
134        _ => false,
135    }
136}
137
138/// Compile a loop body for OSR (On-Stack Replacement) entry.
139///
140/// Emits Cranelift IR for the loop body with the OSR ABI:
141/// `extern "C" fn(ctx_ptr: *mut u8, _: *const u8) -> u64`
142///
143/// - Returns 0 on normal loop exit (locals written back to ctx).
144/// - Returns `u64::MAX` on deoptimization (locals partially written back).
145///
146/// # Arguments
147/// * `jit` - The JIT compiler instance (owns the Cranelift module).
148/// * `function` - The function containing the target loop.
149/// * `instructions` - The full instruction stream of the function.
150/// * `loop_info` - Analysis results for the target loop (from `analyze_loops`).
151/// * `frame_descriptor` - Typed frame layout for slot marshaling.
152pub fn compile_osr_loop(
153    jit: &mut crate::compiler::JITCompiler,
154    function: &shape_vm::bytecode::Function,
155    instructions: &[Instruction],
156    loop_info: &LoopInfo,
157    frame_descriptor: &FrameDescriptor,
158) -> Result<OsrCompilationResult, String> {
159    // Validate the loop bounds are within the instruction stream.
160    if loop_info.header_idx >= instructions.len() {
161        return Err(format!(
162            "OSR loop header {} is out of bounds (instruction count: {})",
163            loop_info.header_idx,
164            instructions.len()
165        ));
166    }
167    if loop_info.end_idx >= instructions.len() {
168        return Err(format!(
169            "OSR loop end {} is out of bounds (instruction count: {})",
170            loop_info.end_idx,
171            instructions.len()
172        ));
173    }
174
175    // Preflight: reject loops containing unsupported opcodes.
176    for idx in loop_info.header_idx..=loop_info.end_idx {
177        let instr = &instructions[idx];
178        if !is_osr_supported_opcode(instr.opcode, &instr.operand) {
179            return Err(format!(
180                "OSR unsupported opcode {:?} at instruction {}",
181                instr.opcode, idx
182            ));
183        }
184    }
185
186    // Compute live locals: union of read and written sets.
187    let mut live_locals: Vec<u16> = loop_info
188        .body_locals_read
189        .union(&loop_info.body_locals_written)
190        .copied()
191        .collect();
192    live_locals.sort_unstable();
193
194    // Check all locals fit within JIT locals capacity.
195    for &local_idx in &live_locals {
196        if local_idx as usize >= JIT_LOCALS_CAP {
197            return Err(format!(
198                "OSR local index {} exceeds JIT_LOCALS_CAP ({})",
199                local_idx, JIT_LOCALS_CAP
200            ));
201        }
202    }
203
204    // Map each live local to its NativeKind from the frame descriptor.
205    // Per ADR-006 §2.7.7 the deleted `NativeKind::Unknown` placeholder
206    // is gone — slots whose kind is missing from the frame descriptor
207    // fall back to `Int64` (legacy I64-NaN-box ABI width). OSR entry
208    // is a re-entry into a hot loop; if the descriptor is incomplete
209    // here the caller is in pre-§2.7.7 legacy territory.
210    let local_kinds: Vec<NativeKind> = live_locals
211        .iter()
212        .map(|&slot| {
213            frame_descriptor
214                .slots
215                .get(slot as usize)
216                .copied()
217                .unwrap_or(NativeKind::Int64)
218        })
219        .collect();
220
221    let entry_point = OsrEntryPoint {
222        bytecode_ip: loop_info.header_idx,
223        live_locals: live_locals.clone(),
224        local_kinds: local_kinds.clone(),
225        exit_ip: loop_info.end_idx + 1,
226    };
227
228    // Body locals written — used for epilogue (only write back modified locals).
229    let body_locals_written: HashSet<u16> = loop_info.body_locals_written.clone();
230
231    // --- Cranelift compilation ---
232
233    // Declare the OSR function: (i64, i64) -> i64
234    let func_name = format!("osr_loop_f{}_ip{}", function.arity, loop_info.header_idx);
235    let mut sig = jit.module_mut().make_signature();
236    sig.params.push(AbiParam::new(types::I64)); // ctx_ptr
237    sig.params.push(AbiParam::new(types::I64)); // unused
238    sig.returns.push(AbiParam::new(types::I64)); // result (0 or u64::MAX)
239
240    let func_id = jit
241        .module_mut()
242        .declare_function(&func_name, Linkage::Export, &sig)
243        .map_err(|e| format!("Failed to declare OSR function: {}", e))?;
244
245    let mut ctx = cranelift::codegen::Context::new();
246    ctx.func.signature = sig;
247
248    {
249        let mut builder = FunctionBuilder::new(&mut ctx.func, jit.builder_context_mut());
250
251        // Create blocks
252        let entry_block = builder.create_block();
253        let exit_block = builder.create_block();
254        let deopt_block = builder.create_block();
255
256        // Pre-scan for jump targets inside the loop body to create blocks
257        let mut block_map: HashMap<usize, Block> = HashMap::new();
258        // The loop header gets its own block (this is the main loop block)
259        let header_block = builder.create_block();
260        block_map.insert(loop_info.header_idx, header_block);
261
262        for idx in loop_info.header_idx..=loop_info.end_idx {
263            let instr = &instructions[idx];
264            match instr.opcode {
265                OpCode::Jump
266                | OpCode::JumpIfFalse
267                | OpCode::JumpIfFalseTrusted
268                | OpCode::JumpIfTrue => {
269                    if let Some(Operand::Offset(off)) = instr.operand {
270                        let target = (idx as i64 + off as i64 + 1) as usize;
271                        if target >= loop_info.header_idx
272                            && target <= loop_info.end_idx + 1
273                            && !block_map.contains_key(&target)
274                        {
275                            let blk = builder.create_block();
276                            block_map.insert(target, blk);
277                        }
278                    }
279                }
280                _ => {}
281            }
282            // Also create a block for the instruction after a conditional branch
283            // (fall-through target)
284            match instr.opcode {
285                OpCode::JumpIfFalse | OpCode::JumpIfFalseTrusted | OpCode::JumpIfTrue => {
286                    let fall_through = idx + 1;
287                    if fall_through >= loop_info.header_idx
288                        && fall_through <= loop_info.end_idx
289                        && !block_map.contains_key(&fall_through)
290                    {
291                        let blk = builder.create_block();
292                        block_map.insert(fall_through, blk);
293                    }
294                }
295                _ => {}
296            }
297        }
298
299        // Declare Cranelift variables for all live locals
300        let max_local = live_locals.iter().copied().max().unwrap_or(0) as usize;
301        for local_idx in 0..=max_local {
302            builder.declare_var(Variable::new(local_idx), types::I64);
303        }
304        // Declare compile-time stack variables (generous upper bound)
305        let stack_var_base = JIT_LOCALS_CAP;
306        let max_stack_depth = 32usize;
307        for s in 0..max_stack_depth {
308            builder.declare_var(Variable::new(stack_var_base + s), types::I64);
309        }
310
311        // ---- Entry block: load live locals from JIT context buffer ----
312        builder.append_block_params_for_function_params(entry_block);
313        builder.switch_to_block(entry_block);
314        builder.seal_block(entry_block);
315
316        let ctx_ptr = builder.block_params(entry_block)[0];
317
318        // Load live locals from context buffer
319        for &local_idx in &live_locals {
320            let offset = LOCALS_BYTE_OFFSET + (local_idx as i32) * 8;
321            let val = builder
322                .ins()
323                .load(types::I64, MemFlags::trusted(), ctx_ptr, offset);
324            builder.def_var(Variable::new(local_idx as usize), val);
325        }
326
327        // Jump to loop header block
328        builder.ins().jump(header_block, &[]);
329
330        // ---- Compile loop body instructions ----
331        // Compile-time operand stack depth tracker
332        let mut stack_depth: usize = 0;
333        // Manual block termination tracking (replaces builder.is_filled())
334        let mut block_terminated: bool = false;
335
336        macro_rules! stack_push {
337            ($builder:expr, $val:expr, $depth:expr) => {{
338                let var = Variable::new(stack_var_base + $depth);
339                $builder.def_var(var, $val);
340                $depth += 1;
341            }};
342        }
343        macro_rules! stack_pop {
344            ($builder:expr, $depth:expr) => {{
345                $depth -= 1;
346                let var = Variable::new(stack_var_base + $depth);
347                $builder.use_var(var)
348            }};
349        }
350
351        for idx in loop_info.header_idx..=loop_info.end_idx {
352            // Switch to the block for this instruction if one exists
353            if let Some(&blk) = block_map.get(&idx) {
354                if idx != loop_info.header_idx || block_terminated {
355                    if !block_terminated {
356                        builder.ins().jump(blk, &[]);
357                    }
358                }
359                builder.switch_to_block(blk);
360                block_terminated = false;
361                // Don't seal loop header yet (it has a back-edge)
362                if idx != loop_info.header_idx {
363                    builder.seal_block(blk);
364                }
365            }
366
367            // Skip instruction emission if block already terminated
368            if block_terminated {
369                continue;
370            }
371
372            let instr = &instructions[idx];
373            match instr.opcode {
374                OpCode::Nop | OpCode::LoopStart => {
375                    // No-ops in JIT
376                }
377
378                OpCode::LoopEnd => {
379                    // Back-edge: jump to header
380                    builder.ins().jump(header_block, &[]);
381                    block_terminated = true;
382                }
383
384                OpCode::PushNull => {
385                    let null = builder
386                        .ins()
387                        .iconst(types::I64, crate::ffi::value_ffi::TAG_NULL as i64);
388                    stack_push!(builder, null, stack_depth);
389                }
390
391                OpCode::PushConst => {
392                    if let Some(Operand::Const(_const_idx)) = instr.operand {
393                        // For OSR MVP, we deopt on constants we can't resolve inline.
394                        // The JitCompilationBackend will provide constant resolution
395                        // in a future pass.
396                        let null = builder
397                            .ins()
398                            .iconst(types::I64, crate::ffi::value_ffi::TAG_NULL as i64);
399                        stack_push!(builder, null, stack_depth);
400                    }
401                }
402
403                OpCode::Pop => {
404                    if stack_depth > 0 {
405                        let _ = stack_pop!(builder, stack_depth);
406                    }
407                }
408
409                OpCode::Dup => {
410                    if stack_depth > 0 {
411                        let var = Variable::new(stack_var_base + stack_depth - 1);
412                        let val = builder.use_var(var);
413                        stack_push!(builder, val, stack_depth);
414                    }
415                }
416
417                OpCode::Swap => {
418                    if stack_depth >= 2 {
419                        let var_a = Variable::new(stack_var_base + stack_depth - 1);
420                        let var_b = Variable::new(stack_var_base + stack_depth - 2);
421                        let a = builder.use_var(var_a);
422                        let b = builder.use_var(var_b);
423                        builder.def_var(var_a, b);
424                        builder.def_var(var_b, a);
425                    }
426                }
427
428                OpCode::LoadLocal | OpCode::LoadLocalTrusted => {
429                    if let Some(Operand::Local(local_idx)) = instr.operand {
430                        let val = builder.use_var(Variable::new(local_idx as usize));
431                        stack_push!(builder, val, stack_depth);
432                    }
433                }
434
435                OpCode::StoreLocal => {
436                    if let Some(Operand::Local(local_idx)) = instr.operand {
437                        if stack_depth > 0 {
438                            let val = stack_pop!(builder, stack_depth);
439                            builder.def_var(Variable::new(local_idx as usize), val);
440                        }
441                    }
442                }
443
444                OpCode::StoreLocalTyped => {
445                    if let Some(Operand::TypedLocal(local_idx, _width)) = instr.operand {
446                        if stack_depth > 0 {
447                            let val = stack_pop!(builder, stack_depth);
448                            // OSR MVP: store without truncation (width enforcement
449                            // is done by the interpreter; JIT uses same raw i64).
450                            builder.def_var(Variable::new(local_idx as usize), val);
451                        }
452                    }
453                }
454
455                // Integer arithmetic: values in JIT context are raw i64 for Int64 slots.
456                OpCode::AddInt => {
457                    if stack_depth >= 2 {
458                        let b = stack_pop!(builder, stack_depth);
459                        let a = stack_pop!(builder, stack_depth);
460                        let result = builder.ins().iadd(a, b);
461                        stack_push!(builder, result, stack_depth);
462                    }
463                }
464                OpCode::SubInt => {
465                    if stack_depth >= 2 {
466                        let b = stack_pop!(builder, stack_depth);
467                        let a = stack_pop!(builder, stack_depth);
468                        let result = builder.ins().isub(a, b);
469                        stack_push!(builder, result, stack_depth);
470                    }
471                }
472                OpCode::MulInt => {
473                    if stack_depth >= 2 {
474                        let b = stack_pop!(builder, stack_depth);
475                        let a = stack_pop!(builder, stack_depth);
476                        let result = builder.ins().imul(a, b);
477                        stack_push!(builder, result, stack_depth);
478                    }
479                }
480                OpCode::DivInt => {
481                    if stack_depth >= 2 {
482                        let b = stack_pop!(builder, stack_depth);
483                        let a = stack_pop!(builder, stack_depth);
484                        let result = builder.ins().sdiv(a, b);
485                        stack_push!(builder, result, stack_depth);
486                    }
487                }
488                OpCode::ModInt => {
489                    if stack_depth >= 2 {
490                        let b = stack_pop!(builder, stack_depth);
491                        let a = stack_pop!(builder, stack_depth);
492                        let result = builder.ins().srem(a, b);
493                        stack_push!(builder, result, stack_depth);
494                    }
495                }
496                OpCode::PowInt => {
497                    // Power is complex — deopt for now
498                    builder.ins().jump(deopt_block, &[]);
499                    block_terminated = true;
500                }
501
502                // Float arithmetic: values are NaN-boxed f64 bit patterns.
503                // Bitcast to f64, operate, bitcast back.
504                OpCode::AddNumber => {
505                    if stack_depth >= 2 {
506                        let b = stack_pop!(builder, stack_depth);
507                        let a = stack_pop!(builder, stack_depth);
508                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
509                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
510                        let r_f = builder.ins().fadd(a_f, b_f);
511                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), r_f);
512                        stack_push!(builder, result, stack_depth);
513                    }
514                }
515                OpCode::SubNumber => {
516                    if stack_depth >= 2 {
517                        let b = stack_pop!(builder, stack_depth);
518                        let a = stack_pop!(builder, stack_depth);
519                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
520                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
521                        let r_f = builder.ins().fsub(a_f, b_f);
522                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), r_f);
523                        stack_push!(builder, result, stack_depth);
524                    }
525                }
526                OpCode::MulNumber => {
527                    if stack_depth >= 2 {
528                        let b = stack_pop!(builder, stack_depth);
529                        let a = stack_pop!(builder, stack_depth);
530                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
531                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
532                        let r_f = builder.ins().fmul(a_f, b_f);
533                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), r_f);
534                        stack_push!(builder, result, stack_depth);
535                    }
536                }
537                OpCode::DivNumber => {
538                    if stack_depth >= 2 {
539                        let b = stack_pop!(builder, stack_depth);
540                        let a = stack_pop!(builder, stack_depth);
541                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
542                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
543                        let r_f = builder.ins().fdiv(a_f, b_f);
544                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), r_f);
545                        stack_push!(builder, result, stack_depth);
546                    }
547                }
548                OpCode::ModNumber => {
549                    if stack_depth >= 2 {
550                        let b = stack_pop!(builder, stack_depth);
551                        let a = stack_pop!(builder, stack_depth);
552                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
553                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
554                        // fmod: a - trunc(a/b) * b
555                        let div = builder.ins().fdiv(a_f, b_f);
556                        let trunced = builder.ins().trunc(div);
557                        let prod = builder.ins().fmul(trunced, b_f);
558                        let r_f = builder.ins().fsub(a_f, prod);
559                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), r_f);
560                        stack_push!(builder, result, stack_depth);
561                    }
562                }
563                OpCode::PowNumber => {
564                    // Power is complex — deopt
565                    builder.ins().jump(deopt_block, &[]);
566                    block_terminated = true;
567                }
568
569                OpCode::NegInt => {
570                    if stack_depth >= 1 {
571                        let val = stack_pop!(builder, stack_depth);
572                        let result = builder.ins().ineg(val);
573                        stack_push!(builder, result, stack_depth);
574                    }
575                }
576                OpCode::NegNumber => {
577                    if stack_depth >= 1 {
578                        let val = stack_pop!(builder, stack_depth);
579                        let f = builder.ins().bitcast(types::F64, MemFlags::new(), val);
580                        let neg_f = builder.ins().fneg(f);
581                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), neg_f);
582                        stack_push!(builder, result, stack_depth);
583                    }
584                }
585
586                // Integer comparisons: compare raw i64, produce i64 (0 or 1)
587                OpCode::LtInt => {
588                    if stack_depth >= 2 {
589                        let b = stack_pop!(builder, stack_depth);
590                        let a = stack_pop!(builder, stack_depth);
591                        let cmp = builder.ins().icmp(IntCC::SignedLessThan, a, b);
592                        let result = builder.ins().uextend(types::I64, cmp);
593                        stack_push!(builder, result, stack_depth);
594                    }
595                }
596                OpCode::GtInt => {
597                    if stack_depth >= 2 {
598                        let b = stack_pop!(builder, stack_depth);
599                        let a = stack_pop!(builder, stack_depth);
600                        let cmp = builder.ins().icmp(IntCC::SignedGreaterThan, a, b);
601                        let result = builder.ins().uextend(types::I64, cmp);
602                        stack_push!(builder, result, stack_depth);
603                    }
604                }
605                OpCode::LteInt => {
606                    if stack_depth >= 2 {
607                        let b = stack_pop!(builder, stack_depth);
608                        let a = stack_pop!(builder, stack_depth);
609                        let cmp = builder.ins().icmp(IntCC::SignedLessThanOrEqual, a, b);
610                        let result = builder.ins().uextend(types::I64, cmp);
611                        stack_push!(builder, result, stack_depth);
612                    }
613                }
614                OpCode::GteInt => {
615                    if stack_depth >= 2 {
616                        let b = stack_pop!(builder, stack_depth);
617                        let a = stack_pop!(builder, stack_depth);
618                        let cmp = builder.ins().icmp(IntCC::SignedGreaterThanOrEqual, a, b);
619                        let result = builder.ins().uextend(types::I64, cmp);
620                        stack_push!(builder, result, stack_depth);
621                    }
622                }
623                OpCode::EqInt => {
624                    if stack_depth >= 2 {
625                        let b = stack_pop!(builder, stack_depth);
626                        let a = stack_pop!(builder, stack_depth);
627                        let cmp = builder.ins().icmp(IntCC::Equal, a, b);
628                        let result = builder.ins().uextend(types::I64, cmp);
629                        stack_push!(builder, result, stack_depth);
630                    }
631                }
632                OpCode::NeqInt => {
633                    if stack_depth >= 2 {
634                        let b = stack_pop!(builder, stack_depth);
635                        let a = stack_pop!(builder, stack_depth);
636                        let cmp = builder.ins().icmp(IntCC::NotEqual, a, b);
637                        let result = builder.ins().uextend(types::I64, cmp);
638                        stack_push!(builder, result, stack_depth);
639                    }
640                }
641
642                // Float comparisons: bitcast to f64, compare, produce i64
643                OpCode::LtNumber => {
644                    if stack_depth >= 2 {
645                        let b = stack_pop!(builder, stack_depth);
646                        let a = stack_pop!(builder, stack_depth);
647                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
648                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
649                        let cmp = builder.ins().fcmp(FloatCC::LessThan, a_f, b_f);
650                        let result = builder.ins().uextend(types::I64, cmp);
651                        stack_push!(builder, result, stack_depth);
652                    }
653                }
654                OpCode::GtNumber => {
655                    if stack_depth >= 2 {
656                        let b = stack_pop!(builder, stack_depth);
657                        let a = stack_pop!(builder, stack_depth);
658                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
659                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
660                        let cmp = builder.ins().fcmp(FloatCC::GreaterThan, a_f, b_f);
661                        let result = builder.ins().uextend(types::I64, cmp);
662                        stack_push!(builder, result, stack_depth);
663                    }
664                }
665                OpCode::LteNumber => {
666                    if stack_depth >= 2 {
667                        let b = stack_pop!(builder, stack_depth);
668                        let a = stack_pop!(builder, stack_depth);
669                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
670                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
671                        let cmp = builder.ins().fcmp(FloatCC::LessThanOrEqual, a_f, b_f);
672                        let result = builder.ins().uextend(types::I64, cmp);
673                        stack_push!(builder, result, stack_depth);
674                    }
675                }
676                OpCode::GteNumber => {
677                    if stack_depth >= 2 {
678                        let b = stack_pop!(builder, stack_depth);
679                        let a = stack_pop!(builder, stack_depth);
680                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
681                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
682                        let cmp = builder.ins().fcmp(FloatCC::GreaterThanOrEqual, a_f, b_f);
683                        let result = builder.ins().uextend(types::I64, cmp);
684                        stack_push!(builder, result, stack_depth);
685                    }
686                }
687                OpCode::EqNumber => {
688                    if stack_depth >= 2 {
689                        let b = stack_pop!(builder, stack_depth);
690                        let a = stack_pop!(builder, stack_depth);
691                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
692                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
693                        let cmp = builder.ins().fcmp(FloatCC::Equal, a_f, b_f);
694                        let result = builder.ins().uextend(types::I64, cmp);
695                        stack_push!(builder, result, stack_depth);
696                    }
697                }
698                OpCode::NeqNumber => {
699                    if stack_depth >= 2 {
700                        let b = stack_pop!(builder, stack_depth);
701                        let a = stack_pop!(builder, stack_depth);
702                        let a_f = builder.ins().bitcast(types::F64, MemFlags::new(), a);
703                        let b_f = builder.ins().bitcast(types::F64, MemFlags::new(), b);
704                        let cmp = builder.ins().fcmp(FloatCC::NotEqual, a_f, b_f);
705                        let result = builder.ins().uextend(types::I64, cmp);
706                        stack_push!(builder, result, stack_depth);
707                    }
708                }
709
710                // EqString: compare two string pointers via FFI (deopt for safety)
711                OpCode::EqString => {
712                    // String equality requires heap comparison — deopt to interpreter.
713                    builder.ins().jump(deopt_block, &[]);
714                    block_terminated = true;
715                }
716                // GtString/LtString/GteString/LteString: string ordered comparison (deopt)
717                OpCode::GtString | OpCode::LtString | OpCode::GteString | OpCode::LteString => {
718                    // String ordered comparison requires heap comparison — deopt to interpreter.
719                    builder.ins().jump(deopt_block, &[]);
720                    block_terminated = true;
721                }
722                // EqDecimal: compare two decimal pointers via FFI (deopt for safety)
723                OpCode::EqDecimal => {
724                    // Decimal equality requires heap comparison — deopt to interpreter.
725                    builder.ins().jump(deopt_block, &[]);
726                    block_terminated = true;
727                }
728                // IsNull: check if top-of-stack is null (0 / tagged null)
729                OpCode::IsNull => {
730                    if stack_depth >= 1 {
731                        let val = stack_pop!(builder, stack_depth);
732                        let zero = builder.ins().iconst(types::I64, 0);
733                        let cmp = builder.ins().icmp(IntCC::Equal, val, zero);
734                        let result = builder.ins().uextend(types::I64, cmp);
735                        stack_push!(builder, result, stack_depth);
736                    }
737                }
738
739                // Logic: operands are i64 (0 = false, nonzero = true)
740                OpCode::And => {
741                    if stack_depth >= 2 {
742                        let b = stack_pop!(builder, stack_depth);
743                        let a = stack_pop!(builder, stack_depth);
744                        let result = builder.ins().band(a, b);
745                        stack_push!(builder, result, stack_depth);
746                    }
747                }
748                OpCode::Or => {
749                    if stack_depth >= 2 {
750                        let b = stack_pop!(builder, stack_depth);
751                        let a = stack_pop!(builder, stack_depth);
752                        let result = builder.ins().bor(a, b);
753                        stack_push!(builder, result, stack_depth);
754                    }
755                }
756                OpCode::Not => {
757                    if stack_depth >= 1 {
758                        let val = stack_pop!(builder, stack_depth);
759                        let zero = builder.ins().iconst(types::I64, 0);
760                        let cmp = builder.ins().icmp(IntCC::Equal, val, zero);
761                        let result = builder.ins().uextend(types::I64, cmp);
762                        stack_push!(builder, result, stack_depth);
763                    }
764                }
765
766                // Coercion
767                OpCode::IntToNumber => {
768                    if stack_depth >= 1 {
769                        let val = stack_pop!(builder, stack_depth);
770                        // Raw i64 → f64 → bitcast to i64 (NaN-boxed)
771                        let f = builder.ins().fcvt_from_sint(types::F64, val);
772                        let result = builder.ins().bitcast(types::I64, MemFlags::new(), f);
773                        stack_push!(builder, result, stack_depth);
774                    }
775                }
776                OpCode::NumberToInt => {
777                    if stack_depth >= 1 {
778                        let val = stack_pop!(builder, stack_depth);
779                        // NaN-boxed f64 → f64 → truncate to i64
780                        let f = builder.ins().bitcast(types::F64, MemFlags::new(), val);
781                        let result = builder.ins().fcvt_to_sint_sat(types::I64, f);
782                        stack_push!(builder, result, stack_depth);
783                    }
784                }
785
786                OpCode::CastWidth => {
787                    if stack_depth >= 1 {
788                        if let Some(Operand::Width(width)) = &instr.operand {
789                            if let Some(int_w) = width.to_int_width() {
790                                let val = stack_pop!(builder, stack_depth);
791                                let mask = int_w.mask() as i64;
792                                let mask_val = builder.ins().iconst(types::I64, mask);
793                                let truncated = builder.ins().band(val, mask_val);
794                                let result = if int_w.is_signed() {
795                                    let bits = int_w.bits() as i64;
796                                    let shift = 64 - bits;
797                                    let shift_val = builder.ins().iconst(types::I64, shift);
798                                    let shifted = builder.ins().ishl(truncated, shift_val);
799                                    builder.ins().sshr(shifted, shift_val)
800                                } else {
801                                    truncated
802                                };
803                                stack_push!(builder, result, stack_depth);
804                            }
805                        }
806                    }
807                }
808
809                // Control flow
810                OpCode::Jump => {
811                    if let Some(Operand::Offset(off)) = instr.operand {
812                        let target = (idx as i64 + off as i64 + 1) as usize;
813                        if target > loop_info.end_idx {
814                            builder.ins().jump(exit_block, &[]);
815                        } else if let Some(&blk) = block_map.get(&target) {
816                            builder.ins().jump(blk, &[]);
817                        } else {
818                            builder.ins().jump(deopt_block, &[]);
819                        }
820                        block_terminated = true;
821                    }
822                }
823
824                OpCode::JumpIfFalse | OpCode::JumpIfFalseTrusted => {
825                    if let Some(Operand::Offset(off)) = instr.operand {
826                        let target = (idx as i64 + off as i64 + 1) as usize;
827                        if stack_depth > 0 {
828                            let cond = stack_pop!(builder, stack_depth);
829                            let zero = builder.ins().iconst(types::I64, 0);
830                            let is_false = builder.ins().icmp(IntCC::Equal, cond, zero);
831
832                            let target_block = if target > loop_info.end_idx {
833                                exit_block
834                            } else {
835                                block_map.get(&target).copied().unwrap_or(deopt_block)
836                            };
837                            let fall_through =
838                                block_map.get(&(idx + 1)).copied().unwrap_or(deopt_block);
839
840                            builder
841                                .ins()
842                                .brif(is_false, target_block, &[], fall_through, &[]);
843                            block_terminated = true;
844                        }
845                    }
846                }
847
848                OpCode::JumpIfTrue => {
849                    if let Some(Operand::Offset(off)) = instr.operand {
850                        let target = (idx as i64 + off as i64 + 1) as usize;
851                        if stack_depth > 0 {
852                            let cond = stack_pop!(builder, stack_depth);
853                            let zero = builder.ins().iconst(types::I64, 0);
854                            let is_true = builder.ins().icmp(IntCC::NotEqual, cond, zero);
855
856                            let target_block = if target > loop_info.end_idx {
857                                exit_block
858                            } else {
859                                block_map.get(&target).copied().unwrap_or(deopt_block)
860                            };
861                            let fall_through =
862                                block_map.get(&(idx + 1)).copied().unwrap_or(deopt_block);
863
864                            builder
865                                .ins()
866                                .brif(is_true, target_block, &[], fall_through, &[]);
867                            block_terminated = true;
868                        }
869                    }
870                }
871
872                OpCode::Break => {
873                    builder.ins().jump(exit_block, &[]);
874                    block_terminated = true;
875                }
876
877                OpCode::Continue => {
878                    builder.ins().jump(header_block, &[]);
879                    block_terminated = true;
880                }
881
882                OpCode::Return | OpCode::ReturnValue => {
883                    builder.ins().jump(exit_block, &[]);
884                    block_terminated = true;
885                }
886
887                OpCode::Halt => {
888                    builder.ins().jump(exit_block, &[]);
889                    block_terminated = true;
890                }
891
892                // Module bindings: not in JIT context buffer. Deopt if encountered.
893                OpCode::LoadModuleBinding
894                | OpCode::StoreModuleBinding
895                | OpCode::StoreModuleBindingTyped => {
896                    builder.ins().jump(deopt_block, &[]);
897                    block_terminated = true;
898                }
899
900                // Builtin calls: math functions. Deopt for MVP.
901                OpCode::BuiltinCall => {
902                    builder.ins().jump(deopt_block, &[]);
903                    block_terminated = true;
904                }
905
906                _ => {
907                    // Unsupported opcode — should have been caught by preflight
908                    builder.ins().jump(deopt_block, &[]);
909                    block_terminated = true;
910                }
911            }
912        }
913
914        // Seal the loop header block (all predecessors are now known)
915        builder.seal_block(header_block);
916
917        // ---- Exit block: store modified locals back, return 0 ----
918        builder.switch_to_block(exit_block);
919        builder.seal_block(exit_block);
920
921        for &local_idx in &live_locals {
922            if body_locals_written.contains(&local_idx) {
923                let val = builder.use_var(Variable::new(local_idx as usize));
924                let offset = LOCALS_BYTE_OFFSET + (local_idx as i32) * 8;
925                builder
926                    .ins()
927                    .store(MemFlags::trusted(), val, ctx_ptr, offset);
928            }
929        }
930        let zero_ret = builder.ins().iconst(types::I64, 0);
931        builder.ins().return_(&[zero_ret]);
932
933        // ---- Deopt block: store ALL live locals back, return u64::MAX ----
934        builder.switch_to_block(deopt_block);
935        builder.seal_block(deopt_block);
936
937        for &local_idx in &live_locals {
938            let val = builder.use_var(Variable::new(local_idx as usize));
939            let offset = LOCALS_BYTE_OFFSET + (local_idx as i32) * 8;
940            builder
941                .ins()
942                .store(MemFlags::trusted(), val, ctx_ptr, offset);
943        }
944        let deopt_sentinel = builder.ins().iconst(types::I64, u64::MAX as i64);
945        builder.ins().return_(&[deopt_sentinel]);
946
947        builder.finalize();
948    }
949
950    // Compile and define the function
951    jit.module_mut()
952        .define_function(func_id, &mut ctx)
953        .map_err(|e| format!("Failed to define OSR function: {}", e))?;
954    jit.module_mut().clear_context(&mut ctx);
955    jit.module_mut()
956        .finalize_definitions()
957        .map_err(|e| format!("Failed to finalize OSR function: {}", e))?;
958
959    let code_ptr = jit.module_mut().get_finalized_function(func_id);
960
961    Ok(OsrCompilationResult {
962        native_code: code_ptr,
963        entry_point,
964        deopt_points: Vec::new(),
965    })
966}