///|
fn emit_load_vmctx_for_gc(
  ctx : @lower.LoweringContext,
  block : @block.Block,
) -> @abi.VReg {
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  vmctx_vreg
}

///|
fn emit_load_stack_args_base(
  ctx : @lower.LoweringContext,
  block : @block.Block,
) -> @abi.VReg {
  let sp_vreg = ctx.machv_function().new_vreg(Int)
  let load_sp = @instr.Inst(LoadSP)
  load_sp.add_def({ reg: Virtual(sp_vreg) })
  block.add_inst(load_sp)
  sp_vreg
}

///|
fn emit_load_safepoint_id(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  root_count : Int,
) -> @abi.VReg {
  let safepoint_vreg = ctx.machv_function().new_vreg(Int)
  let load_safepoint = @instr.Inst(LoadSafepointId(root_count))
  load_safepoint.add_def({ reg: Virtual(safepoint_vreg) })
  block.add_inst(load_safepoint)
  safepoint_vreg
}

///|
fn emit_gc_struct_slow_alloc(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  dst : @abi.VReg,
  type_idx : Int,
  num_fields : Int,
) -> Unit {
  let vmctx_vreg = emit_load_vmctx_for_gc(ctx, block)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let fields_ptr_vreg = emit_load_stack_args_base(ctx, block)
  let num_fields_vreg = @lower.materialize_imm(
    ctx,
    block,
    num_fields.to_int64(),
  )
  let safepoint_vreg = emit_load_safepoint_id(ctx, block, num_fields)
  @lower.lower_c_libcall_direct(
    ctx,
    block,
    rt_gc_alloc_struct_slow(ctx),
    [
      vmctx_vreg, type_idx_vreg, fields_ptr_vreg, num_fields_vreg, safepoint_vreg,
    ],
    Some(dst),
  )
}

///|
fn emit_gc_array_slow_alloc(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  dst : @abi.VReg,
  type_idx : Int,
  len_vreg : @abi.VReg,
  init_vreg : @abi.VReg,
) -> Unit {
  let vmctx_vreg = emit_load_vmctx_for_gc(ctx, block)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let safepoint_vreg = emit_load_safepoint_id(ctx, block, 1)
  @lower.lower_c_libcall_direct(
    ctx,
    block,
    rt_gc_alloc_array_slow(ctx),
    [vmctx_vreg, type_idx_vreg, len_vreg, init_vreg, safepoint_vreg],
    Some(dst),
  )
}

///|
fn align_up(value : Int, align : Int) -> Int {
  (value + align - 1) / align * align
}

///|
fn gc_header_low(kind : Int, type_idx : Int, total_size : Int) -> Int64 {
  kind.to_int64() |
  ((type_idx & 0xFFFF).to_int64() << 16) |
  (total_size.to_int64() << 32)
}

///|
fn emit_load_context_slot(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  offset : Int,
) -> @abi.VReg {
  let dst = ctx.new_vreg(Int)
  let inst = @instr.Inst(LoadPtr(I64, offset))
  inst.add_def({ reg: Virtual(dst) })
  inst.add_use(Physical(@lower.context_preg(ctx)))
  block.add_inst(inst)
  dst
}

///|
fn emit_store_context_slot(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  offset : Int,
  value : @abi.VReg,
) -> Unit {
  let inst = @instr.Inst(StorePtr(I64, offset))
  inst.add_use(Physical(@lower.context_preg(ctx)))
  inst.add_use(Virtual(value))
  block.add_inst(inst)
}

///|
fn emit_load_ptr_i64(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  base : @abi.VReg,
  offset : Int,
) -> @abi.VReg {
  let dst = ctx.new_vreg(Int)
  let inst = @instr.Inst(LoadPtr(I64, offset))
  inst.add_def({ reg: Virtual(dst) })
  inst.add_use(Virtual(base))
  block.add_inst(inst)
  dst
}

///|
fn emit_store_ptr_i64(
  block : @block.Block,
  base : @abi.VReg,
  offset : Int,
  value : @abi.VReg,
) -> Unit {
  let inst = @instr.Inst(StorePtr(I64, offset))
  inst.add_use(Virtual(base))
  inst.add_use(Virtual(value))
  block.add_inst(inst)
}

///|
fn emit_add_const_i64(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  lhs : @abi.VReg,
  imm : Int,
) -> @abi.VReg {
  let rhs = @lower.materialize_imm(ctx, block, imm.to_int64())
  let dst = ctx.new_vreg(Int)
  let inst = @instr.Inst(Add(true))
  inst.add_def({ reg: Virtual(dst) })
  inst.add_use(Virtual(lhs))
  inst.add_use(Virtual(rhs))
  block.add_inst(inst)
  dst
}

///|
fn emit_jump_result(
  block : @block.Block,
  done : @block.Block,
  value : @abi.VReg,
) -> Unit {
  block.set_terminator(Jump(done.id, [Virtual(value)]))
}

///|
fn emit_gc_inline_alloc_from_stack(
  ctx : @lower.LoweringContext,
  block : @block.Block,
  dst : @abi.VReg,
  type_idx : Int,
  item_count : Int,
  kind : Int,
  payload_offset : Int,
  register_symbol : @instr.ExternalName,
  slow_symbol : @instr.ExternalName,
) -> @block.Block {
  let gc_header_size = 16
  let gc_align = 16
  let total_size = align_up(payload_offset + item_count * 8, gc_align)
  let header = gc_header_low(kind, type_idx, total_size)
  let layout = ctx.embedding_context_layout()
  let fast_block = ctx.machv_function().new_block()
  let slow_block = ctx.machv_function().new_block()
  let done_block = ctx.machv_function().new_block()
  done_block.params.push(dst)

  let obj = emit_load_context_slot(
    ctx,
    block,
    layout.require_allocation_cursor_offset(),
  )
  let end = emit_add_const_i64(ctx, block, obj, total_size)
  let limit = emit_load_context_slot(
    ctx,
    block,
    layout.require_allocation_limit_offset(),
  )
  block.set_terminator(
    BranchCmp(
      Virtual(end),
      Virtual(limit),
      Hi,
      true,
      slow_block.id,
      fast_block.id,
    ),
  )

  let header_vreg = @lower.materialize_imm(ctx, fast_block, header)
  emit_store_ptr_i64(fast_block, obj, 0, header_vreg)
  if kind == 1 {
    let count_vreg = @lower.materialize_imm(
      ctx,
      fast_block,
      item_count.to_int64(),
    )
    emit_store_ptr_i64(fast_block, obj, 8, count_vreg)
  } else {
    let zero_vreg = @lower.materialize_imm(ctx, fast_block, 0L)
    emit_store_ptr_i64(fast_block, obj, 8, zero_vreg)
    let len_vreg = @lower.materialize_imm(
      ctx,
      fast_block,
      item_count.to_int64(),
    )
    emit_store_ptr_i64(fast_block, obj, gc_header_size, len_vreg)
  }
  let stack_base = emit_load_stack_args_base(ctx, fast_block)
  for i in 0.. Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let src = ctx.get_vreg_for_use(inst.operands[0], block)

  // Step 1: Zero-extend i32 to i64
  let ext_vreg = ctx.machv_function().new_vreg(Int)
  let extend_inst = @instr.Inst(Extend(Unsigned32To64))
  extend_inst.add_def({ reg: Virtual(ext_vreg) })
  extend_inst.add_use(Virtual(src))
  block.add_inst(extend_inst)

  // Step 2: Shift left by immediate 1
  let shifted_vreg = ctx.machv_function().new_vreg(Int)
  let shift_inst = @instr.Inst(ShlImm(1, true))
  shift_inst.add_def({ reg: Virtual(shifted_vreg) })
  shift_inst.add_use(Virtual(ext_vreg))
  block.add_inst(shift_inst)

  // Step 3: OR with immediate 1 to set low bit
  let or_inst = @instr.Inst(OrImm(1L, true))
  or_inst.add_def({ reg: Virtual(dst) })
  or_inst.add_use(Virtual(shifted_vreg))
  block.add_inst(or_inst)
}

///|
/// Lower i31.get_s: Sign-extend 31 bits to i32
/// Encoding is (value << 1) | 1, so:
/// 1. Check for null (0) and trap
/// 2. Shift right by 1 to decode (removes the tag bit)
/// 3. Sign-extend from 31 bits
pub fn lower_i31_get_s(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let src = ctx.get_vreg_for_use(inst.operands[0], block)

  // Step 0: Null check - trap if i31 ref is null (0)
  let trap_inst = @instr.Inst(TrapIfZero(true, 2))
  trap_inst.add_use(Virtual(src))
  block.add_inst(trap_inst)

  // Step 1: Shift right by immediate 1 to decode the i31 value (64-bit)
  let decoded = ctx.new_vreg(Int)
  let lsr_inst = @instr.Inst(LShrImm(1, true))
  lsr_inst.add_def({ reg: Virtual(decoded) })
  lsr_inst.add_use(Virtual(src))
  block.add_inst(lsr_inst)

  // Step 2: Truncate to 32 bits
  let tmp = ctx.new_vreg(Int)
  let trunc_inst = @instr.Inst(Truncate)
  trunc_inst.add_def({ reg: Virtual(tmp) })
  trunc_inst.add_use(Virtual(decoded))
  block.add_inst(trunc_inst)

  // Step 3: Sign-extend from 31 bits by shifting left then arithmetic right
  let tmp2 = ctx.new_vreg(Int)
  let shl_inst = @instr.Inst(ShlImm(1, false))
  shl_inst.add_def({ reg: Virtual(tmp2) })
  shl_inst.add_use(Virtual(tmp))
  block.add_inst(shl_inst)
  let asr_inst = @instr.Inst(AShrImm(1, false))
  asr_inst.add_def({ reg: Virtual(dst) })
  asr_inst.add_use(Virtual(tmp2))
  block.add_inst(asr_inst)
}

///|
/// Lower i31.get_u: Zero-extend 31 bits
/// Encoding is (value << 1) | 1, so:
/// 1. Check for null (0) and trap
/// 2. Shift right by 1 to decode (removes the tag bit)
/// 3. Mask with 0x7FFFFFFF for 31 bits
pub fn lower_i31_get_u(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let src = ctx.get_vreg_for_use(inst.operands[0], block)

  // Step 0: Null check - trap if i31 ref is null (0)
  let trap_inst = @instr.Inst(TrapIfZero(true, 2))
  trap_inst.add_use(Virtual(src))
  block.add_inst(trap_inst)

  // Step 1: Shift right by immediate 1 to decode the i31 value (64-bit)
  let decoded = ctx.new_vreg(Int)
  let lsr_inst = @instr.Inst(LShrImm(1, true))
  lsr_inst.add_def({ reg: Virtual(decoded) })
  lsr_inst.add_use(Virtual(src))
  block.add_inst(lsr_inst)

  // Step 2: Truncate to 32 bits
  let tmp = ctx.new_vreg(Int)
  let trunc_inst = @instr.Inst(Truncate)
  trunc_inst.add_def({ reg: Virtual(tmp) })
  trunc_inst.add_use(Virtual(decoded))
  block.add_inst(trunc_inst)

  // Step 3: Mask with 0x7FFFFFFF for 31 bits (already decoded, just ensure upper bit is clear)
  let mask = ctx.new_vreg(Int)
  let load_mask = @instr.Inst(LoadConst(0x7FFFFFFFL))
  load_mask.add_def({ reg: Virtual(mask) })
  block.add_inst(load_mask)
  let and_inst = @instr.Inst(And(false))
  and_inst.add_def({ reg: Virtual(dst) })
  and_inst.add_use(Virtual(tmp))
  and_inst.add_use(Virtual(mask))
  block.add_inst(and_inst)
}

///|
/// Lower GC type conversions (any.convert_extern, extern.convert_any)
/// These are no-ops in the JIT - just pass the reference through
pub fn lower_gc_convert(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let src = ctx.get_vreg_for_use(inst.operands[0], block)
  // Just move the value - the representation is the same
  let machv_inst = @instr.Inst(Move)
  machv_inst.add_def({ reg: Virtual(dst) })
  machv_inst.add_use(Virtual(src))
  block.add_inst(machv_inst)
}

///|
/// Lower ref.test: Test if reference matches type
/// Uses runtime libcall: gc_ref_test_impl(ref, type_idx, nullable) -> 0 or 1
pub fn lower_ref_test(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  nullable : Bool,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let nullable_vreg = @lower.materialize_imm(
    ctx,
    block,
    if nullable {
      1L
    } else {
      0L
    },
  )
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_ref_test(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call: gc_ref_test_impl(ref, type_idx, nullable) -> result
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, nullable_vreg],
    Some(dst),
  )
}

///|
/// Lower ref.cast: Cast reference to type (traps on failure)
/// Uses runtime libcall: gc_ref_cast_impl(ref, type_idx, nullable) -> ref or trap
pub fn lower_ref_cast(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  nullable : Bool,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let nullable_vreg = @lower.materialize_imm(
    ctx,
    block,
    if nullable {
      1L
    } else {
      0L
    },
  )
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_ref_cast(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call: gc_ref_cast_impl(ref, type_idx, nullable) -> result
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, nullable_vreg],
    Some(dst),
  )
}

///|
/// Lower ref.eq: Compare two references for equality
/// Returns 1 if equal, 0 otherwise
pub fn lower_ref_eq(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref1_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let ref2_vreg = ctx.get_vreg_for_use(inst.operands[1], block)

  // Compare two references as i64 values
  // Use Cmp(Eq, true) for 64-bit equality comparison
  let cmp_inst = @instr.Inst(Cmp(Eq, true))
  cmp_inst.add_def({ reg: Virtual(dst) })
  cmp_inst.add_use(Virtual(ref1_vreg))
  cmp_inst.add_use(Virtual(ref2_vreg))
  block.add_inst(cmp_inst)
}

///|
/// Lower struct.new: Allocate struct with field values
/// Uses runtime libcall:
///   gc_alloc_struct_slow(vmctx, type_idx, fields_ptr, num_fields, safepoint_id) -> struct_ref
pub fn lower_struct_new(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> @block.Block {
  guard inst.first_result() is Some(result) else { return block }
  let dst = ctx.get_vreg(result)
  let num_fields = inst.operands.length()

  // Fast path: inline bump allocation with slow-path fallback.
  // Keep `struct.new_default` as pure slow path (num_fields == 0).
  // Keep a conservative field-count cap for inline lowering.
  if num_fields > 0 && num_fields <= 4092 {
    // Use outgoing args area as temporary field buffer.
    let stack_space = (num_fields * 8 + 15) / 16 * 16
    ctx.machv_function().update_max_outgoing_args_size(stack_space)
    for i in 0.. 0 {
    let stack_space = (num_fields * 8 + 15) / 16 * 16
    ctx.machv_function().update_max_outgoing_args_size(stack_space)
    for i in 0.. struct_ref
pub fn lower_struct_new_default(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  emit_gc_struct_slow_alloc(ctx, block, dst, type_idx, 0)
}

///|
/// Lower struct.get: Get struct field value
/// Uses runtime libcall: gc_struct_get_impl(ref, type_idx, field_idx) -> value
pub fn lower_struct_get(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  field_idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let field_idx_vreg = @lower.materialize_imm(ctx, block, field_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_struct_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Check if result is a float type - C libcall returns int64, need bitcast
  let is_float = result.ty is (F32 | F64)
  if is_float {
    // For float results: call returns int64, then bitcast to float
    let int_result = ctx.machv_function().new_vreg(Int)
    @lower.lower_c_libcall(
      ctx,
      block,
      func_ptr_vreg,
      [ref_vreg, type_idx_vreg, field_idx_vreg],
      Some(int_result),
    )
    // Bitcast from int to float
    let bitcast = @instr.Inst(Bitcast)
    bitcast.add_def({ reg: Virtual(dst) })
    bitcast.add_use(Virtual(int_result))
    block.add_inst(bitcast)
  } else {
    // For integer results: call returns directly
    @lower.lower_c_libcall(
      ctx,
      block,
      func_ptr_vreg,
      [ref_vreg, type_idx_vreg, field_idx_vreg],
      Some(dst),
    )
  }
}

///|
/// Lower struct.get_s: Get struct field value with sign extension
/// Uses runtime libcall then sign extends based on byte_width
pub fn lower_struct_get_s(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  field_idx : Int,
  byte_width : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let field_idx_vreg = @lower.materialize_imm(ctx, block, field_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_struct_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call and get raw value
  let raw_result = ctx.machv_function().new_vreg(Int)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, field_idx_vreg],
    Some(raw_result),
  )
  // Sign extend based on byte_width
  // For i8 (byte_width=1): sign extend byte to i32
  // For i16 (byte_width=2): sign extend halfword to i32
  if byte_width == 1 {
    // SXTB: Sign extend byte to 32-bit
    let ext = @instr.Inst(Extend(Signed8To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else if byte_width == 2 {
    // SXTH: Sign extend halfword to 32-bit
    let ext = @instr.Inst(Extend(Signed16To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else {
    // No extension needed, just move
    let mov = @instr.Inst(Move)
    mov.add_def({ reg: Virtual(dst) })
    mov.add_use(Virtual(raw_result))
    block.add_inst(mov)
  }
}

///|
/// Lower struct.get_u: Get struct field value with zero extension
/// Uses runtime libcall then zero extends based on byte_width
pub fn lower_struct_get_u(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  field_idx : Int,
  byte_width : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let field_idx_vreg = @lower.materialize_imm(ctx, block, field_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_struct_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call and get raw value
  let raw_result = ctx.machv_function().new_vreg(Int)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, field_idx_vreg],
    Some(raw_result),
  )
  // Zero extend based on byte_width
  // For i8 (byte_width=1): mask with 0xFF
  // For i16 (byte_width=2): mask with 0xFFFF
  if byte_width == 1 {
    // UXTB: Zero extend byte to 32-bit
    let ext = @instr.Inst(Extend(Unsigned8To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else if byte_width == 2 {
    // UXTH: Zero extend halfword to 32-bit
    let ext = @instr.Inst(Extend(Unsigned16To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else {
    // No extension needed, just move
    let mov = @instr.Inst(Move)
    mov.add_def({ reg: Virtual(dst) })
    mov.add_use(Virtual(raw_result))
    block.add_inst(mov)
  }
}

///|
/// Lower struct.set: Set struct field value
/// Uses runtime libcall: gc_struct_set_impl(ref, type_idx, field_idx, value)
pub fn lower_struct_set(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  field_idx : Int,
) -> Unit {
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let val_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  // Materialize immediate arguments
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let field_idx_vreg = @lower.materialize_imm(ctx, block, field_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_struct_set(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Check if value is a float type - C libcall expects int64, need bitcast
  let is_float = inst.operands[1].ty is (F32 | F64)
  let actual_val_vreg = if is_float {
    // For float values: bitcast to int before passing to C function
    let int_val = ctx.machv_function().new_vreg(Int)
    let bitcast = @instr.Inst(Bitcast)
    bitcast.add_def({ reg: Virtual(int_val) })
    bitcast.add_use(Virtual(val_vreg))
    block.add_inst(bitcast)
    int_val
  } else {
    val_vreg
  }
  // Call: gc_struct_set_impl(ref, type_idx, field_idx, value)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, field_idx_vreg, actual_val_vreg],
    None,
  )
}

///|
/// Lower array.new: Allocate array with init value and length
/// Uses runtime libcall:
///   gc_alloc_array_slow(vmctx, type_idx, length, init_value, safepoint_id) -> array_ref
pub fn lower_array_new(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let init_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let len_vreg = ctx.get_vreg_for_use(inst.operands[1], block)

  // Check if init value is float - C libcall expects int64, need bitcast
  let is_float = inst.operands[0].ty is (F32 | F64)
  let actual_init_vreg = if is_float {
    let int_val = ctx.machv_function().new_vreg(Int)
    let bitcast = @instr.Inst(Bitcast)
    bitcast.add_def({ reg: Virtual(int_val) })
    bitcast.add_use(Virtual(init_vreg))
    block.add_inst(bitcast)
    int_val
  } else {
    init_vreg
  }
  emit_gc_array_slow_alloc(
    ctx, block, dst, type_idx, len_vreg, actual_init_vreg,
  )
}

///|
/// Lower array.new_default: Allocate array with default values
/// Uses runtime libcall:
///   gc_alloc_array_slow(vmctx, type_idx, length, init_value=0, safepoint_id) -> array_ref
pub fn lower_array_new_default(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let len_vreg = ctx.get_vreg_for_use(inst.operands[0], block)

  // Load default value (0) for the init value.
  let zero_vreg = @lower.materialize_imm(ctx, block, 0L)
  emit_gc_array_slow_alloc(ctx, block, dst, type_idx, len_vreg, zero_vreg)
}

///|
/// Lower array.new_fixed: Allocate fixed-size array with element values
/// Uses runtime libcalls: gc_alloc_array_slow and gc_array_set_impl
pub fn lower_array_new_fixed(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  len : Int,
) -> @block.Block {
  guard inst.first_result() is Some(result) else { return block }
  let dst = ctx.get_vreg(result)

  // Fast path: inline fixed-array allocation with slow-path fallback.
  // Keep a conservative element-count cap for inline lowering.
  if len > 0 && len <= 4092 {
    // Spill all element values to outgoing args area as contiguous int64_t[].
    let stack_space = (len * 8 + 15) / 16 * 16
    ctx.machv_function().update_max_outgoing_args_size(stack_space)
    let is_float = inst.operands[0].ty is (F32 | F64)
    for i in 0.. 1 {
    let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
    let array_set_fp = ctx.machv_function().new_vreg(Int)
    let load_set_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_set(ctx)))
    load_set_fp.add_def({ reg: Virtual(array_set_fp) })
    block.add_inst(load_set_fp)
    for i in 1.. value
pub fn lower_array_get(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let idx_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  // Materialize type_idx
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Check if result is a float type - C libcall returns int64, need bitcast
  let is_float = result.ty is (F32 | F64)
  if is_float {
    // For float results: call returns int64, then bitcast to float
    let int_result = ctx.machv_function().new_vreg(Int)
    @lower.lower_c_libcall(
      ctx,
      block,
      func_ptr_vreg,
      [ref_vreg, type_idx_vreg, idx_vreg],
      Some(int_result),
    )
    // Bitcast from int to float
    let bitcast = @instr.Inst(Bitcast)
    bitcast.add_def({ reg: Virtual(dst) })
    bitcast.add_use(Virtual(int_result))
    block.add_inst(bitcast)
  } else {
    // For integer results: call returns directly
    @lower.lower_c_libcall(
      ctx,
      block,
      func_ptr_vreg,
      [ref_vreg, type_idx_vreg, idx_vreg],
      Some(dst),
    )
  }
}

///|
/// Lower array.get_s: Get array element with sign extension
/// Uses runtime libcall then sign extends based on byte_width
pub fn lower_array_get_s(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  byte_width : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let idx_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  // Materialize type_idx
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call and get raw value
  let raw_result = ctx.machv_function().new_vreg(Int)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, idx_vreg],
    Some(raw_result),
  )
  // Sign extend based on byte_width
  if byte_width == 1 {
    let ext = @instr.Inst(Extend(Signed8To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else if byte_width == 2 {
    let ext = @instr.Inst(Extend(Signed16To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else {
    let mov = @instr.Inst(Move)
    mov.add_def({ reg: Virtual(dst) })
    mov.add_use(Virtual(raw_result))
    block.add_inst(mov)
  }
}

///|
/// Lower array.get_u: Get array element with zero extension
/// Uses runtime libcall then zero extends based on byte_width
pub fn lower_array_get_u(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  byte_width : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let idx_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  // Materialize type_idx
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_get(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call and get raw value
  let raw_result = ctx.machv_function().new_vreg(Int)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, idx_vreg],
    Some(raw_result),
  )
  // Zero extend based on byte_width
  if byte_width == 1 {
    let ext = @instr.Inst(Extend(Unsigned8To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else if byte_width == 2 {
    let ext = @instr.Inst(Extend(Unsigned16To32))
    ext.add_def({ reg: Virtual(dst) })
    ext.add_use(Virtual(raw_result))
    block.add_inst(ext)
  } else {
    let mov = @instr.Inst(Move)
    mov.add_def({ reg: Virtual(dst) })
    mov.add_use(Virtual(raw_result))
    block.add_inst(mov)
  }
}

///|
/// Lower array.set: Set array element
/// Uses runtime libcall: gc_array_set_impl(ref, type_idx, idx, value)
pub fn lower_array_set(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
) -> Unit {
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let idx_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let val_vreg = ctx.get_vreg_for_use(inst.operands[2], block)
  // Materialize type_idx
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_set(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Check if value is a float type - C libcall expects int64, need bitcast
  let is_float = inst.operands[2].ty is (F32 | F64)
  let actual_val_vreg = if is_float {
    // For float values: bitcast to int before passing to C function
    let int_val = ctx.machv_function().new_vreg(Int)
    let bitcast = @instr.Inst(Bitcast)
    bitcast.add_def({ reg: Virtual(int_val) })
    bitcast.add_use(Virtual(val_vreg))
    block.add_inst(bitcast)
    int_val
  } else {
    val_vreg
  }
  // Call: gc_array_set_impl(ref, type_idx, idx, value)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, type_idx_vreg, idx_vreg, actual_val_vreg],
    None,
  )
}

///|
/// Lower array.len: Get array length
/// Uses runtime libcall: gc_array_len_impl(ref) -> length
pub fn lower_array_len(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_len(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call: gc_array_len_impl(ref) -> result
  @lower.lower_c_libcall(ctx, block, func_ptr_vreg, [ref_vreg], Some(dst))
}

///|
/// Lower array.fill: Fill array elements with a value
/// Uses runtime libcall: gc_array_fill_impl(ref, offset, value, count)
pub fn lower_array_fill(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  _type_idx : Int,
) -> Unit {
  // Operands: ref, offset, value, count
  let ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let offset_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let val_vreg = ctx.get_vreg_for_use(inst.operands[2], block)
  let count_vreg = ctx.get_vreg_for_use(inst.operands[3], block)
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_fill(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call: gc_array_fill_impl(ref, offset, value, count)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [ref_vreg, offset_vreg, val_vreg, count_vreg],
    None,
  )
}

///|
/// Lower array.copy: Copy elements between arrays
/// Uses runtime libcall: gc_array_copy_impl(dst_ref, dst_off, src_ref, src_off, count)
pub fn lower_array_copy(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  _dst_type : Int,
  _src_type : Int,
) -> Unit {
  // Operands: dst_ref, dst_offset, src_ref, src_offset, count
  let dst_ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let dst_off_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let src_ref_vreg = ctx.get_vreg_for_use(inst.operands[2], block)
  let src_off_vreg = ctx.get_vreg_for_use(inst.operands[3], block)
  let count_vreg = ctx.get_vreg_for_use(inst.operands[4], block)
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_copy(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Call: gc_array_copy_impl(dst_ref, dst_off, src_ref, src_off, count)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [dst_ref_vreg, dst_off_vreg, src_ref_vreg, src_off_vreg, count_vreg],
    None,
  )
}

///|
/// Lower array.new_data instruction
/// Creates an array from a data segment
pub fn lower_array_new_data(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  data_idx : Int,
) -> Unit {
  // Operands: data_offset, length
  let data_off_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let length_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let result_vreg = match inst.first_result() {
    Some(r) => ctx.get_vreg(r)
    None => ctx.machv_function().new_vreg(Int)
  }
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_new_data(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Get vmctx
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let data_idx_vreg = @lower.materialize_imm(ctx, block, data_idx.to_int64())
  // Call: gc_array_new_data_impl(vmctx, type_idx, data_idx, data_off, length) -> arrayref
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, type_idx_vreg, data_idx_vreg, data_off_vreg, length_vreg],
    Some(result_vreg),
  )
}

///|
/// Lower array.new_elem instruction
/// Creates an array from an element segment
pub fn lower_array_new_elem(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  elem_idx : Int,
) -> Unit {
  // Operands: elem_offset, length
  let elem_off_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let length_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let result_vreg = match inst.first_result() {
    Some(r) => ctx.get_vreg(r)
    None => ctx.machv_function().new_vreg(Int)
  }
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_new_elem(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Get vmctx
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let elem_idx_vreg = @lower.materialize_imm(ctx, block, elem_idx.to_int64())
  // Call: gc_array_new_elem_impl(vmctx, type_idx, elem_idx, elem_off, length) -> arrayref
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, type_idx_vreg, elem_idx_vreg, elem_off_vreg, length_vreg],
    Some(result_vreg),
  )
}

///|
/// Lower array.init_data instruction
/// Initializes array elements from a data segment
pub fn lower_array_init_data(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  data_idx : Int,
) -> Unit {
  // Operands: arrayref, arr_offset, data_offset, length
  let array_ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let arr_off_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let data_off_vreg = ctx.get_vreg_for_use(inst.operands[2], block)
  let length_vreg = ctx.get_vreg_for_use(inst.operands[3], block)
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_init_data(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Get vmctx
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let data_idx_vreg = @lower.materialize_imm(ctx, block, data_idx.to_int64())
  // Call: gc_array_init_data_impl(vmctx, type_idx, data_idx, arrayref, arr_off, data_off, length)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [
      vmctx_vreg, type_idx_vreg, data_idx_vreg, array_ref_vreg, arr_off_vreg, data_off_vreg,
      length_vreg,
    ],
    None,
  )
}

///|
/// Lower array.init_elem instruction
/// Initializes array elements from an element segment
pub fn lower_array_init_elem(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  type_idx : Int,
  elem_idx : Int,
) -> Unit {
  // Operands: arrayref, arr_offset, elem_offset, length
  let array_ref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)
  let arr_off_vreg = ctx.get_vreg_for_use(inst.operands[1], block)
  let elem_off_vreg = ctx.get_vreg_for_use(inst.operands[2], block)
  let length_vreg = ctx.get_vreg_for_use(inst.operands[3], block)
  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_gc_array_init_elem(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)
  // Get vmctx
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  let type_idx_vreg = @lower.materialize_imm(ctx, block, type_idx.to_int64())
  let elem_idx_vreg = @lower.materialize_imm(ctx, block, elem_idx.to_int64())
  // Call: gc_array_init_elem_impl(vmctx, type_idx, elem_idx, arrayref, arr_off, elem_off, length)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [
      vmctx_vreg, type_idx_vreg, elem_idx_vreg, array_ref_vreg, arr_off_vreg, elem_off_vreg,
      length_vreg,
    ],
    None,
  )
}