///|
/// Exception Handling Lowering
/// Lowers IR exception opcodes to MachV instructions that call C runtime helpers
///|
/// Helper to get vmctx register as vreg
fn emit_load_vmctx(
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
}
///|
/// Lower Throw(tag_idx) instruction
/// Calls exception_throw(ctx, tag_idx, values_ptr, count)
pub fn lower_throw(
ctx : @lower.LoweringContext,
inst : @milkir.Inst,
block : @block.Block,
tag_idx : Int,
) -> Unit {
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize tag index
let tag_idx_vreg = @lower.materialize_imm(ctx, block, tag_idx.to_int64())
// Allocate space for exception values (if any).
//
// Important: do NOT dynamically adjust SP here.
// Our JIT assumes a fixed SP within the function body (after prologue), so
// spills/stack slots use constant offsets from SP.
//
// Instead, reuse the pre-allocated outgoing-args area at SP and ensure it is
// large enough for the values array.
let num_operands = inst.operands.length()
let (values_ptr_vreg, count_vreg) = if num_operands > 0 {
let stack_space = (num_operands * 8 + 15) / 16 * 16
ctx.machv_function().update_max_outgoing_args_size(stack_space)
// Store each value to the outgoing args area at SP.
for i in 0.. Unit {
guard inst.operands.length() > 0 else { return }
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Get exnref from operand
let exnref_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_exception_throw_ref(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_throw_ref(ctx, exnref)
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, exnref_vreg],
None,
)
}
///|
/// Lower TryTableBegin(handler_id) instruction
/// Calls exception_try_begin(ctx, handler_id) -> jmp_buf ptr
/// Then calls sigsetjmp(jmp_buf_ptr, 0) -> result
///
/// The TryTableBegin IR instruction returns 0 normally, or handler_id on catch.
/// This is the result of sigsetjmp.
pub fn lower_try_table_begin(
ctx : @lower.LoweringContext,
inst : @milkir.Inst,
block : @block.Block,
handler_id : Int,
) -> Unit {
guard inst.first_result() is Some(result) else { return }
let dst = ctx.get_vreg(result)
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize handler_id
let handler_id_vreg = @lower.materialize_imm(
ctx,
block,
handler_id.to_int64(),
)
// Load function pointer for try_begin
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_try_begin(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_try_begin(ctx, handler_id) -> jmp_buf ptr
let jmp_buf_ptr = ctx.machv_function().new_vreg(Int)
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, handler_id_vreg],
Some(jmp_buf_ptr),
)
// Now call sigsetjmp(jmp_buf_ptr, 0) to actually set the jump point
// sigsetjmp returns 0 normally, or handler_id when longjmp is called
//
// Load sigsetjmp function pointer
let setjmp_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_setjmp = @instr.Inst(
LoadExternalFuncAddr(rt_exception_sigsetjmp(ctx)),
)
load_setjmp.add_def({ reg: Virtual(setjmp_ptr_vreg) })
block.add_inst(load_setjmp)
// Second argument: savemask = 0 (don't save signal mask)
let savemask_vreg = @lower.materialize_imm(ctx, block, 0L)
// Call: sigsetjmp(jmp_buf_ptr, 0) -> result
// Result is 0 on first call, handler_id on longjmp
@lower.lower_c_libcall(
ctx,
block,
setjmp_ptr_vreg,
[jmp_buf_ptr, savemask_vreg],
Some(dst),
)
}
///|
/// Lower TryTableEnd(handler_id) instruction
/// Calls exception_try_end(ctx, handler_id)
pub fn lower_try_table_end(
ctx : @lower.LoweringContext,
_inst : @milkir.Inst,
block : @block.Block,
handler_id : Int,
) -> Unit {
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize handler_id
let handler_id_vreg = @lower.materialize_imm(
ctx,
block,
handler_id.to_int64(),
)
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_try_end(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_try_end(ctx, handler_id)
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, handler_id_vreg],
None,
)
}
///|
/// Lower GetExceptionTag instruction
/// Calls exception_get_tag(ctx) -> tag
pub fn lower_get_exception_tag(
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)
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_get_tag(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_get_tag(ctx) -> tag
@lower.lower_c_libcall(ctx, block, func_ptr_vreg, [vmctx_vreg], Some(dst))
}
///|
/// Lower GetExceptionValue(idx) instruction
/// Calls exception_get_value(ctx, idx) -> value
pub fn lower_get_exception_value(
ctx : @lower.LoweringContext,
inst : @milkir.Inst,
block : @block.Block,
idx : Int,
) -> Unit {
guard inst.first_result() is Some(result) else { return }
let dst = ctx.get_vreg(result)
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize index
let idx_vreg = @lower.materialize_imm(ctx, block, idx.to_int64())
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_get_value(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_get_value(ctx, idx) -> value
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, idx_vreg],
Some(dst),
)
}
///|
/// Lower GetExceptionValueCount instruction
/// Calls exception_get_value_count(ctx) -> count
pub fn lower_get_exception_value_count(
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)
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(
LoadExternalFuncAddr(rt_exception_get_value_count(ctx)),
)
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_get_value_count(ctx) -> count
@lower.lower_c_libcall(ctx, block, func_ptr_vreg, [vmctx_vreg], Some(dst))
}
///|
/// Lower Delegate(depth) instruction
/// Calls exception_delegate(ctx, depth)
pub fn lower_delegate(
ctx : @lower.LoweringContext,
_inst : @milkir.Inst,
block : @block.Block,
depth : Int,
) -> Unit {
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize depth
let depth_vreg = @lower.materialize_imm(ctx, block, depth.to_int64())
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_delegate(ctx)))
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_delegate(ctx, depth)
// This is noreturn
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, depth_vreg],
None,
)
}
///|
/// Lower SpillLocalsForThrow(count) instruction
/// Calls exception_spill_locals(ctx, locals_ptr, count)
/// This saves all locals to memory so catch handlers can see throw-time values
pub fn lower_spill_locals_for_throw(
ctx : @lower.LoweringContext,
inst : @milkir.Inst,
block : @block.Block,
count : Int,
) -> Unit {
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// If no locals, pass null and 0
let (locals_ptr_vreg, count_vreg) = if count > 0 {
// Use pre-allocated outgoing args area for temporary local snapshots.
// Do not adjust SP dynamically inside function bodies.
let stack_space = (count * 8 + 15) / 16 * 16
ctx.machv_function().update_max_outgoing_args_size(stack_space)
// Store each local value to outgoing args area at SP + offset.
for i in 0.. value
pub fn lower_get_spilled_local(
ctx : @lower.LoweringContext,
inst : @milkir.Inst,
block : @block.Block,
idx : Int,
) -> Unit {
guard inst.first_result() is Some(result) else { return }
let dst = ctx.get_vreg(result)
// Get vmctx
let vmctx_vreg = emit_load_vmctx(ctx, block)
// Materialize index
let idx_vreg = @lower.materialize_imm(ctx, block, idx.to_int64())
// Load function pointer
let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
let load_fp = @instr.Inst(
LoadExternalFuncAddr(rt_exception_get_spilled_local(ctx)),
)
load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
block.add_inst(load_fp)
// Call: exception_get_spilled_local(ctx, idx) -> value
@lower.lower_c_libcall(
ctx,
block,
func_ptr_vreg,
[vmctx_vreg, idx_vreg],
Some(dst),
)
}