fix: inline 回调抛原始异常值时 catch 保留原值不再包装成 Error

dispatch_native_call 的 NativeResult::Err 分支此前把非对象异常值
(用户 throw 的数字/字符串/null/undefined/false)包装成 Error 对象,
违反 ECMAScript throw 任意值语义:catch 应收到原始值。raise_call_error
与 TailCall 分支已通过 last_uncaught_value 保留原值,此处为唯一不一致点。
统一为原样保留异常值,并补数组回调/嵌套 inline/finally 组合回归测试。
This commit is contained in:
2026-08-15 05:49:59 +08:00
parent 40ee642622
commit 96da78e817
3 changed files with 160 additions and 13 deletions
+5 -13
View File
@@ -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()
}
@@ -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();
@@ -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"
);
}