From 428c8cc9243b3a50091fd5be438c39943aa78cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 26 Aug 2026 23:52:25 +0200 Subject: [PATCH 1/2] fix(runtime): permit dynamic construct throws to unwind --- changelog.d/8873-release-construct-unwind.md | 1 + .../perry-runtime/src/object/class_registry/construct.rs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelog.d/8873-release-construct-unwind.md diff --git a/changelog.d/8873-release-construct-unwind.md b/changelog.d/8873-release-construct-unwind.md new file mode 100644 index 0000000000..a8d103ef52 --- /dev/null +++ b/changelog.d/8873-release-construct-unwind.md @@ -0,0 +1 @@ +Fix catchable dynamic-constructor TypeErrors aborting at the runtime construct boundary. diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index f66cd9dc43..454accd707 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -230,7 +230,11 @@ pub(crate) unsafe fn nm_ctor_stream( } #[no_mangle] -pub unsafe extern "C" fn js_new_function_construct( +// This is a generated-code boundary whose TypeError paths unwind to the +// caller's JavaScript catch landing pad. A plain `extern "C"` installs an +// abort-on-unwind guard in the debug/static runtime, so `new ()` +// aborts instead of remaining catchable. +pub unsafe extern "C-unwind" fn js_new_function_construct( func_value: f64, args_ptr: *const f64, args_len: usize, @@ -1160,6 +1164,8 @@ pub unsafe extern "C" fn js_new_function_construct( super::super::object_ops::throw_object_type_error(b"is not a constructor") } +const _: unsafe extern "C-unwind" fn(f64, *const f64, usize) -> f64 = js_new_function_construct; + /// `new (...spread)` — spread-bearing construction. Codegen builds a /// single JS array containing every argument in evaluation order (regular args /// pushed, spread sources expanded via `js_array_like_to_array` + concat), then From e4e2ac485635be42f8d5000f2a2d373c435a906b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 26 Aug 2026 23:54:47 +0200 Subject: [PATCH 2/2] test: retain dynamic construct ABI regression --- .../issue_5253_construct_reference_source_location.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/perry/tests/issue_5253_construct_reference_source_location.rs b/crates/perry/tests/issue_5253_construct_reference_source_location.rs index dc92362c57..a9faa7285e 100644 --- a/crates/perry/tests/issue_5253_construct_reference_source_location.rs +++ b/crates/perry/tests/issue_5253_construct_reference_source_location.rs @@ -98,11 +98,13 @@ fn run_fixture(fixture: &str, extra_args: &[&str]) -> String { let bin = root.join("main_bin"); let run = Command::new(&bin).output().expect("run compiled binary"); // Both fixtures catch the throw and log it, so the program must exit - // cleanly. Preserve stderr here so an ABI abort cannot masquerade as an - // empty-output assertion failure. + // cleanly. This is also the ABI regression assertion: every runtime + // boundary between the throw helper and generated code's catch landing + // pad must permit unwinding. Preserve stderr so an ABI abort cannot + // masquerade as an empty-output assertion failure. assert!( run.status.success(), - "compiled binary must exit successfully; status: {:?}\nstdout:\n{}\nstderr:\n{}", + "compiled binary must catch the runtime throw and exit successfully; status: {:?}\nstdout:\n{}\nstderr:\n{}", run.status, String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr),