Skip to main content

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