fix: 修复 Function call/apply/bind 调用非原生函数问题

This commit is contained in:
2026-06-09 19:12:14 +08:00
parent 8161597acf
commit a6982bdf56
5 changed files with 93 additions and 58 deletions
+10 -4
View File
@@ -525,8 +525,8 @@ fn discover_tests(test262_root: &Path) -> Vec<PathBuf> {
/// exactly one `TestResult`. Worker-owned state (`kernel`, `harness_sources`,
/// `harness_cache`) never crosses a thread boundary.
fn process_path(
path: &Path, filter: &Option<String>, no_skip: bool, kernel: &Arc<OxideKernel>,
harness_sources: &HarnessSources, harness_cache: &mut HarnessPrefixCache,
path: &Path, filter: &Option<String>, no_skip: bool, kernel: &Arc<OxideKernel>, harness_sources: &HarnessSources,
harness_cache: &mut HarnessPrefixCache,
) -> TestResult {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
@@ -691,7 +691,10 @@ fn run_tests() -> bool {
// own kernel + harness-source registry + prefix cache. Only `PathBuf` and
// `TestResult` (both `Send`) cross thread boundaries. Workers pull test
// indices from a shared atomic cursor for dynamic load balancing.
let workers = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).min(total.max(1));
let workers = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.min(total.max(1));
eprintln!("running on {workers} worker thread(s)");
@@ -748,7 +751,10 @@ fn run_tests() -> bool {
})
.collect();
handles.into_iter().map(|h| h.join().expect("test262 worker thread panicked")).collect()
handles
.into_iter()
.map(|h| h.join().expect("test262 worker thread panicked"))
.collect()
});
// Reduce partial stats and restore discovery order for deterministic output.
+36 -48
View File
@@ -2,34 +2,42 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
use crate::native::{NativeFn, NativeResult};
use crate::native::NativeResult;
use crate::vm::Vm;
/// Rebuild a typed JS error object from `call_function_sync`'s `String` error.
///
/// `call_function_sync` collapses thrown exceptions to a `String` shaped like
/// `"TypeError: message"`. Native builtins must return `Err(JsValue)`, and
/// test262 negative tests match on the error's `name`, so we parse the prefix
/// back into the right error kind instead of flattening everything to `Error`.
fn error_from_text(vm: &mut Vm, text: &str) -> JsValue {
use crate::builtins::error;
let text = text.strip_prefix("uncaught ").unwrap_or(text);
let (kind, msg) = match text.split_once(": ") {
Some((k, m)) => (k, m),
None => ("", text),
};
match kind {
"TypeError" => error::create_type_error(vm, msg),
"ReferenceError" => error::create_reference_error(vm, msg),
"RangeError" => error::create_range_error(vm, msg),
"SyntaxError" => error::create_syntax_error(vm, msg),
_ => error::create_error(vm, text),
}
}
/// Invoke any callee (native or user/bytecode function) with an explicit
/// receiver and register-sourced arguments.
///
/// Delegates to `Vm::call_function_sync`, the engine's unified synchronous call
/// path (also used by getter/setter dispatch). The previous hand-rolled version
/// only supported native targets, so `Function.prototype.call/apply/bind` threw
/// on user functions — a major test262 harness blocker.
fn invoke_target(vm: &mut Vm, target_val: JsValue, this_val: JsValue, arg_regs: &[u8]) -> NativeResult {
if !target_val.is_object() {
return Err(JsValue::undefined());
}
let tgt_ptr = target_val.as_js_object_ptr();
if tgt_ptr.is_null() {
return Err(JsValue::undefined());
}
let tgt = unsafe { &*tgt_ptr };
if !tgt.is_function() || tgt.native_fn().is_none() {
return Err(JsValue::undefined());
}
let func: NativeFn = unsafe { std::mem::transmute(tgt.native_fn().unwrap()) };
let base = 230u8;
let n = arg_regs.len();
let mut args_buf = [0u8; 64];
args_buf[0] = base;
vm.set_reg(base, this_val);
for (i, &reg) in arg_regs.iter().enumerate().take(n.min(63)) {
vm.set_reg(base + 1 + i as u8, vm.reg(reg));
args_buf[i + 1] = base + 1 + i as u8;
}
func(vm, &args_buf[..n + 1])
let args: Vec<JsValue> = arg_regs.iter().map(|&r| vm.reg(r)).collect();
vm.call_function_sync(target_val, this_val, &args)
.map_err(|msg| error_from_text(vm, &msg))
}
fn bind_dispatcher(vm: &mut Vm, args: &[u8]) -> NativeResult {
@@ -44,29 +52,9 @@ fn bind_dispatcher(vm: &mut Vm, args: &[u8]) -> NativeResult {
.and_then(|v| v.get(1).map(|b| **b))
.unwrap_or(JsValue::undefined());
let n = args.len().saturating_sub(1);
let base = 230u8;
let mut args_buf = [0u8; 64];
args_buf[0] = base;
vm.set_reg(base, bound_this);
for (i, &reg) in args.iter().skip(1).enumerate().take(n.min(63)) {
vm.set_reg(base + 1 + i as u8, vm.reg(reg));
args_buf[i + 1] = base + 1 + i as u8;
}
if !bound_target.is_object() {
return Err(JsValue::undefined());
}
let tgt_ptr = bound_target.as_js_object_ptr();
if tgt_ptr.is_null() {
return Err(JsValue::undefined());
}
let tgt = unsafe { &*tgt_ptr };
if !tgt.is_function() || tgt.native_fn().is_none() {
return Err(JsValue::undefined());
}
let func: NativeFn = unsafe { std::mem::transmute(tgt.native_fn().unwrap()) };
func(vm, &args_buf[..n + 1])
// Forward bound call arguments (skip args[0], the bound-wrapper receiver).
let arg_regs: Vec<u8> = args.iter().skip(1).copied().collect();
invoke_target(vm, bound_target, bound_this, &arg_regs)
}
pub fn function_call(vm: &mut Vm, args: &[u8]) -> NativeResult {
+9 -2
View File
@@ -445,7 +445,9 @@ impl Vm {
})
}
fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result<JsValue, String> {
pub(crate) fn call_function_sync(
&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue],
) -> Result<JsValue, String> {
if !callee.is_object() {
return Err("TypeError: accessor is not callable".into());
}
@@ -861,7 +863,12 @@ impl Vm {
}
let exc = self.exception_value.take().unwrap_or(JsValue::undefined());
let kind_str = self.pending_error_kind.take().unwrap_or("Error");
let msg = format!("uncaught {kind_str}: {exc}");
let exc_text = self.error_text(exc);
let msg = if exc_text.starts_with(kind_str) {
format!("uncaught {exc_text}")
} else {
format!("uncaught {kind_str}: {exc_text}")
};
Err(msg)
}
@@ -30,6 +30,34 @@ fn function_bind_creates_wrapper() {
assert!((result.as_double() - 5.0).abs() < 0.0001);
}
#[test]
fn function_call_invokes_bytecode_function() {
let mut vm = Vm::new();
let result = eval(&mut vm, "function add(a, b) { return a + b; } add.call(null, 2, 3)").unwrap();
assert!((result.as_double() - 5.0).abs() < 0.0001);
}
#[test]
fn function_apply_invokes_bytecode_function() {
let mut vm = Vm::new();
let result = eval(&mut vm, "function add(a, b) { return a + b; } add.apply(null, [2, 3])").unwrap();
assert!((result.as_double() - 5.0).abs() < 0.0001);
}
#[test]
fn function_bind_invokes_bytecode_function() {
let mut vm = Vm::new();
let result = eval(&mut vm, "function add1(a) { return a + 1; } var bound = add1.bind(null); bound(2)").unwrap();
assert!((result.as_double() - 3.0).abs() < 0.0001);
}
#[test]
fn function_call_preserves_bytecode_throw_kind() {
let mut vm = Vm::new();
let err = eval(&mut vm, "function fail() { throw new TypeError('boom'); } fail.call(null)").unwrap_err();
assert!(err.contains("uncaught TypeError: boom"), "got: {err}");
}
#[test]
fn function_to_string_includes_function() {
let mut vm = Vm::new();
+10 -4
View File
@@ -83,7 +83,7 @@ fn regression_throw_statement_errors() {
#[test]
fn regression_throw_statement_preserves_type_error_kind() {
assert_eq!(eval("try { throw new TypeError('boom') } catch (e) { e.name == 'TypeError' }"), "true");
assert_eq!(eval("throw new TypeError('boom')"), "vm error: uncaught TypeError: {object}");
assert_eq!(eval("throw new TypeError('boom')"), "vm error: uncaught TypeError: boom");
}
#[test]
@@ -92,7 +92,7 @@ fn regression_throw_statement_preserves_syntax_error_kind() {
eval("try { throw new SyntaxError('boom') } catch (e) { e.name == 'SyntaxError' }"),
"true"
);
assert_eq!(eval("throw new SyntaxError('boom')"), "vm error: uncaught SyntaxError: {object}");
assert_eq!(eval("throw new SyntaxError('boom')"), "vm error: uncaught SyntaxError: boom");
}
#[test]
@@ -137,11 +137,17 @@ fn regression_new_expression_bytecode_constructor_error_is_catchable() {
#[test]
fn regression_delete_static_property_throws_type_error() {
assert_eq!(eval("try { var o={x:1}; delete o.x; 0 } catch (e) { e.name == 'TypeError' }"), "true");
assert_eq!(eval("var o={x:1}; delete o.x"), "vm error: uncaught TypeError: {object}");
assert_eq!(
eval("var o={x:1}; delete o.x"),
"vm error: uncaught TypeError: property deletion not supported"
);
}
#[test]
fn regression_delete_dynamic_property_throws_type_error() {
assert_eq!(eval("try { var o={x:1}; delete o['x']; 0 } catch (e) { e.name == 'TypeError' }"), "true");
assert_eq!(eval("var o={x:1}; var k='x'; delete o[k]"), "vm error: uncaught TypeError: {object}");
assert_eq!(
eval("var o={x:1}; var k='x'; delete o[k]"),
"vm error: uncaught TypeError: property deletion not supported"
);
}