Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/codegen/runtime/data/fixed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin
out.push_str(".comm _exc_handler_top, 8, 3\n");
out.push_str(".comm _exc_call_frame_top, 8, 3\n");
out.push_str(".comm _exc_value, 8, 3\n");
// Exception cleanup stack: array of owning-temporary pointers that must be
// released if a call throws. __rt_eh_push stores a pointer before a
// potentially-throwing call; __rt_eh_pop removes it after the call returns
// normally; __rt_throw_current drains the stack before longjmp.
out.push_str(".comm _eh_cleanup_stack, 2048, 3\n");
out.push_str(".comm _eh_cleanup_top, 8, 3\n");
out.push_str(".comm _fiber_current, 8, 3\n");
out.push_str(".comm _fiber_main_saved_sp, 8, 3\n");
out.push_str(".comm _fiber_main_saved_exc, 8, 3\n");
Expand Down
1 change: 1 addition & 0 deletions src/codegen/runtime/emitters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) {

// Exception runtime functions
exceptions::emit_exception_cleanup_frames(emitter);
exceptions::emit_eh_cleanup_stack(emitter);
exceptions::emit_class_implements_interface(emitter);
exceptions::emit_dynamic_instanceof(emitter);
exceptions::emit_exception_matches(emitter);
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/runtime/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! - Exception matching and unwinding must keep handler-stack, call-frame cleanup, and class metadata invariants aligned.

mod cleanup_frames;
mod cleanup_stack;
mod class_implements;
mod dynamic_instanceof;
mod matches;
Expand All @@ -17,6 +18,7 @@ mod throw_current;

pub use class_implements::emit_class_implements_interface;
pub use cleanup_frames::emit_exception_cleanup_frames;
pub use cleanup_stack::emit_eh_cleanup_stack;
pub use dynamic_instanceof::emit_dynamic_instanceof;
pub use matches::emit_exception_matches;
pub use rethrow_current::emit_rethrow_current;
Expand Down
137 changes: 137 additions & 0 deletions src/codegen/runtime/exceptions/cleanup_stack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Purpose:
//! Emits the `__rt_eh_push`, `__rt_eh_pop`, and `__rt_eh_drain` runtime helpers
//! for the exception cleanup stack. These manage owning-temporary pointers
//! that must be released when a call throws and `longjmp` bypasses the
//! straight-line release code.
//!
//! Called from:
//! - `crate::codegen::runtime::emitters::emit_runtime()` via
//! `crate::codegen::runtime::exceptions`.
//!
//! Key details:
//! - The cleanup stack lives in the global `_eh_cleanup_stack` array (256
//! pointers) with a word counter in `_eh_cleanup_top`.
//! - `__rt_eh_push` stores the pointer and increments the top.
//! - `__rt_eh_pop` decrements the top (the value was already released by the
//! normal straight-line path).
//! - `__rt_eh_drain` pops all remaining entries, calling `__rt_decref_any`
//! on each, and is invoked by `__rt_throw_current` before `longjmp`.

use crate::codegen::emit::Emitter;
use crate::codegen::platform::Arch;

/// Emits `__rt_eh_push`, `__rt_eh_pop`, and `__rt_eh_drain` for the current
/// target.
///
/// # ABI
/// - `__rt_eh_push`: input pointer in the int result register (x0 / rdi).
/// Clobbers scratch only.
/// - `__rt_eh_pop`: no input. Decrements `_eh_cleanup_top`.
/// - `__rt_eh_drain`: no input. Loops over the stack, calling
/// `__rt_decref_any` on each entry, then zeroes the top.
pub fn emit_eh_cleanup_stack(emitter: &mut Emitter) {
if emitter.target.arch == Arch::X86_64 {
emit_eh_cleanup_stack_x86_64(emitter);
return;
}

emitter.blank();
emitter.comment("--- runtime: eh_cleanup_stack ---");

// -- __rt_eh_push: store pointer and increment top --
emitter.label_global("__rt_eh_push");
emitter.instruction("sub sp, sp, #32"); // reserve a small frame for the push helper
emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address
emitter.instruction("str x0, [sp]"); // save the input pointer before clobbering x0
emitter.instruction("adrp x9, _eh_cleanup_top@PAGE"); // x9 = page of the cleanup top counter
emitter.instruction("add x9, x9, _eh_cleanup_top@PAGEOFF"); // x9 = address of the cleanup top counter
emitter.instruction("ldr x9, [x9]"); // x9 = current cleanup stack top index
emitter.instruction("adrp x10, _eh_cleanup_stack@PAGE"); // x10 = page of the cleanup stack array
emitter.instruction("add x10, x10, _eh_cleanup_stack@PAGEOFF"); // x10 = base of the cleanup stack array
emitter.instruction("ldr x0, [sp]"); // reload the saved input pointer
emitter.instruction("str x0, [x10, x9, lsl #3]"); // store the owning temporary pointer at stack[top]
emitter.instruction("add x9, x9, #1"); // increment the top index
emitter.instruction("adrp x10, _eh_cleanup_top@PAGE"); // x10 = page of the cleanup top counter
emitter.instruction("add x10, x10, _eh_cleanup_top@PAGEOFF"); // x10 = address of the cleanup top counter
emitter.instruction("str x9, [x10]"); // persist the updated top index
emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address
emitter.instruction("add sp, sp, #32"); // release the helper frame
emitter.instruction("ret"); // return to the caller

// -- __rt_eh_pop: decrement top (normal path after call succeeds) --
emitter.label_global("__rt_eh_pop");
emitter.instruction("adrp x9, _eh_cleanup_top@PAGE"); // x9 = page of the cleanup top counter
emitter.instruction("add x9, x9, _eh_cleanup_top@PAGEOFF"); // x9 = address of the cleanup top counter
emitter.instruction("ldr x9, [x9]"); // x9 = current cleanup stack top index
emitter.instruction("sub x9, x9, #1"); // decrement the top index
emitter.instruction("adrp x10, _eh_cleanup_top@PAGE"); // x10 = page of the cleanup top counter
emitter.instruction("add x10, x10, _eh_cleanup_top@PAGEOFF"); // x10 = address of the cleanup top counter
emitter.instruction("str x9, [x10]"); // persist the updated top index
emitter.instruction("ret"); // return to the caller

// -- __rt_eh_drain: release all remaining entries and reset top --
emitter.label_global("__rt_eh_drain");
emitter.instruction("sub sp, sp, #32"); // reserve a small frame for the drain helper
emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address
emitter.label("__rt_eh_drain_loop");
emitter.instruction("adrp x9, _eh_cleanup_top@PAGE"); // x9 = page of the cleanup top counter
emitter.instruction("add x9, x9, _eh_cleanup_top@PAGEOFF"); // x9 = address of the cleanup top counter
emitter.instruction("ldr x9, [x9]"); // x9 = current cleanup stack top index
emitter.instruction("cbz x9, __rt_eh_drain_done"); // stop when the stack is empty
emitter.instruction("sub x9, x9, #1"); // decrement to the topmost entry index
emitter.instruction("adrp x10, _eh_cleanup_top@PAGE"); // x10 = page of the cleanup top counter
emitter.instruction("add x10, x10, _eh_cleanup_top@PAGEOFF"); // x10 = address of the cleanup top counter
emitter.instruction("str x9, [x10]"); // persist the decremented top
emitter.instruction("adrp x10, _eh_cleanup_stack@PAGE"); // x10 = page of the cleanup stack array
emitter.instruction("add x10, x10, _eh_cleanup_stack@PAGEOFF"); // x10 = base of the cleanup stack array
emitter.instruction("ldr x0, [x10, x9, lsl #3]"); // load the owning temporary pointer from the stack
emitter.instruction("bl __rt_decref_any"); // release the owning temporary (any type)
emitter.instruction("b __rt_eh_drain_loop"); // continue draining
emitter.label("__rt_eh_drain_done");
emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address
emitter.instruction("add sp, sp, #32"); // release the helper frame
emitter.instruction("ret"); // return to the caller
}

/// Emits the x86_64 Linux variants of the cleanup-stack helpers.
fn emit_eh_cleanup_stack_x86_64(emitter: &mut Emitter) {
emitter.blank();
emitter.comment("--- runtime: eh_cleanup_stack ---");

// -- __rt_eh_push: store pointer and increment top --
emitter.label_global("__rt_eh_push");
emitter.instruction("push rbp"); // save the caller frame pointer
emitter.instruction("mov rbp, rsp"); // establish a stable frame pointer
emitter.instruction("mov r9, QWORD PTR [rip + _eh_cleanup_top]"); // r9 = current cleanup stack top index
emitter.instruction("lea r10, [_eh_cleanup_stack]"); // r10 = base of the cleanup stack array
emitter.instruction("mov QWORD PTR [r10 + r9*8], rdi"); // store the owning temporary pointer at stack[top]
emitter.instruction("add r9, 1"); // increment the top index
emitter.instruction("mov QWORD PTR [rip + _eh_cleanup_top], r9"); // persist the updated top index
emitter.instruction("pop rbp"); // restore the caller frame pointer
emitter.instruction("ret"); // return to the caller

// -- __rt_eh_pop: decrement top (normal path after call succeeds) --
emitter.label_global("__rt_eh_pop");
emitter.instruction("mov r9, QWORD PTR [rip + _eh_cleanup_top]"); // r9 = current cleanup stack top index
emitter.instruction("sub r9, 1"); // decrement the top index
emitter.instruction("mov QWORD PTR [rip + _eh_cleanup_top], r9"); // persist the updated top index
emitter.instruction("ret"); // return to the caller

// -- __rt_eh_drain: release all remaining entries and reset top --
emitter.label_global("__rt_eh_drain");
emitter.instruction("push rbp"); // save the caller frame pointer
emitter.instruction("mov rbp, rsp"); // establish a stable frame pointer
emitter.label("__rt_eh_drain_loop_x86_64");
emitter.instruction("mov r9, QWORD PTR [rip + _eh_cleanup_top]"); // r9 = current cleanup stack top index
emitter.instruction("test r9, r9"); // is the stack empty?
emitter.instruction("jz __rt_eh_drain_done_x86_64"); // stop when the stack is empty
emitter.instruction("sub r9, 1"); // decrement to the topmost entry index
emitter.instruction("mov QWORD PTR [rip + _eh_cleanup_top], r9"); // persist the decremented top
emitter.instruction("lea r10, [_eh_cleanup_stack]"); // r10 = base of the cleanup stack array
emitter.instruction("mov rdi, QWORD PTR [r10 + r9*8]"); // load the owning temporary pointer from the stack
emitter.instruction("call __rt_decref_any"); // release the owning temporary (any type)
emitter.instruction("jmp __rt_eh_drain_loop_x86_64"); // continue draining
emitter.label("__rt_eh_drain_done_x86_64");
emitter.instruction("pop rbp"); // restore the caller frame pointer
emitter.instruction("ret"); // return to the caller
}
4 changes: 4 additions & 0 deletions src/codegen/runtime/exceptions/throw_current.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub fn emit_throw_current(emitter: &mut Emitter) {
emitter.instruction("ldr x0, [x19, #8]"); // x0 = activation record that should survive this catch
emitter.instruction("bl __rt_exception_cleanup_frames"); // run cleanup callbacks for every unwound activation frame
abi::emit_store_reg_to_symbol(emitter, "xzr", "_concat_off", 0);
emitter.instruction("bl __rt_eh_drain"); // release all owning temporaries on the cleanup stack
abi::emit_store_reg_to_symbol(emitter, "xzr", "_eh_cleanup_top", 0); // reset the cleanup stack top after draining
emitter.instruction(&format!("add x0, x19, #{}", TRY_HANDLER_JMP_BUF_OFFSET)); // x0 = jmp_buf base stored inside the active handler record
emitter.instruction("mov x1, #1"); // longjmp return value = 1 to indicate exceptional control flow
emitter.bl_c("longjmp"); // transfer control directly back to the saved catch resume point
Expand Down Expand Up @@ -73,6 +75,8 @@ fn emit_throw_current_linux_x86_64(emitter: &mut Emitter) {
emitter.instruction("mov rdi, QWORD PTR [r12 + 8]"); // rdi = activation record that should survive this catch
emitter.instruction("call __rt_exception_cleanup_frames"); // run cleanup callbacks for every unwound activation frame
abi::emit_store_zero_to_symbol(emitter, "_concat_off", 0);
emitter.instruction("call __rt_eh_drain"); // release all owning temporaries on the cleanup stack
abi::emit_store_zero_to_symbol(emitter, "_eh_cleanup_top", 0); // reset the cleanup stack top after draining
emitter.instruction(&format!("lea rdi, [r12 + {}]", TRY_HANDLER_JMP_BUF_OFFSET)); // rdi = jmp_buf base stored inside the active handler record
emitter.instruction("mov esi, 1"); // longjmp return value = 1 to indicate exceptional control flow
emitter.bl_c("longjmp"); // transfer control directly back to the saved catch resume point
Expand Down
20 changes: 20 additions & 0 deletions src/codegen_ir/lower_inst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId)
Op::Acquire => ownership::lower_acquire(ctx, &inst),
Op::Release => ownership::lower_release(ctx, &inst),
Op::GcCollect => lower_gc_collect(ctx),
Op::EhPush => lower_eh_push(ctx, &inst),
Op::EhPop => lower_eh_pop(ctx),
Op::Move | Op::Borrow => ownership::lower_forward(ctx, &inst),
Op::EchoValue => lower_echo_value(ctx, &inst),
Op::PrintValue => lower_print_value(ctx, &inst),
Expand Down Expand Up @@ -1053,6 +1055,24 @@ fn lower_gc_collect(ctx: &mut FunctionContext<'_>) -> Result<()> {
Ok(())
}

/// Lowers `eh_push`: loads the owning-temporary value into the first integer
/// argument register and calls `__rt_eh_push` to record it on the exception
/// cleanup stack before a potentially-throwing call.
fn lower_eh_push(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
let value = expect_operand(inst, 0)?;
let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0);
ctx.load_value_to_reg(value, arg_reg)?;
abi::emit_call_label(ctx.emitter, "__rt_eh_push");
Ok(())
}

/// Lowers `eh_pop`: calls `__rt_eh_pop` to remove the top entry from the
/// exception cleanup stack after a call returned normally.
fn lower_eh_pop(ctx: &mut FunctionContext<'_>) -> Result<()> {
abi::emit_call_label(ctx.emitter, "__rt_eh_pop");
Ok(())
}

/// Converts a descriptor overflow offset into a caller-stack frame offset.
fn descriptor_entry_caller_stack_offset(
emitter: &crate::codegen::emit::Emitter,
Expand Down
5 changes: 5 additions & 0 deletions src/ir/instr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ pub enum Op {
Move,
Borrow,
EnsureOwned,
EhPush,
EhPop,
Nop,
}

Expand Down Expand Up @@ -440,6 +442,7 @@ impl Op {
ErrorSuppressBegin | ErrorSuppressEnd => E::READS_GLOBAL | E::WRITES_GLOBAL,
ThrowException => E::MAY_THROW | E::WRITES_GLOBAL,
Acquire | Release | EnsureOwned => E::REFCOUNT_OP | E::WRITES_HEAP,
EhPush | EhPop => E::WRITES_GLOBAL,
GcCollect => E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP,
ClassConstant => E::MAY_DEOPT,
}
Expand Down Expand Up @@ -670,6 +673,8 @@ impl Op {
Move => "move",
Borrow => "borrow",
EnsureOwned => "ensure_owned",
EhPush => "eh_push",
EhPop => "eh_pop",
Nop => "nop",
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/ir/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio
| ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler
| CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark
| IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | ConcatReset
| GcCollect | Nop => {
| GcCollect | EhPop | Nop => {
check_count(inst_id, inst, 0, "0")
}
ClosureNew => Ok(()),
Expand Down Expand Up @@ -395,7 +395,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio
check_count(inst_id, inst, 0, "0")
}
StoreLocal | StoreGlobal | StoreStaticLocal | InitStaticLocal | StoreStaticProperty | ExternGlobalStore
| StoreRefCell | BindRefCellPtr | Acquire | Release | Move | Borrow | EnsureOwned
| StoreRefCell | BindRefCellPtr | Acquire | Release | Move | Borrow | EnsureOwned | EhPush
| EchoValue | PrintValue | WriteStdout | WriteStrStdout | VarDump | PrintR
| ThrowException | GeneratorReturn | PtrCheckNonnull => {
check_count(inst_id, inst, 1, "1")
Expand Down
Loading
Loading