From 69500513b34ce5a052c0cd91f30268292fd55784 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 23 Jun 2026 14:38:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=8F=82=E6=95=B0=E9=99=90=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E5=86=85=E5=BB=BA=E8=AF=AD=E4=B9=89=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_test262/src/main.rs | 142 ++++++++++-- crates/oxide_vm/src/builtins/array.rs | 144 +++++++----- crates/oxide_vm/src/builtins/array_buffer.rs | 23 +- crates/oxide_vm/src/builtins/number.rs | 45 +++- crates/oxide_vm/src/dispatch/arithmetic.rs | 7 +- crates/oxide_vm/src/dispatch/bitwise.rs | 97 ++++---- crates/oxide_vm/src/dispatch/member.rs | 29 ++- crates/oxide_vm/src/session_gc.rs | 97 +++++++- crates/oxide_vm/src/vm.rs | 219 ++++++++++++++---- crates/oxide_vm/tests/builtins_array_tests.rs | 27 +++ .../oxide_vm/tests/builtins_boolean_tests.rs | 10 + crates/oxide_vm/tests/builtins_date_tests.rs | 7 + crates/oxide_vm/tests/regression_tests.rs | 17 ++ 13 files changed, 675 insertions(+), 189 deletions(-) diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 725763cb..e7dbc6bc 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -6,6 +6,7 @@ use oxide_vm::vm::Vm; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::process::Command; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock, RwLock}; use std::time::Duration; @@ -511,12 +512,89 @@ fn run_test_inner( } fn discover_tests(test262_root: &Path) -> Vec { - WalkDir::new(test262_root) + let mut paths: Vec = WalkDir::new(test262_root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().is_some_and(|ext| ext == "js")) .map(|e| e.path().to_path_buf()) - .collect() + .collect(); + paths.sort(); + paths +} + +fn parse_summary_count(stdout: &str, label: &str) -> Option { + stdout.lines().find_map(|line| { + let trimmed = line.trim_start(); + let rest = trimmed.strip_prefix(label)?; + let value = rest.trim().split(' ').next()?; + value.parse::().ok() + }) +} + +fn run_chunked(args: &[String], skip_until: usize, end_index: usize, chunk_size: usize) -> bool { + let exe = match std::env::current_exe() { + Ok(path) => path, + Err(err) => { + eprintln!("failed to resolve current executable for chunked mode: {err}"); + return false; + } + }; + + let mut aggregate_pass = 0usize; + let mut aggregate_fail = 0usize; + let mut aggregate_skip = 0usize; + let mut chunk_start = skip_until; + let mut chunk_id = 1usize; + + while chunk_start < end_index { + let chunk_len = (end_index - chunk_start).min(chunk_size); + eprintln!("chunk {chunk_id}: tests [{chunk_start}, {})", chunk_start + chunk_len); + + let output = match Command::new(&exe) + .args(args.iter().skip(1)) + .env("OXIDE_SKIP_UNTIL", chunk_start.to_string()) + .env("OXIDE_MAX_TESTS", chunk_len.to_string()) + .env("OXIDE_TEST262_CHILD_CHUNK", "1") + .env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1") + .output() + { + Ok(output) => output, + Err(err) => { + eprintln!("failed to run chunk {chunk_id}: {err}"); + return false; + } + }; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + print!("{stdout}"); + eprint!("{stderr}"); + + if !output.status.success() { + eprintln!("chunk {chunk_id} crashed or aborted"); + return false; + } + + aggregate_pass += parse_summary_count(&stdout, "pass :").unwrap_or(0); + aggregate_fail += parse_summary_count(&stdout, "fail :").unwrap_or(0); + aggregate_skip += parse_summary_count(&stdout, "skip :").unwrap_or(0); + + chunk_start += chunk_len; + chunk_id += 1; + } + + let aggregate_total = aggregate_pass + aggregate_fail + aggregate_skip; + println!(); + println!("═══════════════════════════════════════"); + println!(" test262 chunked aggregate"); + println!("═══════════════════════════════════════"); + println!(" total : {}", aggregate_total); + println!(" pass : {}", aggregate_pass); + println!(" fail : {}", aggregate_fail); + println!(" skip : {}", aggregate_skip); + println!("═══════════════════════════════════════"); + + aggregate_fail == 0 } /// Per-test pipeline shared by the serial and parallel execution paths: @@ -716,11 +794,16 @@ 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 default_workers = if config.no_skip { + 4 + } else { + std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4) + }; let workers = std::env::var("OXIDE_TEST262_WORKERS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) - .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)) + .unwrap_or(default_workers) .min(total.max(1)); let log_running_tests = std::env::var_os("OXIDE_TEST262_RUNNING_LOG").is_some(); @@ -737,6 +820,25 @@ fn run_tests() -> bool { .and_then(|n| skip_until.checked_add(n)) .map(|n| n.min(total)) .unwrap_or(total); + let kernel_batch = std::env::var("OXIDE_TEST262_KERNEL_BATCH") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&n| n > 0) + .unwrap_or(if config.no_skip { 1000 } else { 5000 }); + let chunk_size = std::env::var("OXIDE_TEST262_CHUNK_SIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&n| n > 0); + let is_chunk_child = std::env::var_os("OXIDE_TEST262_CHILD_CHUNK").is_some(); + let allow_fail_exit = std::env::var_os("OXIDE_TEST262_ALLOW_FAIL_EXIT").is_some(); + + if let Some(chunk_size) = chunk_size { + if !is_chunk_child && filter.is_none() { + eprintln!("chunked mode enabled: {chunk_size} test(s) per child process"); + return run_chunked(&args, skip_until, end_index, chunk_size); + } + } + eprintln!("kernel reset batch: {kernel_batch} test(s)"); let cursor = AtomicUsize::new(skip_until); let progress = AtomicUsize::new(skip_until); let filter = &filter; @@ -744,9 +846,10 @@ fn run_tests() -> bool { let paths_ref = &paths; let harness_cache = Arc::new(RwLock::new(HarnessPrefixCache::new())); - // Each worker returns its partial stats plus (index, result) pairs so the - // final results vector can be restored to discovery order for stable output. - let partials: Vec<(RunStats, Vec<(usize, TestResult)>)> = std::thread::scope(|scope| { + // Keep memory flat: workers return only aggregate stats. Retaining tens of + // thousands of `TestResult`s makes `--no-skip` runs accumulate path/error + // strings until the process OOMs near the end of the suite. + let partials: Vec = std::thread::scope(|scope| { let handles: Vec<_> = (0..workers) .map(|_| { let cursor = &cursor; @@ -757,10 +860,10 @@ fn run_tests() -> bool { std::thread::Builder::new() .stack_size(16 * 1024 * 1024) .spawn_scoped(scope, move || { - let kernel = build_runner_kernel(); + let mut kernel = build_runner_kernel(); let harness_sources = HARNESS.get_or_init(HarnessSources::new); let mut stats = RunStats::default(); - let mut out: Vec<(usize, TestResult)> = Vec::new(); + let mut tests_since_kernel_reset = 0usize; let tid = std::thread::current().id(); loop { @@ -780,18 +883,22 @@ fn run_tests() -> bool { let result = process_path(&paths_ref[i], filter, no_skip, &kernel, harness_sources, &harness_cache); stats.record(&result); - out.push((i, result)); + tests_since_kernel_reset += 1; let done = progress.fetch_add(1, Ordering::Relaxed) + 1; if done % 500 == 0 { kernel.sweep_runner_forges(); } + if tests_since_kernel_reset >= kernel_batch { + kernel = build_runner_kernel(); + tests_since_kernel_reset = 0; + } if done % 500 == 0 || done == total { eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total); } } - (stats, out) + stats }) .expect("failed to spawn test262 worker thread") }) @@ -803,26 +910,23 @@ fn run_tests() -> bool { .collect() }); - // Reduce partial stats and restore discovery order for deterministic output. + // Reduce partial stats without materializing every test result in memory. let mut stats = RunStats::default(); - let mut indexed: Vec<(usize, TestResult)> = Vec::with_capacity(total); - for (partial_stats, partial_results) in partials { + for partial_stats in partials { stats.merge(partial_stats); - indexed.extend(partial_results); } - indexed.sort_by_key(|(i, _)| *i); - let results: Vec = indexed.into_iter().map(|(_, r)| r).collect(); eprintln!(); let ran = stats.pass + stats.fail; + let executed_total = end_index.saturating_sub(skip_until); println!(); println!("═══════════════════════════════════════"); println!(" test262 results"); println!("═══════════════════════════════════════"); - println!(" total : {}", results.len()); - let total = results.len() as f64; + println!(" total : {}", executed_total); + let total = executed_total as f64; println!(" pass : {} ({:.1}%)", stats.pass, stats.pass as f64 / total * 100.0); println!(" fail : {} ({:.1}%)", stats.fail, stats.fail as f64 / total * 100.0); println!(" skip : {} ({:.1}%)", stats.skip, stats.skip as f64 / total * 100.0); @@ -842,7 +946,7 @@ fn run_tests() -> bool { } println!("═══════════════════════════════════════"); - if stats.fail > 0 { + if stats.fail > 0 && !allow_fail_exit { return false; } true diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index 0771b84c..96717476 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -8,7 +8,7 @@ use crate::vm::Vm; macro_rules! array_ptr { ($vm:expr, $args:expr) => {{ - match get_this_array_ref($vm.reg($args[0])) { + match get_this_array_ref($vm, $vm.reg($args[0])) { Ok(ptr) => ptr, Err(err) => return NativeResult::Err(err), } @@ -23,21 +23,36 @@ macro_rules! array_ptr_len { }}; } -fn get_this_array_ref(val: JsValue) -> Result<*mut JsObject, JsValue> { +fn array_type_error(vm: &mut Vm, msg: &str) -> JsValue { + crate::builtins::error::create_type_error(vm, msg) +} + +fn get_this_array_ref(vm: &mut Vm, val: JsValue) -> Result<*mut JsObject, JsValue> { if !val.is_object() { - return Err(JsValue::undefined()); + return Err(array_type_error(vm, "Array method called on incompatible receiver")); } let ptr = val.as_js_object_ptr(); if ptr.is_null() { - return Err(JsValue::undefined()); + return Err(array_type_error(vm, "Array method called on incompatible receiver")); } let obj = unsafe { &*ptr }; if !obj.is_array() { - return Err(JsValue::undefined()); + return Err(array_type_error(vm, "Array method called on incompatible receiver")); } Ok(ptr) } +fn require_callback(vm: &mut Vm, callback_val: JsValue) -> Result { + if !callback_val.is_object() { + return Err(array_type_error(vm, "callback is not a function")); + } + let ptr = callback_val.as_js_object_ptr(); + if ptr.is_null() || !unsafe { &*ptr }.is_function() { + return Err(array_type_error(vm, "callback is not a function")); + } + Ok(callback_val) +} + fn create_new_array(vm: &mut Vm, n: usize) -> *mut JsObject { let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; vm.alloc_object(JsObject::new_array( @@ -431,12 +446,12 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_for_each(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -452,12 +467,12 @@ pub fn array_for_each(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let new_arr = create_new_array(vm, n); for i in 0..n { @@ -479,12 +494,12 @@ pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let mut kept: Vec = Vec::new(); for i in 0..n { @@ -513,15 +528,15 @@ pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if n == 0 && args.len() < 3 { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); } if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let has_initial = args.len() > 2; let mut accumulator; let start_idx; @@ -552,12 +567,12 @@ pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -578,12 +593,12 @@ pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(false)); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Ok(JsValue::bool(false)); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -604,12 +619,12 @@ pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(false)); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Ok(JsValue::bool(false)); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -630,12 +645,12 @@ pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_flat_map(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let callback_val = vm.reg(args[1]); - if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let mut flat: Vec = Vec::new(); for i in 0..n { @@ -794,9 +809,12 @@ pub fn array_last_index_of(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_find_index(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Ok(JsValue::int(-1)); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } - let callback_val = vm.reg(args[1]); + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -819,9 +837,12 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult { (arr_ptr, len as i32) }; if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } - let callback_val = vm.reg(args[1]); + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in (0..n).rev() { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; @@ -841,14 +862,17 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_reduce_right(vm: &mut Vm, args: &[u8]) -> NativeResult { let (arr_ptr, n) = array_ptr_len!(vm, args); if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "callback is not a function")); } - let callback_val = vm.reg(args[1]); + let callback_val = match require_callback(vm, vm.reg(args[1])) { + Ok(callback) => callback, + Err(err) => return NativeResult::Err(err), + }; let (mut acc, start_idx): (JsValue, i32) = if args.len() > 2 { (vm.reg(args[2]), n as i32 - 1) } else { if n == 0 { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); } (unsafe { (*arr_ptr).get_prop_at(n - 1) }, n as i32 - 2) }; @@ -874,15 +898,13 @@ pub fn array_sort(vm: &mut Vm, args: &[u8]) -> NativeResult { let mut vals: Vec = (0..len).map(|i| unsafe { (*arr_ptr).get_prop_at(i) }).collect(); let comparator = if args.len() > 1 { let candidate = vm.reg(args[1]); - if candidate.is_object() { - let ptr = candidate.as_js_object_ptr(); - if !ptr.is_null() && unsafe { &*ptr }.is_function() { - Some(candidate) - } else { - None - } - } else { + if candidate.is_undefined() { None + } else { + match require_callback(vm, candidate) { + Ok(callback) => Some(callback), + Err(err) => return NativeResult::Err(err), + } } } else { None diff --git a/crates/oxide_vm/src/builtins/array_buffer.rs b/crates/oxide_vm/src/builtins/array_buffer.rs index b5892e41..93e71027 100644 --- a/crates/oxide_vm/src/builtins/array_buffer.rs +++ b/crates/oxide_vm/src/builtins/array_buffer.rs @@ -69,14 +69,29 @@ pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec) -> *mut JsObject { vm.alloc_object(obj) } -pub(crate) fn drop_array_buffer_native(obj: &mut JsObject) -> u64 { +fn array_buffer_vec_ptr(obj: &JsObject) -> Option<*mut Vec> { if !obj.is_array_buffer_obj() { - return 0; + return None; } - let Some(ptr) = obj.native_fn() else { + obj.native_fn().map(|ptr| ptr.as_ptr() as *mut Vec) +} + +pub(crate) fn clone_array_buffer_native(old_obj: &JsObject, new_obj: &mut JsObject) { + let Some(ptr) = array_buffer_vec_ptr(old_obj) else { + return; + }; + if ptr.is_null() { + return; + } + let cloned = unsafe { (&*ptr).clone() }; + let cloned_ptr = Box::into_raw(Box::new(cloned)); + new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned_ptr as *const ()) })); +} + +pub(crate) fn drop_array_buffer_native(obj: &mut JsObject) -> u64 { + let Some(data_ptr) = array_buffer_vec_ptr(obj) else { return 0; }; - let data_ptr = ptr.as_ptr() as *mut Vec; if data_ptr.is_null() { return 0; } diff --git a/crates/oxide_vm/src/builtins/number.rs b/crates/oxide_vm/src/builtins/number.rs index b4559292..e96fe66d 100644 --- a/crates/oxide_vm/src/builtins/number.rs +++ b/crates/oxide_vm/src/builtins/number.rs @@ -10,6 +10,37 @@ pub fn number_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { 0.0 }; + let number_proto = vm.session().builtin_world().number_proto.as_ptr() as *mut oxide_types::object::JsObject; + let is_ctor = if let Some(this_reg) = args.first().copied() { + let this_val = vm.reg(this_reg); + if this_val.is_object() { + let ptr = this_val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + let proto_ptr = unsafe { (*ptr).proto().as_js_object_ptr() }; + !proto_ptr.is_null() && std::ptr::eq(proto_ptr, number_proto) + } + } else { + false + } + } else { + false + }; + + if is_ctor { + let this_val = vm.reg(args[0]); + let obj = unsafe { &mut *this_val.as_js_object_ptr() }; + obj.type_tag = oxide_types::object::JsObject::OBJ_TYPE_NUMBER_OBJ; + let boxed = if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { + JsValue::int(n as i32) + } else { + JsValue::float(n) + }; + obj.set_prop_at(0, boxed); + return NativeResult::Ok(this_val); + } + if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { NativeResult::Ok(JsValue::int(n as i32)) } else { @@ -222,5 +253,17 @@ pub fn number_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { if this_val.is_int() || this_val.is_double() { return NativeResult::Ok(this_val); } - NativeResult::Ok(JsValue::float(f64::NAN)) + if this_val.is_object() { + let ptr = this_val.as_js_object_ptr(); + if !ptr.is_null() { + let obj = unsafe { &*ptr }; + if obj.is_number_obj() { + return NativeResult::Ok(obj.get_prop_at(0)); + } + } + } + NativeResult::Err(crate::builtins::error::create_type_error( + vm, + "Number.prototype.valueOf called on incompatible receiver", + )) } diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index f0f6b05a..3f8ec75d 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -4,9 +4,9 @@ use oxide_types::value::JsValue; impl Vm { #[inline(always)] - pub(crate) fn dispatch_add(&mut self, rd: usize, a: usize, b: usize) { - let lhs = self.regs[a]; - let rhs = self.regs[b]; + pub(crate) fn dispatch_add(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let lhs = self.coerce_primitive_bounded(self.regs[a], false)?; + let rhs = self.coerce_primitive_bounded(self.regs[b], false)?; if lhs.is_string() || rhs.is_string() { let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), lhs); let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); @@ -17,5 +17,6 @@ impl Vm { let rn = coercion::to_number(rhs, self.kernel_core.string_forge().as_ref()); self.regs[rd] = JsValue::float(ln + rn); } + Ok(()) } } diff --git a/crates/oxide_vm/src/dispatch/bitwise.rs b/crates/oxide_vm/src/dispatch/bitwise.rs index 0a59aa01..9edc85d9 100644 --- a/crates/oxide_vm/src/dispatch/bitwise.rs +++ b/crates/oxide_vm/src/dispatch/bitwise.rs @@ -1,4 +1,3 @@ -use crate::coercion; use crate::vm::Vm; use oxide_types::value::JsValue; @@ -13,92 +12,98 @@ impl Vm { } #[inline(always)] - pub(crate) fn dispatch_bit_and(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[a], sf) & coercion::to_int32(self.regs[b], sf)); + pub(crate) fn dispatch_bit_and(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[a])? & self.coerce_int32_bounded(self.regs[b])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_bit_or(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[a], sf) | coercion::to_int32(self.regs[b], sf)); + pub(crate) fn dispatch_bit_or(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[a])? | self.coerce_int32_bounded(self.regs[b])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_bit_xor(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[a], sf) ^ coercion::to_int32(self.regs[b], sf)); + pub(crate) fn dispatch_bit_xor(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[a])? ^ self.coerce_int32_bounded(self.regs[b])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_shl(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_int32(self.regs[a], sf); - let shift = coercion::to_uint32(self.regs[b], sf) & 0x1F; + pub(crate) fn dispatch_shl(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let lhs = self.coerce_int32_bounded(self.regs[a])?; + let shift = self.coerce_uint32_bounded(self.regs[b])? & 0x1F; self.regs[rd] = JsValue::int(lhs.wrapping_shl(shift)); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_shr(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_int32(self.regs[a], sf); - let shift = coercion::to_uint32(self.regs[b], sf) & 0x1F; + pub(crate) fn dispatch_shr(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let lhs = self.coerce_int32_bounded(self.regs[a])?; + let shift = self.coerce_uint32_bounded(self.regs[b])? & 0x1F; self.regs[rd] = JsValue::int(lhs >> shift); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_ushr(&mut self, rd: usize, a: usize, b: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_uint32(self.regs[a], sf); - let shift = coercion::to_uint32(self.regs[b], sf) & 0x1F; + pub(crate) fn dispatch_ushr(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let lhs = self.coerce_uint32_bounded(self.regs[a])?; + let shift = self.coerce_uint32_bounded(self.regs[b])? & 0x1F; self.write_ushr_result(rd, lhs >> shift); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_bit_not(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(!coercion::to_int32(self.regs[a], sf)); + pub(crate) fn dispatch_bit_not(&mut self, rd: usize, a: usize) -> Result<(), String> { + self.regs[rd] = JsValue::int(!self.coerce_int32_bounded(self.regs[a])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_bit_and(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[rd], sf) & coercion::to_int32(self.regs[a], sf)); + pub(crate) fn dispatch_compound_bit_and(&mut self, rd: usize, a: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[rd])? & self.coerce_int32_bounded(self.regs[a])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_bit_or(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[rd], sf) | coercion::to_int32(self.regs[a], sf)); + pub(crate) fn dispatch_compound_bit_or(&mut self, rd: usize, a: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[rd])? | self.coerce_int32_bounded(self.regs[a])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_bit_xor(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - self.regs[rd] = JsValue::int(coercion::to_int32(self.regs[rd], sf) ^ coercion::to_int32(self.regs[a], sf)); + pub(crate) fn dispatch_compound_bit_xor(&mut self, rd: usize, a: usize) -> Result<(), String> { + self.regs[rd] = + JsValue::int(self.coerce_int32_bounded(self.regs[rd])? ^ self.coerce_int32_bounded(self.regs[a])?); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_shl(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_int32(self.regs[rd], sf); - let shift = coercion::to_uint32(self.regs[a], sf) & 0x1F; + pub(crate) fn dispatch_compound_shl(&mut self, rd: usize, a: usize) -> Result<(), String> { + let lhs = self.coerce_int32_bounded(self.regs[rd])?; + let shift = self.coerce_uint32_bounded(self.regs[a])? & 0x1F; self.regs[rd] = JsValue::int(lhs.wrapping_shl(shift)); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_shr(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_int32(self.regs[rd], sf); - let shift = coercion::to_uint32(self.regs[a], sf) & 0x1F; + pub(crate) fn dispatch_compound_shr(&mut self, rd: usize, a: usize) -> Result<(), String> { + let lhs = self.coerce_int32_bounded(self.regs[rd])?; + let shift = self.coerce_uint32_bounded(self.regs[a])? & 0x1F; self.regs[rd] = JsValue::int(lhs >> shift); + Ok(()) } #[inline(always)] - pub(crate) fn dispatch_compound_ushr(&mut self, rd: usize, a: usize) { - let sf = self.kernel_core.string_forge().as_ref(); - let lhs = coercion::to_uint32(self.regs[rd], sf); - let shift = coercion::to_uint32(self.regs[a], sf) & 0x1F; + pub(crate) fn dispatch_compound_ushr(&mut self, rd: usize, a: usize) -> Result<(), String> { + let lhs = self.coerce_uint32_bounded(self.regs[rd])?; + let shift = self.coerce_uint32_bounded(self.regs[a])? & 0x1F; self.write_ushr_result(rd, lhs >> shift); + Ok(()) } } diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index 02d77ed9..387f3fab 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -47,7 +47,7 @@ impl Vm { let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - let n = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n + 1.0); self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; @@ -61,7 +61,7 @@ impl Vm { let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - let n = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n - 1.0); self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; @@ -76,7 +76,7 @@ impl Vm { let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; - let n = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n + 1.0); self.ordinary_set(obj, prop_name_si, new_val, receiver)?; self.regs[b] = new_val; @@ -91,7 +91,7 @@ impl Vm { let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; - let n = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n - 1.0); self.ordinary_set(obj, prop_name_si, new_val, receiver)?; self.regs[b] = new_val; @@ -103,9 +103,12 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - let rhs = self.regs[a]; + let prop_raw = { + let obj = unsafe { &mut *obj_ptr }; + self.read_member_prop(obj, prop_name_si, receiver)? + }; + let prop_val = self.coerce_primitive_bounded(prop_raw, false)?; + let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; let new_val = if prop_val.is_string() || rhs.is_string() { let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), prop_val); let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); @@ -115,6 +118,7 @@ impl Vm { let rn = coercion::to_number(rhs, self.kernel_core.string_forge().as_ref()); JsValue::float(ln + rn) }; + let obj = unsafe { &mut *obj_ptr }; self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; Ok(()) @@ -130,12 +134,15 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - let ln = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); - let rn = coercion::to_number(self.regs[a], self.kernel_core.string_forge().as_ref()); + let ln = { + let obj = unsafe { &mut *obj_ptr }; + let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; + self.coerce_number_bounded(prop_val)? + }; + let rn = self.coerce_number_bounded(self.regs[a])?; let new_val = JsValue::float(op(ln, rn)); self.regs[a] = new_val; + let obj = unsafe { &mut *obj_ptr }; self.set_member_prop(obj, prop_name_si, new_val, receiver)?; Ok(()) } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index ba29d700..d76a79bf 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -220,6 +220,8 @@ impl SessionGc { map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_set() { set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_array_buffer_obj() { + array_buffer::clone_array_buffer_native(old_ref, new_ref); } else if old_ref.is_typed_array_obj() { typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value); } else if old_ref.is_data_view_obj() { @@ -399,7 +401,7 @@ mod tests { use oxide_types::object::JsObject; use super::*; - use crate::builtins::{map, set}; + use crate::builtins::{array_buffer, data_view, map, set, typed_array}; use crate::native::NativeResult; use crate::vm::{CallFrame, FrameContinuation}; @@ -655,4 +657,97 @@ mod tests { assert!(vm.is_session_ptr(edges[0].as_js_object_ptr())); assert_eq!(vm.session_object_ptrs.len(), 2); } + + #[test] + fn session_gc_keeps_shared_array_buffer_alive_through_view_native_edges() { + let mut vm = vm_with_low_threshold(); + let root = plain_object(&mut vm); + + vm.regs[1] = JsValue::int(8); + let buffer = native_ok(array_buffer::array_buffer_constructor(&mut vm, &[0, 1])); + + vm.regs[1] = buffer; + let typed = native_ok(typed_array::int32array_constructor(&mut vm, &[0, 1])); + + vm.regs[1] = buffer; + let view = native_ok(data_view::data_view_constructor(&mut vm, &[0, 1])); + + vm.regs[0] = view; + vm.regs[1] = JsValue::int(0); + vm.regs[2] = JsValue::int(42); + vm.regs[3] = JsValue::bool(true); + native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2, 3])); + + unsafe { + (*root).set_prop_at(0, typed); + (*root).set_prop_at(1, view); + } + + let root_session = vm.promote_object(root); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(root_session); + let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + gc.collect(&mut vm); + vm.session_gc = gc; + + let live_root = unsafe { &*vm.regs[0].as_js_object_ptr() }; + let live_typed = live_root.get_prop_at(0); + let live_view = live_root.get_prop_at(1); + let typed_obj = unsafe { &*live_typed.as_js_object_ptr() }; + let view_obj = unsafe { &*live_view.as_js_object_ptr() }; + let typed_edges = typed_array::typed_array_native_edges(typed_obj); + let view_edges = data_view::data_view_native_edges(view_obj); + + assert_eq!(typed_edges.len(), 1); + assert_eq!(view_edges.len(), 1); + assert!(vm.is_session_ptr(typed_edges[0].as_js_object_ptr())); + assert!(vm.is_session_ptr(view_edges[0].as_js_object_ptr())); + assert!(std::ptr::eq(typed_edges[0].as_js_object_ptr(), view_edges[0].as_js_object_ptr())); + + vm.regs[0] = live_typed; + vm.regs[1] = JsValue::int(0); + assert_eq!(native_ok(typed_array::typed_array_at(&mut vm, &[0, 1])), JsValue::int(42)); + + vm.regs[0] = live_view; + vm.regs[1] = JsValue::int(4); + vm.regs[2] = JsValue::int(7); + vm.regs[3] = JsValue::bool(true); + native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2, 3])); + + vm.regs[0] = live_typed; + vm.regs[1] = JsValue::int(1); + assert_eq!(native_ok(typed_array::typed_array_at(&mut vm, &[0, 1])), JsValue::int(7)); + } + + #[test] + fn session_gc_rewrites_buffer_retained_only_by_data_view_native_edge() { + let mut vm = vm_with_low_threshold(); + vm.regs[1] = JsValue::int(8); + let buffer = native_ok(array_buffer::array_buffer_constructor(&mut vm, &[0, 1])); + vm.regs[1] = buffer; + let view = native_ok(data_view::data_view_constructor(&mut vm, &[0, 1])); + + vm.regs[0] = view; + vm.regs[1] = JsValue::int(0); + vm.regs[2] = JsValue::int(9); + native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2])); + + let view_session = vm.promote_object(view.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(view_session); + let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + gc.collect(&mut vm); + vm.session_gc = gc; + + let live_view = vm.regs[0]; + let live_view_obj = unsafe { &*live_view.as_js_object_ptr() }; + let edges = data_view::data_view_native_edges(live_view_obj); + assert_eq!(edges.len(), 1); + assert!(vm.is_session_ptr(edges[0].as_js_object_ptr())); + assert_eq!(vm.session_object_ptrs.len(), 2); + + vm.regs[0] = live_view; + vm.regs[1] = JsValue::int(0); + assert_eq!(native_ok(data_view::data_view_get_int32(&mut vm, &[0, 1])), JsValue::int(9)); + } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index e975e1f0..97a073b3 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -66,16 +66,16 @@ macro_rules! throw_err { macro_rules! binary_arith { ($self:ident, $a:expr, $b:expr, $rd:expr, $op:tt) => {{ - let l = coercion::to_number($self.regs[$a], $self.kernel_core.string_forge().as_ref()); - let r = coercion::to_number($self.regs[$b], $self.kernel_core.string_forge().as_ref()); + let l = $self.coerce_number_bounded($self.regs[$a])?; + let r = $self.coerce_number_bounded($self.regs[$b])?; $self.regs[$rd] = JsValue::float(l $op r); }} } macro_rules! compound_arith { ($self:ident, $rd:expr, $a:expr, $op:tt) => {{ - let l = coercion::to_number($self.regs[$rd], $self.kernel_core.string_forge().as_ref()); - let r = coercion::to_number($self.regs[$a], $self.kernel_core.string_forge().as_ref()); + let l = $self.coerce_number_bounded($self.regs[$rd])?; + let r = $self.coerce_number_bounded($self.regs[$a])?; $self.regs[$rd] = JsValue::float(l $op r); }} } @@ -175,6 +175,96 @@ pub struct Vm { } impl Vm { + const SYNC_NATIVE_ARG_BASE: usize = 0; + const SYNC_NATIVE_ARG_LIMIT: usize = 253; + + pub(crate) fn coerce_primitive_bounded(&mut self, value: JsValue, prefer_string: bool) -> Result { + if !value.is_object() { + return Ok(value); + } + + let obj_ptr = value.as_js_object_ptr(); + if obj_ptr.is_null() { + return Ok(value); + } + + let method_names = if prefer_string { ["toString", "valueOf"] } else { ["valueOf", "toString"] }; + + for method_name in method_names { + let method_si = self.kernel_core.string_forge().intern(method_name).0; + let method = { + let obj = unsafe { &*obj_ptr }; + self.ordinary_get(obj, method_si, value)? + }; + if method.is_undefined() || method.is_null() { + continue; + } + if !method.is_object() { + return Err(format!("TypeError: {method_name} is not callable")); + } + let method_ptr = method.as_js_object_ptr(); + if method_ptr.is_null() || !unsafe { &*method_ptr }.is_function() { + return Err(format!("TypeError: {method_name} is not callable")); + } + + let result = self.call_function_sync(method, value, &[])?; + if !result.is_object() { + return Ok(result); + } + } + + Err("TypeError: Cannot convert object to primitive value".into()) + } + + pub(crate) fn coerce_number_bounded(&mut self, value: JsValue) -> Result { + let primitive = self.coerce_primitive_bounded(value, false)?; + Ok(coercion::to_number(primitive, self.kernel_core.string_forge().as_ref())) + } + + pub(crate) fn coerce_int32_bounded(&mut self, value: JsValue) -> Result { + let n = self.coerce_number_bounded(value)?; + if n == 0.0 || !n.is_finite() { + return Ok(0); + } + let int = n.trunc().rem_euclid(4_294_967_296.0) as u32; + if int > i32::MAX as u32 { + Ok((int as i64 - 4_294_967_296i64) as i32) + } else { + Ok(int as i32) + } + } + + pub(crate) fn coerce_uint32_bounded(&mut self, value: JsValue) -> Result { + let n = self.coerce_number_bounded(value)?; + if n == 0.0 || !n.is_finite() { + return Ok(0); + } + Ok(n.trunc().rem_euclid(4_294_967_296.0) as u32) + } + + fn pack_sync_native_call_args( + &mut self, receiver: JsValue, callee: JsValue, args: &[JsValue], + ) -> Result, String> { + if args.len() > Self::SYNC_NATIVE_ARG_LIMIT { + return Err(format!( + "RangeError: synchronous native call supports at most {} arguments", + Self::SYNC_NATIVE_ARG_LIMIT + )); + } + + self.regs[253] = receiver; + self.regs[254] = callee; + + let mut arg_regs = Vec::with_capacity(args.len() + 1); + arg_regs.push(253); + for (idx, arg) in args.iter().enumerate() { + let reg = (Self::SYNC_NATIVE_ARG_BASE + idx) as u8; + self.regs[reg as usize] = *arg; + arg_regs.push(reg); + } + Ok(arg_regs) + } + pub(crate) fn is_session_ptr(&self, obj_ptr: *mut JsObject) -> bool { if obj_ptr.is_null() { return false; @@ -444,27 +534,14 @@ impl Vm { return Ok(JsValue::undefined()); } let saved_regs = self.regs; - let saved_this = self.regs[254]; - let saved_callee = self.regs[253]; - self.regs[253] = receiver; - self.regs[254] = callee; - for (idx, arg) in args.iter().enumerate() { - self.regs[240 + idx] = *arg; - } - let mut arg_regs = [0u8; 17]; - arg_regs[0] = 253; - for idx in 0..args.len().min(16) { - arg_regs[idx + 1] = 240 + idx as u8; - } + let arg_regs = self.pack_sync_native_call_args(receiver, callee, args)?; // SAFETY: native_fn was set via set_native_fn with a valid NativeFn pointer; // native_fn_ptr_to_fn is the single coercion point for NativeFnPtr → NativeFn. let func: NativeFn = unsafe { native_fn_ptr_to_fn(native_fn) }; self.native_call_depth += 1; - let result = func(self, &arg_regs[..args.len().min(16) + 1]); + let result = func(self, &arg_regs); self.native_call_depth -= 1; self.regs = saved_regs; - self.regs[254] = saved_this; - self.regs[253] = saved_callee; return match result { NativeResult::Ok(val) => Ok(val), NativeResult::Err(err) => Err(self.error_text(err)), @@ -589,7 +666,7 @@ impl Vm { } OpCode::ADD => { - self.dispatch_add(rd, a, b); + self.dispatch_add(rd, a, b)?; } OpCode::SUB => { @@ -609,36 +686,36 @@ impl Vm { } OpCode::NEG => { - let v = coercion::to_number(self.regs[a], self.kernel_core.string_forge().as_ref()); + let v = self.coerce_number_bounded(self.regs[a])?; self.regs[rd] = JsValue::float(-v); } OpCode::BIT_AND => { - self.dispatch_bit_and(rd, a, b); + self.dispatch_bit_and(rd, a, b)?; } OpCode::BIT_OR => { - self.dispatch_bit_or(rd, a, b); + self.dispatch_bit_or(rd, a, b)?; } OpCode::BIT_XOR => { - self.dispatch_bit_xor(rd, a, b); + self.dispatch_bit_xor(rd, a, b)?; } OpCode::SHL => { - self.dispatch_shl(rd, a, b); + self.dispatch_shl(rd, a, b)?; } OpCode::SHR => { - self.dispatch_shr(rd, a, b); + self.dispatch_shr(rd, a, b)?; } OpCode::USHR => { - self.dispatch_ushr(rd, a, b); + self.dispatch_ushr(rd, a, b)?; } OpCode::BIT_NOT => { - self.dispatch_bit_not(rd, a); + self.dispatch_bit_not(rd, a)?; } OpCode::EQ => { @@ -674,7 +751,7 @@ impl Vm { } OpCode::UNARY_PLUS => { - let v = coercion::to_number(self.regs[a], self.kernel_core.string_forge().as_ref()); + let v = self.coerce_number_bounded(self.regs[a])?; self.regs[rd] = JsValue::float(v); } @@ -962,8 +1039,8 @@ impl Vm { } OpCode::COMPOUND_ADD => { - let lhs = self.regs[rd]; - let rhs = self.regs[a]; + let lhs = self.coerce_primitive_bounded(self.regs[rd], false)?; + let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; if lhs.is_string() || rhs.is_string() { let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), lhs); let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); @@ -993,33 +1070,33 @@ impl Vm { } OpCode::COMPOUND_EXP => { - let l = coercion::to_number(self.regs[rd], self.kernel_core.string_forge().as_ref()); - let r = coercion::to_number(self.regs[a], self.kernel_core.string_forge().as_ref()); + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; self.regs[rd] = JsValue::float(l.powf(r)); } OpCode::COMPOUND_AND => { - self.dispatch_compound_bit_and(rd, a); + self.dispatch_compound_bit_and(rd, a)?; } OpCode::COMPOUND_OR => { - self.dispatch_compound_bit_or(rd, a); + self.dispatch_compound_bit_or(rd, a)?; } OpCode::COMPOUND_XOR => { - self.dispatch_compound_bit_xor(rd, a); + self.dispatch_compound_bit_xor(rd, a)?; } OpCode::COMPOUND_SHL => { - self.dispatch_compound_shl(rd, a); + self.dispatch_compound_shl(rd, a)?; } OpCode::COMPOUND_SHR => { - self.dispatch_compound_shr(rd, a); + self.dispatch_compound_shr(rd, a)?; } OpCode::COMPOUND_USHR => { - self.dispatch_compound_ushr(rd, a); + self.dispatch_compound_ushr(rd, a)?; } OpCode::TYPEOF => { @@ -1086,27 +1163,27 @@ impl Vm { } OpCode::INC_PRE => { - let n = coercion::to_number(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(self.regs[rd])?; let result = JsValue::float(n + 1.0); self.regs[rd] = result; self.regs[a] = result; } OpCode::INC_POST => { - let n = coercion::to_number(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(self.regs[rd])?; self.regs[a] = JsValue::float(n); self.regs[rd] = JsValue::float(n + 1.0); } OpCode::DEC_PRE => { - let n = coercion::to_number(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(self.regs[rd])?; let result = JsValue::float(n - 1.0); self.regs[rd] = result; self.regs[a] = result; } OpCode::DEC_POST => { - let n = coercion::to_number(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let n = self.coerce_number_bounded(self.regs[rd])?; self.regs[a] = JsValue::float(n); self.regs[rd] = JsValue::float(n - 1.0); } @@ -1232,6 +1309,15 @@ mod tests { NativeResult::Ok(JsValue::undefined()) } + fn native_return_last_arg(vm: &mut Vm, args: &[u8]) -> NativeResult { + let reg = *args.last().expect("receiver + args"); + NativeResult::Ok(vm.reg(reg)) + } + + fn native_return_arg_count(_vm: &mut Vm, args: &[u8]) -> NativeResult { + NativeResult::Ok(JsValue::int(args.len().saturating_sub(1) as i32)) + } + fn native_function(vm: &mut Vm, f: crate::native::NativeFn) -> JsValue { let proto = vm.session.builtin_world().function_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); @@ -1514,4 +1600,51 @@ mod tests { vm.ordinary_set(obj, x_si, JsValue::int(2), obj_val).expect("set"); assert_eq!(vm.ordinary_get(obj, x_si, obj_val).expect("get"), JsValue::int(2)); } + + #[test] + fn call_function_sync_passes_high_arity_native_args_without_truncation() { + let mut vm = Vm::new(); + let callee = native_function(&mut vm, native_return_last_arg); + let args: Vec = (0..20).map(JsValue::int).collect(); + + let result = vm + .call_function_sync(callee, JsValue::undefined(), &args) + .expect("high-arity sync call should succeed"); + + assert_eq!(result, JsValue::int(19)); + } + + #[test] + fn call_function_sync_reports_actual_native_arg_count() { + let mut vm = Vm::new(); + let callee = native_function(&mut vm, native_return_arg_count); + let args: Vec = (0..32).map(JsValue::int).collect(); + + let result = vm + .call_function_sync(callee, JsValue::undefined(), &args) + .expect("arg count should be preserved"); + + assert_eq!(result, JsValue::int(32)); + } + + #[test] + fn call_function_sync_rejects_unrepresentable_native_arity_without_register_corruption() { + let mut vm = Vm::new(); + let callee = native_function(&mut vm, native_return_arg_count); + vm.set_reg(1, JsValue::int(7)); + vm.set_reg(253, JsValue::int(11)); + vm.set_reg(254, JsValue::int(12)); + vm.set_reg(255, JsValue::int(13)); + + let args = vec![JsValue::undefined(); Vm::SYNC_NATIVE_ARG_LIMIT + 1]; + let err = vm + .call_function_sync(callee, JsValue::undefined(), &args) + .expect_err("too many args should fail cleanly"); + + assert!(err.contains("at most 253 arguments"), "unexpected error: {err}"); + assert_eq!(vm.reg(1), JsValue::int(7)); + assert_eq!(vm.reg(253), JsValue::int(11)); + assert_eq!(vm.reg(254), JsValue::int(12)); + assert_eq!(vm.reg(255), JsValue::int(13)); + } } diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index 9f3adaed..df36ee6f 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -318,6 +318,33 @@ fn array_sort_propagates_comparator_exception() { assert_eq!(to_str(&vm, result), "cmp"); } +#[test] +fn array_callback_type_errors_are_not_sentinels() { + let (vm, result) = eval("try { [1].map(null) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + + let (vm, result) = eval("try { [1].filter(undefined) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + + let (vm, result) = eval("try { [1].findIndex(0) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); +} + +#[test] +fn array_reduce_and_sort_invalid_usage_throw_type_error() { + let (vm, result) = eval("try { [].reduce(function(a, b) { return a + b; }) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + + let (vm, result) = eval("try { [].reduceRight(function(a, b) { return a + b; }) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + + let (vm, result) = eval("try { [1].sort(null) } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + + let (vm, result) = eval("[2,1].sort(undefined).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "1,2"); +} + #[test] fn array_copy_within_copies() { let (_vm, result) = eval("[1,2,3,4,5].copyWithin(0,3)").unwrap(); diff --git a/crates/oxide_vm/tests/builtins_boolean_tests.rs b/crates/oxide_vm/tests/builtins_boolean_tests.rs index bd706b7f..12031ac5 100644 --- a/crates/oxide_vm/tests/builtins_boolean_tests.rs +++ b/crates/oxide_vm/tests/builtins_boolean_tests.rs @@ -126,3 +126,13 @@ fn boolean_primitive_value_of_autoboxes() { let r = eval(&mut vm, "(true).constructor === Boolean").unwrap(); assert_eq!(r, JsValue::bool(true)); } + +#[test] +fn boolean_boxes_participate_in_numeric_coercion() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "+new Boolean(true)").unwrap(); + assert_eq!(r.as_double(), 1.0); + + let r = eval(&mut vm, "new Boolean(false) | 0").unwrap(); + assert_eq!(r.as_int(), 0); +} diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index 27acfda0..55f3d741 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -129,3 +129,10 @@ fn date_to_utc_string_not_empty() { let r = eval(&mut vm, "new Date(0).toUTCString()").unwrap(); assert!(r.is_string()); } + +#[test] +fn date_objects_use_bounded_numeric_coercion() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "+new Date(0)").unwrap(); + assert_eq!(r.as_double(), 0.0); +} diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index 100e86a2..dc5473ed 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -213,3 +213,20 @@ fn test_typed_array_survives_promote_reset() { fn test_flat_infinity_bounded() { assert_eq!(eval("[1,[2,[3,[4]]]].flat(Infinity).length"), "4"); } + +#[test] +fn regression_bounded_object_numeric_coercion() { + assert_eq!(eval("+new Number(1)"), "1"); + assert_eq!(eval("+new Boolean(true)"), "1"); + assert_eq!(eval("new Boolean(false) | 0"), "0"); + assert_eq!(eval("+({ valueOf: function() { return 7; } })"), "7"); + assert_eq!(eval("+({ toString: function() { return '9'; } })"), "9"); + assert_eq!( + eval("+({ valueOf: function() { return {}; }, toString: function() { return {}; } })"), + "vm error: TypeError: Cannot convert object to primitive value" + ); + assert_eq!( + eval("+({ valueOf: function() { throw new RangeError('coerce'); } })"), + "vm error: uncaught RangeError: coerce" + ); +}