From 96da78e81722242036b65c05f95d62c920efc0b5 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 05:49:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20inline=20=E5=9B=9E=E8=B0=83=E6=8A=9B?= =?UTF-8?q?=E5=8E=9F=E5=A7=8B=E5=BC=82=E5=B8=B8=E5=80=BC=E6=97=B6=20catch?= =?UTF-8?q?=20=E4=BF=9D=E7=95=99=E5=8E=9F=E5=80=BC=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8C=85=E8=A3=85=E6=88=90=20Error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatch_native_call 的 NativeResult::Err 分支此前把非对象异常值 (用户 throw 的数字/字符串/null/undefined/false)包装成 Error 对象, 违反 ECMAScript throw 任意值语义:catch 应收到原始值。raise_call_error 与 TailCall 分支已通过 last_uncaught_value 保留原值,此处为唯一不一致点。 统一为原样保留异常值,并补数组回调/嵌套 inline/finally 组合回归测试。 --- crates/oxide_vm/src/dispatch/call.rs | 18 +-- crates/oxide_vm/tests/builtins_array_tests.rs | 34 +++++ .../tests/try_finally_control_flow_tests.rs | 121 ++++++++++++++++++ 3 files changed, 160 insertions(+), 13 deletions(-) diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 458b130..2306a5a 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -53,19 +53,11 @@ impl Vm { Ok(()) } NativeResult::Err(err_val) => { - let (error, kind) = if err_val.is_object() { - (err_val, self.thrown_error_kind(err_val)) - } else { - let msg = if err_val.is_string() { - // SAFETY: err_val 是字符串值。 - unsafe { (*err_val.as_string_ptr()).data.clone() } - } else { - format!("{err_val}") - }; - builtins_debug!("native_call err={}", msg); - (oxide_builtins::error::create_error(self, &msg), "Error") - }; - self.exception_value = Some(error); + // 异常值原样保留:native 内部错误均为 Error 对象(走上方分支), + // 非对象值来自用户回调 throw 的原始异常(经 last_uncaught_value + // 恢复),按 ECMAScript 语义 catch 须收到原值,不得包装成 Error。 + let kind = self.thrown_error_kind(err_val); + self.exception_value = Some(err_val); self.pending_error_kind = Some(kind); self.unwind() } diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index 2e9ae13..739f52a 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -297,6 +297,40 @@ fn array_callback_exceptions_propagate() { assert_eq!(to_str(&vm, result), "f"); } +#[test] +fn array_callback_throw_primitive_keeps_original_value() { + // inline 回调抛原始值(number/string/null/undefined/boolean)时,catch + // 必须收到原值而非被包装的 Error 对象(ECMAScript throw 任意值语义)。 + let (_vm, result) = eval("try{[1].map(()=>{throw 2})}catch(e){e}").unwrap(); + assert_num_eq(result, 2.0); + + let (vm, result) = eval("try{[1].forEach(()=>{throw 's'})}catch(e){typeof e}").unwrap(); + assert_eq!(to_str(&vm, result), "string"); + + let (_vm, result) = eval("try{[1].filter(()=>{throw null})}catch(e){e === null ? 1 : 0}").unwrap(); + assert_num_eq(result, 1.0); + + let (_vm, result) = eval("try{[1].map(()=>{throw undefined})}catch(e){e === undefined ? 1 : 0}").unwrap(); + assert_num_eq(result, 1.0); + + let (_vm, result) = eval("try{[1].map(()=>{throw false})}catch(e){e === false ? 1 : 0}").unwrap(); + assert_num_eq(result, 1.0); +} + +#[test] +fn array_callback_throw_primitive_not_rewrapped_on_nested_call() { + // 嵌套 inline 回调链(外层 map 内再 map)抛原始值,顶层 catch 仍收到原值。 + let (_vm, result) = eval("try{[1].map(()=>{[1].map(()=>{throw 5})})}catch(e){e === 5 ? 1 : 0}").unwrap(); + assert_num_eq(result, 1.0); + + // 外层 finally + 内层 throw:finally 先执行,异常原值传给调用方 catch。 + let (_vm, result) = eval( + "var fin = 0; try { try { [1].map(()=>{throw 6}); } finally { fin = 1; } } catch(e) { fin * 10 + (e === 6 ? 1 : 0) }", + ) + .unwrap(); + assert_num_eq(result, 11.0); +} + #[test] fn array_sort_uses_user_comparator() { let (vm, result) = eval("[3,1,2].sort((a,b)=>a-b).join(',')").unwrap(); diff --git a/crates/oxide_vm/tests/try_finally_control_flow_tests.rs b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs index 78f4adf..55d5143 100644 --- a/crates/oxide_vm/tests/try_finally_control_flow_tests.rs +++ b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs @@ -272,3 +272,124 @@ fn leaked_nested_handler_then_exception_is_caught() { "1" ); } + +// ── inline 同步回调 × 异常 unwind 回归 ── +// inline 调用(数组回调/replacer/getter)不 push 帧,callee 抛出的异常经 +// dispatch_native_call 边界回到调用方 try/catch。以下断言保证:最小复现得 +// 预期值、抛出的原始值不被包装成 Error 对象、嵌套/多级回调与 finally +// 组合的展开路径正确。 + +#[test] +fn inline_callback_throw_caught_by_caller_returns_expected() { + assert_eq!(eval("try { [1].map(function(){ throw 1; }); } catch(e) { 42 }"), "42"); +} + +#[test] +fn inline_callback_throw_primitive_value_kept_original() { + // throw 原始值经 inline 边界传播,catch 须收到原值而非包装的 Error 对象。 + assert_eq!(eval("var r = 0; try { [1].map(function(){ throw 2; }); } catch(e) { r = e; } r"), "2"); + assert_eq!(eval("var r = 0; try { [1].forEach(function(){ throw 's'; }); } catch(e) { r = (typeof e === 'string') ? 1 : 0; } r"), "1"); + assert_eq!( + eval("var r = 0; try { [1].filter(function(){ throw null; }); } catch(e) { r = (e === null) ? 1 : 0; } r"), + "1" + ); + assert_eq!( + eval( + "var r = 0; try { [1].map(function(){ throw undefined; }); } catch(e) { r = (e === undefined) ? 1 : 0; } r" + ), + "1" + ); + assert_eq!( + eval("var r = 0; try { [1].map(function(){ throw false; }); } catch(e) { r = (e === false) ? 1 : 0; } r"), + "1" + ); +} + +#[test] +fn inline_callback_throw_error_object_kept_reference() { + // Error 对象作为异常值传播时保持同一引用,catch 可读 message。 + assert_eq!( + eval("try { [1].map(function(){ throw new Error('myerr'); }); } catch(e) { (e.message === 'myerr') ? 1 : 0 }"), + "1" + ); +} + +#[test] +fn nested_inline_callback_throw_caught_by_outer_catch() { + assert_eq!( + eval("var r = 0; try { [1].map(function(){ [1].map(function(){ throw 5; }); }); } catch(e) { r = e; } r"), + "5" + ); +} + +#[test] +fn multi_level_inline_chain_throw_caught_by_top_catch() { + assert_eq!( + eval("var r = 0; try { [1].map(function(){ [1].map(function(){ [1].map(function(){ throw 3; }); }); }); } catch(e) { r = e; } r"), + "3" + ); +} + +#[test] +fn inline_callback_inner_catch_normal_capture_continues() { + assert_eq!( + eval("var r = [1,2,3].map(function(x){ try { if (x == 2) throw 9; return x; } catch(e) { return 100; } }); r.length * 1000 + r[0] * 100 + r[1] * 10 + r[2]"), + "4103" + ); +} +#[test] +fn inline_callback_inner_finally_runs_before_outer_catch() { + // inline callee 内 finally 先执行,异常再传播给调用方 catch。 + assert_eq!( + eval("var fin = 0; var r = 0; try { [1].map(function(){ try { throw 1; } finally { fin = 5; } }); } catch(e) { r = fin + 1; } r"), + "6" + ); +} + +#[test] +fn inline_callback_finally_throw_overrides_pending() { + assert_eq!( + eval("var r = 0; try { [1].map(function(){ try { throw 1; } finally { throw 2; } }); } catch(e) { r = e; } r"), + "2" + ); +} + +#[test] +fn inline_callback_throw_crosses_outer_finally_to_catch() { + assert_eq!( + eval("var log = []; try { try { [1].map(function(){ throw 1; }); } finally { log.push('f'); } } catch(e) { log.push('c'); } log.length"), + "2" + ); +} + +#[test] +fn inline_callback_throw_caught_by_inner_catch_rethrow_to_outer() { + assert_eq!( + eval("var r = 0; try { try { [1].map(function(){ throw 1; }); } catch(e) { r = 49; throw e; } } catch(e) { r = r * 10 + 50; } r"), + "540" + ); +} + +#[test] +fn inline_replace_replacer_throw_primitive_kept() { + assert_eq!( + eval("var r = 0; try { 'a'.replace(/a/, function(){ throw 8; }); } catch(e) { r = e; } r"), + "8" + ); +} + +#[test] +fn inline_getter_throw_primitive_kept() { + assert_eq!( + eval("var r = 0; var o = { get g() { throw 7; } }; try { var x = o.g; } catch(e) { r = e; } r"), + "7" + ); +} + +#[test] +fn inline_sort_comparator_throw_primitive_kept() { + assert_eq!( + eval("var r = 0; try { [2,1].sort(function(a,b){ [1].map(function(){ throw 13; }); return a - b; }); } catch(e) { r = e; } r"), + "13" + ); +}