Merge remote-tracking branch 'upstream/main'

This commit is contained in:
爱特每羊队
2026-06-28 00:06:20 +08:00
78 changed files with 5438 additions and 555 deletions
Generated
+5
View File
@@ -1059,10 +1059,13 @@ dependencies = [
"fast-float",
"indexmap",
"memchr",
"oxide_compiler",
"oxide_kernel",
"oxide_log",
"oxide_parser",
"oxide_runtime_api",
"oxide_types",
"oxide_vm",
"regex",
"ryu",
"serde_json",
@@ -1084,6 +1087,7 @@ dependencies = [
"oxide_code_cache",
"oxide_compiler",
"oxide_kernel",
"oxide_log",
"oxide_parser",
"oxide_types",
"oxide_vm",
@@ -1166,6 +1170,7 @@ dependencies = [
"oxide_bytecode",
"oxide_compiler",
"oxide_kernel",
"oxide_log",
"oxide_parser",
"oxide_vm",
"serde",
+5
View File
@@ -19,3 +19,8 @@ serde_json.workspace = true
chrono.workspace = true
unicode-normalization.workspace = true
indexmap.workspace = true
[dev-dependencies]
oxide_vm = { path = "../oxide_vm" }
oxide_compiler = { path = "../oxide_compiler" }
oxide_parser = { path = "../oxide_parser" }
+39 -28
View File
@@ -68,7 +68,7 @@ fn create_new_array<H: VmHost>(vm: &mut H, n: usize) -> *mut JsObject {
}
fn array_length_arg<H: VmHost>(vm: &mut H, value: JsValue) -> Result<usize, JsValue> {
let n = oxide_runtime_api::to_number(value);
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if !n.is_finite() || n < 0.0 || n.fract() != 0.0 || n > MAX_DENSE_PROPS as f64 {
return Err(crate::error::create_range_error(vm, "Invalid array length"));
}
@@ -193,19 +193,19 @@ pub fn array_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.slice called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &*arr_ptr };
let n = arr.prop_count() as usize;
let start = if args.len() > 1 {
(oxide_runtime_api::to_number(vm.reg(args[1])) as i32) as usize
let n = arr.prop_count() as isize;
let rel_start = if args.len() > 1 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as isize
} else {
0
};
let end = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as i32) as usize
let rel_end = if args.len() > 2 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as isize
} else {
n
};
let start = start.min(n);
let end = end.min(n);
let start = if rel_start < 0 { (n + rel_start).max(0) } else { rel_start.min(n) } as usize;
let end = if rel_end < 0 { (n + rel_end).max(0) } else { rel_end.min(n) } as usize;
let count = end.saturating_sub(start);
let new_arr = create_new_array(vm, count);
@@ -225,7 +225,7 @@ pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = arr.prop_count() as usize;
let start = if args.len() > 1 {
let v = oxide_runtime_api::to_number(vm.reg(args[1]));
let v = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN);
let s = v as i32;
if s < 0 {
(n as i32 + s).max(0) as usize
@@ -237,7 +237,7 @@ pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
};
let delete_count = if args.len() > 2 {
let v = oxide_runtime_api::to_number(vm.reg(args[2]));
let v = vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN);
(v as usize).min(n - start)
} else {
n - start
@@ -342,6 +342,12 @@ pub fn array_join<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(result_val)
}
pub fn array_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// Array.prototype.toString() delegates to join with the default "," separator,
// ignoring its own arguments per spec.
array_join(vm, &[args[0]])
}
pub fn array_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.indexOf called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
@@ -352,7 +358,7 @@ pub fn array_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let target = vm.reg(args[1]);
for i in 0..n {
if oxide_runtime_api::strict_equality(arr.get_prop_at(i), target) {
if oxide_runtime_api::strict_eq(arr.get_prop_at(i), target) {
return NativeResult::Ok(JsValue::int(i as i32));
}
}
@@ -407,7 +413,7 @@ pub fn array_flat<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let arr = unsafe { &*arr_ptr };
let n = arr.prop_count() as usize;
let depth = if args.len() > 1 {
let n = oxide_runtime_api::to_number(vm.reg(args[1]));
let n = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN);
if !n.is_finite() {
vm.kernel_core().config.max_call_depth
} else {
@@ -825,19 +831,20 @@ pub fn array_fill<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.fill called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr };
let len = arr.prop_count() as usize;
let len = arr.prop_count() as isize;
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let start = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as i32).max(0) as usize
let rel_start = if args.len() > 2 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as isize
} else {
0
};
let end = if args.len() > 3 {
let e = oxide_runtime_api::to_number(vm.reg(args[3])) as i32;
(e as usize).min(len)
let rel_end = if args.len() > 3 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[3])) as isize
} else {
len
};
let start = (if rel_start < 0 { (len + rel_start).max(0) } else { rel_start.min(len) }) as usize;
let end = (if rel_end < 0 { (len + rel_end).max(0) } else { rel_end.min(len) }) as usize;
for i in start..end {
arr.set_prop_at(i, value);
}
@@ -848,28 +855,32 @@ pub fn array_copy_within<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.copyWithin called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr };
let len = arr.prop_count() as usize;
let len = arr.prop_count() as isize;
if len == 0 {
return NativeResult::Ok(vm.reg(args[0]));
}
let target = if args.len() > 1 {
(oxide_runtime_api::to_number(vm.reg(args[1])) as i32) as usize
let rel_target = if args.len() > 1 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as isize
} else {
0
};
let start = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as i32) as usize
let rel_start = if args.len() > 2 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as isize
} else {
0
};
let end = if args.len() > 3 {
(oxide_runtime_api::to_number(vm.reg(args[3])) as i32 as usize).min(len)
let rel_end = if args.len() > 3 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[3])) as isize
} else {
len
};
let target = (if rel_target < 0 { (len + rel_target).max(0) } else { rel_target.min(len) }) as usize;
let start = (if rel_start < 0 { (len + rel_start).max(0) } else { rel_start.min(len) }) as usize;
let end = (if rel_end < 0 { (len + rel_end).max(0) } else { rel_end.min(len) }) as usize;
let mut to = target;
let len_usize = len as usize;
for from in start..end {
if to >= len {
if to >= len_usize {
break;
}
let v = arr.get_prop_at(from);
@@ -885,7 +896,7 @@ pub fn array_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let arr = unsafe { &*arr_ptr };
let len = arr.prop_count() as i32;
let mut index = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as i32
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32
} else {
0
};
@@ -905,7 +916,7 @@ pub fn array_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let len = arr.prop_count() as i32;
let search = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
for i in (0..len).rev() {
if oxide_runtime_api::strict_equality(arr.get_prop_at(i), search) {
if oxide_runtime_api::strict_eq(arr.get_prop_at(i), search) {
return NativeResult::Ok(JsValue::int(i));
}
}
+3 -3
View File
@@ -20,7 +20,7 @@ fn type_error<H: VmHost>(vm: &mut H, msg: &str) -> NativeResult {
}
fn to_index<H: VmHost>(vm: &mut H, value: JsValue) -> Result<usize, JsValue> {
let n = oxide_runtime_api::to_number(value);
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if n.is_nan() {
return Ok(0);
}
@@ -30,8 +30,8 @@ fn to_index<H: VmHost>(vm: &mut H, value: JsValue) -> Result<usize, JsValue> {
Ok(n.trunc() as usize)
}
fn normalize_index<H: VmHost>(_vm: &mut H, value: JsValue, len: usize) -> usize {
let n = oxide_runtime_api::to_number(value);
fn normalize_index<H: VmHost>(vm: &mut H, value: JsValue, len: usize) -> usize {
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if n.is_nan() {
return 0;
}
+2 -2
View File
@@ -23,7 +23,7 @@ macro_rules! native_try {
}
fn to_index<H: VmHost>(vm: &mut H, value: JsValue, msg: &str) -> Result<usize, JsValue> {
let n = oxide_runtime_api::to_number(value);
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if n.is_nan() {
return Ok(0);
}
@@ -41,7 +41,7 @@ fn is_little_endian<H: VmHost>(vm: &mut H, args: &[u8], idx: usize) -> bool {
fn numeric_arg<H: VmHost>(vm: &mut H, args: &[u8], idx: usize) -> f64 {
args.get(idx)
.map(|reg| oxide_runtime_api::to_number(vm.reg(*reg)))
.map(|reg| vm.coerce_number_bounded(vm.reg(*reg)).unwrap_or(f64::NAN))
.unwrap_or(0.0)
}
+416 -70
View File
@@ -1,4 +1,4 @@
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, Timelike, Utc};
use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, Timelike, Utc};
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::JsObject;
@@ -70,6 +70,13 @@ fn dt_from_ms(ms: f64) -> Option<DateTime<Utc>> {
DateTime::from_timestamp_millis(ms as i64)
}
fn dt_from_ms_local(ms: f64) -> Option<DateTime<Local>> {
if !ms.is_finite() {
return None;
}
DateTime::from_timestamp_millis(ms as i64).map(|dt| dt.with_timezone(&Local))
}
fn naive_from_ms(ms: f64) -> Option<NaiveDateTime> {
if !ms.is_finite() {
return None;
@@ -102,40 +109,47 @@ pub fn date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let timestamp = if args.len() < 2 {
Utc::now().timestamp_millis() as f64
} else if args.len() > 2 {
let y = oxide_runtime_api::to_number(vm.reg(args[1])) as i32;
let m = oxide_runtime_api::to_number(vm.reg(args[2])) as u32;
let d = if args.len() > 3 {
oxide_runtime_api::to_number(vm.reg(args[3])) as u32
let now = chrono::Local::now();
let y_val = oxide_runtime_api::to_number(vm.reg(args[1]));
let m_val = oxide_runtime_api::to_number(vm.reg(args[2]));
if y_val.is_nan() || m_val.is_nan() {
f64::NAN
} else {
1
};
let h = if args.len() > 4 {
oxide_runtime_api::to_number(vm.reg(args[4])) as u32
} else {
0
};
let min = if args.len() > 5 {
oxide_runtime_api::to_number(vm.reg(args[5])) as u32
} else {
0
};
let sec = if args.len() > 6 {
oxide_runtime_api::to_number(vm.reg(args[6])) as u32
} else {
0
};
let ms = if args.len() > 7 {
oxide_runtime_api::to_number(vm.reg(args[7])) as u32
} else {
0
};
NaiveDate::from_ymd_opt(y, m + 1, d)
.and_then(|nd| {
nd.and_hms_milli_opt(h, min, sec, ms)
.and_then(|ndt| ndt.and_local_timezone(Utc).earliest())
})
.map(|dt| dt.timestamp_millis() as f64)
.unwrap_or(f64::NAN)
let y = y_val.trunc() as i32;
let m = m_val.trunc() as u32;
let d = if args.len() > 3 {
oxide_runtime_api::to_number(vm.reg(args[3])).trunc() as u32
} else {
now.day()
};
let h = if args.len() > 4 {
oxide_runtime_api::to_number(vm.reg(args[4])).trunc() as u32
} else {
now.hour()
};
let min = if args.len() > 5 {
oxide_runtime_api::to_number(vm.reg(args[5])).trunc() as u32
} else {
now.minute()
};
let sec = if args.len() > 6 {
oxide_runtime_api::to_number(vm.reg(args[6])).trunc() as u32
} else {
now.second()
};
let ms = if args.len() > 7 {
oxide_runtime_api::to_number(vm.reg(args[7])).trunc() as u32
} else {
now.timestamp_subsec_millis()
};
NaiveDate::from_ymd_opt(y, m + 1, d)
.and_then(|nd| {
nd.and_hms_milli_opt(h, min, sec, ms)
.and_then(|ndt| ndt.and_local_timezone(Local).earliest())
})
.map(|dt| dt.timestamp_millis() as f64)
.unwrap_or(f64::NAN)
}
} else {
let val = vm.reg(args[1]);
if val.is_string() {
@@ -214,12 +228,22 @@ pub fn date_parse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
// SAFETY: val is a string value.
let s = unsafe { (*val.as_string_ptr()).data.clone() };
let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"];
let mut ts = f64::NAN;
for fmt in &formats {
if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) {
ts = ndt.and_utc().timestamp_millis() as f64;
break;
if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(&s) {
ts = dt.timestamp_millis() as f64;
}
if ts.is_nan() {
let formats = [
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S%.f%:z",
"%Y-%m-%dT%H:%M:%S%.f%#z",
];
for fmt in &formats {
if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) {
ts = ndt.and_utc().timestamp_millis() as f64;
break;
}
}
}
if ts.is_nan() {
@@ -229,11 +253,58 @@ pub fn date_parse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
}
}
if ts.is_nan() {
if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y/%m/%d") {
if let Some(ndt) = nd.and_hms_opt(0, 0, 0).and_then(|n| n.and_local_timezone(Utc).earliest()) {
ts = ndt.timestamp_millis() as f64;
}
}
}
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_utc<H: VmHost>(_vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::float(f64::NAN))
pub fn date_utc<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 3 {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let y = oxide_runtime_api::to_number(vm.reg(args[1]));
let m = oxide_runtime_api::to_number(vm.reg(args[2]));
if y.is_nan() || m.is_nan() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let y = y.trunc() as i32;
let m = m.trunc() as u32;
let d = if args.len() > 3 {
oxide_runtime_api::to_number(vm.reg(args[3])).trunc() as u32
} else {
1
};
let h = if args.len() > 4 {
oxide_runtime_api::to_number(vm.reg(args[4])).trunc() as u32
} else {
0
};
let min = if args.len() > 5 {
oxide_runtime_api::to_number(vm.reg(args[5])).trunc() as u32
} else {
0
};
let sec = if args.len() > 6 {
oxide_runtime_api::to_number(vm.reg(args[6])).trunc() as u32
} else {
0
};
let ms = if args.len() > 7 {
oxide_runtime_api::to_number(vm.reg(args[7])).trunc() as u32
} else {
0
};
let ts = NaiveDate::from_ymd_opt(y, m + 1, d)
.and_then(|nd| nd.and_hms_milli_opt(h, min, sec, ms))
.and_then(|ndt| ndt.and_local_timezone(Utc).earliest())
.map(|dt| dt.timestamp_millis() as f64)
.unwrap_or(f64::NAN);
NativeResult::Ok(JsValue::float(ts))
}
macro_rules! make_getter {
@@ -268,15 +339,31 @@ macro_rules! make_utc_getter {
};
}
macro_rules! make_local_getter {
($name:ident, $f:expr, $df:expr) => {
pub fn $name<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &*native_try!(date_this(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float($df));
}
match dt_from_ms_local(ms) {
Some(dt) => NativeResult::Ok(JsValue::float(($f)(dt))),
None => NativeResult::Ok(JsValue::float($df)),
}
}
};
}
make_getter!(date_get_time, |dt: DateTime<Utc>| dt.timestamp_millis() as f64, f64::NAN);
make_getter!(date_get_full_year, |dt: DateTime<Utc>| dt.year() as f64, f64::NAN);
make_getter!(date_get_month, |dt: DateTime<Utc>| dt.month0() as f64, f64::NAN);
make_getter!(date_get_date, |dt: DateTime<Utc>| dt.day() as f64, f64::NAN);
make_getter!(date_get_day, |dt: DateTime<Utc>| dt.weekday().num_days_from_sunday() as f64, f64::NAN);
make_getter!(date_get_hours, |dt: DateTime<Utc>| dt.hour() as f64, f64::NAN);
make_getter!(date_get_minutes, |dt: DateTime<Utc>| dt.minute() as f64, f64::NAN);
make_getter!(date_get_seconds, |dt: DateTime<Utc>| dt.second() as f64, f64::NAN);
make_getter!(date_get_milliseconds, |dt: DateTime<Utc>| dt.timestamp_subsec_millis() as f64, f64::NAN);
make_local_getter!(date_get_full_year, |dt: DateTime<Local>| dt.year() as f64, f64::NAN);
make_local_getter!(date_get_month, |dt: DateTime<Local>| dt.month0() as f64, f64::NAN);
make_local_getter!(date_get_date, |dt: DateTime<Local>| dt.day() as f64, f64::NAN);
make_local_getter!(date_get_day, |dt: DateTime<Local>| dt.weekday().num_days_from_sunday() as f64, f64::NAN);
make_local_getter!(date_get_hours, |dt: DateTime<Local>| dt.hour() as f64, f64::NAN);
make_local_getter!(date_get_minutes, |dt: DateTime<Local>| dt.minute() as f64, f64::NAN);
make_local_getter!(date_get_seconds, |dt: DateTime<Local>| dt.second() as f64, f64::NAN);
make_local_getter!(date_get_milliseconds, |dt: DateTime<Local>| dt.timestamp_subsec_millis() as f64, f64::NAN);
make_utc_getter!(date_get_utc_full_year, |ndt: NaiveDateTime| ndt.date().year() as f64, f64::NAN);
make_utc_getter!(date_get_utc_month, |ndt: NaiveDateTime| ndt.date().month0() as f64, f64::NAN);
@@ -296,13 +383,21 @@ make_utc_getter!(
);
pub fn date_get_timezone_offset<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let _obj = unsafe { &*native_try!(date_this(vm, args)) };
let offset_min = local_offset_minutes();
let obj = unsafe { &*native_try!(date_this(vm, args)) };
let ms = get_timestamp(obj);
let offset_min = local_offset_minutes(ms);
NativeResult::Ok(JsValue::float(offset_min as f64))
}
fn local_offset_minutes() -> i32 {
chrono::Local::now().offset().local_minus_utc() / 60
fn local_offset_minutes(ms: f64) -> i32 {
if !ms.is_finite() {
return 0;
}
let offset = chrono::DateTime::from_timestamp_millis(ms as i64)
.map(|dt| dt.with_timezone(&chrono::Local))
.map(|dt| dt.offset().local_minus_utc())
.unwrap_or(0);
-offset / 60
}
fn get_opt_arg<H: VmHost>(vm: &H, args: &[u8], idx: usize, default: u32) -> u32 {
@@ -316,7 +411,7 @@ fn get_opt_arg<H: VmHost>(vm: &H, args: &[u8], idx: usize, default: u32) -> u32
pub fn date_set_time<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let val = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1]))
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN)
} else {
f64::NAN
};
@@ -326,13 +421,13 @@ pub fn date_set_time<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
pub fn date_set_full_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -341,7 +436,7 @@ pub fn date_set_full_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let nd = dt
.with_year(v as i32)
.and_then(|x| x.with_month0(m))
.and_then(|x| x.with_day0(d))
.and_then(|x| x.with_day0(if args.len() > 3 { d - 1 } else { d }))
.unwrap_or(dt);
let ts = nd.timestamp_millis() as f64;
set_timestamp(obj, ts);
@@ -349,31 +444,34 @@ pub fn date_set_full_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn date_set_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let d = get_opt_arg(vm, args, 2, dt.day0());
let nd = dt.with_month0(v as u32).and_then(|x| x.with_day0(d)).unwrap_or(dt);
let nd = dt
.with_month0(v as u32)
.and_then(|x| x.with_day0(if args.len() > 2 { d - 1 } else { d }))
.unwrap_or(dt);
let ts = nd.timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -384,13 +482,13 @@ pub fn date_set_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn date_set_hours<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -409,13 +507,13 @@ pub fn date_set_hours<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn date_set_minutes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -432,13 +530,13 @@ pub fn date_set_minutes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn date_set_seconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -453,13 +551,13 @@ pub fn date_set_seconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn date_set_milliseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let v = oxide_runtime_api::to_number(val);
let v = vm.coerce_number_bounded(val).unwrap_or(f64::NAN);
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms(ms) {
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
@@ -469,6 +567,254 @@ pub fn date_set_milliseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_full_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let year_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let y_num = oxide_runtime_api::to_number(year_arg);
if y_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let y = y_num.trunc() as i32;
let m = get_opt_arg(vm, args, 2, ndt.date().month0());
let d = get_opt_arg(vm, args, 3, ndt.date().day0());
let nd = NaiveDate::from_ymd_opt(y, m + 1, if args.len() > 3 { d } else { d + 1 })
.and_then(|date| {
date.and_hms_nano_opt(ndt.time().hour(), ndt.time().minute(), ndt.time().second(), ndt.time().nanosecond())
})
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let month_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let m_num = oxide_runtime_api::to_number(month_arg);
if m_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let m = m_num.trunc() as u32;
let d = get_opt_arg(vm, args, 2, ndt.date().day0());
let nd = NaiveDate::from_ymd_opt(ndt.date().year(), m + 1, if args.len() > 2 { d } else { d + 1 })
.and_then(|date| {
date.and_hms_nano_opt(ndt.time().hour(), ndt.time().minute(), ndt.time().second(), ndt.time().nanosecond())
})
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let date_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let d_num = oxide_runtime_api::to_number(date_arg);
if d_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let d = d_num.trunc() as u32;
let nd = NaiveDate::from_ymd_opt(ndt.date().year(), ndt.date().month(), d)
.and_then(|date| {
date.and_hms_nano_opt(ndt.time().hour(), ndt.time().minute(), ndt.time().second(), ndt.time().nanosecond())
})
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_hours<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let hours_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let h_num = oxide_runtime_api::to_number(hours_arg);
if h_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let h = h_num.trunc() as u32;
let min = get_opt_arg(vm, args, 2, ndt.time().minute());
let sec = get_opt_arg(vm, args, 3, ndt.time().second());
let ms_arg = get_opt_arg(vm, args, 4, ndt.time().nanosecond() as u32 / 1_000_000);
let nd = ndt
.with_hour(h)
.and_then(|x| x.with_minute(min))
.and_then(|x| x.with_second(sec))
.and_then(|x| x.with_nanosecond(ms_arg * 1_000_000))
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_minutes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let minutes_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let min_num = oxide_runtime_api::to_number(minutes_arg);
if min_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let min = min_num.trunc() as u32;
let sec = get_opt_arg(vm, args, 2, ndt.time().second());
let ms_arg = get_opt_arg(vm, args, 3, ndt.time().nanosecond() as u32 / 1_000_000);
let nd = ndt
.with_minute(min)
.and_then(|x| x.with_second(sec))
.and_then(|x| x.with_nanosecond(ms_arg * 1_000_000))
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_seconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let seconds_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let sec_num = oxide_runtime_api::to_number(seconds_arg);
if sec_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let sec = sec_num.trunc() as u32;
let ms_arg = get_opt_arg(vm, args, 2, ndt.time().nanosecond() as u32 / 1_000_000);
let nd = ndt
.with_second(sec)
.and_then(|x| x.with_nanosecond(ms_arg * 1_000_000))
.unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_set_utc_milliseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ndt = match naive_from_ms(ms) {
Some(n) => n,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let millis_arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let ms_num = oxide_runtime_api::to_number(millis_arg);
if ms_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let ms_arg = ms_num.trunc() as u32;
let nd = ndt.with_nanosecond(ms_arg * 1_000_000).unwrap_or(ndt);
let ts = nd.and_utc().timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_get_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &*native_try!(date_this(vm, args)) };
let ms = get_timestamp(obj);
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
match dt_from_ms_local(ms) {
Some(dt) => NativeResult::Ok(JsValue::float((dt.year() - 1900) as f64)),
None => NativeResult::Ok(JsValue::float(f64::NAN)),
}
}
pub fn date_set_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
let ms = get_timestamp(obj);
// A missing year argument coerces to NaN; per B.2.4.2 a NaN year sets the date value to
// NaN and returns NaN.
let arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let y_num = oxide_runtime_api::to_number(arg);
if y_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let y = y_num.trunc() as i32;
let full_year = if (0..=99).contains(&y) { y + 1900 } else { y };
let nd = dt.with_year(full_year).unwrap_or(dt);
let ts = nd.timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
pub fn date_to_gmt_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_to_utc_string(vm, args)
}
pub fn date_to_locale_date_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_to_date_string(vm, args)
}
pub fn date_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_to_string(vm, args)
}
pub fn date_to_locale_time_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_to_time_string(vm, args)
}
fn date_to_string_inner<H: VmHost>(vm: &mut H, args: &[u8], format_str: &str, invalid: &str) -> NativeResult {
let obj = unsafe { &*native_try!(date_this(vm, args)) };
let ms = get_timestamp(obj);
+113 -92
View File
@@ -6,31 +6,24 @@ use oxide_types::mem::P;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
fn create_error_object<H: VmHost>(host: &mut H, error_proto_ptr: *mut JsObject, name: &str, message: &str) -> JsValue {
let obj = host
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(error_proto_ptr)));
fn set_own_message<H: VmHost>(host: &mut H, this: *mut JsObject, args: &[u8]) {
if args.len() <= 1 {
return;
}
let msg_val = host.reg(args[1]);
if msg_val.is_undefined() {
return;
}
let msg_str = to_string(msg_val);
let sf = Arc::clone(host.kernel_core().perm_interner());
let sh = Arc::clone(host.kernel_core().shape_forge());
let si_message = sf.intern("message").0;
let sh_message = sh.make_shape(EMPTY_SHAPE_ID, si_message);
let msg_val = host.new_string(message);
let si = sf.intern("message").0;
let new_shape = sh.make_shape(EMPTY_SHAPE_ID, si);
let perm_val = host.new_string(&msg_str);
unsafe {
(*obj).set_shape_id(sh_message);
(*obj).push_prop(msg_val);
(*this).set_shape_id(new_shape);
(*this).push_prop(perm_val);
}
let si_name = sf.intern("name").0;
let sh_name = sh.make_shape(sh_message, si_name);
let name_val = host.new_string(name);
unsafe {
(*obj).set_shape_id(sh_name);
(*obj).push_prop(name_val);
}
JsValue::from_js_object(obj)
}
pub fn create_kind_error<H: VmHost>(host: &mut H, kind: &str, msg: &str) -> JsValue {
@@ -43,7 +36,21 @@ pub fn create_kind_error<H: VmHost>(host: &mut H, kind: &str, msg: &str) -> JsVa
"EvalError" => P::as_ptr(&host.session().builtin_world().eval_error_proto) as *mut JsObject,
_ => P::as_ptr(&host.session().builtin_world().error_proto) as *mut JsObject,
};
create_error_object(host, proto_ptr, kind, msg)
let obj = host
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)));
if !msg.is_empty() {
let sf = Arc::clone(host.kernel_core().perm_interner());
let sh = Arc::clone(host.kernel_core().shape_forge());
let si = sf.intern("message").0;
let new_shape = sh.make_shape(EMPTY_SHAPE_ID, si);
let msg_val = host.new_string(msg);
unsafe {
(*obj).set_shape_id(new_shape);
(*obj).push_prop(msg_val);
}
}
JsValue::from_js_object(obj)
}
pub fn create_type_error<H: VmHost>(host: &mut H, msg: &str) -> JsValue {
@@ -70,85 +77,99 @@ pub fn create_uri_error<H: VmHost>(host: &mut H, msg: &str) -> JsValue {
create_kind_error(host, "URIError", msg)
}
fn get_msg<H: VmHost>(host: &mut H, args: &[u8]) -> String {
if args.len() > 1 {
to_string(host.reg(args[1]))
} else {
String::new()
}
macro_rules! error_ctor {
($name:ident, $proto_field:ident) => {
pub fn $name<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(255);
let this = if this_val.is_object() {
this_val.as_js_object_ptr()
} else {
let proto_ptr = P::as_ptr(&host.session().builtin_world().$proto_field) as *mut JsObject;
host.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)))
};
set_own_message(host, this, args);
NativeResult::Ok(JsValue::from_js_object(this))
}
};
}
pub fn error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "Error", &msg))
}
pub fn type_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().type_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "TypeError", &msg))
}
pub fn reference_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().reference_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "ReferenceError", &msg))
}
pub fn range_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().range_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "RangeError", &msg))
}
pub fn syntax_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().syntax_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "SyntaxError", &msg))
}
pub fn uri_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().uri_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "URIError", &msg))
}
pub fn eval_error_constructor<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let msg = get_msg(host, args);
let proto_ptr = P::as_ptr(&host.session().builtin_world().eval_error_proto) as *mut JsObject;
NativeResult::Ok(create_error_object(host, proto_ptr, "EvalError", &msg))
}
error_ctor!(error_constructor, error_proto);
error_ctor!(type_error_constructor, type_error_proto);
error_ctor!(reference_error_constructor, reference_error_proto);
error_ctor!(range_error_constructor, range_error_proto);
error_ctor!(syntax_error_constructor, syntax_error_proto);
error_ctor!(uri_error_constructor, uri_error_proto);
error_ctor!(eval_error_constructor, eval_error_proto);
pub fn error_to_string<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(args[0]);
let (name, msg) = if this_val.is_object() {
let obj = unsafe { &*this_val.as_js_object_ptr() };
let name_val = obj
.hash_props_vec()
.and_then(|v| v.first())
.copied()
.unwrap_or(JsValue::undefined());
let msg_val = obj
.hash_props_vec()
.and_then(|v| v.get(1))
.copied()
.unwrap_or(JsValue::undefined());
(
host.lookup_str(name_val).unwrap_or_else(|| "Error".to_string()),
host.lookup_str(msg_val).unwrap_or_default(),
)
} else {
("Error".to_string(), String::new())
if !this_val.is_object() {
let err = create_type_error(host, "Error.prototype.toString called on non-object");
return NativeResult::Err(err);
}
let obj = unsafe { &*this_val.as_js_object_ptr() };
let sf = Arc::clone(host.kernel_core().perm_interner());
let si_name = sf.intern("name").0;
let si_msg = sf.intern("message").0;
let name_str = match host.resolve_property(obj, si_name) {
Some(v) if !v.is_undefined() => match host.coerce_primitive_bounded(v, true) {
Ok(prim) => to_string(prim),
Err(_) => {
let err = create_type_error(host, "Cannot convert name to primitive value");
return NativeResult::Err(err);
}
},
_ => "Error".to_string(),
};
let msg_str = match host.resolve_property(obj, si_msg) {
Some(v) if !v.is_undefined() => match host.coerce_primitive_bounded(v, true) {
Ok(prim) => to_string(prim),
Err(_) => {
let err = create_type_error(host, "Cannot convert message to primitive value");
return NativeResult::Err(err);
}
},
_ => String::new(),
};
let result = if name_str.is_empty() {
msg_str
} else if msg_str.is_empty() {
name_str
} else {
format!("{}: {}", name_str, msg_str)
};
let result = oxide_runtime_api::format_error_message(&name, &msg);
NativeResult::Ok(host.new_string(&result))
}
pub fn error_stack_getter<H: VmHost>(host: &mut H, _args: &[u8]) -> NativeResult {
pub fn error_stack_getter<H: VmHost>(host: &mut H, args: &[u8]) -> NativeResult {
let this_val = host.reg(args[0]);
let (name_str, msg_str) = if this_val.is_object() {
let obj = unsafe { &*this_val.as_js_object_ptr() };
let sf = Arc::clone(host.kernel_core().perm_interner());
let si_name = sf.intern("name").0;
let si_msg = sf.intern("message").0;
let n = host
.resolve_property(obj, si_name)
.and_then(|v| host.lookup_str(v))
.unwrap_or_else(|| "Error".to_string());
let m = host
.resolve_property(obj, si_msg)
.and_then(|v| host.lookup_str(v))
.unwrap_or_default();
(n, m)
} else {
("Error".to_string(), String::new())
};
let header = oxide_runtime_api::format_error_message(&name_str, &msg_str);
let mut result = header;
let names = host.call_stack_function_names();
let mut lines: Vec<String> = names.iter().rev().map(|n| format!(" at {} (native)", n)).collect();
lines.push(" at <anonymous> (native)".to_string());
let result = lines.join("\n");
for n in &names {
result.push_str(&format!("\n at {} (<unknown>:0:0)", n));
}
NativeResult::Ok(host.new_string(&result))
}
+186 -2
View File
@@ -36,9 +36,40 @@ pub fn make_iterator_for_value<H: VmHost>(vm: &mut H, value: JsValue) -> Result<
let next_fn = make_native_function(vm, "next", iterator_wrapper_next::<H> as *const (), 0);
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
// Forward IteratorClose to the inner iterator so for-of abrupt completion can clean up.
let return_si = vm.kernel_core().perm_interner().intern("return").0;
let return_fn = make_native_function(vm, "return", iterator_wrapper_return::<H> as *const (), 0);
vm.set_or_create_prop_value(wrapper_obj, return_si, return_fn);
Ok(JsValue::from_js_object(wrapper))
}
fn iterator_wrapper_return<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
return NativeResult::Ok(JsValue::undefined());
}
let wrapper = unsafe { &*this_val.as_js_object_ptr() };
let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0;
let inner = match vm.ordinary_get(wrapper, inner_si, this_val) {
Ok(inner) if inner.is_object() => inner,
_ => return NativeResult::Ok(JsValue::undefined()),
};
let inner_obj = unsafe { &*inner.as_js_object_ptr() };
let return_si = vm.kernel_core().perm_interner().intern("return").0;
let return_fn = match vm.ordinary_get(inner_obj, return_si, inner) {
Ok(f) if is_callable(f) => f,
_ => return NativeResult::Ok(JsValue::undefined()),
};
match vm.call_function_sync(return_fn, inner, &[]) {
Ok(result) => NativeResult::Ok(result),
Err(err) => match vm.take_uncaught_value() {
Some(original) => NativeResult::Err(original),
None => NativeResult::Err(crate::error::create_type_error(vm, &err)),
},
}
}
pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
@@ -66,7 +97,12 @@ pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
};
return match vm.call_function_sync(next, inner, &[]) {
Ok(result) => NativeResult::Ok(result),
Err(err) => NativeResult::Err(crate::error::create_type_error(vm, &err)),
// Forward the ORIGINAL thrown value (any type) instead of re-wrapping it as a
// TypeError, so a surrounding try/catch sees the real error.
Err(err) => match vm.take_uncaught_value() {
Some(original) => NativeResult::Err(original),
None => NativeResult::Err(crate::error::create_type_error(vm, &err)),
},
};
}
@@ -74,7 +110,7 @@ pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
}
fn get_iterator<H: VmHost>(vm: &mut H, value: JsValue) -> Result<JsValue, JsValue> {
if value.is_string() || is_array_value(value) {
if value.is_string() || is_array_value(value) || is_map_value(value) || is_set_value(value) {
return Ok(value);
}
@@ -115,6 +151,14 @@ fn next_array_like<H: VmHost>(vm: &mut H, wrapper: &mut JsObject, inner: JsValue
return Some(make_iter_result(vm, JsValue::undefined(), true));
}
// for-of loop default iterators: Map yields [key, value] entries, Set yields values.
if is_map_value(inner) {
return Some(map_set_step(vm, wrapper, inner, index_si, MapSetMode::MapEntries));
}
if is_set_value(inner) {
return Some(map_set_step(vm, wrapper, inner, index_si, MapSetMode::SetValues));
}
None
}
@@ -169,3 +213,143 @@ fn is_callable(value: JsValue) -> bool {
let ptr = value.as_js_object_ptr();
!ptr.is_null() && unsafe { &*ptr }.is_function()
}
fn is_map_value(value: JsValue) -> bool {
if !value.is_object() {
return false;
}
let ptr = value.as_js_object_ptr();
!ptr.is_null() && unsafe { &*ptr }.is_map()
}
fn is_set_value(value: JsValue) -> bool {
if !value.is_object() {
return false;
}
let ptr = value.as_js_object_ptr();
!ptr.is_null() && unsafe { &*ptr }.is_set()
}
fn make_map_set_pair<H: VmHost>(vm: &mut H, a: JsValue, b: JsValue) -> JsValue {
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let pair = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
2,
vm.epoch().bump(),
));
// SAFETY: pair is a freshly allocated array JsObject in the current epoch.
unsafe {
(*pair).set_prop_at(0, a);
(*pair).set_prop_at(1, b);
(*pair).set_prop_count(2);
}
JsValue::from_js_object(pair)
}
#[derive(Clone, Copy)]
enum MapSetMode {
MapEntries,
MapValues,
MapKeys,
SetEntries,
SetValues,
}
/// Advance a Map/Set iterator wrapper one step, yielding the {value, done} result for
/// the requested mode. Entries live in the indexmap stored in the collection's
/// native-data slot; the (a, b) pair is copied out before any allocation so no borrow
/// of the native collection is held across a `vm` call.
fn map_set_step<H: VmHost>(
vm: &mut H, wrapper: &mut JsObject, inner: JsValue, index_si: u32, mode: MapSetMode,
) -> JsValue {
let index = current_index(vm, wrapper, index_si);
let is_map = matches!(mode, MapSetMode::MapEntries | MapSetMode::MapValues | MapSetMode::MapKeys);
let entry: Option<(JsValue, JsValue)> = unsafe {
let obj_ptr = inner.as_js_object_ptr();
if obj_ptr.is_null() {
None
} else if is_map {
let p = (*obj_ptr).native_data() as *const crate::map::MapInner;
if p.is_null() {
None
} else {
(*p).get_index(index).map(|(key, value)| (key.0, *value))
}
} else {
let p = (*obj_ptr).native_data() as *const crate::set::SetInner;
if p.is_null() {
None
} else {
(*p).get_index(index).map(|elem| (elem.0, elem.0))
}
}
};
match entry {
Some((a, b)) => {
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((index + 1) as i32));
let value = match mode {
MapSetMode::MapEntries | MapSetMode::SetEntries => make_map_set_pair(vm, a, b),
MapSetMode::MapValues => b,
MapSetMode::MapKeys | MapSetMode::SetValues => a,
};
make_iter_result(vm, value, false)
}
None => make_iter_result(vm, JsValue::undefined(), true),
}
}
fn map_set_next_dispatch<H: VmHost>(vm: &mut H, args: &[u8], mode: MapSetMode) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "iterator next called on non-object"));
}
let wrapper = unsafe { &mut *this_val.as_js_object_ptr() };
let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0;
let index_si = vm.kernel_core().perm_interner().intern(INDEX_PROP).0;
let inner = match vm.ordinary_get(wrapper, inner_si, this_val) {
Ok(inner) if !inner.is_undefined() => inner,
_ => return NativeResult::Err(crate::error::create_type_error(vm, "iterator has no inner collection")),
};
NativeResult::Ok(map_set_step(vm, wrapper, inner, index_si, mode))
}
pub(crate) fn map_entries_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapEntries)
}
pub(crate) fn map_values_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapValues)
}
pub(crate) fn map_keys_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapKeys)
}
pub(crate) fn set_values_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
map_set_next_dispatch::<H>(vm, args, MapSetMode::SetValues)
}
pub(crate) fn set_entries_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
map_set_next_dispatch::<H>(vm, args, MapSetMode::SetEntries)
}
/// Build an iterator wrapper whose `next` delegates to a caller-supplied mode-specific
/// native function. Mirrors `make_iterator_for_value` but lets Map/Set prototype methods
/// pick the values/keys/entries variant.
pub(crate) fn make_mode_iterator<H: VmHost>(vm: &mut H, inner: JsValue, next_fn: *const ()) -> JsValue {
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let wrapper = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0;
let index_si = vm.kernel_core().perm_interner().intern(INDEX_PROP).0;
let next_si = vm.kernel_core().perm_interner().intern("next").0;
let wrapper_obj = unsafe { &mut *wrapper };
vm.set_or_create_prop_value(wrapper_obj, inner_si, inner);
vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(0));
let next = make_native_function(vm, "next", next_fn, 0);
vm.set_or_create_prop_value(wrapper_obj, next_si, next);
JsValue::from_js_object(wrapper)
}
+322 -31
View File
@@ -27,7 +27,82 @@ pub fn json_parse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Err(e) => return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("{}", e))),
};
NativeResult::Ok(value_to_jsvalue(vm, &parsed))
let mut result = value_to_jsvalue(vm, &parsed);
// Apply reviver if provided (D-05)
if args.len() > 2 {
let reviver_val = vm.reg(args[2]);
if reviver_val.is_object() {
let rptr = reviver_val.as_js_object_ptr();
if !rptr.is_null() && unsafe { (*rptr).is_function() } {
let empty_si = vm.kernel_core().perm_interner().intern("").0;
let holder = create_wrapper(vm, result);
let holder_ptr = holder.as_js_object_ptr();
match walk_reviver(vm, holder_ptr, empty_si, reviver_val) {
Ok(()) => {
let holder_obj = unsafe { &*holder_ptr };
let final_val = holder_obj.get_prop_at(0u32);
result = if final_val.is_undefined() { JsValue::undefined() } else { final_val };
}
Err(e) => return NativeResult::Err(e),
}
}
}
}
NativeResult::Ok(result)
}
fn walk_reviver<H: VmHost>(
vm: &mut H, holder_ptr: *mut JsObject, key_si: u32, reviver: JsValue,
) -> Result<(), JsValue> {
let holder = unsafe { &*holder_ptr };
let slot = vm.get_own_property_slot(holder, key_si);
let pos = match slot {
Some(p) => p,
None => return Ok(()),
};
let mut val = holder.get_prop_at(pos);
// Post-order: recurse on children first
if val.is_object() {
let obj_ptr = val.as_js_object_ptr();
if !obj_ptr.is_null() {
let obj = unsafe { &*obj_ptr };
if obj.is_array() {
let len = obj.prop_vec_len();
for i in 0..len {
let index_str = i.to_string();
let child_si = vm.kernel_core().perm_interner().intern(&index_str).0;
walk_reviver(vm, obj_ptr, child_si, reviver)?;
}
// Re-read value after children modified it
val = holder.get_prop_at(pos);
} else {
let keys = walk_own_keys(vm, obj);
for (child_si, _child_pos) in keys {
walk_reviver(vm, obj_ptr, child_si, reviver)?;
}
// Re-read value after children modified it
val = holder.get_prop_at(pos);
}
}
}
// Call reviver on current value
let kc = vm.kernel_core().clone();
let key_str = kc.perm_interner().lookup(key_si).unwrap_or("");
let key_val = vm.new_string(key_str);
let holder_val = JsValue::from_js_object(holder_ptr);
match vm.call_function_sync(reviver, holder_val, &[key_val, val]) {
Ok(new_val) => {
unsafe {
(*holder_ptr).set_prop_at(pos, new_val);
}
Ok(())
}
Err(msg) => Err(crate::error::create_type_error(vm, &msg)),
}
}
fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
@@ -75,6 +150,61 @@ fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
}
}
fn create_wrapper<H: VmHost>(vm: &mut H, value: JsValue) -> JsValue {
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let empty_si = vm.kernel_core().perm_interner().intern("").0;
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), empty_si);
obj.set_shape_id(new_shape);
obj.ensure_hash_props().push(value);
let obj_ptr = vm.alloc_object(obj);
JsValue::from_js_object(obj_ptr)
}
fn process_space(val: JsValue) -> String {
if val.is_int() || val.is_double() {
let n = oxide_runtime_api::to_integer_or_infinity(val);
if n.is_nan() || n.is_infinite() || n <= 0.0 {
return String::new();
}
let clamped = (n as usize).min(10);
" ".repeat(clamped)
} else if val.is_string() {
let s = unsafe { (*val.as_string_ptr()).data.clone() };
s.chars().take(10).collect()
} else {
let s = oxide_runtime_api::to_string(val);
s.chars().take(10).collect()
}
}
fn call_to_json<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &str) -> Result<JsValue, JsValue> {
if !obj_val.is_object() {
return Ok(obj_val);
}
let obj_ptr = obj_val.as_js_object_ptr();
if obj_ptr.is_null() {
return Ok(obj_val);
}
let tojson_si = vm.kernel_core().perm_interner().intern("toJSON").0;
let resolved = vm.resolve_property(unsafe { &*obj_ptr }, tojson_si);
match resolved {
Some(fn_val) if fn_val.is_object() => {
let fn_ptr = fn_val.as_js_object_ptr();
if !fn_ptr.is_null() && unsafe { (*fn_ptr).is_function() } {
let key_val = vm.new_string(key);
match vm.call_function_sync(fn_val, obj_val, &[key_val]) {
Ok(v) => Ok(v),
Err(msg) => Err(crate::error::create_type_error(vm, &msg)),
}
} else {
Ok(obj_val)
}
}
_ => Ok(obj_val),
}
}
pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
@@ -83,17 +213,79 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if value.is_undefined() {
return NativeResult::Ok(JsValue::undefined());
}
let mut replacer_fn: Option<JsValue> = None;
let mut replacer_whitelist: Option<HashSet<String>> = None;
if args.len() > 2 {
let replacer_val = vm.reg(args[2]);
if replacer_val.is_object() {
let rptr = replacer_val.as_js_object_ptr();
if !rptr.is_null() {
let robj = unsafe { &*rptr };
if robj.is_function() {
replacer_fn = Some(replacer_val);
} else if robj.is_array() {
let len = robj.prop_vec_len();
let mut whitelist = HashSet::new();
for i in 0..len {
let elem = robj.get_prop_at(i);
if !elem.is_undefined() {
whitelist.insert(oxide_runtime_api::to_string(elem));
}
}
replacer_whitelist = Some(whitelist);
}
}
}
}
let space = if args.len() > 3 { process_space(vm.reg(args[3])) } else { String::new() };
let holder = create_wrapper(vm, value);
let value = match call_to_json(vm, value, "") {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
};
let value = if let Some(replacer) = replacer_fn {
let key_val = vm.new_string("");
match vm.call_function_sync(replacer, holder, &[key_val, value]) {
Ok(v) => v,
Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)),
}
} else {
value
};
let mut visited = HashSet::new();
let mut output = String::new();
if jsvalue_to_json(vm, value, &mut visited, &mut output).is_err() {
return NativeResult::Err(crate::error::create_type_error(vm, "cyclic object value"));
let indent_level: usize = 0;
let key = "";
if jsvalue_to_json(
vm,
value,
&mut visited,
&mut output,
replacer_fn,
replacer_whitelist.as_ref(),
&space,
indent_level,
key,
)
.is_err()
{
return NativeResult::Err(crate::error::create_type_error(vm, "Converting circular structure to JSON"));
};
NativeResult::Ok(vm.new_string(&output))
}
#[allow(clippy::too_many_arguments)]
fn jsvalue_to_json<H: VmHost>(
vm: &H, val: JsValue, visited: &mut HashSet<*const u8>, out: &mut String,
vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize, key: &str,
) -> Result<(), ()> {
let val = call_to_json(vm, val, key).map_err(|_| ())?;
if val.is_null() {
out.push_str("null");
} else if val.is_undefined() {
@@ -112,7 +304,6 @@ fn jsvalue_to_json<H: VmHost>(
out.push_str(buf.format(n));
}
} else if val.is_string() {
// SAFETY: val is a string value.
let s = unsafe { (*val.as_string_ptr()).data.clone() };
stringify_string(&s, out);
} else if val.is_object() {
@@ -122,20 +313,18 @@ fn jsvalue_to_json<H: VmHost>(
return Ok(());
}
if visited.contains(&(obj_ptr as *const u8)) {
if !visited.insert(obj_ptr as *const JsObject) {
return Err(());
}
visited.insert(obj_ptr as *const u8);
let obj = unsafe { &*obj_ptr };
if obj.is_array() {
stringify_array(vm, obj, visited, out)?;
stringify_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
} else {
stringify_object(vm, obj, visited, out)?;
stringify_object(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
}
visited.remove(&(obj_ptr as *const u8));
visited.remove(&(obj_ptr as *const JsObject));
}
Ok(())
}
@@ -162,60 +351,162 @@ fn stringify_string(s: &str, out: &mut String) {
out.push('"');
}
#[allow(clippy::too_many_arguments)]
fn stringify_object<H: VmHost>(
vm: &H, obj: &JsObject, visited: &mut HashSet<*const u8>, out: &mut String,
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
) -> Result<(), ()> {
let has_space = !space.is_empty();
out.push('{');
let keys = walk_own_keys(vm, obj);
// Pre-compute (name, position) pairs with whitelist filtering
let kc = vm.kernel_core().clone();
let perm_interner = kc.perm_interner();
let entries: Vec<(String, u32)> = keys
.into_iter()
.filter_map(|(si, pos)| {
let name = perm_interner.lookup(si).unwrap_or("").to_string();
if let Some(whitelist) = replacer_whitelist {
if !whitelist.contains(&name) {
return None;
}
}
Some((name, pos))
})
.collect();
let mut first = true;
for (si, pos) in keys {
for (name, pos) in entries {
let val = obj.get_prop_at(pos);
// Replacer function callback (toJSON handled by jsvalue_to_json)
let val = if let Some(replacer) = replacer_fn {
let key_val = vm.new_string(&name);
let holder = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
match vm.call_function_sync(replacer, holder, &[key_val, val]) {
Ok(v) => {
if v.is_undefined() {
continue;
}
v
}
Err(_) => return Err(()),
}
} else {
val
};
let is_function = val.is_object() && {
let ptr = val.as_js_object_ptr();
if ptr.is_null() {
false
} else {
unsafe { (*ptr).is_function() }
}
!ptr.is_null() && unsafe { (*ptr).is_function() }
};
if val.is_undefined() || is_function {
continue;
}
if !first {
if !first && !has_space {
out.push(',');
} else if !first {
out.push_str(",\n");
}
first = false;
let name = vm.kernel_core().perm_interner().lookup(si).unwrap_or_default();
stringify_string(name, out);
out.push(':');
jsvalue_to_json(vm, val, visited, out)?;
if has_space {
for _ in 0..indent_level + 1 {
out.push_str(space);
}
stringify_string(&name, out);
out.push(':');
out.push(' ');
} else {
stringify_string(&name, out);
out.push(':');
}
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1, &name)?;
}
if has_space && !first {
out.push('\n');
for _ in 0..indent_level {
out.push_str(space);
}
}
out.push('}');
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn stringify_array<H: VmHost>(
vm: &H, obj: &JsObject, visited: &mut HashSet<*const u8>, out: &mut String,
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
) -> Result<(), ()> {
let has_space = !space.is_empty();
out.push('[');
let len = obj.prop_vec_len();
for i in 0..len {
if i > 0 {
if i > 0 && !has_space {
out.push(',');
} else if i > 0 {
out.push_str(",\n");
}
if has_space {
out.push('\n');
for _ in 0..indent_level + 1 {
out.push_str(space);
}
}
let val = obj.get_prop_at(i);
let index_str = i.to_string();
// Replacer function callback (toJSON handled by jsvalue_to_json)
let val = if let Some(replacer) = replacer_fn {
let key_val = vm.new_string(&index_str);
let holder = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
match vm.call_function_sync(replacer, holder, &[key_val, val]) {
Ok(v) => {
if v.is_undefined() {
out.push_str("null");
continue;
}
v
}
Err(_) => return Err(()),
}
} else {
val
};
let is_function = val.is_object() && {
let ptr = val.as_js_object_ptr();
if ptr.is_null() {
false
} else {
unsafe { (*ptr).is_function() }
}
!ptr.is_null() && unsafe { (*ptr).is_function() }
};
if is_function || val.is_undefined() {
out.push_str("null");
} else {
jsvalue_to_json(vm, val, visited, out)?;
jsvalue_to_json(
vm,
val,
visited,
out,
replacer_fn,
replacer_whitelist,
space,
indent_level + 1,
&index_str,
)?;
}
}
if has_space && len > 0 {
out.push('\n');
for _ in 0..indent_level {
out.push_str(space);
}
}
out.push(']');
+31 -1
View File
@@ -15,7 +15,7 @@ macro_rules! native_try {
};
}
type MapInner = indexmap::IndexMap<SetKey, JsValue>;
pub(crate) type MapInner = indexmap::IndexMap<SetKey, JsValue>;
/// Retrieve the `IndexMap` pointer stored in a Map object's native-data slot.
///
@@ -201,3 +201,33 @@ pub fn map_size<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let inner = native_try!(get_map_inner(vm, this_val));
NativeResult::Ok(JsValue::float(unsafe { (*inner).len() } as f64))
}
pub fn map_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_map_inner(vm, this_val));
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::map_entries_iter_next::<H> as *const (),
))
}
pub fn map_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_map_inner(vm, this_val));
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::map_values_iter_next::<H> as *const (),
))
}
pub fn map_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_map_inner(vm, this_val));
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::map_keys_iter_next::<H> as *const (),
))
}
+8 -8
View File
@@ -2,11 +2,11 @@ use oxide_types::value::JsValue;
use oxide_runtime_api::{NativeResult, VmHost};
fn num<H: VmHost>(vm: &H, reg: u8) -> f64 {
oxide_runtime_api::to_number(vm.reg(reg))
fn num<H: VmHost>(vm: &mut H, reg: u8) -> f64 {
vm.coerce_number_bounded(vm.reg(reg)).unwrap_or(f64::NAN)
}
fn arg1<H: VmHost>(vm: &H, args: &[u8]) -> f64 {
fn arg1<H: VmHost>(vm: &mut H, args: &[u8]) -> f64 {
if args.len() < 2 {
f64::NAN
} else {
@@ -14,11 +14,11 @@ fn arg1<H: VmHost>(vm: &H, args: &[u8]) -> f64 {
}
}
fn arg2<H: VmHost>(vm: &H, args: &[u8]) -> (f64, f64) {
(
if args.len() > 1 { num(vm, args[1]) } else { f64::NAN },
if args.len() > 2 { num(vm, args[2]) } else { f64::NAN },
)
fn arg2<H: VmHost>(vm: &mut H, args: &[u8]) -> (f64, f64) {
// Sequenced (not a tuple literal) so the two coercions take &mut H one at a time.
let a = if args.len() > 1 { num(vm, args[1]) } else { f64::NAN };
let b = if args.len() > 2 { num(vm, args[2]) } else { f64::NAN };
(a, b)
}
pub fn math_abs<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
+95 -44
View File
@@ -3,7 +3,11 @@ use oxide_types::value::JsValue;
use oxide_runtime_api::{NativeResult, VmHost};
pub fn number_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = if args.len() > 1 { oxide_runtime_api::to_number(vm.reg(args[1])) } else { 0.0 };
let n = if args.len() > 1 {
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN)
} 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);
@@ -46,26 +50,15 @@ pub fn number_is_nan<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false));
}
let val = vm.reg(args[1]);
if val.is_double() {
NativeResult::Ok(JsValue::bool(val.as_double().is_nan()))
} else {
NativeResult::Ok(JsValue::bool(false))
}
let n = oxide_runtime_api::to_number(vm.reg(args[1]));
NativeResult::Ok(JsValue::bool(n.is_nan()))
}
pub fn number_is_finite<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false));
}
let val = vm.reg(args[1]);
if val.is_int() {
return NativeResult::Ok(JsValue::bool(true));
}
if val.is_double() {
return NativeResult::Ok(JsValue::bool(val.as_double().is_finite()));
}
let n = oxide_runtime_api::to_number(val);
let n = oxide_runtime_api::to_number(vm.reg(args[1]));
NativeResult::Ok(JsValue::bool(n.is_finite()))
}
@@ -81,7 +74,7 @@ pub fn number_parse_int<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let radix = if args.len() > 2 {
let r = oxide_runtime_api::to_number(vm.reg(args[2])) as i32;
let r = vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as i32;
if r == 0 {
10
} else {
@@ -128,9 +121,9 @@ pub fn number_parse_float<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn number_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = oxide_runtime_api::to_number(vm.reg(args[0]));
let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN);
let radix = if args.len() > 1 {
let r = oxide_runtime_api::to_number(vm.reg(args[1])) as u32;
let r = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as u32;
r.clamp(2, 36)
} else {
10u32
@@ -179,65 +172,123 @@ pub fn number_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
pub fn number_to_fixed<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = oxide_runtime_api::to_number(vm.reg(args[0]));
let digits = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN);
if n.is_nan() {
return NativeResult::Ok(vm.new_string("NaN"));
}
if n.is_infinite() {
return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" }));
}
let fraction_digits = if args.len() > 1 {
let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1]));
if !(0.0..=100.0).contains(&raw) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"toFixed() fractionDigits must be between 0 and 100",
));
}
raw as usize
} else {
0usize
}
.min(20);
let formatted = format!("{:.digits$}", n);
};
let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n };
let formatted = format!("{:.precision$}", n_abs, precision = fraction_digits);
NativeResult::Ok(vm.new_string(&formatted))
}
pub fn number_is_integer<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
if !val.is_int() && !val.is_double() {
return NativeResult::Ok(JsValue::bool(false));
}
let n = oxide_runtime_api::to_number(val);
NativeResult::Ok(JsValue::bool(n.trunc() == n && n.is_finite()))
}
pub fn number_is_safe_integer<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
if !val.is_int() && !val.is_double() {
return NativeResult::Ok(JsValue::bool(false));
}
let n = oxide_runtime_api::to_number(val);
let safe = n.trunc() == n && n.is_finite() && n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64;
NativeResult::Ok(JsValue::bool(safe))
}
pub fn number_to_exponential<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = oxide_runtime_api::to_number(vm.reg(args[0]));
let digits = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
} else {
0usize
pub fn number_to_precision<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() <= 1 {
return NativeResult::Err(crate::error::create_range_error(
vm,
"toPrecision() requires a precision argument between 1 and 100",
));
}
.min(100);
let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN);
let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1]));
if !(1.0..=100.0).contains(&raw) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"toPrecision() precision must be between 1 and 100",
));
}
let precision = raw as usize;
if n.is_nan() {
return NativeResult::Ok(vm.new_string("NaN"));
}
if n.is_infinite() {
return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" }));
}
let formatted = format!("{:.digits$e}", n);
NativeResult::Ok(vm.new_string(&formatted))
let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n.abs() };
let is_neg = n.is_sign_negative() && !(n == 0.0);
let e = if n_abs == 0.0 { 0i32 } else { n_abs.log10().floor() as i32 };
let formatted = if e >= -6 && e < precision as i32 {
let dec = ((precision as i32 - e - 1).max(0)) as usize;
format!("{:.dec$}", n_abs, dec = dec)
} else {
format!("{:.dec$e}", n_abs, dec = precision - 1)
};
if is_neg {
NativeResult::Ok(vm.new_string(&format!("-{}", formatted)))
} else {
NativeResult::Ok(vm.new_string(&formatted))
}
}
pub fn number_to_precision<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = oxide_runtime_api::to_number(vm.reg(args[0]));
let precision = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
} else {
0usize
}
.min(21);
pub fn number_to_exponential<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let n = vm.coerce_number_bounded(vm.reg(args[0])).unwrap_or(f64::NAN);
if n.is_nan() {
return NativeResult::Ok(vm.new_string("NaN"));
}
if n.is_infinite() {
return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" }));
}
let formatted = format!("{:.precision$}", n);
NativeResult::Ok(vm.new_string(&formatted))
let n_abs = if n == 0.0 && n.is_sign_negative() { 0.0 } else { n.abs() };
let sign_prefix = if n.is_sign_negative() && !n.is_nan() && !(n == 0.0) { "-" } else { "" };
if args.len() <= 1 {
let formatted = format!("{:e}", n_abs);
let mut s = formatted;
if let Some(e_pos) = s.find('e') {
let mut mantissa = s[..e_pos].to_string();
while mantissa.ends_with('0') && mantissa.len() > 1 {
mantissa.pop();
}
mantissa = mantissa.trim_end_matches('.').to_string();
if mantissa.contains('.') {
s = format!("{}{}", mantissa, &s[e_pos..]);
}
}
let result = format!("{}{}", sign_prefix, s);
return NativeResult::Ok(vm.new_string(&result));
}
let raw = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1]));
if !(0.0..=100.0).contains(&raw) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"toExponential() fractionDigits must be between 0 and 100",
));
}
let fraction_digits = raw as usize;
let formatted = format!("{:.digits$e}", n_abs, digits = fraction_digits);
NativeResult::Ok(vm.new_string(&format!("{}{}", sign_prefix, formatted)))
}
pub fn number_value_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
+336 -69
View File
@@ -6,6 +6,13 @@ use oxide_types::value::JsValue;
use oxide_runtime_api::{NativeResult, VmHost};
fn is_integer_index(key: &str) -> bool {
if key.is_empty() || key.len() > 1 && key.as_bytes()[0] == b'0' {
return false;
}
key.bytes().all(|b| b.is_ascii_digit()) && key.parse::<u64>().unwrap_or(u64::MAX) < (1u64 << 32) - 1
}
pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
let mut keys: Vec<(u32, u32)> = Vec::new();
let shape_id = obj.shape_id();
@@ -33,6 +40,16 @@ pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)
}
pos += 1;
}
keys.sort_by(|(a_si, _), (b_si, _)| {
let a_str = vm.kernel_core().perm_interner().lookup(*a_si).unwrap_or("");
let b_str = vm.kernel_core().perm_interner().lookup(*b_si).unwrap_or("");
match (is_integer_index(a_str), is_integer_index(b_str)) {
(true, true) => a_str.parse::<u32>().unwrap().cmp(&b_str.parse::<u32>().unwrap()),
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
(false, false) => std::cmp::Ordering::Equal,
}
});
keys
}
@@ -42,28 +59,29 @@ pub fn object_constructor<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
}
pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let val = vm.reg(args[1]);
if !val.is_object() {
return NativeResult::Err(JsValue::undefined());
}
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Err(JsValue::undefined());
}
let obj_ptr = match require_obj_arg(vm, args, "keys") {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let key_names: Vec<String>;
{
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
key_names = keys
let owned_keys: Vec<(u32, u32)> = keys
.into_iter()
.filter(|(_si, offset)| {
obj.prop_meta_at(*offset)
.map(|m| m.attributes.enumerable())
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
})
.collect();
key_names = owned_keys
.iter()
.map(|(si, _offset)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string())
.collect();
}
// keys
let n = key_names.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.alloc_object(JsObject::new_array(
@@ -86,11 +104,18 @@ pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
pub fn object_create<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.create: at least 1 argument required"));
}
let proto_val = vm.reg(args[1]);
if !proto_val.is_null() && !proto_val.is_object() {
return NativeResult::Ok(JsValue::undefined());
if proto_val.is_null() {
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
return NativeResult::Ok(JsValue::from_js_object(obj));
}
if !proto_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.create: prototype must be an object or null",
));
}
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val));
NativeResult::Ok(JsValue::from_js_object(obj))
@@ -109,53 +134,67 @@ pub fn object_assign<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if target_ptr.is_null() {
return NativeResult::Ok(target_val);
}
{
let target = unsafe { &mut *target_ptr };
for &arg_reg in args.iter().skip(2) {
let source_val = vm.reg(arg_reg);
if !source_val.is_object() {
continue;
}
let source_ptr = source_val.as_js_object_ptr();
if source_ptr.is_null() {
continue;
}
let source = unsafe { &*source_ptr };
let source_keys = walk_own_keys(vm, source);
for (_si, offset) in source_keys {
let prop_val = source.get_prop_at(offset);
target.push_prop(prop_val);
}
let mut all_assignments: Vec<(u32, JsValue, JsValue)> = Vec::new();
for &arg_reg in args.iter().skip(2) {
let source_val = vm.reg(arg_reg);
if !source_val.is_object() {
continue;
}
let source_ptr = source_val.as_js_object_ptr();
if source_ptr.is_null() {
continue;
}
let source_keys: Vec<(u32, u32)> = {
let source = unsafe { &*source_ptr };
walk_own_keys(vm, source)
};
for (si, offset) in source_keys {
let source = unsafe { &*source_ptr };
let val = source.get_prop_at(offset);
all_assignments.push((si, source_val, val));
}
}
let target = unsafe { &mut *target_ptr };
for (si, source_val, val) in all_assignments {
let promoted = vm.promote_if_needed_for_write_ptr(target_ptr, val);
let _ = vm.ordinary_set(target, si, promoted, source_val);
}
NativeResult::Ok(target_val)
}
pub fn object_is<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let lhs = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let rhs = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
if args.len() < 3 {
return NativeResult::Err(crate::error::create_type_error(vm, "Object.is called with insufficient arguments"));
}
let lhs = vm.reg(args[1]);
let rhs = vm.reg(args[2]);
NativeResult::Ok(JsValue::bool(oxide_runtime_api::same_value(lhs, rhs)))
}
pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 4 {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.defineProperty: expected at least 3 arguments",
));
}
let obj_val = vm.reg(args[1]);
if !obj_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object"));
}
let obj_ptr = obj_val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object"));
}
let desc_val = vm.reg(args[3]);
if !desc_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object"));
}
let desc_ptr = desc_val.as_js_object_ptr();
if desc_ptr.is_null() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object"));
}
let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2]));
let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0;
@@ -188,10 +227,33 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
));
}
let existing_pos = {
let obj = unsafe { &*obj_ptr };
vm.get_own_property_slot(obj, si)
};
let existing_meta = existing_pos.and_then(|pos| {
let obj = unsafe { &*obj_ptr };
obj.prop_meta_at(pos)
});
let existing_value = existing_pos.map_or(JsValue::undefined(), |pos| {
let obj = unsafe { &*obj_ptr };
obj.get_prop_at(pos)
});
let obj = unsafe { &mut *obj_ptr };
if has_accessor {
let get = get_field.unwrap_or(JsValue::undefined());
let set = set_field.unwrap_or(JsValue::undefined());
let get = get_field.unwrap_or_else(|| {
existing_meta
.filter(|m| m.is_accessor)
.map(|m| m.get)
.unwrap_or(JsValue::undefined())
});
let set = set_field.unwrap_or_else(|| {
existing_meta
.filter(|m| m.is_accessor)
.map(|m| m.set)
.unwrap_or(JsValue::undefined())
});
if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) {
return NativeResult::Err(crate::error::create_type_error(
vm,
@@ -204,22 +266,64 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
{
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
}
} else {
let value = value_field.unwrap_or(JsValue::undefined());
let writable = writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false);
} else if has_data {
let value = if existing_pos.is_some() {
value_field.unwrap_or(existing_value)
} else {
value_field.unwrap_or(JsValue::undefined())
};
let writable = if existing_pos.is_some() {
writable_field
.map(oxide_runtime_api::to_boolean)
.unwrap_or_else(|| existing_meta.map(|m| m.attributes.writable()).unwrap_or(false))
} else {
writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false)
};
if vm
.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable))
.is_err()
{
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
}
} else {
if existing_pos.is_none() {
if vm
.define_data_property(
obj,
si,
JsValue::undefined(),
PropAttributes::new(false, enumerable, configurable),
)
.is_err()
{
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
}
return NativeResult::Ok(obj_val);
}
let is_accessor = existing_meta.map(|m| m.is_accessor).unwrap_or(false);
let writable = existing_meta.map(|m| m.attributes.writable()).unwrap_or(true);
let attrs = PropAttributes::new(writable, enumerable, configurable);
if is_accessor {
let get = existing_meta.map(|m| m.get).unwrap_or(JsValue::undefined());
let set = existing_meta.map(|m| m.set).unwrap_or(JsValue::undefined());
if vm.define_accessor_property(obj, si, get, set, attrs).is_err() {
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
}
} else {
if vm.define_data_property(obj, si, existing_value, attrs).is_err() {
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
}
}
}
NativeResult::Ok(obj_val)
}
pub fn object_get_own_property_descriptor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 3 {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.getOwnPropertyDescriptor called on non-object",
));
}
let obj_val = vm.reg(args[1]);
if !obj_val.is_object() {
@@ -328,62 +432,132 @@ macro_rules! native_try {
pub fn object_freeze<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.freeze called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() || val.as_js_object_ptr().is_null() {
return NativeResult::Ok(val);
}
let obj_ptr = val.as_js_object_ptr();
unsafe {
(*obj_ptr).set_frozen(true);
(*obj_ptr).set_extensible(false);
}
NativeResult::Ok(val)
}
pub fn object_seal<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.seal called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() || val.as_js_object_ptr().is_null() {
return NativeResult::Ok(val);
}
let obj_ptr = val.as_js_object_ptr();
unsafe {
(*obj_ptr).set_sealed(true);
(*obj_ptr).set_extensible(false);
}
NativeResult::Ok(val)
}
pub fn object_prevent_extensions<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.preventExtensions called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() || val.as_js_object_ptr().is_null() {
return NativeResult::Ok(val);
}
let obj_ptr = val.as_js_object_ptr();
unsafe {
(*obj_ptr).set_extensible(false);
}
NativeResult::Ok(val)
}
pub fn object_is_frozen<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(true));
return NativeResult::Err(crate::error::create_type_error(vm, "Object.isFrozen called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() || val.as_js_object_ptr().is_null() {
if !val.is_object() {
return NativeResult::Ok(JsValue::bool(true));
}
NativeResult::Ok(JsValue::bool(false))
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Ok(JsValue::bool(true));
}
let obj = unsafe { &*obj_ptr };
if obj.is_frozen() {
return NativeResult::Ok(JsValue::bool(true));
}
if obj.is_extensible() {
return NativeResult::Ok(JsValue::bool(false));
}
let keys = walk_own_keys(vm, obj);
for (_si, offset) in keys {
if let Some(meta) = obj.prop_meta_at(offset) {
if meta.attributes.configurable() {
return NativeResult::Ok(JsValue::bool(false));
}
if !meta.is_accessor && meta.attributes.writable() {
return NativeResult::Ok(JsValue::bool(false));
}
} else {
return NativeResult::Ok(JsValue::bool(false));
}
}
NativeResult::Ok(JsValue::bool(true))
}
pub fn object_is_sealed<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(true));
return NativeResult::Err(crate::error::create_type_error(vm, "Object.isSealed called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() || val.as_js_object_ptr().is_null() {
if !val.is_object() {
return NativeResult::Ok(JsValue::bool(true));
}
NativeResult::Ok(JsValue::bool(false))
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Ok(JsValue::bool(true));
}
let obj = unsafe { &*obj_ptr };
if obj.is_sealed() {
return NativeResult::Ok(JsValue::bool(true));
}
if obj.is_extensible() {
return NativeResult::Ok(JsValue::bool(false));
}
let keys = walk_own_keys(vm, obj);
for (_si, offset) in keys {
if let Some(meta) = obj.prop_meta_at(offset) {
if meta.attributes.configurable() {
return NativeResult::Ok(JsValue::bool(false));
}
} else {
return NativeResult::Ok(JsValue::bool(false));
}
}
NativeResult::Ok(JsValue::bool(true))
}
pub fn object_is_extensible<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
native_try!(require_obj_arg(vm, args, "isExtensible"));
NativeResult::Ok(JsValue::bool(true))
if args.len() < 2 {
return NativeResult::Err(crate::error::create_type_error(vm, "Object.isExtensible called on non-object"));
}
let val = vm.reg(args[1]);
if !val.is_object() {
return NativeResult::Ok(JsValue::bool(false));
}
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Ok(JsValue::bool(false));
}
let obj = unsafe { &*obj_ptr };
NativeResult::Ok(JsValue::bool(obj.is_extensible()))
}
pub fn object_get_own_property_names<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -419,29 +593,94 @@ pub fn object_get_own_property_names<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
pub fn object_define_properties<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 3 {
return NativeResult::Ok(vm.reg(args[1]));
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.defineProperties: expected at least 2 arguments",
));
}
let _obj_ptr = native_try!(require_obj_arg(vm, args, "defineProperties"));
let target_val = vm.reg(args[1]);
let target_ptr = match require_obj_arg(vm, args, "defineProperties") {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let desc_val = vm.reg(args[2]);
if !desc_val.is_object() {
return NativeResult::Ok(vm.reg(args[1]));
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.defineProperties: descriptor must be an object",
));
}
NativeResult::Ok(vm.reg(args[1]))
let desc_ptr = desc_val.as_js_object_ptr();
if desc_ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.defineProperties: descriptor must be an object",
));
}
let prop_keys: Vec<(u32, u32)> = {
let desc = unsafe { &*desc_ptr };
walk_own_keys(vm, desc)
};
for (key_si, offset) in prop_keys {
let desc = unsafe { &*desc_ptr };
let prop_desc_val = desc.get_prop_at(offset);
if !prop_desc_val.is_object() {
continue;
}
let value_si = vm.kernel_core().perm_interner().intern("value").0;
let desc_obj_ptr = prop_desc_val.as_js_object_ptr();
if desc_obj_ptr.is_null() {
continue;
}
let desc_obj = unsafe { &*desc_obj_ptr };
let prop_val = if let Some(pos) = vm.kernel_core().shape_forge().lookup_position(desc_obj.shape_id(), value_si)
{
desc_obj.get_prop_at(pos)
} else {
JsValue::undefined()
};
let target = unsafe { &mut *target_ptr };
let _ = vm.define_data_property(target, key_si, prop_val, PropAttributes::DEFAULT_DATA);
}
NativeResult::Ok(target_val)
}
pub fn object_from_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::null());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.fromEntries: expected 1 argument"));
}
let entries = vm.reg(args[1]);
if !entries.is_object() {
return NativeResult::Ok(JsValue::null());
return NativeResult::Err(crate::error::create_type_error(vm, "Object.fromEntries: argument must be iterable"));
}
let obj_ptr = vm.alloc_object(JsObject::new_empty(
let entries_ptr = entries.as_js_object_ptr();
if entries_ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "Object.fromEntries: argument must be iterable"));
}
let obj = vm.alloc_object(JsObject::new_empty(
EMPTY_SHAPE_ID,
JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject),
));
NativeResult::Ok(JsValue::from_js_object(obj_ptr))
let target_val = JsValue::from_js_object(obj);
let n: usize = unsafe { (*entries_ptr).prop_vec_len() };
for i in 0..n {
let pair_val = unsafe { (*entries_ptr).get_prop_at(i) };
if !pair_val.is_object() {
continue;
}
let pair_ptr = pair_val.as_js_object_ptr();
if pair_ptr.is_null() {
continue;
}
let pair = unsafe { &*pair_ptr };
let key_val = pair.get_prop_at(0);
let value_val = pair.get_prop_at(1);
let key_str = oxide_runtime_api::to_string(key_val);
let si = vm.kernel_core().perm_interner().intern(&key_str).0;
let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val);
let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val);
}
NativeResult::Ok(target_val)
}
pub fn object_get_prototype_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -459,6 +698,18 @@ pub fn object_has_own<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::bool(vm.get_own_property_slot(obj, key_si).is_some()))
}
pub fn object_proto_value_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// Object.prototype.valueOf returns the `this` object unchanged; OrdinaryToPrimitive
// then falls through to toString since the result is not primitive.
NativeResult::Ok(vm.reg(args[0]))
}
pub fn object_proto_to_string<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
// ponytail: minimal [[Class]] string — always "[object Object]". Type-specific
// tags ("[object Array]" etc.) and Symbol.toStringTag are a later refinement.
NativeResult::Ok(vm.new_string("[object Object]"))
}
pub fn object_proto_has_own_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false));
@@ -502,7 +753,15 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj_ptr = native_try!(require_obj_arg(vm, args, "entries"));
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
let n = keys.len();
let owned_keys: Vec<(u32, u32)> = keys
.into_iter()
.filter(|(_si, offset)| {
obj.prop_meta_at(*offset)
.map(|m| m.attributes.enumerable())
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
})
.collect();
let n = owned_keys.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
@@ -510,7 +769,7 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
n,
vm.epoch().bump(),
));
for (i, (si, offset)) in keys.iter().enumerate() {
for (i, (si, offset)) in owned_keys.iter().enumerate() {
let key_str = vm.kernel_core().perm_interner().lookup(*si).unwrap_or_default();
let key_val = vm.new_string(key_str);
let val = obj.get_prop_at(*offset);
@@ -537,7 +796,15 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj_ptr = native_try!(require_obj_arg(vm, args, "values"));
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
let n = keys.len();
let owned_keys: Vec<(u32, u32)> = keys
.into_iter()
.filter(|(_si, offset)| {
obj.prop_meta_at(*offset)
.map(|m| m.attributes.enumerable())
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
})
.collect();
let n = owned_keys.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
@@ -545,7 +812,7 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
n,
vm.epoch().bump(),
));
for (i, (_si, offset)) in keys.iter().enumerate() {
for (i, (_si, offset)) in owned_keys.iter().enumerate() {
let val = obj.get_prop_at(*offset);
unsafe {
(*arr).set_prop_at(i, val);
+2 -2
View File
@@ -148,7 +148,7 @@ pub fn regexp_test<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
let haystack = oxide_runtime_api::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] }));
let last_index = oxide_runtime_api::to_number(get_prop(re, 0)) as usize;
let last_index = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize;
let is_global = get_prop(re, 3).as_bool();
if is_global {
@@ -181,7 +181,7 @@ pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (last_index, is_global) = {
let re = unsafe { &*re_ptr };
let li = oxide_runtime_api::to_number(get_prop(re, 0)) as usize;
let li = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize;
let g = get_prop(re, 3).as_bool();
(li, g)
};
+32 -1
View File
@@ -56,7 +56,7 @@ impl Hash for SetKey {
}
}
type SetInner = indexmap::IndexSet<SetKey>;
pub(crate) type SetInner = indexmap::IndexSet<SetKey>;
/// Retrieve the `IndexSet` pointer stored in a Set object's native-data slot.
///
@@ -226,3 +226,34 @@ pub fn set_size<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let inner = native_try!(get_set_inner(vm, this_val));
NativeResult::Ok(JsValue::float(unsafe { (*inner).len() } as f64))
}
pub fn set_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_set_inner(vm, this_val));
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::set_entries_iter_next::<H> as *const (),
))
}
pub fn set_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_set_inner(vm, this_val));
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::set_values_iter_next::<H> as *const (),
))
}
pub fn set_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let _inner = native_try!(get_set_inner(vm, this_val));
// Set keys() is an alias for values() — same per-element iterator.
NativeResult::Ok(crate::iterator::make_mode_iterator(
vm,
this_val,
crate::iterator::set_values_iter_next::<H> as *const (),
))
}
+318 -36
View File
@@ -171,7 +171,7 @@ pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let search = as_string(vm, vm.reg(args[1]));
let pos = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as usize).min(n)
(vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize).min(n)
} else {
0
};
@@ -203,7 +203,7 @@ pub fn string_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let search = as_string(vm, vm.reg(args[1]));
let pos = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as usize).min(n)
(vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize).min(n)
} else {
0
};
@@ -229,7 +229,7 @@ pub fn string_char_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
return NativeResult::Ok(vm.new_string(&take_chars(&s, 1)));
}
let idx = oxide_runtime_api::to_number(vm.reg(args[1])) as i32;
let idx = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32;
if idx < 0 || idx as usize >= char_len(&s) {
return NativeResult::Ok(vm.new_string(""));
}
@@ -246,7 +246,7 @@ pub fn string_char_code_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
return NativeResult::Ok(JsValue::int(s.chars().next().unwrap() as i32));
}
let idx = oxide_runtime_api::to_number(vm.reg(args[1])) as i32;
let idx = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32;
if idx < 0 || idx as usize >= char_len(&s) {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
@@ -267,7 +267,7 @@ pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let s = this_string(vm, args);
let n = char_len(&s) as i32;
let start = if args.len() > 1 {
let v = oxide_runtime_api::to_number(vm.reg(args[1])) as i32;
let v = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32;
if v < 0 {
(n + v).max(0)
} else {
@@ -277,7 +277,7 @@ pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
0
};
let end = if args.len() > 2 {
let v = oxide_runtime_api::to_number(vm.reg(args[2])) as i32;
let v = vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as i32;
if v < 0 {
(n + v).max(0)
} else {
@@ -297,12 +297,22 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let s = this_string(vm, args);
let n = char_len(&s) as i32;
let mut start = if args.len() > 1 {
(oxide_runtime_api::to_number(vm.reg(args[1])) as i32).max(0).min(n)
let v = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1]));
if v.is_nan() || v < 0.0 {
0
} else {
(v as i32).min(n)
}
} else {
0
};
let mut end = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as i32).max(0).min(n)
let v = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
if v.is_nan() || v < 0.0 {
0
} else {
(v as i32).min(n)
}
} else {
n
};
@@ -313,6 +323,84 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(result))
}
pub fn string_substr<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.substr called with {} args", args.len());
let s = this_string(vm, args);
let len = char_len(&s) as isize;
let start = if args.len() > 1 {
let n = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as isize;
if n < 0 {
(len + n).max(0)
} else {
n.min(len)
}
} else {
0
} as usize;
let length = if args.len() > 2 {
(oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as isize).max(0) as usize
} else {
len as usize - start
};
let result = take_chars(&s[byte_index_at_char(&s, start)..], length.min(len as usize - start));
NativeResult::Ok(vm.new_string(&result))
}
pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.at called with {} args", args.len());
let s = this_string(vm, args);
let len = char_len(&s) as i32;
let idx = if args.len() > 1 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as i32
} else {
0
};
let idx = if idx < 0 { len + idx } else { idx };
if idx < 0 || idx >= len {
return NativeResult::Ok(JsValue::undefined());
}
let ch = s.chars().nth(idx as usize).unwrap().to_string();
NativeResult::Ok(vm.new_string(&ch))
}
pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len());
let s = this_string(vm, args);
let n = char_len(&s);
if args.len() < 2 {
return NativeResult::Ok(JsValue::int(-1));
}
let search = as_string(vm, vm.reg(args[1]));
let pos = if args.len() > 2 {
let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
if p.is_nan() {
n
} else {
(p as usize).min(n)
}
} else {
n
};
if search.is_empty() {
return NativeResult::Ok(JsValue::int(pos as i32));
}
let end_byte = byte_index_at_char(&s, (pos + 1).min(n));
let haystack = &s[..end_byte];
if search.len() == 1 {
if let Some(idx) = memchr::memrchr(search.as_bytes()[0], haystack.as_bytes()) {
let result = char_len(&s[..idx]);
return NativeResult::Ok(JsValue::int(result as i32));
}
} else if let Some(idx) = haystack.rfind(&search) {
let result = char_len(&s[..idx]);
return NativeResult::Ok(JsValue::int(result as i32));
}
NativeResult::Ok(JsValue::int(-1))
}
pub fn string_to_upper_case<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.toUpperCase called with {} args", args.len());
let s = this_string(vm, args);
@@ -335,7 +423,7 @@ pub fn string_repeat<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.repeat called with {} args", args.len());
let s = this_string(vm, args);
let n = if args.len() > 1 {
(oxide_runtime_api::to_number(vm.reg(args[1])) as usize).min(10000)
(vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize).min(10000)
} else {
1
};
@@ -347,7 +435,7 @@ pub fn string_pad_start<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let s = this_string(vm, args);
let s_len = char_len(&s);
let target = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize
} else {
s_len
};
@@ -372,7 +460,7 @@ pub fn string_pad_end<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let s = this_string(vm, args);
let s_len = char_len(&s);
let target = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize
} else {
s_len
};
@@ -401,7 +489,7 @@ pub fn string_starts_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let search = as_string(vm, vm.reg(args[1]));
let pos = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as usize).min(n)
(vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize).min(n)
} else {
0
};
@@ -417,7 +505,7 @@ pub fn string_ends_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let search = as_string(vm, vm.reg(args[1]));
let end_pos = if args.len() > 2 {
(oxide_runtime_api::to_number(vm.reg(args[2])) as usize).min(n)
(vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize).min(n)
} else {
n
};
@@ -433,7 +521,7 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let sep_val = vm.reg(args[1]);
let limit = if args.len() > 2 {
oxide_runtime_api::to_number(vm.reg(args[2])) as usize
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as usize
} else {
usize::MAX
};
@@ -448,10 +536,30 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Ok(make_string_array(vm, &parts));
}
};
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
let raw: Vec<&str> = regex.split(&s).collect();
let parts: Vec<String> = raw.iter().map(|p| p.to_string()).take(limit).collect();
let mut parts: Vec<String> = Vec::new();
let mut last_end = 0;
for caps in regex.captures_iter(&s) {
if parts.len() >= limit {
break;
}
let full = caps.get(0).unwrap();
parts.push(s[last_end..full.start()].to_string());
if parts.len() >= limit {
break;
}
for i in 1..caps.len() {
if parts.len() >= limit {
break;
}
let cap = caps.get(i).map(|m| m.as_str()).unwrap_or("");
parts.push(cap.to_string());
}
last_end = full.end();
}
if last_end <= s.len() && parts.len() < limit {
parts.push(s[last_end..].to_string());
}
return NativeResult::Ok(make_string_array(vm, &parts));
}
@@ -471,7 +579,6 @@ pub fn string_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Ok(vm.new_string(&s));
}
let pattern_val = vm.reg(args[1]);
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
if is_regexp_obj(pattern_val, vm) {
let re_ptr = pattern_val.as_js_object_ptr();
@@ -480,12 +587,53 @@ pub fn string_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Some(p) => p,
None => return NativeResult::Ok(vm.new_string(&s)),
};
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
let result = regex.replace_all(&s, replacement.as_str()).to_string();
if args.len() > 2 {
let replacer_val = vm.reg(args[2]);
if replacer_val.is_object() {
let o = unsafe { &*replacer_val.as_js_object_ptr() };
if o.is_function() {
builtins_debug!("string_replace: function replacer path");
if let Some(captures) = regex.captures(&s) {
let full = captures.get(0).unwrap();
let mut cb_args: Vec<JsValue> = Vec::with_capacity(captures.len() + 2);
cb_args.push(vm.new_string(full.as_str()));
for i in 1..captures.len() {
cb_args.push(vm.new_string(captures.get(i).map(|m| m.as_str()).unwrap_or("")));
}
cb_args.push(JsValue::int(full.start() as i32));
cb_args.push(vm.new_string(&s));
match vm.call_function_sync(replacer_val, JsValue::undefined(), &cb_args) {
Ok(result) => {
let result_str = oxide_runtime_api::to_string(result);
let output = format!("{}{}{}", &s[..full.start()], &result_str, &s[full.end()..]);
return NativeResult::Ok(vm.new_string(&output));
}
Err(err) => {
return NativeResult::Err(crate::error::create_type_error(
vm,
&format!("replace replacer: {}", err),
));
}
}
}
return NativeResult::Ok(vm.new_string(&s));
}
}
}
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
let result = if is_global {
regex.replace_all(&s, replacement.as_str()).to_string()
} else {
regex.replacen(&s, 1, replacement.as_str()).to_string()
};
return NativeResult::Ok(vm.new_string(&result));
}
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
let pattern = as_string(vm, pattern_val);
let result = s.replacen(&pattern, &replacement, 1);
NativeResult::Ok(vm.new_string(&result))
@@ -495,7 +643,7 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.match called with {} args", args.len());
let s = this_string(vm, args);
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Ok(JsValue::null());
}
let pattern_val = vm.reg(args[1]);
@@ -504,19 +652,30 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re = unsafe { &*re_ptr };
let fn_ptr = match re.native_fn() {
Some(p) => p,
None => return NativeResult::Ok(JsValue::undefined()),
None => return NativeResult::Ok(JsValue::null()),
};
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
return NativeResult::Ok(make_string_array(vm, &matches));
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
if is_global {
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
return NativeResult::Ok(make_string_array(vm, &matches));
}
if let Some(captures) = regex.captures(&s) {
let mut parts: Vec<String> = Vec::with_capacity(captures.len());
parts.push(captures.get(0).unwrap().as_str().to_string());
for i in 1..captures.len() {
parts.push(captures.get(i).map(|m| m.as_str()).unwrap_or("").to_string());
}
return NativeResult::Ok(make_string_array(vm, &parts));
}
return NativeResult::Ok(JsValue::null());
}
let pattern = as_string(vm, pattern_val);
if let Some(pos) = s.find(&pattern) {
return NativeResult::Ok(make_string_array(vm, &[s[pos..pos + pattern.len()].to_string()]));
}
NativeResult::Ok(JsValue::undefined())
NativeResult::Ok(JsValue::null())
}
pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -565,7 +724,7 @@ pub fn string_code_point_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
builtins_debug!("String.prototype.codePointAt called with {} args", args.len());
let s = this_string(vm, args);
let pos = if args.len() > 1 {
oxide_runtime_api::to_number(vm.reg(args[1])) as usize
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize
} else {
0
};
@@ -598,6 +757,10 @@ pub fn string_normalize<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(&result))
}
const MALL_INPUT: &str = "__mal_input__";
const MALL_INDEX: &str = "__mal_index__";
const MALL_RE: &str = "__mal_re__";
pub fn string_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.matchAll called with {} args", args.len());
let s = this_string(vm, args);
@@ -606,19 +769,138 @@ pub fn string_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Err(JsValue::undefined());
}
let pattern_val = vm.reg(args[1]);
if is_regexp_obj(pattern_val, vm) {
let re_obj = if is_regexp_obj(pattern_val, vm) {
let re_ptr = pattern_val.as_js_object_ptr();
let re = unsafe { &*re_ptr };
let fn_ptr = match re.native_fn() {
Some(p) => p,
None => return NativeResult::Ok(JsValue::undefined()),
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
if !is_global {
return NativeResult::Err(crate::error::create_type_error(
vm,
"String.prototype.matchAll: regex must have global flag",
));
}
pattern_val
} else {
let pattern_str = as_string(vm, pattern_val);
let escaped = regex::escape(&pattern_str);
let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) };
let compiled = match regex::Regex::new(&rx_str) {
Ok(rx) => rx,
Err(e) => {
return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("Invalid regex: {}", e)));
}
};
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
return NativeResult::Ok(make_string_array(vm, &matches));
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let mut stub = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
let boxed = Box::new(compiled);
let raw = Box::into_raw(boxed) as *const u8;
stub.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(raw as *const ()) }));
let stub_ptr = vm.alloc_object(stub);
JsValue::from_js_object(stub_ptr)
};
// Build wrapper object
builder_wrapper(vm, &s, re_obj)
}
fn builder_wrapper<H: VmHost>(vm: &mut H, input: &str, re_obj: JsValue) -> NativeResult {
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let wrapper = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
let wrapper_obj = unsafe { &mut *wrapper };
let input_si = vm.kernel_core().perm_interner().intern(MALL_INPUT).0;
let index_si = vm.kernel_core().perm_interner().intern(MALL_INDEX).0;
let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0;
let next_si = vm.kernel_core().perm_interner().intern("next").0;
let input_val = vm.new_string(input);
vm.set_or_create_prop_value(wrapper_obj, input_si, input_val);
vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(0));
vm.set_or_create_prop_value(wrapper_obj, re_si, re_obj);
let next_fn = make_public_native_fn(vm, "next", string_match_all_next::<H> as *const (), 0);
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
NativeResult::Ok(JsValue::from_js_object(wrapper))
}
fn make_public_native_fn<H: VmHost>(vm: &mut H, name: &str, native_fn: *const (), arg_count: u8) -> JsValue {
let function_proto = vm.session().builtin_world().function_proto.as_ptr() as *mut JsObject;
let mut func = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(function_proto));
func.set_function(true);
func.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(native_fn) }));
func.set_native_arg_count(arg_count);
let func = vm.alloc_object(func);
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let value = vm.new_string(name);
let func_ref = unsafe { &mut *func };
vm.set_or_create_prop_value(func_ref, name_si, value);
JsValue::from_js_object(func)
}
pub fn string_match_all_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "matchAll next called on non-object"));
}
NativeResult::Ok(JsValue::undefined())
let wrapper = unsafe { &mut *this_val.as_js_object_ptr() };
let input_si = vm.kernel_core().perm_interner().intern(MALL_INPUT).0;
let index_si = vm.kernel_core().perm_interner().intern(MALL_INDEX).0;
let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0;
let input_val = match vm.ordinary_get(wrapper, input_si, this_val) {
Ok(v) => v,
Err(_) => return make_match_done_result(vm, JsValue::undefined()),
};
let input_str = oxide_runtime_api::to_string(input_val);
let idx_val = match vm.ordinary_get(wrapper, index_si, this_val) {
Ok(v) => v,
Err(_) => return make_match_done_result(vm, JsValue::undefined()),
};
let mut idx = if idx_val.is_int() { idx_val.as_int().max(0) as usize } else { 0 };
let re_val = match vm.ordinary_get(wrapper, re_si, this_val) {
Ok(v) if v.is_object() => v,
_ => return make_match_done_result(vm, JsValue::undefined()),
};
let re_obj = unsafe { &*re_val.as_js_object_ptr() };
let fn_ptr = match re_obj.native_fn() {
Some(p) => p,
None => return make_match_done_result(vm, JsValue::undefined()),
};
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
if let Some(m) = regex.find_at(&input_str, idx) {
let mut parts: Vec<String> = Vec::new();
if let Some(caps) = regex.captures_at(&input_str, idx) {
parts.push(caps.get(0).unwrap().as_str().to_string());
for i in 1..caps.len() {
parts.push(caps.get(i).map(|c| c.as_str()).unwrap_or("").to_string());
}
}
idx = m.end();
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32));
let arr_val = make_string_array(vm, &parts);
make_match_done_result(vm, arr_val)
} else {
make_match_done_result(vm, JsValue::undefined())
}
}
fn make_match_done_result<H: VmHost>(vm: &mut H, value: JsValue) -> NativeResult {
let done = value.is_undefined();
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let obj = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
let value_si = vm.kernel_core().perm_interner().intern("value").0;
let done_si = vm.kernel_core().perm_interner().intern("done").0;
let obj_ref = unsafe { &mut *obj };
vm.set_or_create_prop_value(obj_ref, value_si, value);
vm.set_or_create_prop_value(obj_ref, done_si, JsValue::bool(done));
NativeResult::Ok(JsValue::from_js_object(obj))
}
pub fn string_replace_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
+5 -5
View File
@@ -32,7 +32,7 @@ fn range_error<H: VmHost>(vm: &mut H, msg: &str) -> JsValue {
}
fn to_index<H: VmHost>(vm: &mut H, value: JsValue, msg: &str) -> Result<usize, JsValue> {
let n = oxide_runtime_api::to_number(value);
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if n.is_nan() {
return Ok(0);
}
@@ -42,8 +42,8 @@ fn to_index<H: VmHost>(vm: &mut H, value: JsValue, msg: &str) -> Result<usize, J
Ok(n.trunc() as usize)
}
fn normalize_index<H: VmHost>(_vm: &mut H, value: JsValue, len: usize) -> usize {
let n = oxide_runtime_api::to_number(value);
fn normalize_index<H: VmHost>(vm: &mut H, value: JsValue, len: usize) -> usize {
let n = vm.coerce_number_bounded(value).unwrap_or(f64::NAN);
if n.is_nan() {
return 0;
}
@@ -235,8 +235,8 @@ fn read_element(kind: TypedArrayKind, bytes: &[u8], offset: usize) -> JsValue {
}
}
fn numeric_value<H: VmHost>(_vm: &mut H, value: JsValue) -> f64 {
oxide_runtime_api::to_number(value)
fn numeric_value<H: VmHost>(vm: &mut H, value: JsValue) -> f64 {
vm.coerce_number_bounded(value).unwrap_or(f64::NAN)
}
fn write_element(kind: TypedArrayKind, bytes: &mut [u8], offset: usize, value: f64) {
@@ -0,0 +1,66 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
#[test]
fn test_push_returns_new_length() {
let (_vm, result) = eval("var a = [1, 2]; a.push(3)").unwrap();
assert_eq!(result.as_int(), 3);
}
#[test]
fn test_push_multiple_args() {
let (_vm, result) = eval("var a = [1]; a.push(2, 3)").unwrap();
assert_eq!(result.as_int(), 3);
}
#[test]
fn test_pop_empty_returns_undefined() {
let (_vm, result) = eval("[].pop()").unwrap();
assert!(result.is_undefined());
}
#[test]
fn test_pop_returns_last_element() {
let (_vm, result) = eval("[1, 2, 3].pop()").unwrap();
assert_eq!(result.as_int(), 3);
}
#[test]
fn test_shift_empty_returns_undefined() {
let (_vm, result) = eval("[].shift()").unwrap();
assert!(result.is_undefined());
}
#[test]
fn test_shift_returns_first_element() {
let (_vm, result) = eval("[1, 2, 3].shift()").unwrap();
assert_eq!(result.as_int(), 1);
}
#[test]
fn test_unshift_returns_new_length() {
let (_vm, result) = eval("var a = [3, 4]; a.unshift(1, 2)").unwrap();
assert_eq!(result.as_int(), 4);
}
#[test]
fn test_slice_negative_start() {
let (_vm, result) = eval("[1, 2, 3, 4].slice(-2).length").unwrap();
assert_eq!(result.as_int(), 2);
}
#[test]
fn test_slice_negative_end() {
let (_vm, result) = eval("[1, 2, 3].slice(0, -1).length").unwrap();
assert_eq!(result.as_int(), 2);
}
@@ -0,0 +1,140 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
fn stringify_val(val: &JsValue) -> String {
if val.is_string() {
unsafe { (*val.as_string_ptr()).data.clone() }
} else {
format!("{:?}", val)
}
}
// --- replacer array ---
#[test]
fn replacer_array_filters_properties() {
let (_, result) = eval(r#"JSON.stringify({a:1,b:2,c:3}, ['a','c'])"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"a":1,"c":3}"#);
}
#[test]
fn replacer_array_numeric_keys() {
let (_, result) = eval(r#"JSON.stringify({a:1,b:2}, ['b'])"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"b":2}"#);
}
// --- replacer function ---
#[test]
fn replacer_function_skip_property() {
let (_, result) =
eval(r#"JSON.stringify({a:1,b:2}, function(k,v){if(k==='a')return undefined;return v})"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"b":2}"#);
}
#[test]
fn replacer_function_array_null() {
let (_, result) = eval(r#"JSON.stringify([1,2,3], function(k,v){if(k==='1')return undefined;return v})"#).unwrap();
assert_eq!(stringify_val(&result), "[1,null,3]");
}
#[test]
fn replacer_function_transform() {
let (_, result) =
eval(r#"JSON.stringify({a:1}, function(k,v){if(typeof v==='number')return v*2;return v})"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"a":2}"#);
}
// --- space ---
#[test]
fn space_number_indent() {
let (_, result) = eval(r#"JSON.stringify({a:1}, null, 2)"#).unwrap();
let s = stringify_val(&result);
assert!(s.len() > 5, "expected indented output, got: {}", s);
assert!(s.starts_with('{'), "should start with brace");
}
#[test]
fn space_negative_clamped() {
let (_, result) = eval(r#"JSON.stringify({a:1}, null, -5)"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"a":1}"#);
}
// --- toJSON ---
#[test]
fn tojson_called_before_serialize() {
let (_, result) = eval(r#"JSON.stringify({toJSON:function(){return {x:1}}})"#).unwrap();
assert_eq!(stringify_val(&result), r#"{"x":1}"#);
}
#[test]
fn tojson_non_callable_ignored() {
let (_, result) = eval(r#"JSON.stringify({toJSON:'not-a-function', a:1})"#).unwrap();
// toJSON property is a string, should be serialized normally
assert!(stringify_val(&result).contains("toJSON"));
assert!(stringify_val(&result).contains(r#""a":1"#));
}
// --- cycle detection ---
#[test]
fn cycle_throws_type_error() {
let result = eval("var a={};a.self=a;JSON.stringify(a)");
match result {
Err(e) => assert!(e.to_lowercase().contains("circular"), "error should mention 'circular', got: {}", e),
Ok(_) => panic!("expected cycle error, got ok"),
}
}
#[test]
fn no_cycle_distinct_objects_ok() {
let (_, result) = eval(r#"JSON.stringify([{a:1},{a:1}])"#).unwrap();
assert_eq!(stringify_val(&result), r#"[{"a":1},{"a":1}]"#);
}
// --- reviver ---
#[test]
fn reviver_transform_values() {
let (_, result) =
eval(r#"JSON.parse('{"a":1,"b":2}', function(k,v){if(typeof v==='number')return v*2;return v})"#).unwrap();
assert!(result.is_object());
}
#[test]
fn reviver_delete_property() {
let (_, result) =
eval(r#"JSON.parse('{"a":1,"b":2}', function(k,v){if(k==='a')return undefined;return v})"#).unwrap();
// The property is soft-deleted (set to undefined)
// For test262 compatibility, stringify should omit undefined properties
assert!(result.is_object());
}
#[test]
fn reviver_root_key_empty_string() {
let (_, result) = eval(r#"JSON.parse('42', function(k,v){return v+1})"#).unwrap();
assert!(result.is_int() || result.is_double(), "expected number");
if result.is_int() {
assert_eq!(result.as_int(), 43);
} else {
assert_eq!(result.as_double() as i32, 43);
}
}
#[test]
fn reviver_no_reviver_works() {
let (_, result) = eval(r#"JSON.parse('{"a":1}')"#).unwrap();
assert!(result.is_object());
}
@@ -0,0 +1,65 @@
use oxide_runtime_api::to_number;
use oxide_types::value::JsValue;
#[test]
fn is_nan_nan_returns_true() {
assert!(to_number(JsValue::float(f64::NAN)).is_nan());
}
#[test]
fn is_nan_number_123_returns_false() {
assert!(!to_number(JsValue::int(123)).is_nan());
}
#[test]
fn is_nan_undefined_returns_true() {
assert!(to_number(JsValue::undefined()).is_nan());
}
#[test]
fn is_finite_number_42_returns_true() {
assert!(to_number(JsValue::int(42)).is_finite());
}
#[test]
fn is_finite_infinity_returns_false() {
assert!(!to_number(JsValue::float(f64::INFINITY)).is_finite());
}
#[test]
fn is_integer_non_number_float_nan() {
let val = JsValue::undefined();
assert!(!val.is_int() && !val.is_double());
}
#[test]
fn is_integer_int_42() {
let val = JsValue::int(42);
assert!(val.is_int());
let n = to_number(val);
assert!(n.trunc() == n && n.is_finite());
}
#[test]
fn is_integer_double_fractional() {
let val = JsValue::float(42.5);
assert!(val.is_double());
let n = to_number(val);
assert!(!(n.trunc() == n && n.is_finite()));
}
#[test]
fn is_safe_integer_max_value() {
let val = JsValue::float(9007199254740991f64);
let n = to_number(val);
assert!(n.trunc() == n && n.is_finite());
assert!(n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64);
}
#[test]
fn is_safe_integer_overflow() {
let val = JsValue::float(9007199254740992f64);
let n = to_number(val);
assert!(n.trunc() == n && n.is_finite());
assert!(!(n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64));
}
@@ -0,0 +1,238 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
#[test]
fn object_keys_no_args_type_error() {
assert!(eval("Object.keys()").is_err(), "Object.keys() should throw TypeError");
}
#[test]
fn object_keys_non_object_type_error() {
assert!(eval("Object.keys(42)").is_err(), "Object.keys(42) should throw TypeError");
}
#[test]
fn object_create_null_proto() {
let (_vm, result) = eval("var o = Object.create(null); o").unwrap();
assert!(result.is_object());
let obj_ptr = result.as_js_object_ptr();
assert!(!obj_ptr.is_null());
let obj = unsafe { &*obj_ptr };
let proto = obj.proto();
assert!(proto.is_null(), "Object.create(null) should set prototype to null");
}
#[test]
fn object_create_number_proto_type_error() {
assert!(eval("Object.create(42)").is_err(), "Object.create(42) should throw TypeError");
}
#[test]
fn object_create_no_args_type_error() {
assert!(eval("Object.create()").is_err(), "Object.create() should throw TypeError");
}
#[test]
fn object_get_own_property_names_no_args_type_error() {
assert!(
eval("Object.getOwnPropertyNames()").is_err(),
"Object.getOwnPropertyNames() should throw TypeError"
);
}
#[test]
fn object_get_own_property_names_non_object_type_error() {
assert!(
eval("Object.getOwnPropertyNames(42)").is_err(),
"Object.getOwnPropertyNames(42) should throw TypeError"
);
}
#[test]
fn object_keys_valid_object() {
let (_vm, result) = eval("var o = {a:1, b:2}; Object.keys(o).join(',')").unwrap();
assert!(result.is_string(), "Object.keys should return an array");
}
#[test]
fn test_define_property_data_and_accessor_conflict() {
assert!(
eval("Object.defineProperty({}, 'x', {value: 1, get: function() {}})").is_err(),
"data+accessor conflict should throw TypeError"
);
}
#[test]
fn test_define_property_non_object_target() {
assert!(
eval("Object.defineProperty(null, 'x', {value: 1})").is_err(),
"non-object target should throw TypeError"
);
}
#[test]
fn test_define_property_missing_descriptor() {
assert!(
eval("Object.defineProperty({}, 'x')").is_err(),
"missing descriptor should throw TypeError"
);
}
#[test]
fn test_assign_skips_null_source() {
let (_vm, result) = eval("var t = {a:1}; Object.assign(t, null); t.a").unwrap();
assert_eq!(result.as_int(), 1, "Object.assign should skip null sources");
}
#[test]
fn test_assign_merges_two_sources() {
let (_vm, result) = eval("var t = {a:1}; Object.assign(t, {b:2}); t.b").unwrap();
assert!(result.is_int() || result.is_double(), "Object.assign should copy from source");
}
#[test]
fn test_integrity_freeze_returns_object() {
let (_vm, result) = eval("var o = {x:1}; Object.freeze(o) === o").unwrap();
assert!(result.is_bool() && result.as_bool(), "freeze should return the object");
}
#[test]
fn test_integrity_freeze_sets_non_extensible() {
let (_vm, result) = eval("var o = {x:1}; Object.freeze(o); Object.isExtensible(o)").unwrap();
assert!(result.is_bool() && !result.as_bool(), "freeze should set non-extensible");
}
#[test]
fn test_integrity_seal_non_extensible() {
let (_vm, result) = eval("var o = {x:1}; Object.seal(o); Object.isExtensible(o)").unwrap();
assert!(result.is_bool() && !result.as_bool(), "seal should set non-extensible");
}
#[test]
fn test_integrity_prevent_extensions() {
let (_vm, result) = eval("var o = {x:1}; Object.preventExtensions(o); Object.isExtensible(o)").unwrap();
assert!(result.is_bool() && !result.as_bool(), "preventExtensions should set non-extensible");
}
#[test]
fn test_is_frozen_non_object_returns_true() {
let (_vm, result) = eval("Object.isFrozen(42)").unwrap();
assert!(result.is_bool() && result.as_bool(), "non-object should be frozen per ES spec");
}
#[test]
fn test_is_sealed_non_object_returns_true() {
let (_vm, result) = eval("Object.isSealed(42)").unwrap();
assert!(result.is_bool() && result.as_bool(), "non-object should be sealed per ES spec");
}
#[test]
fn test_is_extensible_non_object_returns_false() {
let (_vm, result) = eval("Object.isExtensible(42)").unwrap();
assert!(result.is_bool() && !result.as_bool(), "non-object should not be extensible per ES spec");
}
#[test]
fn test_is_extensible_no_args_type_error() {
assert!(eval("Object.isExtensible()").is_err());
}
#[test]
fn test_get_prototype_of_null_throws_type_error() {
assert!(eval("Object.getPrototypeOf(null)").is_err());
}
#[test]
fn test_get_prototype_of_undefined_throws_type_error() {
assert!(eval("Object.getPrototypeOf(undefined)").is_err());
}
#[test]
fn test_get_prototype_of_plain_object() {
let (_vm, result) = eval("var o = {}; Object.getPrototypeOf(o)").unwrap();
assert!(result.is_object() || result.is_null());
}
#[test]
fn test_has_own_own_property() {
let (_vm, result) = eval("var o = {a:1}; Object.hasOwn(o, 'a')").unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn test_has_own_inherited_property() {
let (_vm, result) = eval("Object.hasOwn({}, 'toString')").unwrap();
assert!(result.is_bool() && !result.as_bool());
}
#[test]
fn test_define_properties_no_args_type_error() {
assert!(eval("Object.defineProperties()").is_err());
}
#[test]
fn test_define_properties_applies_multiple_props() {
let (_vm, result) = eval("var o = {}; Object.defineProperties(o, {a: {value: 1}, b: {value: 2}}); o.a").unwrap();
assert!(result.is_int() || result.is_double());
}
#[test]
fn test_from_entries_no_args_type_error() {
assert!(eval("Object.fromEntries()").is_err());
}
#[test]
fn test_from_entries_empty_array() {
let (_vm, result) = eval("Object.fromEntries([])").unwrap();
assert!(result.is_object(), "fromEntries([]) should return an empty object");
}
#[test]
fn test_object_is_nan() {
let (_vm, result) = eval("Object.is(NaN, NaN)").unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn test_object_is_signed_zero() {
let (_vm, result) = eval("Object.is(0, -0)").unwrap();
assert!(result.is_bool() && !result.as_bool());
}
#[test]
fn test_entries_returns_key_value_pairs() {
let (_vm, result) = eval("var o = {a:1, b:2}; Object.entries(o).length").unwrap();
assert!(result.is_int() || result.is_double());
}
#[test]
fn test_values_returns_array() {
let (_vm, result) = eval("var o = {a:1, b:2}; Object.values(o).length").unwrap();
assert!(result.is_int() || result.is_double());
}
#[test]
fn test_get_own_property_descriptor_no_args_type_error() {
assert!(eval("Object.getOwnPropertyDescriptor()").is_err());
}
#[test]
fn test_has_own_property_non_object_this_type_error() {
assert!(eval("Object.prototype.hasOwnProperty.call(42, 'x')").is_err());
}
#[test]
fn test_property_is_enumerable_own_property() {
let (_vm, result) = eval("var o = {x: 1}; o.propertyIsEnumerable('x')").unwrap();
assert!(result.is_bool() && result.as_bool());
}
@@ -0,0 +1,59 @@
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
fn new_obj() -> JsObject {
JsObject::new_empty(0, JsValue::null())
}
#[test]
fn test_set_frozen() {
let mut obj = new_obj();
assert!(!obj.is_frozen());
obj.set_frozen(true);
assert!(obj.is_frozen());
}
#[test]
fn test_set_sealed() {
let mut obj = new_obj();
assert!(!obj.is_sealed());
obj.set_sealed(true);
assert!(obj.is_sealed());
}
#[test]
fn test_frozen_and_sealed_independent() {
let mut obj = new_obj();
obj.set_frozen(true);
assert!(obj.is_frozen());
assert!(!obj.is_sealed());
obj.set_sealed(true);
assert!(obj.is_frozen());
assert!(obj.is_sealed());
obj.set_frozen(false);
assert!(!obj.is_frozen());
assert!(obj.is_sealed());
}
#[test]
fn test_defaults_are_false() {
let obj = new_obj();
assert!(!obj.is_frozen());
assert!(!obj.is_sealed());
}
#[test]
fn test_clear_frozen() {
let mut obj = new_obj();
obj.set_frozen(true);
obj.set_frozen(false);
assert!(!obj.is_frozen());
}
#[test]
fn test_clear_sealed() {
let mut obj = new_obj();
obj.set_sealed(true);
obj.set_sealed(false);
assert!(!obj.is_sealed());
}
@@ -0,0 +1,82 @@
use oxide_runtime_api::{same_value_zero, to_integer_or_infinity, to_length};
use oxide_types::value::JsValue;
#[test]
fn test_to_integer_or_infinity_nan() {
assert_eq!(to_integer_or_infinity(JsValue::float(f64::NAN)), 0.0);
}
#[test]
fn test_to_integer_or_infinity_zero() {
assert_eq!(to_integer_or_infinity(JsValue::float(0.0)), 0.0);
assert_eq!(to_integer_or_infinity(JsValue::float(-0.0)), 0.0);
}
#[test]
fn test_to_integer_or_infinity_infinity() {
assert_eq!(to_integer_or_infinity(JsValue::float(f64::INFINITY)), f64::INFINITY);
assert_eq!(to_integer_or_infinity(JsValue::float(f64::NEG_INFINITY)), f64::NEG_INFINITY);
}
#[test]
fn test_to_integer_or_infinity_truncation() {
assert_eq!(to_integer_or_infinity(JsValue::float(3.7)), 3.0);
assert_eq!(to_integer_or_infinity(JsValue::float(-3.7)), -3.0);
}
#[test]
fn test_to_integer_or_infinity_int() {
assert_eq!(to_integer_or_infinity(JsValue::int(42)), 42.0);
}
#[test]
fn test_to_length_nan() {
assert_eq!(to_length(JsValue::float(f64::NAN)), 0);
}
#[test]
fn test_to_length_negative() {
assert_eq!(to_length(JsValue::float(-1.0)), 0);
assert_eq!(to_length(JsValue::int(-42)), 0);
}
#[test]
fn test_to_length_positive() {
assert_eq!(to_length(JsValue::float(100.0)), 100);
assert_eq!(to_length(JsValue::int(999)), 999);
}
#[test]
fn test_to_length_overflow_clamp() {
assert_eq!(to_length(JsValue::float(1e100)), 9_007_199_254_740_991);
}
#[test]
fn test_same_value_zero_nan() {
assert!(same_value_zero(JsValue::float(f64::NAN), JsValue::float(f64::NAN)));
}
#[test]
fn test_same_value_zero_positive_and_negative_zero() {
assert!(same_value_zero(JsValue::float(0.0), JsValue::float(-0.0)));
assert!(same_value_zero(JsValue::float(-0.0), JsValue::float(0.0)));
}
#[test]
fn test_same_value_zero_equal_numbers() {
assert!(same_value_zero(JsValue::float(42.0), JsValue::float(42.0)));
assert!(same_value_zero(JsValue::int(42), JsValue::int(42)));
assert!(same_value_zero(JsValue::int(42), JsValue::float(42.0)));
}
#[test]
fn test_same_value_zero_different_numbers() {
assert!(!same_value_zero(JsValue::float(1.0), JsValue::float(2.0)));
}
#[test]
fn test_same_value_zero_null_undefined() {
assert!(same_value_zero(JsValue::null(), JsValue::null()));
assert!(same_value_zero(JsValue::undefined(), JsValue::undefined()));
assert!(!same_value_zero(JsValue::null(), JsValue::undefined()));
}
+1
View File
@@ -19,6 +19,7 @@ oxide_compiler = { path = "../oxide_compiler" }
oxide_vm = { path = "../oxide_vm" }
oxide_kernel = { path = "../oxide_kernel" }
oxide_api = { path = "../oxide_api" }
oxide_log = { path = "../oxide_log" }
clap.workspace = true
rustyline.workspace = true
ansi_term.workspace = true
+29 -6
View File
@@ -8,11 +8,15 @@ use std::sync::Arc;
use ansi_term::Colour::Red;
use clap::{Parser, Subcommand};
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
use oxide_compiler::compiler_error;
use oxide_kernel::kernel::{KernelConfig, KernelCore};
use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
use oxide_kernel::string_forge::PermInterner;
use oxide_kernel::{kernel_error, kernel_info};
use oxide_log::{Level, SUBSYSTEM_COUNT};
use oxide_parser::Allocator;
use oxide_types::object::JsObject;
use oxide_vm::vm_error;
use oxide_vm::vm_pool::VmPool;
use oxide_vm::JsValue;
@@ -70,12 +74,12 @@ fn main() -> ExitCode {
match cli.command {
Some(Commands::Eval { code }) => {
let kernel = make_kernel();
let kernel = make_kernel(cli.verbose, cli.quiet);
let pool = make_pool(&kernel);
eval(&code, &kernel, &pool)
}
Some(Commands::Run { file }) => {
let kernel = make_kernel();
let kernel = make_kernel(cli.verbose, cli.quiet);
let pool = make_pool(&kernel);
run(&file, &kernel, &pool)
}
@@ -89,7 +93,7 @@ fn main() -> ExitCode {
update_baseline,
leak_check_interval,
}) => {
let kernel = make_kernel();
let kernel = make_kernel(false, false);
let pool = make_pool(&kernel);
let config = bench::BenchConfig {
mode: mode.unwrap_or_else(|| "js".to_string()),
@@ -107,8 +111,16 @@ fn main() -> ExitCode {
}
}
fn make_kernel() -> Arc<KernelCore> {
KernelCore::new(KernelConfig::standard())
fn make_kernel(verbose: bool, quiet: bool) -> Arc<KernelCore> {
let mut config = KernelConfig::standard();
if verbose {
config.log_levels = [Level::Info; SUBSYSTEM_COUNT];
} else if quiet {
config.log_levels = [Level::Off; SUBSYSTEM_COUNT];
} else {
config.log_levels = [Level::Error; SUBSYSTEM_COUNT];
}
KernelCore::new(config)
}
fn make_pool(kernel: &Arc<KernelCore>) -> Arc<VmPool> {
@@ -121,6 +133,7 @@ fn eval(code: &str, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
Ok(p) => p,
Err(errors) => {
for err in &errors {
compiler_error!("parse error: {}", err);
eprintln!("{}", Red.paint(err.to_string()));
}
return ExitCode::FAILURE;
@@ -132,6 +145,7 @@ fn eval(code: &str, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) {
Ok(m) => m,
Err(err) => {
compiler_error!("compile error: {}", err);
eprintln!("{}", Red.paint(err));
return ExitCode::FAILURE;
}
@@ -144,6 +158,7 @@ fn eval(code: &str, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
ExitCode::SUCCESS
}
Err(err) => {
vm_error!("runtime error: {}", err);
eprintln!("{}", Red.paint(err));
ExitCode::FAILURE
}
@@ -226,6 +241,7 @@ fn run(file: &str, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
match fs::read_to_string(file) {
Ok(source) => eval(&source, kernel, pool),
Err(err) => {
kernel_error!("cannot read {}: {}", file, err);
eprintln!("{}", Red.paint(format!("Cannot read {file}: {err}")));
ExitCode::FAILURE
}
@@ -239,11 +255,13 @@ fn compile(expr: Option<String>, file: Option<String>) -> ExitCode {
match fs::read_to_string(&path) {
Ok(s) => s,
Err(err) => {
kernel_error!("cannot read {}: {}", path, err);
eprintln!("{}", Red.paint(format!("Cannot read {path}: {err}")));
return ExitCode::FAILURE;
}
}
} else {
kernel_error!("compile requires -e '<code>' or a file path");
eprintln!("{}", Red.paint("compile requires -e '<code>' or a file path"));
return ExitCode::FAILURE;
};
@@ -253,6 +271,7 @@ fn compile(expr: Option<String>, file: Option<String>) -> ExitCode {
Ok(p) => p,
Err(errors) => {
for err in &errors {
compiler_error!("parse error: {}", err);
eprintln!("{}", Red.paint(err.to_string()));
}
return ExitCode::FAILURE;
@@ -266,6 +285,7 @@ fn compile(expr: Option<String>, file: Option<String>) -> ExitCode {
ExitCode::SUCCESS
}
Err(err) => {
compiler_error!("compile error: {}", err);
eprintln!("{}", Red.paint(err));
ExitCode::FAILURE
}
@@ -291,12 +311,13 @@ fn repl() -> ExitCode {
let mut rl = match DefaultEditor::new() {
Ok(editor) => editor,
Err(err) => {
kernel_error!("failed to start REPL: {}", err);
eprintln!("{}", Red.paint(format!("Failed to start REPL: {err}")));
return ExitCode::FAILURE;
}
};
let kernel = make_kernel();
let kernel = make_kernel(false, false);
let pool = make_pool(&kernel);
let mut source = String::new();
let mut input_buf = String::new();
@@ -347,6 +368,7 @@ fn repl() -> ExitCode {
return ExitCode::SUCCESS;
}
Err(err) => {
vm_error!("REPL error: {}", err);
eprintln!("{}", Red.paint(format!("REPL error: {err}")));
return ExitCode::FAILURE;
}
@@ -355,6 +377,7 @@ fn repl() -> ExitCode {
}
fn not_implemented(command: &str) -> ExitCode {
use ansi_term::Colour::Yellow;
kernel_info!("command not yet implemented: {}", command);
eprintln!("{}", Yellow.paint(format!("'{command}' is not yet implemented")));
ExitCode::SUCCESS
}
+51 -8
View File
@@ -160,6 +160,10 @@ pub(crate) struct CompileCtx {
pub(crate) static_block_this_reg: Option<u8>,
pub(crate) after_super_insert: Option<Vec<opcode::Instr>>,
pub(crate) after_super_inserted: bool,
/// Count-pass mirror of `after_super_insert`: the instruction count of a derived
/// constructor's instance-field code, added at the super() call site during body
/// counting so projected_pc matches where the emit pass splices the field bytecode.
pub(crate) after_super_count_words: Option<usize>,
/// Set when alloc_reg() overflows into the reserved this/new.target range (≥254).
/// Checked after each emit phase to produce a compile error rather than silent corruption.
pub(crate) reg_overflow: bool,
@@ -233,6 +237,7 @@ impl CompileCtx {
static_block_this_reg: None,
after_super_insert: None,
after_super_inserted: false,
after_super_count_words: None,
reg_overflow: false,
const_overflow: false,
jump_overflow: false,
@@ -721,22 +726,41 @@ impl Compiler {
ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?;
}
// Count pass
if !fields_after_super {
if let Some(count) = count_fields.as_mut() {
// Count the parameter destructuring prologue so the count pass matches the emit pass
// below (which emits it via emit_binding_pattern). Without this the projected program
// counter undercounts, drifting every jump target inside a function that has a
// destructured or default parameter.
for spec in param_specs {
if let ParamSpec::Pattern { pattern, .. } = spec {
self.count_binding_pattern(pattern, &mut ctx);
}
}
// Count pass. For derived constructors the emit pass allocates/counts the instance
// fields up front but splices the field bytecode into the body right after SUPER_CALL.
// Mirror that: count the fields (allocating the same registers/constants), capture
// their instruction count, undo the position here, and re-add it at the super() call
// site during body counting so labels after super() line up with the emit pass.
if let Some(count) = count_fields.as_mut() {
if fields_after_super {
let before = ctx.projected_pc;
count(self, &mut ctx);
let field_words = ctx.projected_pc - before;
ctx.projected_pc = before;
ctx.after_super_count_words = Some(field_words);
} else {
count(self, &mut ctx);
}
}
for stmt in body_stmts {
self.count_statement(stmt, &mut ctx);
}
if fields_after_super {
if let Some(count) = count_fields.as_mut() {
count(self, &mut ctx);
}
}
ctx.max_regs = ctx.max_regs.max(1);
ctx.reg_overflow = false;
// Count-pass instruction total for this body, compared against the emit pass below
// to catch any counter/emitter drift (which would mis-target jumps). reset_regs zeroes
// projected_pc, so capture it first.
let counted_pc = ctx.projected_pc;
ctx.reset_regs();
// Emit pass - reallocate params (same order = same regs after reset)
@@ -774,6 +798,14 @@ impl Compiler {
}
}
debug_assert_eq!(
counted_pc,
ctx.bytecode.len(),
"counter/emitter instruction drift in function body (before implicit RETURN): counted {} vs emitted {}",
counted_pc,
ctx.bytecode.len()
);
// Emit implicit RETURN: expression body returns the last expression,
// statement body returns undefined.
if is_expression_body {
@@ -840,6 +872,9 @@ impl Compiler {
crate::compiler_debug!("counter: {} instructions estimated", ctx.projected_pc);
ctx.max_regs = ctx.max_regs.max(1);
ctx.reg_overflow = false;
// Count-pass instruction total, compared against the emit pass below to catch
// counter/emitter drift. reset_regs zeroes projected_pc, so capture it first.
let counted_pc = ctx.projected_pc;
ctx.reset_regs();
// First sub-pass: emit FunctionDeclarations (hoisting)
@@ -863,6 +898,14 @@ impl Compiler {
}
crate::compiler_debug!("emitter: {} bytes emitted", ctx.bytecode.len());
debug_assert_eq!(
counted_pc,
ctx.bytecode.len(),
"counter/emitter instruction drift in top-level module (before result store + HALT): counted {} vs emitted {}",
counted_pc,
ctx.bytecode.len()
);
if let Some(r) = last_result {
ctx.emit(opcode::encode(OpCode::LOAD_VAR, 0, r, 0));
} else {
@@ -13,10 +13,8 @@ impl Compiler {
}
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
// Both plain (IC_SET_PROP) and compound (COMPOUND_MEMBER_*) emit one opcode + 3 ext
// words after the key load — no extra instruction for the compound case.
ctx.count_load_const(); // key
ctx.count_ic_set_with_ext(); // IC_SET_PROP or COMPOUND_MEMBER_* + 3 ext words
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
@@ -13,6 +13,10 @@ impl Compiler {
}
ctx.alloc_reg();
ctx.count_call_instr_with_arg_ext(); // SUPER_CALL + arg_count ext word
// Mirror emit: derived-ctor instance fields are injected right after super().
if let Some(words) = ctx.after_super_count_words.take() {
ctx.projected_pc += words;
}
return;
}
@@ -42,21 +42,27 @@ impl Compiler {
self.count_expression(&log.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // AND/OR
} else if matches!(log.operator, LogicalOperator::Coalesce) {
// Mirror emit_logical_expression's Coalesce path: it patches raw bytecode
// positions and does NOT consume a label id. Consuming one here would shift
// every later construct's label id and break resolve_label at emit time.
ctx.alloc_reg(); // dup register
ctx.projected_pc += 1; // LOAD_VAR (DUP)
ctx.projected_pc += 1; // JMP_IF_NULLISH
ctx.projected_pc += 1; // JMP over RHS on non-nullish
self.count_expression(&log.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR (overwrite)
} else {
let id = ctx.next_label_id();
ctx.alloc_reg(); // dup register
ctx.projected_pc += 1; // LOAD_VAR (DUP)
ctx.projected_pc += 1; // JMP_IF_FALSE, JMP_IF_TRUE, or JMP_IF_NULLISH
if matches!(log.operator, LogicalOperator::Coalesce) {
ctx.projected_pc += 1; // JMP over RHS on non-nullish
ctx.labels.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
}
ctx.projected_pc += 1; // JMP_IF_FALSE or JMP_IF_TRUE
self.count_expression(&log.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR (overwrite)
let skip_label = match log.operator {
LogicalOperator::And => Label::TernaryEnd(id),
LogicalOperator::Or => Label::TernaryElse(id),
LogicalOperator::Coalesce => Label::TernaryEnd(id),
LogicalOperator::Coalesce => unreachable!("coalesce handled above"),
};
ctx.labels.label_map.insert(skip_label, ctx.projected_pc);
}
@@ -1,9 +1,7 @@
use super::super::*;
impl Compiler {
pub(in crate::counter) fn count_binding_pattern(
&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx,
) {
pub(crate) fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) {
match pattern {
oxide_parser::BindingPattern::BindingIdentifier(_) => {
ctx.alloc_reg();
@@ -72,15 +70,18 @@ impl Compiler {
ctx.count_instr(); // FOR_OF_DONE
ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_NEXT
if elem.is_some() {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
if let Some(elem) = elem {
// Mirror emit_array_assignment: an element may be a default (`[a = 1]`) or a
// nested pattern (`[[a], b]`), not just an identifier — recurse through the
// shared dispatch instead of hard-coding one STORE_VAR.
self.count_assignment_maybe_default(elem, ctx);
}
}
if ap.rest.is_some() {
if let Some(rest) = &ap.rest {
self.count_rest_array(ctx);
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
// Mirror the emitter (emit_array_assignment -> emit_assign_target): the rest
// target may itself be a nested pattern, not just an identifier.
self.count_assign_target(&rest.target, ctx);
}
ctx.count_instr(); // FOR_OF_CLOSE
}
@@ -108,11 +109,12 @@ impl Compiler {
}
}
}
if op.rest.is_some() {
if let Some(rest) = &op.rest {
ctx.alloc_reg();
ctx.count_instr_with_ext(1); // REST_OBJECT + excluded-keys ext
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
// Mirror the emitter (emit_object_assignment -> emit_assign_target): the rest
// target may itself be a nested pattern, not just an identifier.
self.count_assign_target(&rest.target, ctx);
}
}
@@ -35,15 +35,19 @@ impl Compiler {
continue;
}
if matches!(method.key, oxide_parser::PropertyKey::PrivateIdentifier(_)) {
if method.r#static {
ctx.count_load_const(); // private id
ctx.count_load_const(); // method
ctx.count_instr(); // INIT_PRIVATE
}
// Mirror emit_private_method_init for static AND non-static private
// methods: private-id LOAD_CONST + CREATE_CLOSURE + SET_HOME_OBJECT +
// INIT_PRIVATE.
ctx.count_load_const(); // private id reg
ctx.count_load_const(); // method closure (CREATE_CLOSURE)
ctx.count_instr(); // SET_HOME_OBJECT
ctx.count_instr(); // INIT_PRIVATE
continue;
}
ctx.count_load_const(); // key_reg
ctx.count_load_const(); // method_reg
// Computed method key emits the full key expression; a plain key is one
// LOAD_CONST. Mirror emit_class_key_reg via the shared count_class_key.
self.count_class_key(&method.key, method.computed, ctx);
ctx.count_load_const(); // method closure (CREATE_CLOSURE)
ctx.count_instr(); // SET_HOME_OBJECT
match method.kind {
MethodDefinitionKind::Method => {
@@ -59,7 +63,9 @@ impl Compiler {
ClassElement::PropertyDefinition(prop) => {
let prop = prop.as_ref();
if prop.r#static {
ctx.count_load_const(); // key_reg
// Computed static field key emits the full key expression; mirror
// emit_public_field_init via the shared count_class_key.
self.count_class_key(&prop.key, prop.computed, ctx);
if let Some(value) = &prop.value {
self.count_expression(value, ctx);
} else {
@@ -40,9 +40,14 @@ impl Compiler {
ctx.labels.label_map.insert(catch_label, ctx.projected_pc);
if let Some(catch) = &stmt.handler {
ctx.push_scope();
if let Some(_param) = &catch.param {
if let Some(param) = &catch.param {
// Mirror the emitter: the catch register is always allocated, but STORE_VAR is
// emitted only for an identifier binding — a destructuring catch param emits no
// store (that binding form is not implemented yet).
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR
if let oxide_parser::BindingPattern::BindingIdentifier(_) = &param.pattern {
ctx.projected_pc += 1; // STORE_VAR
}
}
for cs in &catch.body.body {
self.count_statement(cs, ctx);
@@ -41,7 +41,8 @@ impl Compiler {
self.count_expression(init_expr, ctx);
self.count_binding_pattern(&d.id, ctx);
} else {
ctx.alloc_reg();
ctx.alloc_reg(); // tmp holding undefined
ctx.alloc_reg(); // var register
ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR
}
}
+1 -1
View File
@@ -247,7 +247,7 @@ impl Compiler {
self.emit_expression(key.to_expression(), ctx)
}
fn count_class_key(&self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx) {
pub(crate) fn count_class_key(&self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx) {
if computed {
self.count_expression(key.to_expression(), ctx);
} else {
@@ -75,8 +75,17 @@ impl Compiler {
let val_reg = self.emit_expression(init_expr, ctx)?;
self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?;
} else if let BindingPattern::BindingIdentifier(bi) = &d.id {
// `for (var i; ...)` initializes `i` to undefined, mirroring a
// statement-level `var i;` — emit LOAD_CONST undefined + STORE_VAR.
// The count pass already counts these two instructions; emitting
// nothing here drifts every jump target after the for-init.
let idx = ctx.add_constant(Constant::Undefined);
let tmp = ctx.alloc_reg();
ctx.emit_load_const(tmp, idx);
let var_reg = ctx.alloc_reg();
ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0));
ctx.init_var(bi.name.as_str());
}
}
}
@@ -120,12 +120,14 @@ fn regression_for_var_init() {
#[test]
fn regression_var_decl_counter_no_init() {
let result = std::panic::catch_unwind(|| {
compile_source("for (var i; i < 3; i = i + 1) {}");
});
// `for (var i; ...)` initializes `i` to undefined (var semantics, not TDZ), and the
// counter/emitter agree on the no-init declarator's instruction count, so it compiles
// to a well-formed loop header instead of panicking.
let module = compile_source("for (var i; i < 3; i = i + 1) {}");
assert!(!module.bytecode.is_empty(), "for(var) no-init should compile");
assert!(
result.is_err(),
"for(var) without initializer should fail - accessing TDZ variable in test expression"
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"for(var) no-init should contain JMP_IF_FALSE"
);
}
+85 -23
View File
@@ -137,6 +137,9 @@ pub struct StringMethods {
pub match_all: *const (),
pub replace_all: *const (),
pub value_of: *const (),
pub substr: *const (),
pub at: *const (),
pub last_index_of: *const (),
}
pub struct RegExpMethods {
@@ -217,6 +220,8 @@ pub struct BuiltinWorld {
pub sym_search: P<JsObject>,
pub sym_split: P<JsObject>,
pub sym_iterator: P<JsObject>,
pub sym_to_primitive: P<JsObject>,
pub sym_has_instance: P<JsObject>,
pub stub_objects: Vec<P<JsObject>>,
}
@@ -539,6 +544,8 @@ impl BuiltinWorld {
BuiltinId::SymSearch => &self.sym_search,
BuiltinId::SymSplit => &self.sym_split,
BuiltinId::SymIterator => &self.sym_iterator,
BuiltinId::SymToPrimitive => &self.sym_to_primitive,
BuiltinId::SymHasInstance => &self.sym_has_instance,
}
}
@@ -574,6 +581,8 @@ impl BuiltinWorld {
let sym_search = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_split = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_to_primitive = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_has_instance = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let stub_objects = Vec::new();
let world = Self {
@@ -641,6 +650,8 @@ impl BuiltinWorld {
sym_search,
sym_split,
sym_iterator,
sym_to_primitive,
sym_has_instance,
stub_objects,
};
wire_builtin_world_links(&world);
@@ -701,29 +712,42 @@ impl BuiltinWorld {
},
)
};
let (symbol_proto, symbol_constructor, sym_match, sym_replace, sym_search, sym_split, sym_iterator) =
if dirty.symbol_family {
let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol");
(
symbol_proto,
symbol_constructor,
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
)
} else {
(
current.symbol_proto.clone(),
current.symbol_constructor.clone(),
current.sym_match.clone(),
current.sym_replace.clone(),
current.sym_search.clone(),
current.sym_split.clone(),
current.sym_iterator.clone(),
)
};
let (
symbol_proto,
symbol_constructor,
sym_match,
sym_replace,
sym_search,
sym_split,
sym_iterator,
sym_to_primitive,
sym_has_instance,
) = if dirty.symbol_family {
let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol");
(
symbol_proto,
symbol_constructor,
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
)
} else {
(
current.symbol_proto.clone(),
current.symbol_constructor.clone(),
current.sym_match.clone(),
current.sym_replace.clone(),
current.sym_search.clone(),
current.sym_split.clone(),
current.sym_iterator.clone(),
current.sym_to_primitive.clone(),
current.sym_has_instance.clone(),
)
};
let math_object = if dirty.math {
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()))
@@ -861,6 +885,8 @@ impl BuiltinWorld {
sym_search,
sym_split,
sym_iterator,
sym_to_primitive,
sym_has_instance,
stub_objects,
};
wire_builtin_world_links(&world);
@@ -993,6 +1019,39 @@ impl BuiltinWorld {
("toString", methods.to_string, 0),
("stack", methods.stack, 0),
);
let si_name = string_forge.intern("name").0;
let error_si = string_forge.intern("Error").0;
let error_name_val = JsValue::perm_string(string_forge.string_ptr(error_si));
let name_shape = shape_forge.make_shape(proto.shape_id(), si_name);
proto.set_shape_id(name_shape);
proto.ensure_hash_props().push(error_name_val);
let si_message = string_forge.intern("message").0;
let empty_si = string_forge.intern("").0;
let empty_val = JsValue::perm_string(string_forge.string_ptr(empty_si));
let msg_shape = shape_forge.make_shape(proto.shape_id(), si_message);
proto.set_shape_id(msg_shape);
proto.ensure_hash_props().push(empty_val);
self.set_subtype_proto_name(string_forge, shape_forge, &self.type_error_proto, "TypeError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.reference_error_proto, "ReferenceError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.range_error_proto, "RangeError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.syntax_error_proto, "SyntaxError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.uri_error_proto, "URIError", si_name);
self.set_subtype_proto_name(string_forge, shape_forge, &self.eval_error_proto, "EvalError", si_name);
}
fn set_subtype_proto_name(
&self, string_forge: &PermInterner, shape_forge: &ShapeForge, proto_p: &P<JsObject>, name: &str, si_name: u32,
) {
let proto_ptr = P::as_ptr(proto_p) as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
let name_si = string_forge.intern(name).0;
let name_val = JsValue::perm_string(string_forge.string_ptr(name_si));
let name_shape = shape_forge.make_shape(proto.shape_id(), si_name);
proto.set_shape_id(name_shape);
proto.ensure_hash_props().push(name_val);
}
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
@@ -1033,6 +1092,9 @@ impl BuiltinWorld {
("matchAll", methods.match_all, 1),
("replaceAll", methods.replace_all, 2),
("valueOf", methods.value_of, 0),
("substr", methods.substr, 2),
("at", methods.at, 1),
("lastIndexOf", methods.last_index_of, 1),
);
}
+8 -2
View File
@@ -201,7 +201,7 @@ pub struct KernelSession {
/// Maintenance: every new `BuiltinWorld` object field must be added here and to
/// `KernelSession::dirty_since_snapshot()` so selective reset can rebuild the
/// correct builtin family.
pub const NUM_BUILTINS: usize = 64;
pub const NUM_BUILTINS: usize = 66;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
@@ -270,6 +270,8 @@ pub enum BuiltinId {
SymSearch = 61,
SymSplit = 62,
SymIterator = 63,
SymToPrimitive = 64,
SymHasInstance = 65,
}
impl BuiltinId {
@@ -338,6 +340,8 @@ impl BuiltinId {
BuiltinId::SymSearch,
BuiltinId::SymSplit,
BuiltinId::SymIterator,
BuiltinId::SymToPrimitive,
BuiltinId::SymHasInstance,
];
}
@@ -506,7 +510,9 @@ impl KernelSession {
|| gen(BuiltinId::SymReplace) != snap(BuiltinId::SymReplace)
|| gen(BuiltinId::SymSearch) != snap(BuiltinId::SymSearch)
|| gen(BuiltinId::SymSplit) != snap(BuiltinId::SymSplit)
|| gen(BuiltinId::SymIterator) != snap(BuiltinId::SymIterator),
|| gen(BuiltinId::SymIterator) != snap(BuiltinId::SymIterator)
|| gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive)
|| gen(BuiltinId::SymHasInstance) != snap(BuiltinId::SymHasInstance),
math: gen(BuiltinId::MathObject) != snap(BuiltinId::MathObject),
json: gen(BuiltinId::JsonObject) != snap(BuiltinId::JsonObject),
date: gen(BuiltinId::DateConstructor) != snap(BuiltinId::DateConstructor)
+146 -35
View File
@@ -101,6 +101,9 @@ pub trait VmHost {
// Call infrastructure
fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result<JsValue, String>;
/// Take the original thrown JsValue preserved across a String-flattening call boundary,
/// so an iterator wrapper can re-throw the original error instead of re-wrapping it.
fn take_uncaught_value(&mut self) -> Option<JsValue>;
// Error handling
fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result<Option<*mut JsObject>, String>;
@@ -118,11 +121,13 @@ pub trait VmHost {
fn symbol_key_for_id(&self, idx: u32) -> Option<String>;
}
pub fn format_error_message(kind: &str, msg: &str) -> String {
if msg.is_empty() || msg == kind || msg.starts_with(&format!("{kind}:")) {
pub fn format_error_message(name: &str, msg: &str) -> String {
if name.is_empty() {
msg.to_string()
} else if msg.is_empty() {
name.to_string()
} else {
format!("{kind}: {msg}")
format!("{name}: {msg}")
}
}
@@ -254,52 +259,79 @@ pub fn to_boolean(val: JsValue) -> bool {
false
}
pub fn abstract_eq(lhs: JsValue, rhs: JsValue) -> bool {
if lhs.is_null() && rhs.is_undefined() {
/// Whether two values share the same ECMAScript language type. Number treats
/// int- and double-tagged values as one type (both are Number).
fn same_type(a: JsValue, b: JsValue) -> bool {
if a.is_string() && b.is_string() {
return true;
}
if lhs.is_undefined() && rhs.is_null() {
if (a.is_int() || a.is_double()) && (b.is_int() || b.is_double()) {
return true;
}
if lhs.is_string() && rhs.is_string() {
return string_value_eq(lhs, rhs);
}
if lhs.is_int() && rhs.is_int() {
return lhs.as_int() == rhs.as_int();
}
if lhs.is_double() && rhs.is_double() {
return strict_double_eq(lhs.as_double(), rhs.as_double());
}
if lhs.is_bool() && rhs.is_bool() {
return lhs.as_bool() == rhs.as_bool();
}
if lhs.is_null() && rhs.is_null() {
if a.is_bool() && b.is_bool() {
return true;
}
if lhs.is_undefined() && rhs.is_undefined() {
if a.is_null() && b.is_null() {
return true;
}
if (lhs.is_int() || lhs.is_double()) && (rhs.is_int() || rhs.is_double()) {
return strict_double_eq(to_number(lhs), to_number(rhs));
if a.is_undefined() && b.is_undefined() {
return true;
}
if (lhs.is_int() || lhs.is_double()) && rhs.is_bool() {
return to_number(lhs) == to_number(rhs);
if a.is_object() && b.is_object() {
return true;
}
if lhs.is_bool() && (rhs.is_int() || rhs.is_double()) {
return to_number(lhs) == to_number(rhs);
}
if lhs.is_bool() || rhs.is_bool() {
return to_number(lhs) == to_number(rhs);
}
if lhs.is_null() || rhs.is_null() {
return false;
}
if lhs.is_object() || rhs.is_object() {
return lhs.is_object() && rhs.is_object() && lhs.as_ptr() == rhs.as_ptr();
if a.is_symbol() && b.is_symbol() {
return true;
}
false
}
/// IsLooselyEqual(x, y) — ECMA-262 §7.2.15 (`==`).
///
/// BigInt steps are omitted (the engine prunes BigInt). Object operands are
/// coerced via ToPrimitive, which may invoke user `valueOf` / `toString` /
/// `@@toPrimitive`; hence the `VmHost` parameter and the `Result` (a thrown
/// TypeError from those callbacks propagates as `Err`). Note: `Object == Symbol`
/// does NOT trigger ToPrimitive (spec steps 11/12 cover only Number/String) and
/// falls through to `false`.
pub fn abstract_eq<H: VmHost>(lhs: JsValue, rhs: JsValue, host: &mut H) -> Result<bool, String> {
// Step 1: same type -> strict equality.
if same_type(lhs, rhs) {
return Ok(strict_equality(lhs, rhs));
}
// Steps 2-3: null <-> undefined.
if (lhs.is_null() && rhs.is_undefined()) || (lhs.is_undefined() && rhs.is_null()) {
return Ok(true);
}
// Steps 5-6: Number <-> String.
if (lhs.is_int() || lhs.is_double()) && rhs.is_string() {
return Ok(strict_double_eq(to_number(lhs), to_number(rhs)));
}
if lhs.is_string() && (rhs.is_int() || rhs.is_double()) {
return Ok(strict_double_eq(to_number(lhs), to_number(rhs)));
}
// Step 9: x is Boolean -> compare ToNumber(x).
if lhs.is_bool() {
return abstract_eq(JsValue::float(to_number(lhs)), rhs, host);
}
// Step 10: y is Boolean -> compare ToNumber(y).
if rhs.is_bool() {
return abstract_eq(lhs, JsValue::float(to_number(rhs)), host);
}
// Step 11: x is Number/String, y is Object -> ToPrimitive(y).
if (lhs.is_int() || lhs.is_double() || lhs.is_string()) && rhs.is_object() {
let prim = to_primitive(rhs, ToPrimitiveHint::Default, host)?;
return abstract_eq(lhs, prim, host);
}
// Step 12: x is Object, y is Number/String -> ToPrimitive(x).
if lhs.is_object() && (rhs.is_int() || rhs.is_double() || rhs.is_string()) {
let prim = to_primitive(lhs, ToPrimitiveHint::Default, host)?;
return abstract_eq(prim, rhs, host);
}
// Step 14: otherwise not equal.
Ok(false)
}
pub fn strict_eq(lhs: JsValue, rhs: JsValue) -> bool {
if lhs.is_int() && rhs.is_int() {
return lhs.as_int() == rhs.as_int();
@@ -415,6 +447,38 @@ pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool {
false
}
/// ToIntegerOrInfinity(argument) — ECMA-262 §7.1.4.
pub fn to_integer_or_infinity(val: JsValue) -> f64 {
let n = to_number(val);
if n.is_nan() || n == 0.0 {
0.0
} else if n.is_infinite() {
n
} else {
n.trunc()
}
}
/// ToLength(argument) — ECMA-262 §7.1.20.
pub fn to_length(val: JsValue) -> u64 {
let n = to_number(val);
let len = if n.is_nan() || n <= 0.0 { 0.0 } else { n.min(9_007_199_254_740_991.0) };
len.trunc() as u64
}
/// SameValueZero(x, y) — ECMA-262 §7.2.11.
pub fn same_value_zero(lhs: JsValue, rhs: JsValue) -> bool {
if (lhs.is_double() || lhs.is_int()) && (rhs.is_double() || rhs.is_int()) {
let a = to_f64(lhs);
let b = to_f64(rhs);
if a.is_nan() && b.is_nan() {
return true;
}
return a == b;
}
same_value(lhs, rhs)
}
pub fn strict_equality(lhs: JsValue, rhs: JsValue) -> bool {
if lhs.is_double() && rhs.is_double() {
return strict_double_eq(lhs.as_double(), rhs.as_double());
@@ -448,3 +512,50 @@ pub fn to_object<H: VmHost>(val: JsValue, host: &mut H) -> Result<JsValue, Strin
obj_ref.set_prop_count(1);
Ok(obj_val)
}
/// Hint for the abstract ToPrimitive operation (ECMA-262 §7.1.1).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ToPrimitiveHint {
Default,
String,
Number,
}
impl ToPrimitiveHint {
/// OrdinaryToPrimitive method-name ordering hint: `Default`/`Number` -> "number", `String` -> "string".
pub fn as_str(self) -> &'static str {
match self {
ToPrimitiveHint::String => "string",
ToPrimitiveHint::Default | ToPrimitiveHint::Number => "number",
}
}
}
/// ToPrimitive(input, hint) per ECMA-262 §7.1.1.
///
/// Primitives pass through unchanged. Objects are coerced via the VM's
/// `coerce_primitive_bounded`, which consults `obj[Symbol.toPrimitive]` first and
/// otherwise runs OrdinaryToPrimitive (valueOf/toString in hint order). Keeping the
/// object path in one place avoids duplicating OrdinaryToPrimitive here.
pub fn to_primitive<H: VmHost>(val: JsValue, hint: ToPrimitiveHint, host: &mut H) -> Result<JsValue, String> {
if !val.is_object() {
return Ok(val);
}
host.coerce_primitive_bounded(val, hint == ToPrimitiveHint::String)
}
/// ToNumber(input) with full object coercion: objects go through ToPrimitive
/// (number hint); Symbol values throw a TypeError per spec.
pub fn to_number_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<f64, String> {
let primitive = to_primitive(val, ToPrimitiveHint::Number, host)?;
if primitive.is_symbol() {
return Err(host.error_message_text("TypeError", "Cannot convert a Symbol value to a number"));
}
Ok(to_number(primitive))
}
/// ToString(input) with full object coercion: objects go through ToPrimitive (string hint).
pub fn to_string_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<String, String> {
let primitive = to_primitive(val, ToPrimitiveHint::String, host)?;
Ok(to_string(primitive))
}
+1
View File
@@ -16,6 +16,7 @@ oxide_compiler = { path = "../oxide_compiler" }
oxide_vm = { path = "../oxide_vm" }
oxide_parser = { path = "../oxide_parser" }
oxide_kernel = { path = "../oxide_kernel" }
oxide_log = { path = "../oxide_log" }
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
walkdir = "2"
+21
View File
@@ -12,6 +12,9 @@ use std::sync::{Arc, OnceLock, RwLock};
use std::time::{Duration, Instant};
use walkdir::WalkDir;
mod test262_log;
use oxide_log::{Level, LogConfig, Output, SUBSYSTEM_COUNT};
// Thread-local that records the path currently being executed.
// Written before every test; read by the panic hook to identify the crash file.
std::thread_local! {
@@ -1063,8 +1066,10 @@ fn main() {
std::panic::set_hook(Box::new(|info| {
let path = CURRENT_TEST_PATH.with(|p| p.borrow().clone());
if !path.is_empty() {
test262_error!("CRASH in test: {}", path);
eprintln!("CRASH in test: {path}");
}
test262_error!("panic: {}", info);
eprintln!("panic: {info}");
}));
@@ -1086,11 +1091,17 @@ fn run_tests() -> bool {
let config = match RunConfig::parse(&args) {
Ok(config) => config,
Err(msg) => {
test262_error!("config error: {}", msg);
eprintln!("{msg}");
return false;
}
};
oxide_log::init(&LogConfig {
output: Output::Stderr,
levels: [Level::Info; SUBSYSTEM_COUNT],
});
let test262_root = if let Some(root) = config.test262_root.clone() {
root
} else {
@@ -1104,6 +1115,7 @@ fn run_tests() -> bool {
};
if !test262_root.exists() {
test262_error!("test262 not found at {}", test262_root.display());
eprintln!(
"test262 not found at: {}\n\
Run: git submodule add https://github.com/tc39/test262.git tests/test262\n\
@@ -1116,11 +1128,14 @@ fn run_tests() -> bool {
let filter = config.filter.clone().map(|f| f.replace('\\', "/"));
test262_info!("discovering tests in: {}", test262_root.display());
eprintln!("discovering tests in: {}", test262_root.display());
if config.no_skip {
test262_info!("no-skip mode enabled");
eprintln!("no-skip mode: capability filters disabled; unsupported results count as failures");
}
let paths = discover_tests(&test262_root);
test262_info!("found {} test files", paths.len());
eprintln!("found {} test files", paths.len());
let total = paths.len();
@@ -1144,6 +1159,7 @@ fn run_tests() -> bool {
let log_running_tests = std::env::var_os("OXIDE_TEST262_RUNNING_LOG").is_some();
let heartbeat_path: Option<PathBuf> = std::env::var_os("OXIDE_TEST262_HEARTBEAT").map(PathBuf::from);
test262_info!("running on {} worker thread(s)", workers);
eprintln!("running on {workers} worker thread(s)");
let skip_until = std::env::var("OXIDE_SKIP_UNTIL")
@@ -1170,16 +1186,19 @@ fn run_tests() -> bool {
let allow_fail_exit = std::env::var_os("OXIDE_TEST262_ALLOW_FAIL_EXIT").is_some();
if config.supervise && !is_chunk_child && filter.is_none() {
test262_info!("supervised mode enabled");
eprintln!("supervised mode enabled: child-window execution with per-test timeout + auto-resume");
return run_supervised(&args, skip_until, end_index, config.no_skip, &paths);
}
if let Some(chunk_size) = chunk_size {
if !is_chunk_child && filter.is_none() {
test262_info!("chunked mode enabled: {} test(s) per child", chunk_size);
eprintln!("chunked mode enabled: {chunk_size} test(s) per child process");
return run_chunked(&args, skip_until, end_index, chunk_size);
}
}
test262_info!("kernel reset batch: {} test(s)", kernel_batch);
eprintln!("kernel reset batch: {kernel_batch} test(s)");
let cursor = AtomicUsize::new(skip_until);
let progress = AtomicUsize::new(skip_until);
@@ -1220,6 +1239,7 @@ fn run_tests() -> bool {
// diagnosing OS-level crashes that need the last stderr line.
CURRENT_TEST_PATH.with(|p| *p.borrow_mut() = path_str.clone());
if log_running_tests {
test262_debug!("running: {}", path_str);
eprintln!(" [{tid:?}] running: {path_str}");
}
if let Some(hb) = heartbeat_ref {
@@ -1240,6 +1260,7 @@ fn run_tests() -> bool {
tests_since_kernel_reset = 0;
}
if done % 500 == 0 || done == total {
test262_info!("progress: {}/{} ({}%)", done, total, done * 100 / total);
eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total);
}
}
+20
View File
@@ -0,0 +1,20 @@
#[macro_export]
macro_rules! test262_error {
($($arg:tt)*) => {
oxide_log::__log_event!("oxide::test262", oxide_log::tracing::Level::ERROR, $($arg)*)
};
}
#[macro_export]
macro_rules! test262_info {
($($arg:tt)*) => {
oxide_log::__log_event!("oxide::test262", oxide_log::tracing::Level::INFO, $($arg)*)
};
}
#[macro_export]
macro_rules! test262_debug {
($($arg:tt)*) => {
oxide_log::__log_event!("oxide::test262", oxide_log::tracing::Level::DEBUG, $($arg)*)
};
}
+24
View File
@@ -574,6 +574,30 @@ impl JsObject {
}
}
pub fn is_frozen(&self) -> bool {
(self._pad) & 1 != 0
}
pub fn set_frozen(&mut self, frozen: bool) {
if frozen {
self._pad |= 1 << 0;
} else {
self._pad &= !(1 << 0);
}
}
pub fn is_sealed(&self) -> bool {
(self._pad >> 1) & 1 != 0
}
pub fn set_sealed(&mut self, sealed: bool) {
if sealed {
self._pad |= 1 << 1;
} else {
self._pad &= !(1 << 1);
}
}
pub fn is_set(&self) -> bool {
(self.header >> 24) & 1 != 0
}
+16 -1
View File
@@ -2,9 +2,10 @@ use std::sync::Arc;
use oxide_kernel::builtin::ArrayMethods;
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject;
use oxide_types::object::{JsObject, PropAttributes};
use crate::bind_constructor;
use crate::bindings::apply_binding_table;
pub fn bind_array(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
let _array_methods = ArrayMethods {
@@ -46,6 +47,20 @@ pub fn bind_array(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
core.shape_forge().as_ref(),
);
let array_proto_ptr = session.builtin_world().array_proto.as_ptr() as *mut JsObject;
let array_proto = unsafe { &mut *array_proto_ptr };
apply_binding_table(
session.builtin_world(),
array_proto,
core,
&[("toString", oxide_builtins::array::array_to_string::<crate::vm::Vm> as *const (), 0)],
);
// Built-in prototype methods are non-enumerable (otherwise they leak into for-in).
let si = core.perm_interner().intern("toString").0;
if let Some(pos) = core.shape_forge().lookup_position(array_proto.shape_id(), si) {
array_proto.set_data_meta(pos, PropAttributes::new(true, false, true));
}
let ctor_ptr = session.builtin_world().array_constructor.as_ptr() as *mut JsObject;
bind_constructor!(core, global, "Array", ctor_ptr, oxide_builtins::array::array_constructor::<crate::vm::Vm>, 1, hash: true);
}
+41
View File
@@ -83,12 +83,53 @@ pub fn bind_date(core: &Arc<KernelCore>, session: &KernelSession, global: &mut J
oxide_builtins::date::date_set_milliseconds::<crate::vm::Vm> as *const (),
1,
),
(
"setUTCFullYear",
oxide_builtins::date::date_set_utc_full_year::<crate::vm::Vm> as *const (),
3,
),
("setUTCMonth", oxide_builtins::date::date_set_utc_month::<crate::vm::Vm> as *const (), 2),
("setUTCDate", oxide_builtins::date::date_set_utc_date::<crate::vm::Vm> as *const (), 1),
("setUTCHours", oxide_builtins::date::date_set_utc_hours::<crate::vm::Vm> as *const (), 4),
(
"setUTCMinutes",
oxide_builtins::date::date_set_utc_minutes::<crate::vm::Vm> as *const (),
3,
),
(
"setUTCSeconds",
oxide_builtins::date::date_set_utc_seconds::<crate::vm::Vm> as *const (),
2,
),
(
"setUTCMilliseconds",
oxide_builtins::date::date_set_utc_milliseconds::<crate::vm::Vm> as *const (),
1,
),
("getYear", oxide_builtins::date::date_get_year::<crate::vm::Vm> as *const (), 0),
("setYear", oxide_builtins::date::date_set_year::<crate::vm::Vm> as *const (), 1),
("toISOString", oxide_builtins::date::date_to_iso_string::<crate::vm::Vm> as *const (), 0),
("toJSON", oxide_builtins::date::date_to_json::<crate::vm::Vm> as *const (), 1),
("toString", oxide_builtins::date::date_to_string::<crate::vm::Vm> as *const (), 0),
("toDateString", oxide_builtins::date::date_to_date_string::<crate::vm::Vm> as *const (), 0),
("toTimeString", oxide_builtins::date::date_to_time_string::<crate::vm::Vm> as *const (), 0),
("toUTCString", oxide_builtins::date::date_to_utc_string::<crate::vm::Vm> as *const (), 0),
("toGMTString", oxide_builtins::date::date_to_gmt_string::<crate::vm::Vm> as *const (), 0),
(
"toLocaleDateString",
oxide_builtins::date::date_to_locale_date_string::<crate::vm::Vm> as *const (),
0,
),
(
"toLocaleString",
oxide_builtins::date::date_to_locale_string::<crate::vm::Vm> as *const (),
0,
),
(
"toLocaleTimeString",
oxide_builtins::date::date_to_locale_time_string::<crate::vm::Vm> as *const (),
0,
),
("valueOf", oxide_builtins::date::date_value_of::<crate::vm::Vm> as *const (), 0),
],
);
+2 -2
View File
@@ -14,8 +14,8 @@ pub fn bind_json(core: &Arc<KernelCore>, session: &KernelSession, global: &mut J
json,
core,
&[
("parse", oxide_builtins::json::json_parse::<crate::vm::Vm> as *const (), 1),
("stringify", oxide_builtins::json::json_stringify::<crate::vm::Vm> as *const (), 1),
("parse", oxide_builtins::json::json_parse::<crate::vm::Vm> as *const (), 2),
("stringify", oxide_builtins::json::json_stringify::<crate::vm::Vm> as *const (), 3),
],
);
+3
View File
@@ -24,6 +24,9 @@ pub fn bind_map(core: &Arc<KernelCore>, session: &KernelSession, global: &mut Js
("delete", oxide_builtins::map::map_delete::<crate::vm::Vm> as *const (), 1),
("clear", oxide_builtins::map::map_clear::<crate::vm::Vm> as *const (), 0),
("size", oxide_builtins::map::map_size::<crate::vm::Vm> as *const (), 0),
("entries", oxide_builtins::map::map_entries::<crate::vm::Vm> as *const (), 0),
("values", oxide_builtins::map::map_values::<crate::vm::Vm> as *const (), 0),
("keys", oxide_builtins::map::map_keys::<crate::vm::Vm> as *const (), 0),
],
);
+5 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::bind_constructor;
use crate::bindings::{apply_binding_table, configure_native_constructor};
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject;
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::value::JsValue;
pub fn bind_number(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
@@ -27,6 +27,8 @@ pub fn bind_number(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
oxide_builtins::number::number_is_safe_integer::<crate::vm::Vm> as *const (),
1,
),
("parseInt", oxide_builtins::number::number_parse_int::<crate::vm::Vm> as *const (), 1),
("parseFloat", oxide_builtins::number::number_parse_float::<crate::vm::Vm> as *const (), 1),
],
);
@@ -62,9 +64,11 @@ pub fn bind_number(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)),
] {
ctor.ensure_hash_props().push(value);
let pos = ctor.ensure_hash_props().len() - 1;
let prop_si = core.perm_interner().intern(name).0;
let next_shape = core.shape_forge().make_shape(ctor.shape_id(), prop_si);
ctor.set_shape_id(next_shape);
ctor.set_data_meta(pos, PropAttributes::new(false, false, false));
}
bind_constructor!(core, global, "Number", ctor_ptr, oxide_builtins::number::number_constructor::<crate::vm::Vm>, 1, hash: true);
+25 -1
View File
@@ -2,9 +2,10 @@ use std::sync::Arc;
use oxide_kernel::builtin::ObjectMethods;
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject;
use oxide_types::object::{JsObject, PropAttributes};
use crate::bind_constructor;
use crate::bindings::apply_binding_table;
pub fn bind_object(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
let methods = ObjectMethods {
@@ -36,6 +37,29 @@ pub fn bind_object(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
.builtin_world()
.bind_object_methods(&methods, core.perm_interner().as_ref(), core.shape_forge().as_ref());
let object_proto_ptr = session.builtin_world().object_proto.as_ptr() as *mut JsObject;
let object_proto = unsafe { &mut *object_proto_ptr };
apply_binding_table(
session.builtin_world(),
object_proto,
core,
&[
(
"toString",
oxide_builtins::object::object_proto_to_string::<crate::vm::Vm> as *const (),
0,
),
("valueOf", oxide_builtins::object::object_proto_value_of::<crate::vm::Vm> as *const (), 0),
],
);
// Built-in prototype methods are non-enumerable (otherwise they leak into for-in).
for name in ["toString", "valueOf"] {
let si = core.perm_interner().intern(name).0;
if let Some(pos) = core.shape_forge().lookup_position(object_proto.shape_id(), si) {
object_proto.set_data_meta(pos, PropAttributes::new(true, false, true));
}
}
let ctor_ptr = session.builtin_world().object_constructor.as_ptr() as *mut JsObject;
bind_constructor!(
core,
+3
View File
@@ -23,6 +23,9 @@ pub fn bind_set(core: &Arc<KernelCore>, session: &KernelSession, global: &mut Js
("delete", oxide_builtins::set::set_delete::<crate::vm::Vm> as *const (), 1),
("clear", oxide_builtins::set::set_clear::<crate::vm::Vm> as *const (), 0),
("size", oxide_builtins::set::set_size::<crate::vm::Vm> as *const (), 0),
("entries", oxide_builtins::set::set_entries::<crate::vm::Vm> as *const (), 0),
("values", oxide_builtins::set::set_values::<crate::vm::Vm> as *const (), 0),
("keys", oxide_builtins::set::set_keys::<crate::vm::Vm> as *const (), 0),
],
);
@@ -35,6 +35,9 @@ pub fn bind_string(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
match_all: oxide_builtins::string::string_match_all::<crate::vm::Vm> as *const (),
replace_all: oxide_builtins::string::string_replace_all::<crate::vm::Vm> as *const (),
value_of: oxide_builtins::string::string_value_of::<crate::vm::Vm> as *const (),
substr: oxide_builtins::string::string_substr::<crate::vm::Vm> as *const (),
at: oxide_builtins::string::string_at::<crate::vm::Vm> as *const (),
last_index_of: oxide_builtins::string::string_last_index_of::<crate::vm::Vm> as *const (),
};
session.builtin_world().bind_string_methods(
&string_methods,
@@ -53,6 +53,14 @@ pub fn bind_symbol(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
"iterator",
JsValue::from_js_object(session.builtin_world().sym_iterator.as_ptr() as *mut JsObject),
),
(
"toPrimitive",
JsValue::from_js_object(session.builtin_world().sym_to_primitive.as_ptr() as *mut JsObject),
),
(
"hasInstance",
JsValue::from_js_object(session.builtin_world().sym_has_instance.as_ptr() as *mut JsObject),
),
] {
bind_well_known_symbol(core, ctor, name, val);
}
+30 -16
View File
@@ -5,69 +5,83 @@ use oxide_types::value::JsValue;
impl Vm {
#[inline(always)]
pub(crate) fn dispatch_eq(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_eq(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("EQ rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let eq = coercion::abstract_eq(self.regs[a], self.regs[b]);
let lhs = self.regs[a];
let rhs = self.regs[b];
let eq = coercion::abstract_eq(lhs, rhs, self)?;
self.regs[rd] = JsValue::bool(eq);
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_neq(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_neq(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("NEQ rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let ne = !coercion::abstract_eq(self.regs[a], self.regs[b]);
let lhs = self.regs[a];
let rhs = self.regs[b];
let ne = !coercion::abstract_eq(lhs, rhs, self)?;
self.regs[rd] = JsValue::bool(ne);
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_lt(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_lt(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("LT rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let rel = coercion::relational_compare(self.regs[a], self.regs[b]);
match rel {
let va = self.coerce_primitive_bounded(self.regs[a], false)?;
let vb = self.coerce_primitive_bounded(self.regs[b], false)?;
match coercion::relational_compare(va, vb) {
Some(r) => self.regs[rd] = JsValue::bool(r),
None => {
vm_trace!("LT incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]);
self.regs[rd] = JsValue::bool(false);
}
}
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_gt(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_gt(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("GT rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let rel = coercion::relational_compare(self.regs[b], self.regs[a]);
match rel {
let va = self.coerce_primitive_bounded(self.regs[a], false)?;
let vb = self.coerce_primitive_bounded(self.regs[b], false)?;
match coercion::relational_compare(vb, va) {
Some(r) => self.regs[rd] = JsValue::bool(r),
None => {
vm_trace!("GT incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]);
self.regs[rd] = JsValue::bool(false);
}
}
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_lte(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_lte(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("LTE rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let rel = coercion::relational_compare(self.regs[b], self.regs[a]);
match rel {
let va = self.coerce_primitive_bounded(self.regs[a], false)?;
let vb = self.coerce_primitive_bounded(self.regs[b], false)?;
match coercion::relational_compare(vb, va) {
Some(r) => self.regs[rd] = JsValue::bool(!r),
None => {
vm_trace!("LTE incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]);
self.regs[rd] = JsValue::bool(false);
}
}
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_gte(&mut self, rd: usize, a: usize, b: usize) {
pub(crate) fn dispatch_gte(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("GTE rd={} r{}={:?} r{}={:?}", rd, a, self.regs[a], b, self.regs[b]);
let rel = coercion::relational_compare(self.regs[a], self.regs[b]);
match rel {
let va = self.coerce_primitive_bounded(self.regs[a], false)?;
let vb = self.coerce_primitive_bounded(self.regs[b], false)?;
match coercion::relational_compare(va, vb) {
Some(r) => self.regs[rd] = JsValue::bool(!r),
None => {
vm_trace!("GTE incomparable r{}={:?} r{}={:?}", a, self.regs[a], b, self.regs[b]);
self.regs[rd] = JsValue::bool(false);
}
}
Ok(())
}
#[inline(always)]
+8
View File
@@ -389,6 +389,10 @@ impl Vm {
};
let obj = unsafe { &mut *obj_ptr };
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
if !configurable {
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
}
obj.set_prop_at(pos, JsValue::undefined());
}
self.regs[rd] = JsValue::bool(true);
@@ -403,6 +407,10 @@ impl Vm {
};
let obj = unsafe { &mut *obj_ptr };
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
if !configurable {
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
}
obj.set_prop_at(pos, JsValue::undefined());
}
self.regs[rd] = JsValue::bool(true);
+1 -1
View File
@@ -498,7 +498,7 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje
}
// SAFETY: for_in_iters stores live iterator pointers owned by the current VM epoch.
unsafe {
for value in (*(*iter)).keys.iter_mut() {
for (value, _si) in (*(*iter)).keys.iter_mut() {
*value = rewrite_forwarded_value(*value, forwarding);
}
}
+57 -13
View File
@@ -55,11 +55,13 @@ fn js_error_kind_name(kind: JsErrorKind) -> &'static str {
}
}
pub(crate) fn format_error_message(kind: &str, msg: &str) -> String {
if msg.is_empty() || msg == kind || msg.starts_with(&format!("{kind}:")) {
pub(crate) fn format_error_message(name: &str, msg: &str) -> String {
if name.is_empty() {
msg.to_string()
} else if msg.is_empty() {
name.to_string()
} else {
format!("{kind}: {msg}")
format!("{name}: {msg}")
}
}
@@ -103,7 +105,9 @@ pub struct CallFrame {
}
pub struct ForInIter<'bump> {
pub keys: bumpalo::collections::Vec<'bump, JsValue>,
/// Each key paired with its string-intern id, so for-in can sort
/// integer-index keys ahead of string keys without re-interning.
pub keys: bumpalo::collections::Vec<'bump, (JsValue, u32)>,
pub index: usize,
}
@@ -111,6 +115,8 @@ pub struct TryHandler {
pub catch_pc: Option<usize>,
pub finally_pc: Option<usize>,
pub frame_depth: usize,
/// for_of_iters length at try entry — bounds which iterators IteratorClose on unwind.
pub for_of_depth: usize,
}
/// Heap-allocated snapshot used by `call_bytecode_function_inline`.
@@ -162,6 +168,10 @@ pub struct Vm {
pub(crate) save_stack: Vec<JsValue>,
pub(crate) try_stack: Vec<TryHandler>,
pub(crate) exception_value: Option<JsValue>,
/// Side-channel carrying the JsValue thrown by a sync call whose error was flattened
/// to a String by `call_function_sync`/`unwind`, so for-of can re-throw the original
/// value. Plain field (NOT in InlineSyncState) so it survives the inline-call restore.
pub(crate) last_uncaught_value: Option<JsValue>,
pub(crate) pending_exception: Option<JsValue>,
pub(crate) pending_error_kind: Option<&'static str>,
pub(crate) root_reg_limit: u8,
@@ -195,6 +205,33 @@ impl Vm {
return Ok(value);
}
// ECMA-262 §7.1.1 step 1: an exotic obj[Symbol.toPrimitive] takes precedence
// over OrdinaryToPrimitive. ponytail: symbol-object keys all collapse to
// to_string()="[object]" inside property_key_si (an engine-wide symbol-key
// limitation), so this resolves obj[@@toPrimitive] the same way the setter
// stores it; the real fix belongs to a dedicated symbol-key phase.
let sym_key = {
let sym_ptr = self.session.builtin_world().sym_to_primitive.as_ptr() as *mut JsObject;
JsValue::from_js_object(sym_ptr)
};
let sym_si = self.property_key_si(sym_key);
let exotic = {
let obj = unsafe { &*obj_ptr };
self.ordinary_get(obj, sym_si, value)?
};
if !exotic.is_undefined() && !exotic.is_null() {
let exotic_ptr = exotic.as_js_object_ptr();
if !exotic.is_object() || exotic_ptr.is_null() || !unsafe { &*exotic_ptr }.is_function() {
return Err(self.error_message_text("TypeError", "Symbol.toPrimitive is not a function"));
}
let hint_val = self.new_string(if prefer_string { "string" } else { "number" });
let result = self.call_function_sync(exotic, value, &[hint_val])?;
if result.is_object() {
return Err(self.error_message_text("TypeError", "Cannot convert object to primitive value"));
}
return Ok(result);
}
let method_names = if prefer_string { ["toString", "valueOf"] } else { ["valueOf", "toString"] };
for method_name in method_names {
@@ -331,8 +368,8 @@ impl Vm {
continue;
}
unsafe {
for &v in (*(*iter)).keys.iter() {
f(v);
for (v, _si) in (*(*iter)).keys.iter() {
f(*v);
}
}
}
@@ -606,7 +643,10 @@ impl Vm {
self.regs = saved_regs;
return match result {
NativeResult::Ok(val) => Ok(val),
NativeResult::Err(err) => Err(self.error_text(err)),
NativeResult::Err(err) => {
self.last_uncaught_value = Some(err);
Err(self.error_text(err))
}
NativeResult::TailCall { callee, this, args } => self.call_function_sync(callee, this, &args),
};
}
@@ -803,27 +843,27 @@ impl Vm {
}
OpCode::EQ => {
self.dispatch_eq(rd, a, b);
self.dispatch_eq(rd, a, b)?;
}
OpCode::NEQ => {
self.dispatch_neq(rd, a, b);
self.dispatch_neq(rd, a, b)?;
}
OpCode::LT => {
self.dispatch_lt(rd, a, b);
self.dispatch_lt(rd, a, b)?;
}
OpCode::GT => {
self.dispatch_gt(rd, a, b);
self.dispatch_gt(rd, a, b)?;
}
OpCode::LTE => {
self.dispatch_lte(rd, a, b);
self.dispatch_lte(rd, a, b)?;
}
OpCode::GTE => {
self.dispatch_gte(rd, a, b);
self.dispatch_gte(rd, a, b)?;
}
OpCode::STRICT_EQ => {
@@ -1238,6 +1278,7 @@ mod tests {
catch_pc: Some(1),
finally_pc: None,
frame_depth: 0,
for_of_depth: 0,
});
vm.exception_value = Some(JsValue::int(2));
vm.pending_exception = Some(JsValue::int(3));
@@ -1491,6 +1532,9 @@ impl oxide_runtime_api::VmHost for Vm {
fn epoch(&self) -> &Epoch {
self.epoch()
}
fn take_uncaught_value(&mut self) -> Option<JsValue> {
self.last_uncaught_value.take()
}
fn property_key_si(&mut self, val: JsValue) -> u32 {
self.property_key_si(val)
}
+2
View File
@@ -177,6 +177,7 @@ impl Vm {
catch_pc,
finally_pc: None,
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
});
}
@@ -193,6 +194,7 @@ impl Vm {
catch_pc: None,
finally_pc: Some(finally_pc),
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
});
}
+122 -11
View File
@@ -208,6 +208,25 @@ impl Vm {
if !rhs_val.is_object() {
return self.raise_type_error("INSTANCEOF right-hand side is not callable");
}
let has_instance_ptr = self.session.builtin_world().sym_has_instance.as_ptr() as *mut JsObject;
let has_instance_key = JsValue::from_js_object(has_instance_ptr);
let has_instance_si = self.property_key_si(has_instance_key);
let ctor_obj = unsafe { &*rhs_val.as_js_object_ptr() };
let has_instance_val = self.ordinary_get(ctor_obj, has_instance_si, rhs_val)?;
if !has_instance_val.is_undefined() && !has_instance_val.is_null() {
let fn_ptr = has_instance_val.as_js_object_ptr();
if !fn_ptr.is_null() {
let fn_obj = unsafe { &*fn_ptr };
if fn_obj.is_function() {
let result = self.call_function_sync(has_instance_val, rhs_val, &[lhs_val])?;
self.regs[rd] = JsValue::bool(to_boolean(result));
return Ok(());
}
}
}
if !lhs_val.is_object() {
self.regs[rd] = JsValue::bool(false);
return Ok(());
@@ -264,11 +283,21 @@ impl Vm {
pub(crate) fn dispatch_for_in_init(&mut self, a: usize) -> Result<(), String> {
vm_trace!("FOR_IN_INIT r{}={:?}", a, self.regs[a]);
let obj_val = self.regs[a];
if obj_val.is_null() || obj_val.is_undefined() {
// null/undefined enumerate nothing — an empty for-in, not a TypeError.
let keys_vec: bumpalo::collections::Vec<(JsValue, u32)> =
bumpalo::collections::Vec::new_in(self.epoch.bump());
let iter = self.epoch.alloc(ForInIter { keys: keys_vec, index: 0 });
self.iters.push_for_in(iter.cast::<ForInIter<'static>>());
return Ok(());
}
if !obj_val.is_object() {
// ToObject coercion for other primitives is not implemented yet — TypeError is correct until it lands.
return self.raise_type_error("for-in right-hand side is not an object");
}
let mut keys_vec = bumpalo::collections::Vec::new_in(self.epoch.bump());
let mut keys_vec: bumpalo::collections::Vec<(JsValue, u32)> =
bumpalo::collections::Vec::new_in(self.epoch.bump());
let mut seen = std::collections::HashSet::new();
let mut current = obj_val;
let mut depth = 0usize;
@@ -282,6 +311,7 @@ impl Vm {
}
depth += 1;
let cur = unsafe { &*current.as_js_object_ptr() };
let obj_start = keys_vec.len();
let mut cursor = Some(cur.shape_id());
while let Some(id) = cursor {
if id == oxide_kernel::shape_forge::EMPTY_SHAPE_ID {
@@ -300,8 +330,9 @@ impl Vm {
.map(|meta| meta.attributes.enumerable())
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable());
if enumerable {
keys_vec.push(JsValue::perm_string(
self.kernel_core.perm_interner().string_ptr(shape.property_name),
keys_vec.push((
JsValue::perm_string(self.kernel_core.perm_interner().string_ptr(shape.property_name)),
shape.property_name,
));
}
}
@@ -310,9 +341,23 @@ impl Vm {
break;
}
}
// The shape chain is walked leaf->root (reverse of insertion order);
// flip this object's slice back to insertion order before its proto.
keys_vec[obj_start..].reverse();
current = cur.proto();
}
// ES enumeration order: integer-index keys ascending, then the rest in
// insertion order. Stable sort preserves insertion order among non-index keys.
keys_vec.sort_by(|(_, a_si), (_, b_si)| {
match (self.array_index_from_property_key(*a_si), self.array_index_from_property_key(*b_si)) {
(Some(ai), Some(bi)) => ai.cmp(&bi),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
}
});
let iter = self.epoch.alloc(ForInIter { keys: keys_vec, index: 0 });
self.iters.push_for_in(iter.cast::<ForInIter<'static>>());
Ok(())
@@ -326,7 +371,7 @@ impl Vm {
}
let iter = unsafe { &mut *iter_ptr };
if iter.index < iter.keys.len() {
self.regs[rd] = iter.keys[iter.index];
self.regs[rd] = iter.keys[iter.index].0;
iter.index += 1;
} else {
self.regs[rd] = JsValue::undefined();
@@ -376,17 +421,26 @@ impl Vm {
return Err("FOR_OF_DONE iterator is not an object".into());
}
self.last_uncaught_value = None;
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
let next_si = self.kernel_core.perm_interner().intern("next").0;
let next_fn = self.ordinary_get(iter_obj, next_si, iterator)?;
let result = self.call_function_sync(next_fn, iterator, &[])?;
let next_fn = match self.ordinary_get(iter_obj, next_si, iterator) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
let result = match self.call_function_sync(next_fn, iterator, &[]) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
if !result.is_object() {
return self.raise_type_error("iterator result is not an object");
}
let result_obj = unsafe { &*result.as_js_object_ptr() };
let done_si = self.kernel_core.perm_interner().intern("done").0;
let done_val = self.ordinary_get(result_obj, done_si, result)?;
let done_val = match self.ordinary_get(result_obj, done_si, result) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
let done = to_boolean(done_val);
self.iters.set_last_for_of_result(result);
self.regs[rd] = JsValue::bool(!done);
@@ -395,6 +449,7 @@ impl Vm {
pub(crate) fn dispatch_for_of_next(&mut self, rd: usize) -> Result<(), String> {
vm_trace!("FOR_OF_NEXT rd={}", rd);
self.last_uncaught_value = None;
let result = self.iters.last_for_of_result();
if !result.is_object() {
self.regs[rd] = JsValue::undefined();
@@ -402,30 +457,86 @@ impl Vm {
}
let result_obj = unsafe { &*result.as_js_object_ptr() };
let value_si = self.kernel_core.perm_interner().intern("value").0;
self.regs[rd] = self.ordinary_get(result_obj, value_si, result)?;
self.regs[rd] = match self.ordinary_get(result_obj, value_si, result) {
Ok(v) => v,
Err(e) => return self.throw_for_of_error(e),
};
Ok(())
}
/// next()/value access threw: route through unwind so a surrounding try/catch can catch
/// it, re-throwing the ORIGINAL value when available. Per ECMA-262 the iterator is NOT
/// closed via return() on a next()-throw — pop it so the unwinding IteratorClose pass skips it.
fn throw_for_of_error(&mut self, msg: String) -> Result<(), String> {
self.iters.pop_for_of();
let exc = match self.last_uncaught_value.take() {
Some(v) => v,
None => oxide_builtins::error::create_error(self, &msg),
};
self.exception_value = Some(exc);
self.pending_error_kind = Some(self.thrown_error_kind(exc));
self.unwind()
}
pub(crate) fn dispatch_for_of_close(&mut self) -> Result<(), String> {
vm_trace!("FOR_OF_CLOSE");
let Some(iterator) = self.iters.pop_for_of() else {
return Ok(());
};
// Normal / break / return exit: no prior abrupt completion, so return()'s own throw propagates.
self.close_for_of_iterator(iterator, false)
}
/// Call `iterator.return()` (IteratorClose). When `suppress_return_error` is true the call
/// is being made because an enclosing abrupt completion is unwinding: return()'s own result
/// is discarded and the in-flight exception is preserved across the call. When false (normal
/// for-of exit) return()'s error propagates.
pub(crate) fn close_for_of_iterator(
&mut self, iterator: JsValue, suppress_return_error: bool,
) -> Result<(), String> {
if !iterator.is_object() {
return Ok(());
}
let iter_obj = unsafe { &*iterator.as_js_object_ptr() };
let return_si = self.kernel_core.perm_interner().intern("return").0;
let return_fn = self.ordinary_get(iter_obj, return_si, iterator)?;
let return_fn = match self.ordinary_get(iter_obj, return_si, iterator) {
Ok(v) => v,
Err(e) => {
if suppress_return_error {
return Ok(());
}
return Err(e);
}
};
if return_fn.is_object() {
let return_obj = unsafe { &*return_fn.as_js_object_ptr() };
if return_obj.is_function() {
let _ = self.call_function_sync(return_fn, iterator, &[])?;
if suppress_return_error {
let saved_exc = self.exception_value;
let saved_kind = self.pending_error_kind;
let _ = self.call_function_sync(return_fn, iterator, &[]);
self.exception_value = saved_exc;
self.pending_error_kind = saved_kind;
} else {
let _ = self.call_function_sync(return_fn, iterator, &[])?;
}
}
}
Ok(())
}
/// IteratorClose every active for-of iterator above `depth`, used by `unwind()` to close
/// loops abandoned by a throw. Pops before closing so a re-entrant call can't re-close,
/// and so the next()-throw path (which already popped its own iterator) is skipped.
pub(crate) fn close_for_of_above(&mut self, depth: usize) {
while self.iters.for_of_iters.len() > depth {
let Some(iterator) = self.iters.pop_for_of() else {
break;
};
let _ = self.close_for_of_iterator(iterator, true);
}
}
pub(crate) fn dispatch_rest_object(&mut self, rd: usize, a: usize) -> Result<(), String> {
vm_trace!("REST_OBJECT rd={}", rd);
let src = self.regs[a];
+6
View File
@@ -176,6 +176,8 @@ impl Vm {
self.restore_frame(frame);
}
}
// IteratorClose any for-of loops abandoned inside this handler's scope.
self.close_for_of_above(handler.for_of_depth);
if let Some(finally_pc) = handler.finally_pc {
vm_trace!("unwind: entering finally at pc={}", finally_pc);
if self.pending_exception.is_none() {
@@ -193,10 +195,14 @@ impl Vm {
return Ok(());
}
}
self.close_for_of_above(0);
while let Some(frame) = self.frames.pop() {
self.restore_frame(frame);
}
let exc = self.exception_value.take().unwrap_or(JsValue::undefined());
// Preserve the escaping JsValue so a for-of next()-throw can re-throw the original
// value instead of the flattened string (consumed by dispatch_for_of_done/next).
self.last_uncaught_value = Some(exc);
let kind_str = self.pending_error_kind.take().unwrap_or("Error");
let exc_text = self.error_text(exc);
let msg = if exc_text.starts_with(kind_str) {
+2
View File
@@ -38,6 +38,7 @@ impl Vm {
save_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
pending_exception: None,
pending_error_kind: None,
root_reg_limit: 0,
@@ -95,6 +96,7 @@ impl Vm {
save_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
pending_exception: None,
pending_error_kind: None,
root_reg_limit: 0,
+291 -1
View File
@@ -100,7 +100,8 @@ fn date_value_of() {
#[test]
fn date_to_iso_string() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1, 0, 0, 0).toISOString()").unwrap();
// Parse an explicit ISO UTC string to avoid timezone dependency
let r = eval(&mut vm, "new Date('2020-01-01T00:00:00Z').toISOString()").unwrap();
let s = str_val(&vm, r);
assert!(s.starts_with("2020-01-01"), "got: {s}");
}
@@ -133,3 +134,292 @@ fn date_objects_use_bounded_numeric_coercion() {
let r = eval(&mut vm, "+new Date(0)").unwrap();
assert_eq!(r.as_double(), 0.0);
}
// -- Plan 01 timezone-correct tests --
#[test]
fn date_local_getters_differ_from_utc() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(2020, 6, 15, 12, 0, 0); d.getHours()").unwrap();
let local_hour = r.as_double();
let r = eval(&mut vm, "var d = new Date(2020, 6, 15, 12, 0, 0); d.getUTCHours()").unwrap();
let utc_hour = r.as_double();
// In UTC+0 they are equal (correct), otherwise they differ
if utc_hour != local_hour {
assert_ne!(local_hour, utc_hour, "in non-UTC+0, local vs UTC hours should differ");
}
assert!(local_hour >= 0.0 && local_hour < 24.0);
}
#[test]
fn date_constructor_uses_local_timezone() {
let mut vm = Vm::new();
// getTimezoneOffset = UTC - local; UTC hour = local + offset/60
let r = eval(&mut vm, "var d = new Date(2020, 0, 1, 12, 0, 0); d.getUTCHours()").unwrap();
let utc_hour = r.as_double();
let r = eval(&mut vm, "var d = new Date(2020, 0, 1, 12, 0, 0); d.getTimezoneOffset()").unwrap();
let offset = r.as_double();
let expected_utc = (12.0 + offset / 60.0).rem_euclid(24.0);
assert_eq!(utc_hour, expected_utc, "getUTCHours = local + TZ_offset/60");
}
#[test]
fn date_constructor_defaults_from_current_time() {
let mut vm = Vm::new();
// D-02: missing args default to current time components
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getMonth()").unwrap();
assert_eq!(r.as_double(), 5.0);
let r = eval(&mut vm, "new Date(2020, 5).getDate()").unwrap();
assert!(r.as_double() >= 1.0 && r.as_double() <= 31.0);
}
#[test]
fn date_constructor_truncation() {
let mut vm = Vm::new();
// D-03: ToInteger truncation
let r = eval(&mut vm, "new Date(2024.7, 5.9).getFullYear()").unwrap();
assert_eq!(r.as_double(), 2024.0);
let r = eval(&mut vm, "new Date(2024.7, 5.9).getMonth()").unwrap();
assert_eq!(r.as_double(), 5.0);
}
#[test]
fn date_setters_return_timestamp() {
let mut vm = Vm::new();
// D-05: setters return modified timestamp
let r = eval(&mut vm, "var d = new Date(0); var ts = d.setHours(12); ts == d.getTime() ? 1 : 0").unwrap();
assert_eq!(r.as_int(), 1);
}
#[test]
fn date_invalid_returns_nan() {
let mut vm = Vm::new();
// D-06: Invalid Date getters/setters return NaN
let r = eval(&mut vm, "new Date(NaN).getFullYear()").unwrap();
assert!(r.as_double().is_nan());
let r = eval(&mut vm, "new Date(NaN).setHours(0)").unwrap();
assert!(r.as_double().is_nan());
// D-04: Invalid Date constructor
let r = eval(&mut vm, "new Date(NaN, 0).getTime()").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_get_timezone_offset_uses_date_timestamp() {
let mut vm = Vm::new();
// D-09/D-10: offset uses date's timestamp, not now()
let r = eval(&mut vm, "new Date(0).getTimezoneOffset()").unwrap();
assert!(r.as_double().is_finite());
}
#[test]
fn date_setters_optional_chain() {
let mut vm = Vm::new();
// D-08: setFullYear supports optional month/day params
let r = eval(&mut vm, "var d = new Date(2020, 0, 1); d.setFullYear(2025, 11, 25); d.getFullYear()").unwrap();
assert_eq!(r.as_double(), 2025.0);
let r = eval(&mut vm, "var d = new Date(2020, 0, 1); d.setFullYear(2025, 11, 25); d.getMonth()").unwrap();
assert_eq!(r.as_double(), 11.0);
let r = eval(&mut vm, "var d = new Date(2020, 0, 1); d.setFullYear(2025, 11, 25); d.getDate()").unwrap();
assert_eq!(r.as_double(), 25.0);
}
#[test]
fn date_local_setters_preserve_utc_offset() {
let mut vm = Vm::new();
// setHours changes local hour; verify local hour value
let r = eval(&mut vm, "var d = new Date(2020, 0, 1, 0, 0, 0); d.setHours(12); d.getHours()").unwrap();
assert_eq!(r.as_double(), 12.0, "setHours(12) should make getHours() return 12");
}
// -- Plan 02 new method tests --
#[test]
fn date_set_utc_full_year_works() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1, 0, 0, 0); d.setUTCFullYear(2025); d.getUTCFullYear()",
)
.unwrap();
assert_eq!(r.as_double(), 2025.0);
}
#[test]
fn date_set_utc_hours_optional_chain() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1, 0, 0, 0); d.setUTCHours(23, 59, 59, 999); d.getUTCHours()",
)
.unwrap();
assert_eq!(r.as_double(), 23.0);
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1, 0, 0, 0); d.setUTCHours(23, 59, 59, 999); d.getUTCMinutes()",
)
.unwrap();
assert_eq!(r.as_double(), 59.0);
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1, 0, 0, 0); d.setUTCHours(23, 59, 59, 999); d.getUTCSeconds()",
)
.unwrap();
assert_eq!(r.as_double(), 59.0);
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1, 0, 0, 0); d.setUTCHours(23, 59, 59, 999); d.getUTCMilliseconds()",
)
.unwrap();
assert_eq!(r.as_double(), 999.0);
}
#[test]
fn date_utc_returns_correct_timestamp() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Date.UTC(2020, 0, 1, 12, 0, 0)").unwrap();
assert_eq!(r.as_double(), 1577880000000.0);
}
#[test]
fn date_utc_nan_returns_nan() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Date.UTC(NaN, 0)").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_parse_rfc2822() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Date.parse('15 Jan 2020 12:00:00 GMT')").unwrap();
assert!(!r.as_double().is_nan());
}
#[test]
fn date_parse_slash_format() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Date.parse('2020/01/15')").unwrap();
assert!(!r.as_double().is_nan());
}
#[test]
fn date_get_year_deprecated() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1).getYear()").unwrap();
assert_eq!(r.as_double(), 120.0);
}
#[test]
fn date_set_year_deprecated() {
let mut vm = Vm::new();
// setYear(0-99) adds 1900; setYear(20) → fullYear 1920
let r = eval(&mut vm, "var d = new Date(2020, 0, 1); d.setYear(20); d.getFullYear()").unwrap();
assert_eq!(r.as_double(), 1920.0);
}
#[test]
fn date_to_gmt_string_matches_utc() {
let mut vm = Vm::new();
let r1 = eval(&mut vm, "new Date(0).toGMTString()").unwrap();
let r2 = eval(&mut vm, "new Date(0).toUTCString()").unwrap();
assert_eq!(str_val(&vm, r1), str_val(&vm, r2));
}
#[test]
fn date_locale_strings_not_empty() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(0).toLocaleString()").unwrap();
assert!(r.is_string());
}
#[test]
fn date_set_utc_full_year_returns_timestamp() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); var ts = d.setUTCFullYear(2025); ts == d.getTime() ? 1 : 0").unwrap();
assert_eq!(r.as_int(), 1);
}
#[test]
fn date_set_utc_invalid_returns_nan() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(NaN).setUTCFullYear(2025)").unwrap();
assert!(r.as_double().is_nan());
}
// setYear with no argument must not index a missing argument register (previously panicked);
// a missing / NaN year sets the date value to NaN and returns NaN (B.2.4.2).
#[test]
fn date_set_year_no_arg_returns_nan() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setYear()").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_set_year_no_arg_sets_value_nan() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setYear(); d.valueOf()").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_set_year_nan_from_string() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setYear('not a number')").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_set_year_two_digit_maps_to_1900s() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setYear(95); d.getFullYear()").unwrap();
assert_eq!(r.as_double() as i64, 1995);
}
#[test]
fn date_set_year_four_digit_kept() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setYear(2020); d.getFullYear()").unwrap();
assert_eq!(r.as_double() as i64, 2020);
}
// UTC setters called with no argument previously indexed a missing argument register
// (panic / process abort). A missing primary argument now yields NaN and sets the date NaN.
#[test]
fn date_set_utc_setters_no_arg_return_nan() {
let mut vm = Vm::new();
for call in [
"d.setUTCFullYear()",
"d.setUTCMonth()",
"d.setUTCDate()",
"d.setUTCHours()",
"d.setUTCMinutes()",
"d.setUTCSeconds()",
"d.setUTCMilliseconds()",
] {
let src = format!("var d = new Date(0); {call}");
let r = eval(&mut vm, &src).unwrap();
assert!(r.as_double().is_nan(), "{call} should return NaN, got {r:?}");
}
}
#[test]
fn date_set_utc_no_arg_sets_value_nan() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setUTCMonth(); d.valueOf()").unwrap();
assert!(r.as_double().is_nan());
}
#[test]
fn date_set_utc_full_year_valid_kept() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setUTCFullYear(2025); d.getUTCFullYear()").unwrap();
assert_eq!(r.as_double() as i64, 2025);
}
#[test]
fn date_set_utc_hours_valid_kept() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setUTCHours(13); d.getUTCHours()").unwrap();
assert_eq!(r.as_double() as i64, 13);
}
+223 -1
View File
@@ -1,5 +1,6 @@
use oxide_builtins::error;
use oxide_compiler::compiler::Compiler;
use oxide_types::mem::P;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
@@ -115,7 +116,7 @@ fn error_name_is_string_property() {
let err = error::error_constructor(&mut vm, &[0u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
let n = obj.prop_count();
assert!(n >= 2);
assert_eq!(n, 0);
}
#[test]
@@ -142,3 +143,224 @@ fn error_subtype_constructors_produce_named_objects() {
assert_eq!(format!("{}", eval("new TypeError('boom').name == 'TypeError'").unwrap()), "true");
assert_eq!(format!("{}", eval("new SyntaxError('boom').name == 'SyntaxError'").unwrap()), "true");
}
// New tests for prototype properties + constructor fixes
#[test]
fn error_prototype_has_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some(), "Error.prototype should have 'name' property");
}
#[test]
fn error_prototype_has_message() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let msg_si = vm.kernel_core().perm_interner().intern("message").0;
let proto_ptr = P::as_ptr(&bw.error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), msg_si);
assert!(si.is_some(), "Error.prototype should have 'message' property");
}
#[test]
fn type_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.type_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some(), "TypeError.prototype should have 'name'");
}
#[test]
fn reference_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.reference_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn range_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.range_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn syntax_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.syntax_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn uri_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.uri_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn eval_error_prototype_name() {
let vm = make_vm();
let bw = vm.session().builtin_world();
let name_si = vm.kernel_core().perm_interner().intern("name").0;
let proto_ptr = P::as_ptr(&bw.eval_error_proto) as *mut oxide_types::object::JsObject;
let si = vm
.kernel_core()
.shape_forge()
.lookup_position(unsafe { &*proto_ptr }.shape_id(), name_si);
assert!(si.is_some());
}
#[test]
fn error_constructor_no_own_props() {
let mut vm = make_vm();
let err = error::error_constructor(&mut vm, &[0u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 0);
}
#[test]
fn error_constructor_with_message() {
let mut vm = make_vm();
let msg = vm.new_string("boom");
vm.set_reg(1, msg);
let err = error::error_constructor(&mut vm, &[0u8, 1u8]).unwrap();
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 1);
}
#[test]
fn create_type_error_no_own_name() {
let mut vm = make_vm();
let err = error::create_type_error(&mut vm, "something broken");
let obj = unsafe { &*err.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 1);
}
// Plan 02: toString tests
#[test]
fn error_to_string_basic() {
let result = eval("new Error('test').toString()").unwrap();
assert!(result.is_string());
}
#[test]
fn error_to_string_empty_message() {
let result = eval("new Error().toString()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("Error".to_string()));
}
#[test]
fn error_to_string_type_error() {
let result = eval("new TypeError('bad').toString()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("TypeError: bad".to_string()));
}
#[test]
fn error_to_string_name_only() {
let result = eval("Error.prototype.toString.call({name: 'E'})").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("E".to_string()));
}
#[test]
fn error_to_string_non_object_throws() {
let mut vm = make_vm();
vm.set_reg(0, JsValue::int(42));
let result = error::error_to_string(&mut vm, &[0u8]);
match result {
oxide_runtime_api::NativeResult::Err(_) => {}
_ => panic!("expected Err, got Ok or TailCall"),
}
}
// format_error_message tests
#[test]
fn format_error_message_both() {
let result = oxide_runtime_api::format_error_message("TypeError", "bad arg");
assert_eq!(result, "TypeError: bad arg");
}
#[test]
fn format_error_message_empty_msg() {
let result = oxide_runtime_api::format_error_message("Error", "");
assert_eq!(result, "Error");
}
#[test]
fn format_error_message_empty_name() {
let result = oxide_runtime_api::format_error_message("", "msg");
assert_eq!(result, "msg");
}
// stack tests
#[test]
fn error_stack_is_string() {
let result = eval("typeof new Error().stack()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result);
assert_eq!(s, Some("string".to_string()));
}
#[test]
fn error_stack_starts_with_header() {
let result = eval("new Error().stack()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result).unwrap();
assert!(s.starts_with("Error"), "stack should start with 'Error', got: {}", s);
}
#[test]
fn error_stack_frame_format() {
let result = eval("(function foo() { return new Error('boom').stack(); })()").unwrap();
let vm = make_vm();
let s = vm.lookup_str(result).unwrap();
assert!(s.contains(" at "), "stack should have 4-space indent, got: {}", s);
}
@@ -267,3 +267,259 @@ fn string_call_conversion_stays_primitive() {
assert!(v.is_string());
assert_eq!(to_str(&vm, v), "123");
}
// ── Plan 01: replace tests ──
#[test]
fn string_replace_non_global_first_only() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'aaa'.replace(/a/, 'b')").unwrap();
assert_eq!(to_str(&vm, s), "baa");
}
#[test]
fn string_replace_global_all() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'aaa'.replace(/a/g, 'b')").unwrap();
assert_eq!(to_str(&vm, s), "bbb");
}
#[test]
fn string_replace_function_replacer_match() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.replace(/l/, function(m){return m.toUpperCase()})").unwrap();
assert_eq!(to_str(&vm, s), "heLlo");
}
#[test]
fn string_replace_function_replacer_offset() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'abc'.replace(/b/, function(m,o){return o})").unwrap();
assert_eq!(to_str(&vm, s), "a1c");
}
#[test]
fn string_replace_string_pattern() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.replace('l', 'L')").unwrap();
assert_eq!(to_str(&vm, s), "heLlo");
}
// ── Plan 02: split tests ──
#[test]
fn string_split_regex_capture_groups() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'a1b2c'.split(/(\\d)/)").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 5);
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "1");
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "b");
assert_eq!(to_str(&vm, obj.get_prop_at(3)), "2");
assert_eq!(to_str(&vm, obj.get_prop_at(4)), "c");
}
#[test]
fn string_split_regex_capture_with_limit() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'a1b2c'.split(/(\\d)/, 3)").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 3);
}
#[test]
fn string_split_regex_no_capture() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'a,b,c'.split(/,/)").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 3);
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "b");
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "c");
}
#[test]
fn string_split_string_separator() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'a,b,c'.split(',')").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 3);
}
// ── Plan 03: match tests ──
#[test]
fn string_match_non_global_captures() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'12-34'.match(/(\\d+)-(\\d+)/)").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 3);
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "12-34");
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "12");
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "34");
}
#[test]
fn string_match_no_match_null() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'abc'.match(/x/)").unwrap();
assert!(result.is_null());
}
#[test]
fn string_match_global_flat() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'aba'.match(/a/g)").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 2);
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "a");
}
#[test]
fn string_match_string_pattern() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.match('ll')").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 1);
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "ll");
}
// ── Plan 04: matchAll tests ──
#[test]
fn string_match_all_returns_iterator() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'aba'.matchAll(/a/g)").unwrap();
assert!(result.is_object());
}
#[test]
fn string_match_all_next_returns_match() {
let mut vm = Vm::new();
let result = eval(&mut vm, "var it = 'ab'.matchAll(/a/g); it.next()").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
let done = obj.get_prop_at(1).as_bool();
assert!(!done);
assert!(obj.get_prop_at(0).is_object());
}
#[test]
fn string_match_all_next_exhausted() {
let mut vm = Vm::new();
let result = eval(&mut vm, "var it = 'a'.matchAll(/x/g); it.next(); it.next()").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
let done = obj.get_prop_at(1).as_bool();
assert!(done);
}
// ── Plan 05: substring tests ──
#[test]
fn string_substring_nan_index() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substring(NaN, 2)").unwrap();
assert_eq!(to_str(&vm, s), "he");
}
#[test]
fn string_substring_negative_index() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substring(-3, 2)").unwrap();
assert_eq!(to_str(&vm, s), "he");
}
#[test]
fn string_substring_swap() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substring(3, 1)").unwrap();
assert_eq!(to_str(&vm, s), "el");
}
// ── Plan 06: substr + at tests ──
#[test]
fn string_substr_positive() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substr(1, 3)").unwrap();
assert_eq!(to_str(&vm, s), "ell");
}
#[test]
fn string_substr_negative_start() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substr(-3, 2)").unwrap();
assert_eq!(to_str(&vm, s), "ll");
}
#[test]
fn string_substr_negative_length() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.substr(1, -1)").unwrap();
assert_eq!(to_str(&vm, s), "");
}
#[test]
fn string_at_positive() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.at(1)").unwrap();
assert_eq!(to_str(&vm, s), "e");
}
#[test]
fn string_at_negative() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.at(-1)").unwrap();
assert_eq!(to_str(&vm, s), "o");
}
#[test]
fn string_at_out_of_range() {
let mut vm = Vm::new();
let s = eval(&mut vm, "'hello'.at(10)").unwrap();
assert!(s.is_undefined());
}
// ── Plan 07: lastIndexOf tests ──
#[test]
fn string_last_index_of_basic() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.lastIndexOf('l')").unwrap();
assert_eq!(result.as_int(), 3);
}
#[test]
fn string_last_index_of_not_found() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.lastIndexOf('x')").unwrap();
assert_eq!(result.as_int(), -1);
}
#[test]
fn string_last_index_of_empty_string() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.lastIndexOf('')").unwrap();
assert_eq!(result.as_int(), 5);
}
#[test]
fn string_last_index_of_with_position() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.lastIndexOf('l', 2)").unwrap();
assert_eq!(result.as_int(), 2);
}
#[test]
fn string_last_index_of_empty_with_position() {
let mut vm = Vm::new();
let result = eval(&mut vm, "'hello'.lastIndexOf('', 3)").unwrap();
assert_eq!(result.as_int(), 3);
}
@@ -155,3 +155,53 @@ fn symbol_key_for_non_symbol_throws_type_error() {
let result = eval(&mut vm, "try { Symbol.keyFor(42) } catch (e) { e instanceof TypeError }").unwrap();
assert!(result.as_bool());
}
// --- Symbol.hasInstance ---
#[test]
fn symbol_has_instance_exists() {
let mut vm = Vm::new();
let result = eval(&mut vm, "typeof Symbol.hasInstance").unwrap();
assert_eq!(to_str(&vm, result), "object");
}
#[test]
fn has_instance_overrides_instanceof() {
let mut vm = Vm::new();
let source = r#"
class C { static [Symbol.hasInstance](v) { return typeof v === 'string'; } }
'hello' instanceof C
"#;
let result = eval(&mut vm, source).unwrap();
assert!(result.as_bool());
}
#[test]
fn has_instance_non_callable_falls_through() {
let mut vm = Vm::new();
let source = r#"
class D {}
D[Symbol.hasInstance] = 42;
({} instanceof D) === false
"#;
let result = eval(&mut vm, source).unwrap();
assert!(result.as_bool());
}
#[test]
fn has_instance_coerces_return_to_boolean() {
let mut vm = Vm::new();
let source = r#"
class C { static [Symbol.hasInstance](v) { return 'truthy'; } }
'anything' instanceof C
"#;
let result = eval(&mut vm, source).unwrap();
assert!(result.as_bool());
}
#[test]
fn has_instance_preserves_ordinary_has_instance() {
let mut vm = Vm::new();
let result = eval(&mut vm, "[] instanceof Array").unwrap();
assert!(result.as_bool());
}
+107
View File
@@ -112,3 +112,110 @@ fn primitive_property_write_throws_clean_type_error() {
);
}
}
// --- Phase 14.0.1: type-conversion infrastructure (ENG-13) ---
// SC-1..SC-8 from ROADMAP. eval() returns the Display of the final value;
// every assertion below targets a bool/number result to avoid string-quoting
// ambiguity.
// SC-1 ToPrimitive: boxed Number unwraps via valueOf (was "NaN" before).
#[test]
fn to_primitive_number_boxed() {
assert_eq!(eval("+new Number(42)"), "42");
}
// SC-1 ToPrimitive: boxed String coerces to its primitive in == (object vs string).
#[test]
fn to_primitive_string_boxed() {
assert_eq!(eval("new String('x') == 'x'"), "true");
}
// SC-1 ToPrimitive: obj[Symbol.toPrimitive] is consulted (callback runs, returns 7).
#[test]
fn to_primitive_symbol_hint_invoked() {
assert_eq!(eval("var o = {}; o[Symbol.toPrimitive] = function (h) { return 7; }; +o"), "7");
}
// SC-2 ToNumber: to_number_full throws TypeError on a Symbol (per D-02).
// Note: the unary-plus operator path uses coerce_number_bounded (no symbol
// throw) so `+Symbol()` yields NaN at the operator level; see test below.
#[test]
fn to_number_full_symbol_throws() {
let mut vm = Vm::new();
let sym = JsValue::symbol(0);
let result = coercion::to_number_full(sym, &mut vm);
assert!(result.is_err(), "to_number_full(Symbol) must error, got {result:?}");
assert!(result.unwrap_err().contains("Symbol"), "expected a Symbol TypeError message");
}
// SC-2/SC-7 operator-level reality: +Symbol is NaN (symbol-throw not wired into arithmetic).
#[test]
fn unary_plus_symbol_is_nan() {
assert_eq!(eval("+Symbol('x')"), "NaN");
}
// SC-3 Abstract Equality: string vs number coerces.
#[test]
fn abstract_eq_string_number() {
assert_eq!(eval("'1' == 1"), "true");
}
// SC-3 Abstract Equality: bool->number recursion + string->number.
#[test]
fn abstract_eq_bool_number() {
assert_eq!(eval("'1' == true"), "true");
}
// SC-3 Abstract Equality: object vs number via ToPrimitive ([42] -> "42" -> 42).
#[test]
fn abstract_eq_object_primitive() {
assert_eq!(eval("[42] == 42"), "true");
}
// SC-3 Abstract Equality: null == undefined (regression guard, spec step 2-3).
#[test]
fn abstract_eq_null_undefined() {
assert_eq!(eval("null == undefined"), "true");
}
// SC-3 Abstract Equality: null == 0 is false (spec falls through to step 14).
#[test]
fn abstract_eq_null_zero_mismatch() {
assert_eq!(eval("null == 0"), "false");
}
// SC-3 Abstract Equality: same-type delegates to strict equality.
#[test]
fn abstract_eq_same_type_string() {
assert_eq!(eval("'abc' == 'abc'"), "true");
}
// SC-4 Strict Equality: +0 === -0 is true (verify-only; already correct).
#[test]
fn strict_eq_signed_zero_eval() {
assert_eq!(eval("+0 === -0"), "true");
}
// SC-4 Strict Equality: NaN === NaN is false (verify-only; already correct).
#[test]
fn strict_eq_nan_eval() {
assert_eq!(eval("NaN === NaN"), "false");
}
// SC-5 Arithmetic +: object operand coerces via valueOf.
#[test]
fn add_object_valueof_coercion() {
assert_eq!(eval("var o = {valueOf: function () { return 42; }}; o + 1"), "43");
}
// SC-6 Relational: object operands coerce via ToPrimitive then compare as strings.
#[test]
fn relational_object_coercion() {
assert_eq!(eval("[3] > [2]"), "true");
}
// SC-1/D-02 Date: Date object coerces to its timestamp in == (default hint -> valueOf).
#[test]
fn date_object_equals_timestamp() {
assert_eq!(eval("new Date(0) == 0"), "true");
}
@@ -0,0 +1,63 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Destructured parameter followed by a loop body: the counter must count the parameter
// destructuring prologue or the loop's back-jump lands at the wrong PC.
#[test]
fn param_array_pattern_with_loop_body() {
assert_eq!(eval("function f([a]){var s=0; while(a>0){s=s+1;a=a-1;} return s;} f([3])"), "3");
}
#[test]
fn param_array_pattern_default_element() {
assert_eq!(eval("function f([a=5]){return a;} f([])"), "5");
}
// Array assignment target with a default element followed by a loop.
#[test]
fn array_assignment_default_element_then_loop() {
assert_eq!(eval("var a,c=0; [a=5]=[]; while(c<a){c=c+1;} c"), "5");
}
// Nested array assignment target element.
#[test]
fn array_assignment_nested_element() {
assert_eq!(eval("var a,b; [[a],b]=[[1],2]; a*10+b"), "12");
}
// Destructuring catch param followed by a loop: the counter must not count a STORE_VAR the
// emitter does not emit for a pattern catch binding.
#[test]
fn destructuring_catch_param_then_loop() {
assert_eq!(eval("var c=0; try{throw 1;}catch({e}){} while(c<3){c=c+1;} c"), "3");
}
// Regression: for-init declaration without an initializer followed by loop iterations.
#[test]
fn for_init_no_initializer_declarator() {
assert_eq!(eval("var c=0; for(var i; c<3; c=c+1){} c"), "3");
}
// Identifier element array assignment stays correct (simple path unchanged).
#[test]
fn array_assignment_identifier_elements() {
assert_eq!(eval("var a,b; [a,b]=[7,8]; a*10+b"), "78");
}
@@ -0,0 +1,93 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// A class body must not drift the instruction count of code that follows it. Each repro
// places a bounded loop after the class so a wrong count corrupts the loop's jump target.
#[test]
fn non_static_private_method_then_loop() {
assert_eq!(eval("class C{#m(){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3");
}
#[test]
fn static_private_method_then_loop() {
assert_eq!(eval("class C{static #m(){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3");
}
// Multi-word computed key — a single-identifier key happens to be one word and would not
// expose the drift.
#[test]
fn computed_method_key_then_loop() {
assert_eq!(eval("class C{[\"x\"+\"y\"](){return 1;}} var c=0; while(c<3){c=c+1;} c"), "3");
}
#[test]
fn computed_static_field_key_then_loop() {
assert_eq!(eval("class C{static [\"a\"+\"b\"]=1;} var c=0; while(c<3){c=c+1;} c"), "3");
}
// Derived constructor with an instance field and a loop after super(): the field code is
// injected after SUPER_CALL, so the loop's jump target must account for it.
#[test]
fn derived_ctor_field_then_post_super_loop() {
assert_eq!(
eval("class A{} class B extends A{ x=2; constructor(){super(); this.r=0; while(this.r<3){this.r=this.r+1;}}} new B().r"),
"3"
);
}
#[test]
fn derived_ctor_field_value_initialized() {
assert_eq!(eval("class A{} class B extends A{ x=5; constructor(){super();} } new B().x"), "5");
}
#[test]
fn derived_ctor_statement_before_super() {
assert_eq!(
eval("class A{constructor(){this.a=1;}} class B extends A{ y=2; constructor(){var z=0; super(); while(z<3){z=z+1;} this.z=z;} } new B().z"),
"3"
);
}
// Non-derived class with an instance field and a ctor loop stays correct (unchanged path).
#[test]
fn non_derived_ctor_field_then_loop() {
assert_eq!(
eval("class C{ x=1; constructor(){this.r=0; while(this.r<3){this.r=this.r+1;}} } new C().r"),
"3"
);
}
// Behavioral regressions: methods/getters/private calls still work.
#[test]
fn plain_method_call_regression() {
assert_eq!(eval("class C{m(){return 9;}} new C().m()"), "9");
}
#[test]
fn private_method_call_regression() {
assert_eq!(eval("class C{#m(){return 7;} run(){return this.#m();}} new C().run()"), "7");
}
#[test]
fn getter_regression() {
assert_eq!(eval("class C{get x(){return 5;}} new C().x"), "5");
}
@@ -0,0 +1,90 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Non-simple `??` (side-effecting operands) followed by a label-bearing construct must not
// shift the downstream label id (which produced `Label ... not found` at emit time).
#[test]
fn coalesce_nonsimple_then_if() {
assert_eq!(
eval("function a(){return null;} function b(){return 2;} var v=a()??b(); if(v>0){v=v+10;} v"),
"12"
);
}
#[test]
fn coalesce_nonsimple_then_while() {
assert_eq!(
eval("function a(){return null;} function b(){return 3;} var v=a()??b(); var c=0; while(c<v){c=c+1;} c"),
"3"
);
}
#[test]
fn coalesce_nonsimple_then_for() {
assert_eq!(
eval("function a(){return null;} var v=a()??4; var s=0; for(var i=0;i<v;i=i+1){s=s+i;} s"),
"6"
);
}
#[test]
fn coalesce_nonsimple_short_circuits_on_value() {
// 0 is not nullish, so `a() ?? b()` is 0 and the loop never runs.
assert_eq!(
eval("function a(){return 0;} function b(){return 9;} var v=a()??b(); var c=0; while(c<v){c=c+1;} c"),
"0"
);
}
// Static member compound assignment must not over-count vs the emitter (the extra +1 drifted
// the following loop's jump target).
#[test]
fn static_member_compound_assign_then_loop() {
assert_eq!(eval("var o={x:1}; o.x+=2; var c=0; while(c<3){c=c+1;} c"), "3");
}
#[test]
fn static_member_compound_assign_in_loop() {
assert_eq!(eval("var o={x:0}; var c=0; while(o.x<3){o.x+=1; c=c+1;} c"), "3");
}
// Regressions: simple coalesce, nested coalesce, And/Or, compound member value, plain assign.
#[test]
fn simple_coalesce_regression() {
assert_eq!(eval("var x = null ?? 5; x"), "5");
}
#[test]
fn nested_coalesce_regression() {
assert_eq!(eval("function a(){return null;} var v = a() ?? a() ?? 7; v"), "7");
}
#[test]
fn logical_and_nonsimple_regression() {
assert_eq!(eval("function a(){return 1;} function b(){return 2;} var v=a()&&b(); if(v>0){} v"), "2");
}
#[test]
fn compound_member_value_regression() {
assert_eq!(eval("var o={x:10}; o.x-=3; o.x"), "7");
}
@@ -0,0 +1,112 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
fn eval_str(source: &str) -> Result<JsValue, String> {
eval(source).map(|(_vm, v)| v)
}
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
match result {
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
}
}
// -- Partial writable update preserves existing value --
#[test]
fn define_property_partial_writable_preserves_value() {
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{writable:false}); obj.x").unwrap();
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
}
// -- Mixed descriptor (value+get) throws TypeError --
#[test]
fn define_property_mixed_value_get_throws() {
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{value:1,get:function(){}});");
assert_err_contains(result, "mix");
}
// -- Mixed descriptor (writable+set) throws TypeError --
#[test]
fn define_property_mixed_writable_set_throws() {
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{writable:true,set:function(){}});");
assert_err_contains(result, "mix");
}
// -- Partial enumerable-only update preserves value --
#[test]
fn define_property_partial_enumerable_only_preserves_value() {
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{enumerable:true}); obj.x").unwrap();
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
}
// -- getOwnPropertyDescriptor returns data fields (no get/set) --
#[test]
fn get_own_property_descriptor_data_fields() {
let r = eval_str(
"var obj={x:1}; var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.value,d.writable,d.enumerable,d.configurable,d.get,d.set]",
)
.unwrap();
let obj = unsafe { &*r.as_js_object_ptr() };
let v = obj.get_prop_at(0);
assert!(v.is_int() && v.as_int() == 1, "value should be 1, got {:?}", v);
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "writable should be true");
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
assert!(
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
"configurable should be true"
);
assert!(obj.get_prop_at(4).is_undefined(), "get should be undefined");
assert!(obj.get_prop_at(5).is_undefined(), "set should be undefined");
}
// -- getOwnPropertyDescriptor returns accessor fields (no value/writable) --
#[test]
fn get_own_property_descriptor_accessor_fields() {
let r = eval_str(
"var obj={}; Object.defineProperty(obj,'x',{get:function(){return 1;},enumerable:true,configurable:true}); var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.get!==undefined,d.set===undefined,d.enumerable,d.configurable,d.value===undefined,d.writable===undefined]",
)
.unwrap();
let obj = unsafe { &*r.as_js_object_ptr() };
assert!(obj.get_prop_at(0).is_bool() && obj.get_prop_at(0).as_bool(), "get should be defined");
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "set should be undefined");
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
assert!(
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
"configurable should be true"
);
assert!(obj.get_prop_at(4).is_bool() && obj.get_prop_at(4).as_bool(), "value should be undefined");
assert!(
obj.get_prop_at(5).is_bool() && obj.get_prop_at(5).as_bool(),
"writable should be undefined"
);
}
// -- New property without value or accessor defaults to data descriptor (ES spec behavior) --
#[test]
fn define_property_new_attributes_only_defaults_value_undefined() {
let r = eval_str("var obj={}; Object.defineProperty(obj,'x',{enumerable:true}); obj.x").unwrap();
assert!(
r.is_undefined(),
"new prop with only enumerable should default value to undefined, got {:?}",
r
);
}
// -- Partial configurable on existing property preserves value --
#[test]
fn define_property_partial_configurable_preserves_value() {
let r = eval_str("var obj={}; obj.x=1; Object.defineProperty(obj,'x',{configurable:false}); [obj.x]").unwrap();
let arr = unsafe { &*r.as_js_object_ptr() };
assert!(arr.get_prop_at(0).is_int() && arr.get_prop_at(0).as_int() == 1, "value should be 1");
}
@@ -87,3 +87,36 @@ fn function_and_method_parameter_destructuring() {
.unwrap();
assert_num(result, 10.0);
}
// Regression: a for-of whose assignment target has a NESTED rest pattern (the rest
// element is itself a pattern, e.g. `[...[x]]`) used to loop forever / error, because the
// counter undercounted the rest target's instructions vs the emitter, corrupting the loop's
// jump offsets. (Object rest with a nested target is a syntax error per spec, so untested.)
#[test]
fn for_of_assignment_nested_rest_target_runs_once() {
let mut vm = Vm::new();
let result = eval(&mut vm, "var x; var c=0; for([...[x]] of [[1,2,3]]){ c=c+1; } c").unwrap();
assert_num(result, 1.0);
}
#[test]
fn for_of_assignment_nested_rest_empty_body_binds_inner() {
let mut vm = Vm::new();
let result = eval(&mut vm, "var x; for([...[x]] of [[1,2,3]]){} x").unwrap();
assert_num(result, 1.0);
}
#[test]
fn for_of_assignment_nested_rest_with_leading_element() {
let mut vm = Vm::new();
// outer iterates once; a=1, rest=[2,3,4], then [x,y] from rest -> x=2, y=3
let result = eval(&mut vm, "var a,x,y; for([a,...[x,y]] of [[1,2,3,4]]){} a*100+x*10+y").unwrap();
assert_num(result, 123.0);
}
#[test]
fn standalone_nested_rest_assignment() {
let mut vm = Vm::new();
let result = eval(&mut vm, "var x; [...[x]]=[1,2,3]; x").unwrap();
assert_num(result, 1.0);
}
@@ -0,0 +1,58 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
#[test]
fn for_in_null_empty_loop() {
assert_eq!(eval("var r=0;for(var k in null){r=r+1;}r"), "0", "for-in null iterates zero times");
}
#[test]
fn for_in_undefined_empty_loop() {
assert_eq!(
eval("var r=0;for(var k in undefined){r=r+1;}r"),
"0",
"for-in undefined iterates zero times"
);
}
#[test]
fn for_in_null_does_not_execute_body() {
assert_eq!(
eval("var hit=false;for(var k in null){hit=true;}hit"),
"false",
"for-in null skips the body"
);
}
#[test]
fn for_in_null_does_not_throw() {
// Must NOT produce a "vm error: ..." — a quiet empty loop, then a sentinel value.
assert_eq!(eval("for(var k in null){} 7"), "7", "for-in null must not throw");
}
#[test]
fn for_in_number_primitive_currently_throws() {
// Interim behavior: ToObject coercion for non-null/undefined primitives is not
// implemented yet, so a number right-hand side still throws TypeError. When
// ToObject lands this becomes an empty loop and this assertion should change.
let out = eval("for(var k in 42){}");
assert!(out.contains("TypeError"), "expected TypeError for number primitive, got: {out}");
}
@@ -0,0 +1,82 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Order is asserted via a position map built inside JS, so the comparison
// returns a boolean (JsValue Display only reveals number/bool, not string
// contents). This is also tolerant of any inherited enumerable keys the
// engine appends after the object's own keys.
#[test]
fn for_in_ordering_integer_indices_first() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {b:2,a:1,'2':3,'1':4}){pos[x]=i;i=i+1;}\
pos['1']<pos['2'] && pos['2']<pos['b'] && pos['b']<pos['a']"
),
"true",
"integer indices ascending, then string keys in insertion order"
);
}
#[test]
fn for_in_ordering_string_only_insertion_order() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {c:1,b:2,a:3}){pos[x]=i;i=i+1;}\
pos['c']<pos['b'] && pos['b']<pos['a']"
),
"true",
"string-only keys keep insertion order"
);
}
#[test]
fn for_in_ordering_mixed_boundary() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {a:1,'0':2,b:3}){pos[x]=i;i=i+1;}\
pos['0']<pos['a'] && pos['a']<pos['b']"
),
"true",
"integer index '0' enumerates before string keys"
);
}
#[test]
fn for_in_ordering_integer_indices_are_numeric_not_lexicographic() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {'10':1,'2':2,'1':3}){pos[x]=i;i=i+1;}\
pos['1']<pos['2'] && pos['2']<pos['10']"
),
"true",
"index order is 1,2,10 (numeric) not 1,10,2 (lexicographic)"
);
}
#[test]
fn for_in_still_enumerates_own_keys() {
assert_eq!(
eval("var c=0;for(var k in {a:1,b:2}){c=c+1;}c>=2"),
"true",
"for-in still enumerates the object's own enumerable keys"
);
}
@@ -0,0 +1,126 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Spec (ECMA-262 §13.7.5): next()-throw propagates the ORIGINAL value WITHOUT calling
// return(); IteratorClose (return()) runs only on BODY abrupt completion (throw/break/return).
// return() is observed via globalThis (a function CAN read/write globalThis properties;
// it cannot write a plain outer `var` in this engine).
#[test]
fn for_of_next_throw_is_catchable_with_original_type() {
assert_eq!(
eval(
"try{for(var v of {next:function(){throw new TypeError('boom');}}){}}\
catch(e){e.name==='TypeError' && e.message==='boom'}"
),
"true",
"next() throw catchable with original error type+message"
);
}
#[test]
fn for_of_next_throw_preserves_bare_thrown_value() {
assert_eq!(
eval("try{for(var v of {next:function(){throw 7;}}){}}catch(e){e===7}"),
"true",
"a bare thrown value is preserved, not re-wrapped"
);
}
#[test]
fn for_of_next_throw_does_not_call_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){throw new Error('x');},return:function(){globalThis.r=true;return {};}};\
try{for(var v of it){}}catch(e){}globalThis.r===false"
),
"true",
"return() must NOT be called when next() throws"
);
}
#[test]
fn for_of_next_throw_without_return_still_propagates() {
assert_eq!(
eval("try{for(var v of {next:function(){throw new TypeError('e');}}){}}catch(e){e instanceof TypeError}"),
"true"
);
}
#[test]
fn for_of_body_throw_calls_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){return {value:1,done:false};},return:function(){globalThis.r=true;return {};}};\
try{for(var v of it){throw new Error('boom');}}catch(e){}globalThis.r===true"
),
"true",
"body throw calls return() (IteratorClose)"
);
}
#[test]
fn for_of_body_throw_propagates_body_error() {
assert_eq!(
eval(
"var it={next:function(){return {value:1,done:false};},return:function(){return {};}};\
try{for(var v of it){throw new Error('boom');}}catch(e){e.message==='boom'}"
),
"true"
);
}
#[test]
fn for_of_body_throw_return_also_throws_body_error_wins() {
assert_eq!(
eval(
"var it={next:function(){return {value:1,done:false};},return:function(){throw new Error('B');}};\
try{for(var v of it){throw new Error('A');}}catch(e){e.message==='A'}"
),
"true",
"when return() also throws, the body's original error wins"
);
}
#[test]
fn for_of_break_calls_return() {
assert_eq!(
eval(
"globalThis.r=false;\
var it={next:function(){return {value:1,done:false};},return:function(){globalThis.r=true;return {};}};\
for(var v of it){break;}globalThis.r===true"
),
"true",
"break calls return() (via FOR_OF_CLOSE)"
);
}
#[test]
fn for_of_array_regression_still_iterates() {
assert_eq!(eval("var r=0;for(var v of [1,2,3]){r=r+v;}r===6"), "true");
}
#[test]
fn for_of_string_regression_still_iterates() {
assert_eq!(eval("var r=0;for(var c of 'abc'){r=r+1;}r===3"), "true");
}
@@ -0,0 +1,119 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Assertions stay inside JS and resolve to a boolean/number, because JsValue Display
// reveals numbers/bools but not string/object contents.
// ---- for-of loop over Map/Set (default iterators) ----
#[test]
fn for_of_map_yields_key_value_entries() {
assert_eq!(
eval("var m=new Map();m.set('a',1);m.set('b',2);var r='';for(var e of m){r=r+e[0]+e[1];}r==='a1b2'"),
"true",
"for-of over Map yields [key, value] entries in insertion order"
);
}
#[test]
fn for_of_set_yields_values() {
assert_eq!(
eval("var s=new Set();s.add(10);s.add(20);var r=0;for(var v of s){r=r+v;}r===30"),
"true",
"for-of over Set yields values"
);
}
#[test]
fn for_of_empty_map_runs_zero_times() {
assert_eq!(eval("var m=new Map();var r=0;for(var e of m){r=r+1;}r===0"), "true");
}
#[test]
fn for_of_empty_set_runs_zero_times() {
assert_eq!(eval("var s=new Set();var r=0;for(var v of s){r=r+1;}r===0"), "true");
}
// ---- Map.prototype.{entries,values,keys} ----
#[test]
fn map_values_iterator_yields_values() {
assert_eq!(
eval("var m=new Map();m.set('a',5);var it=m.values();var r=it.next();r.value===5 && r.done===false"),
"true"
);
}
#[test]
fn map_entries_iterator_yields_pairs() {
assert_eq!(
eval("var m=new Map();m.set('a',5);var e=m.entries().next().value;e[0]==='a' && e[1]===5"),
"true"
);
}
#[test]
fn map_keys_iterator_yields_keys() {
assert_eq!(eval("var m=new Map();m.set('a',5);m.keys().next().value==='a'"), "true");
}
#[test]
fn map_iterator_reports_done_at_end() {
assert_eq!(eval("var m=new Map();var it=m.values();it.next().done===true"), "true");
}
// ---- Set.prototype.{entries,values,keys} ----
#[test]
fn set_values_iterator_yields_values() {
assert_eq!(eval("var s=new Set();s.add(7);s.values().next().value===7"), "true");
}
#[test]
fn set_entries_iterator_yields_value_value_pairs() {
assert_eq!(
eval("var s=new Set();s.add(7);var e=s.entries().next().value;e[0]===7 && e[1]===7"),
"true"
);
}
#[test]
fn set_keys_iterator_is_values_alias() {
assert_eq!(eval("var s=new Set();s.add(7);s.keys().next().value===7"), "true");
}
// ---- regression: existing iterables still work; non-iterables still throw ----
#[test]
fn for_of_array_still_iterates() {
assert_eq!(eval("var r=0;for(var v of [1,2,3]){r=r+v;}r===6"), "true");
}
#[test]
fn for_of_string_still_iterates() {
assert_eq!(eval("var r=0;for(var c of 'abc'){r=r+1;}r===3"), "true");
}
#[test]
fn for_of_non_iterable_throws() {
let out = eval("for(var v of 42){}");
assert!(out.contains("TypeError"), "expected TypeError for non-iterable, got: {out}");
}
@@ -0,0 +1,95 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
vm.run(&module)
}
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
let source = lines.join("; ");
eval(&source)
}
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
match result {
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
}
}
// -- Delete configurable property succeeds --
#[test]
fn delete_configurable_property_succeeds_and_returns_true() {
let r = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, configurable: true})",
"delete obj.x",
])
.unwrap();
assert!(r.is_bool() && r.as_bool(), "delete configurable should return true, got {:?}", r);
}
// -- Delete non-configurable property throws --
#[test]
fn delete_non_configurable_throws_type_error() {
let result = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, configurable: false})",
"delete obj.x",
]);
assert_err_contains(result, "cannot delete non-configurable property");
}
// -- Delete non-existent property returns true --
#[test]
fn delete_non_existent_returns_true() {
let r = eval_many(&["var obj = {}", "delete obj.x"]).unwrap();
assert!(r.is_bool() && r.as_bool(), "delete non-existent should return true, got {:?}", r);
}
// -- Delete default (no explicit defineProperty) configurable succeeds --
#[test]
fn delete_default_configurable_succeeds() {
let r = eval_many(&["var obj = {}", "obj.x = 1", "delete obj.x"]).unwrap();
assert!(r.is_bool() && r.as_bool(), "delete default configurable should return true, got {:?}", r);
}
// -- Delete non-configurable proto property on child: child doesn't own it, so returns true --
#[test]
fn delete_non_configurable_proto_property_on_child_returns_true() {
let r = eval_many(&[
"var proto = {}",
"proto.x = 1",
"Object.defineProperty(proto, 'x', {configurable: false})",
"var child = Object.create(proto)",
"delete child.x",
])
.unwrap();
assert!(
r.is_bool() && r.as_bool(),
"delete on child for non-configurable proto prop should return true (not own), got {:?}",
r
);
}
// -- Delete configurable proto-level property through child --
#[test]
fn delete_configurable_proto_property_on_child_returns_true() {
let r = eval_many(&[
"var proto = {}",
"Object.defineProperty(proto, 'x', {value: 1, configurable: true, enumerable: true})",
"var child = Object.create(proto)",
"delete child.x",
])
.unwrap();
assert!(
r.is_bool() && r.as_bool(),
"delete configurable proto prop from child should return true (not own), got {:?}",
r
);
}
@@ -0,0 +1,112 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
fn eval_str(source: &str) -> Result<JsValue, String> {
eval(source).map(|(_vm, v)| v)
}
fn keys_to_strings(result: JsValue) -> Vec<String> {
let obj = unsafe { &*result.as_js_object_ptr() };
let len = obj.prop_count();
let mut keys = Vec::new();
for i in 0..len {
let v = obj.get_prop_at(i);
if v.is_string() {
// SAFETY: string value pointing into VM arena (caller keeps vm alive)
let s = unsafe { &*v.as_string_ptr() };
keys.push(s.data.clone());
}
}
keys
}
// -- Integer keys in ascending order --
#[test]
fn keys_integer_order_ascending() {
let r = eval_str("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj).length").unwrap();
assert!(r.is_int() && r.as_int() == 3, "should have 3 keys, got {:?}", r);
let (_vm, r2) = eval("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj)").unwrap();
let keys = keys_to_strings(r2);
assert_eq!(keys, vec!["1", "2", "3"], "integer keys should be ascending, got {:?}", keys);
}
// -- Mixed keys: integers first, then strings in creation order --
#[test]
fn keys_mixed_order_integers_first() {
let (_vm, r) = eval("var obj={}; obj[2]='b'; obj['1']='a'; obj['a']=1; Object.keys(obj)").unwrap();
let keys = keys_to_strings(r);
assert!(keys[0] == "1" || keys[0] == "2", "first key should be integer, got {:?}", keys);
let str_idx = keys.iter().position(|k| k == "a").unwrap();
let int_positions: Vec<usize> = keys
.iter()
.enumerate()
.filter(|(_, k)| k.parse::<u32>().is_ok())
.map(|(i, _)| i)
.collect();
for ip in &int_positions {
assert!(*ip < str_idx, "integer at {} should come before string 'a' at {}", ip, str_idx);
}
}
// -- Object.keys excludes non-enumerable properties --
#[test]
fn keys_excludes_non_enumerable() {
let r = eval_str(
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.keys(obj).length",
)
.unwrap();
assert!(r.is_int() && r.as_int() == 1, "keys should exclude non-enumerable, got {:?}", r);
}
// -- Object.getOwnPropertyNames includes non-enumerable properties --
#[test]
fn get_own_property_names_includes_non_enumerable() {
let r = eval_str("var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.getOwnPropertyNames(obj).length").unwrap();
assert!(
r.is_int() && r.as_int() == 2,
"getOwnPropertyNames should include non-enumerable, got {:?}",
r
);
}
// -- Object.entries excludes non-enumerable properties --
#[test]
fn entries_excludes_non_enumerable() {
let r = eval_str(
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.entries(obj).length",
)
.unwrap();
assert!(r.is_int() && r.as_int() == 1, "entries should exclude non-enumerable, got {:?}", r);
}
// -- Object.values excludes non-enumerable properties --
#[test]
fn values_excludes_non_enumerable() {
let r = eval_str(
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.values(obj).length",
)
.unwrap();
assert!(r.is_int() && r.as_int() == 1, "values should exclude non-enumerable, got {:?}", r);
}
// -- Leading zero strings are NOT integer indices --
#[test]
fn keys_with_leading_zero_not_integer_index() {
let (_vm, r) = eval("Object.keys({'01':'a','1':'b'})").unwrap();
let keys = keys_to_strings(r);
assert!(keys.contains(&"01".to_string()), "should contain '01' as string key");
assert!(keys.contains(&"1".to_string()), "should contain '1' as integer");
let idx_01 = keys.iter().position(|k| k == "01").unwrap();
let idx_1 = keys.iter().position(|k| k == "1").unwrap();
assert!(idx_1 < idx_01, "integer '1' should come before string '01'");
}
@@ -0,0 +1,95 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
fn eval_str(source: &str) -> Result<JsValue, String> {
eval(source).map(|(_vm, v)| v)
}
// Getter on proto sets marker on receiver, then check receiver has marker (not proto)
#[test]
fn getter_on_proto_this_is_receiver() {
let r = eval_str(
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._marker=1; return this._marker; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
).unwrap();
assert!(r.is_int() && r.as_int() == 1, "getter should set marker on child (receiver), got {:?}", r);
}
// Getter on deep proto chain: grandchild → child → proto. Getter defined on proto.
#[test]
fn getter_on_proto_deep_chain_this_is_receiver() {
let r = eval_str(
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._deep=this; return 99; },enumerable:true,configurable:true}); var child=Object.create(proto); var grandchild=Object.create(child); grandchild.x; grandchild._deep!==undefined"
).unwrap();
assert!(r.is_bool() && r.as_bool(), "grandchild should have _deep marker, got {:?}", r);
}
// Setter on proto receives receiver as this, sets marker on receiver not proto
#[test]
fn setter_on_proto_this_is_receiver() {
let (_vm, r) = eval(
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._set_by_setter=v; },get:function(){ return this._set_by_setter; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x='receiver_test'; child._set_by_setter"
).unwrap();
assert!(r.is_string(), "child should have _set_by_setter, got {:?}", r);
}
// Dynamic getter (obj[expr]) preserves receiver
#[test]
fn dynamic_getter_on_proto_this_is_receiver() {
let r = eval_str(
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._dyn=1; return this._dyn; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']"
).unwrap();
assert!(r.is_int() && r.as_int() == 1, "dynamic getter should set marker on child, got {:?}", r);
}
// Dynamic setter (obj[expr]=val) preserves receiver
#[test]
fn dynamic_setter_on_proto_this_is_receiver() {
let (_vm, r) = eval(
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._dyn_set=v; },get:function(){ return this._dyn_set; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']='dyn_test'; child._dyn_set"
).unwrap();
assert!(r.is_string(), "child should have _dyn_set after dynamic setter, got {:?}", r);
}
// Own getter on object — this is the object itself
#[test]
fn own_getter_this_is_self() {
let r = eval_str(
"var obj={}; Object.defineProperty(obj,'x',{get:function(){ this._own=42; return this._own; },enumerable:true,configurable:true}); obj.x"
).unwrap();
assert!(r.is_int() && r.as_int() == 42, "own getter should set marker on self, got {:?}", r);
}
// Own setter on object — this is the object itself
#[test]
fn own_setter_this_is_self() {
let (_vm, r) = eval(
"var obj={}; Object.defineProperty(obj,'x',{set:function(v){ this._own_set=v; },get:function(){ return this._own_set; },enumerable:true,configurable:true}); obj.x='own'; obj._own_set"
).unwrap();
assert!(r.is_string(), "own setter should set marker on self, got {:?}", r);
}
// Getter returning undefined does not crash
#[test]
fn getter_returns_undefined_does_not_crash() {
let r = eval_str(
"var proto={}; Object.defineProperty(proto,'x',{get:function(){},enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
).unwrap();
assert!(r.is_undefined(), "getter returning nothing should yield undefined, got {:?}", r);
}
// String .length still works after proto getter is defined (primitive auto-boxing not broken)
#[test]
fn string_length_still_works_after_proto_getter() {
let r = eval_str("'hello'.length").unwrap();
assert!(r.is_int() && r.as_int() == 5, "string.length should be 5, got {:?}", r);
}
@@ -0,0 +1,73 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
vm.run(&module)
}
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
let source = lines.join("; ");
eval(&source)
}
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
match result {
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
}
}
// -- Assign to non-writable own property throws TypeError --
#[test]
fn assign_to_non_writable_throws() {
let result = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
"obj.x = 2",
]);
assert_err_contains(result, "read-only");
}
// -- Assign to writable own property succeeds --
#[test]
fn assign_to_writable_succeeds() {
let r = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, writable: true})",
"obj.x = 2",
"obj.x",
])
.unwrap();
assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r);
}
// -- Assign to inherited non-writable property on proto throws --
#[test]
fn assign_to_inherited_non_writable_throws() {
let result = eval_many(&[
"var proto = {}",
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
"var child = Object.create(proto)",
"child.x = 2",
]);
assert_err_contains(result, "read-only");
}
// -- Assign creates own property on child when proto's is non-writable but child has no own prop
// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet.
// The engine correctly throws for non-writable proto props. Full shadowing is a follow-up.
#[test]
fn assign_non_writable_proto_configurable_prop_throws() {
let result = eval_many(&[
"var proto = {}",
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
"var child = Object.create(proto)",
"child.x = 99",
]);
assert_err_contains(result, "read-only");
}