diff --git a/src/codegen/runtime/data/fixed.rs b/src/codegen/runtime/data/fixed.rs index 5f47e3e754..1e599ce36b 100644 --- a/src/codegen/runtime/data/fixed.rs +++ b/src/codegen/runtime/data/fixed.rs @@ -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"); diff --git a/src/codegen/runtime/emitters.rs b/src/codegen/runtime/emitters.rs index 246bee71b2..4c542df6d8 100644 --- a/src/codegen/runtime/emitters.rs +++ b/src/codegen/runtime/emitters.rs @@ -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); diff --git a/src/codegen/runtime/exceptions.rs b/src/codegen/runtime/exceptions.rs index 0b82980d98..d1f962150c 100644 --- a/src/codegen/runtime/exceptions.rs +++ b/src/codegen/runtime/exceptions.rs @@ -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; @@ -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; diff --git a/src/codegen/runtime/exceptions/cleanup_stack.rs b/src/codegen/runtime/exceptions/cleanup_stack.rs new file mode 100644 index 0000000000..a37cd03e39 --- /dev/null +++ b/src/codegen/runtime/exceptions/cleanup_stack.rs @@ -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 +} \ No newline at end of file diff --git a/src/codegen/runtime/exceptions/throw_current.rs b/src/codegen/runtime/exceptions/throw_current.rs index b15e0120bb..71e695a118 100644 --- a/src/codegen/runtime/exceptions/throw_current.rs +++ b/src/codegen/runtime/exceptions/throw_current.rs @@ -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 @@ -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 diff --git a/src/codegen_ir/lower_inst.rs b/src/codegen_ir/lower_inst.rs index 560c8c3daf..fbcaa9b41d 100644 --- a/src/codegen_ir/lower_inst.rs +++ b/src/codegen_ir/lower_inst.rs @@ -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), @@ -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, diff --git a/src/ir/instr.rs b/src/ir/instr.rs index e24252281a..558e8fb1e6 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -360,6 +360,8 @@ pub enum Op { Move, Borrow, EnsureOwned, + EhPush, + EhPop, Nop, } @@ -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, } @@ -670,6 +673,8 @@ impl Op { Move => "move", Borrow => "borrow", EnsureOwned => "ensure_owned", + EhPush => "eh_push", + EhPop => "eh_pop", Nop => "nop", } } diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 2e43f9daf0..bb715947d5 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -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(()), @@ -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") diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7d7b9fe08b..780b4bacb8 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1696,6 +1696,7 @@ fn emit_builtin_call_value( span: Span, ) -> LoweredValue { let data = ctx.intern_function_name(name); + let eh_count = eh_push_owning_call_temps(ctx, &operands, None, None, span); let call = ctx.emit_value( Op::BuiltinCall, operands.clone(), @@ -1704,6 +1705,7 @@ fn emit_builtin_call_value( effects_lookup::builtin_effects(name), Some(span), ); + eh_pop_owning_call_temps(ctx, eh_count, span); release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), span); call } @@ -8253,6 +8255,7 @@ fn lower_method_call( let arg_values = lower_args_with_signature(ctx, sig.as_ref(), args); operands.extend(arg_values.iter().copied()); let data = ctx.intern_string(dispatch_method); + let eh_count = eh_push_owning_call_temps(ctx, &arg_values, None, Some(object), expr.span); let call = ctx.emit_value( op, operands, @@ -8261,6 +8264,7 @@ fn lower_method_call( op.default_effects(), Some(expr.span), ); + eh_pop_owning_call_temps(ctx, eh_count, expr.span); release_owned_call_arg_temporaries(ctx, &arg_values, Some(call.value), expr.span); release_owning_receiver_temporary(ctx, object, expr.span); call @@ -8535,6 +8539,7 @@ fn lower_method_call_with_receiver( let arg_values = lower_args_with_signature(ctx, sig.as_ref(), args); operands.extend(arg_values.iter().copied()); let data = ctx.intern_string(dispatch_method); + let eh_count = eh_push_owning_call_temps(ctx, &arg_values, None, Some(object), expr.span); let call = ctx.emit_value( op, operands, @@ -8543,6 +8548,7 @@ fn lower_method_call_with_receiver( op.default_effects(), Some(expr.span), ); + eh_pop_owning_call_temps(ctx, eh_count, expr.span); release_owned_call_arg_temporaries(ctx, &arg_values, Some(call.value), expr.span); release_owning_receiver_temporary(ctx, object, expr.span); call @@ -8623,6 +8629,74 @@ fn release_owning_receiver_temporary( } } +/// Pushes owning call-argument and receiver temporaries onto the exception +/// cleanup stack before a potentially-throwing call. If the call throws, +/// `__rt_throw_current` drains the stack and releases each entry. +/// +/// Returns the number of entries pushed so the caller can emit matching +/// `eh_pop` instructions after the call returns normally. +fn eh_push_owning_call_temps( + ctx: &mut LoweringContext<'_, '_>, + args: &[crate::ir::ValueId], + result: Option, + receiver: Option, + span: Span, +) -> usize { + let mut count = 0usize; + for value in args { + let php_type = ctx.builder.value_php_type(*value); + let lowered = LoweredValue { + value: *value, + ir_type: value_ir_type(&php_type), + }; + if ctx.value_is_owning_temporary(lowered) { + if call_result_may_alias_arg(ctx, *value, result) { + continue; + } + ctx.emit_void( + Op::EhPush, + vec![*value], + None, + Op::EhPush.default_effects(), + Some(span), + ); + count += 1; + } + } + if let Some(receiver) = receiver { + if ctx.value_is_owning_temporary(receiver) { + ctx.emit_void( + Op::EhPush, + vec![receiver.value], + None, + Op::EhPush.default_effects(), + Some(span), + ); + count += 1; + } + } + count +} + +/// Pops `count` entries from the exception cleanup stack after a call returned +/// normally. The normal-path release code that follows will handle the actual +/// refcount decrement. +fn eh_pop_owning_call_temps( + ctx: &mut LoweringContext<'_, '_>, + count: usize, + span: Span, +) { + for _ in 0..count { + ctx.emit_void( + Op::EhPop, + Vec::new(), + None, + Op::EhPop.default_effects(), + Some(span), + ); + } +} + /// Returns the checked signature for an instance method call when metadata is available. fn method_signature( ctx: &LoweringContext<'_, '_>, diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 64a9c23084..2bdb8573f7 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -472,3 +472,94 @@ fn test_sequential_try_catch_does_not_blow_up_codegen() { let out = compile_and_run(&php); assert_eq!(out, expected); } + +/// Verifies that an owning method-call-receiver temporary is released when a +/// subsequent method call throws an exception (#399). +/// +/// `$t->make()->boom()` creates an intermediate `N` from `make()` that is an +/// owning temporary. When `boom()` throws, the `longjmp` bypasses the +/// straight-line release code for that temporary. The exception cleanup stack +/// must drain it so its destructor runs before `end` is printed. +#[test] +fn test_chained_method_call_throws_releases_intermediate_temporary() { + let out = compile_and_run( + r#"make()->boom(); + } catch (Exception $e) {} + echo "caught\n"; +} +run(); +echo "end\n"; +"#, + ); + assert_eq!(out, "dtor\ncaught\ndtor\nend\n"); +} + +/// Verifies that owning argument temporaries are released when a method call +/// throws an exception (#399). +/// +/// `f(new S("a"))` passes a string temporary as an argument. When `boom()` +/// throws, the string temporary must be released by the cleanup stack drain. +#[test] +fn test_method_call_throws_releases_owning_arg_temporary() { + let out = compile_and_run( + r#"v = $v; } + public function __destruct() { echo "dtor:{$this->v}\n"; } +} +class C { + public function take(S $s): void { throw new Exception("x"); } +} +function run(): void { + $c = new C(); + try { + $c->take(new S("a")); + } catch (Exception $e) {} + echo "caught\n"; +} +run(); +echo "end\n"; +"#, + ); + assert_eq!(out, "dtor:a\ncaught\nend\n"); +} + +/// Verifies that the cleanup stack does not leak temporaries on the normal +/// (non-throwing) path. The `eh_pop` must remove the entry so that a +/// subsequent throw does not drain an already-released value (#399). +#[test] +fn test_cleanup_stack_balanced_on_normal_path() { + let out = compile_and_run( + r#"make()->ok(); + } catch (Exception $e) {} + echo "r=$r\n"; +} +run(); +echo "end\n"; +"#, + ); + // On the normal path: make() temp is released (first dtor), then $t is + // released at scope end (second dtor). If eh_pop failed to remove the + // entry, a subsequent throw would double-free the make() temp. + assert_eq!(out, "dtor\nr=42\ndtor\nend\n"); +}