diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index b5b7291..ff74ea6 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -525,8 +525,8 @@ fn discover_tests(test262_root: &Path) -> Vec { /// exactly one `TestResult`. Worker-owned state (`kernel`, `harness_sources`, /// `harness_cache`) never crosses a thread boundary. fn process_path( - path: &Path, filter: &Option, no_skip: bool, kernel: &Arc, - harness_sources: &HarnessSources, harness_cache: &mut HarnessPrefixCache, + path: &Path, filter: &Option, no_skip: bool, kernel: &Arc, 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. diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index cd08f62..629bd6a 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -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, ®) 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 = 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, ®) 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 = 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 { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f3f92e9..97fce85 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -445,7 +445,9 @@ impl Vm { }) } - fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result { + pub(crate) fn call_function_sync( + &mut self, callee: JsValue, receiver: JsValue, args: &[JsValue], + ) -> Result { 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) } diff --git a/crates/oxide_vm/tests/builtins_function_tests.rs b/crates/oxide_vm/tests/builtins_function_tests.rs index cf9746b..d2b294a 100644 --- a/crates/oxide_vm/tests/builtins_function_tests.rs +++ b/crates/oxide_vm/tests/builtins_function_tests.rs @@ -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(); diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index b8a7580..26a3df1 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -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" + ); }