fix: 修复同步调用参数限制与内建语义错误

This commit is contained in:
2026-06-23 14:38:58 +08:00
parent 2d166cbfa5
commit 69500513b3
13 changed files with 675 additions and 189 deletions
+123 -19
View File
@@ -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<PathBuf> {
WalkDir::new(test262_root)
let mut paths: Vec<PathBuf> = 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<usize> {
stdout.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix(label)?;
let value = rest.trim().split(' ').next()?;
value.parse::<usize>().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::<usize>().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::<usize>().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::<usize>().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<RunStats> = 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<TestResult> = 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
+83 -61
View File
@@ -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<JsValue, JsValue> {
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<JsValue> = 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<JsValue> = 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<JsValue> = (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
+19 -4
View File
@@ -69,14 +69,29 @@ pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec<u8>) -> *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<u8>> {
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<u8>)
}
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<u8>;
if data_ptr.is_null() {
return 0;
}
+44 -1
View File
@@ -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",
))
}
+4 -3
View File
@@ -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(())
}
}
+51 -46
View File
@@ -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(())
}
}
+18 -11
View File
@@ -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(())
}
+96 -1
View File
@@ -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));
}
}
+176 -43
View File
@@ -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<JsValue, String> {
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<f64, String> {
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<i32, String> {
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<u32, String> {
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<Vec<u8>, 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<JsValue> = (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<JsValue> = (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));
}
}
@@ -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();
@@ -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);
}
@@ -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);
}
+17
View File
@@ -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"
);
}