diff --git a/Cargo.lock b/Cargo.lock index 0f441c8d..917b0d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/oxide_builtins/Cargo.toml b/crates/oxide_builtins/Cargo.toml index dea88971..8171484e 100644 --- a/crates/oxide_builtins/Cargo.toml +++ b/crates/oxide_builtins/Cargo.toml @@ -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" } diff --git a/crates/oxide_builtins/src/array.rs b/crates/oxide_builtins/src/array.rs index 66c123e9..95786600 100644 --- a/crates/oxide_builtins/src/array.rs +++ b/crates/oxide_builtins/src/array.rs @@ -68,7 +68,7 @@ fn create_new_array(vm: &mut H, n: usize) -> *mut JsObject { } fn array_length_arg(vm: &mut H, value: JsValue) -> Result { - 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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(result_val) } +pub fn array_to_string(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(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(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(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(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(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(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(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)); } } diff --git a/crates/oxide_builtins/src/array_buffer.rs b/crates/oxide_builtins/src/array_buffer.rs index 3b928bae..9edf406a 100644 --- a/crates/oxide_builtins/src/array_buffer.rs +++ b/crates/oxide_builtins/src/array_buffer.rs @@ -20,7 +20,7 @@ fn type_error(vm: &mut H, msg: &str) -> NativeResult { } fn to_index(vm: &mut H, value: JsValue) -> Result { - 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(vm: &mut H, value: JsValue) -> Result { Ok(n.trunc() as usize) } -fn normalize_index(_vm: &mut H, value: JsValue, len: usize) -> usize { - let n = oxide_runtime_api::to_number(value); +fn normalize_index(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; } diff --git a/crates/oxide_builtins/src/data_view.rs b/crates/oxide_builtins/src/data_view.rs index 2550048e..5dbf63f5 100644 --- a/crates/oxide_builtins/src/data_view.rs +++ b/crates/oxide_builtins/src/data_view.rs @@ -23,7 +23,7 @@ macro_rules! native_try { } fn to_index(vm: &mut H, value: JsValue, msg: &str) -> Result { - 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(vm: &mut H, args: &[u8], idx: usize) -> bool { fn numeric_arg(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) } diff --git a/crates/oxide_builtins/src/date.rs b/crates/oxide_builtins/src/date.rs index d8112c78..b26a52e6 100644 --- a/crates/oxide_builtins/src/date.rs +++ b/crates/oxide_builtins/src/date.rs @@ -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::from_timestamp_millis(ms as i64) } +fn dt_from_ms_local(ms: f64) -> Option> { + 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 { if !ms.is_finite() { return None; @@ -102,40 +109,47 @@ pub fn date_constructor(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(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(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(_vm: &mut H, _args: &[u8]) -> NativeResult { - NativeResult::Ok(JsValue::float(f64::NAN)) +pub fn date_utc(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(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| dt.timestamp_millis() as f64, f64::NAN); -make_getter!(date_get_full_year, |dt: DateTime| dt.year() as f64, f64::NAN); -make_getter!(date_get_month, |dt: DateTime| dt.month0() as f64, f64::NAN); -make_getter!(date_get_date, |dt: DateTime| dt.day() as f64, f64::NAN); -make_getter!(date_get_day, |dt: DateTime| dt.weekday().num_days_from_sunday() as f64, f64::NAN); -make_getter!(date_get_hours, |dt: DateTime| dt.hour() as f64, f64::NAN); -make_getter!(date_get_minutes, |dt: DateTime| dt.minute() as f64, f64::NAN); -make_getter!(date_get_seconds, |dt: DateTime| dt.second() as f64, f64::NAN); -make_getter!(date_get_milliseconds, |dt: DateTime| dt.timestamp_subsec_millis() as f64, f64::NAN); +make_local_getter!(date_get_full_year, |dt: DateTime| dt.year() as f64, f64::NAN); +make_local_getter!(date_get_month, |dt: DateTime| dt.month0() as f64, f64::NAN); +make_local_getter!(date_get_date, |dt: DateTime| dt.day() as f64, f64::NAN); +make_local_getter!(date_get_day, |dt: DateTime| dt.weekday().num_days_from_sunday() as f64, f64::NAN); +make_local_getter!(date_get_hours, |dt: DateTime| dt.hour() as f64, f64::NAN); +make_local_getter!(date_get_minutes, |dt: DateTime| dt.minute() as f64, f64::NAN); +make_local_getter!(date_get_seconds, |dt: DateTime| dt.second() as f64, f64::NAN); +make_local_getter!(date_get_milliseconds, |dt: DateTime| 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(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(vm: &H, args: &[u8], idx: usize, default: u32) -> u32 { @@ -316,7 +411,7 @@ fn get_opt_arg(vm: &H, args: &[u8], idx: usize, default: u32) -> u32 pub fn date_set_time(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(vm: &mut H, args: &[u8]) -> NativeResult { pub fn date_set_full_year(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(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn date_set_month(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(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn date_set_hours(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn date_set_minutes(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn date_set_seconds(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn date_set_milliseconds(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(vm: &mut H, args: &[u8]) -> NativeResult NativeResult::Ok(JsValue::float(ts)) } +pub fn date_set_utc_full_year(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(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(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { + date_to_utc_string(vm, args) +} + +pub fn date_to_locale_date_string(vm: &mut H, args: &[u8]) -> NativeResult { + date_to_date_string(vm, args) +} + +pub fn date_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { + date_to_string(vm, args) +} + +pub fn date_to_locale_time_string(vm: &mut H, args: &[u8]) -> NativeResult { + date_to_time_string(vm, args) +} + fn date_to_string_inner(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); diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index 79a95558..598a4576 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -6,31 +6,24 @@ use oxide_types::mem::P; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -fn create_error_object(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(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(host: &mut H, kind: &str, msg: &str) -> JsValue { @@ -43,7 +36,21 @@ pub fn create_kind_error(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(host: &mut H, msg: &str) -> JsValue { @@ -70,85 +77,99 @@ pub fn create_uri_error(host: &mut H, msg: &str) -> JsValue { create_kind_error(host, "URIError", msg) } -fn get_msg(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(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(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(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(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(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(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(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(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(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(host: &mut H, _args: &[u8]) -> NativeResult { +pub fn error_stack_getter(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 = names.iter().rev().map(|n| format!(" at {} (native)", n)).collect(); - lines.push(" at (native)".to_string()); - let result = lines.join("\n"); + for n in &names { + result.push_str(&format!("\n at {} (:0:0)", n)); + } NativeResult::Ok(host.new_string(&result)) } diff --git a/crates/oxide_builtins/src/iterator.rs b/crates/oxide_builtins/src/iterator.rs index ffe9884b..ee8457aa 100644 --- a/crates/oxide_builtins/src/iterator.rs +++ b/crates/oxide_builtins/src/iterator.rs @@ -36,9 +36,40 @@ pub fn make_iterator_for_value(vm: &mut H, value: JsValue) -> Result< let next_fn = make_native_function(vm, "next", iterator_wrapper_next:: 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:: 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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult } fn get_iterator(vm: &mut H, value: JsValue) -> Result { - 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(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(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( + 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(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(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapEntries) +} + +pub(crate) fn map_values_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapValues) +} + +pub(crate) fn map_keys_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapKeys) +} + +pub(crate) fn set_values_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::SetValues) +} + +pub(crate) fn set_entries_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(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(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) +} diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index b19df612..15cf6c1c 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -27,7 +27,82 @@ pub fn json_parse(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( + 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(vm: &mut H, val: &serde_json::Value) -> JsValue { @@ -75,6 +150,61 @@ fn value_to_jsvalue(vm: &mut H, val: &serde_json::Value) -> JsValue { } } +fn create_wrapper(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(vm: &mut H, obj_val: JsValue, key: &str) -> Result { + 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(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::undefined()); @@ -83,17 +213,79 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { if value.is_undefined() { return NativeResult::Ok(JsValue::undefined()); } + + let mut replacer_fn: Option = None; + let mut replacer_whitelist: Option> = 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( - 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, + replacer_whitelist: Option<&HashSet>, 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( 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( 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( - 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, + replacer_whitelist: Option<&HashSet>, 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( - 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, + replacer_whitelist: Option<&HashSet>, 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(']'); diff --git a/crates/oxide_builtins/src/map.rs b/crates/oxide_builtins/src/map.rs index 49c86742..8057cf72 100644 --- a/crates/oxide_builtins/src/map.rs +++ b/crates/oxide_builtins/src/map.rs @@ -15,7 +15,7 @@ macro_rules! native_try { }; } -type MapInner = indexmap::IndexMap; +pub(crate) type MapInner = indexmap::IndexMap; /// Retrieve the `IndexMap` pointer stored in a Map object's native-data slot. /// @@ -201,3 +201,33 @@ pub fn map_size(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(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:: as *const (), + )) +} + +pub fn map_values(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:: as *const (), + )) +} + +pub fn map_keys(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:: as *const (), + )) +} diff --git a/crates/oxide_builtins/src/math.rs b/crates/oxide_builtins/src/math.rs index 23866870..c48be7ed 100644 --- a/crates/oxide_builtins/src/math.rs +++ b/crates/oxide_builtins/src/math.rs @@ -2,11 +2,11 @@ use oxide_types::value::JsValue; use oxide_runtime_api::{NativeResult, VmHost}; -fn num(vm: &H, reg: u8) -> f64 { - oxide_runtime_api::to_number(vm.reg(reg)) +fn num(vm: &mut H, reg: u8) -> f64 { + vm.coerce_number_bounded(vm.reg(reg)).unwrap_or(f64::NAN) } -fn arg1(vm: &H, args: &[u8]) -> f64 { +fn arg1(vm: &mut H, args: &[u8]) -> f64 { if args.len() < 2 { f64::NAN } else { @@ -14,11 +14,11 @@ fn arg1(vm: &H, args: &[u8]) -> f64 { } } -fn arg2(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(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(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/src/number.rs b/crates/oxide_builtins/src/number.rs index fb0998a8..ce4232e9 100644 --- a/crates/oxide_builtins/src/number.rs +++ b/crates/oxide_builtins/src/number.rs @@ -3,7 +3,11 @@ use oxide_types::value::JsValue; use oxide_runtime_api::{NativeResult, VmHost}; pub fn number_constructor(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn number_to_string(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(vm: &mut H, args: &[u8]) -> NativeResult { } pub fn number_to_fixed(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 8758769b..3e6a6946 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -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::().unwrap_or(u64::MAX) < (1u64 << 32) - 1 +} + pub(crate) fn walk_own_keys(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(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::().unwrap().cmp(&b_str.parse::().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(vm: &mut H, _args: &[u8]) -> NativeResult { } pub fn object_keys(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; { 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(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_create(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(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(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(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(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(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(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { @@ -419,29 +593,94 @@ pub fn object_get_own_property_names(vm: &mut H, args: &[u8]) -> Nati pub fn object_define_properties(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(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(vm: &mut H, args: &[u8]) -> NativeResult { @@ -459,6 +698,18 @@ pub fn object_has_own(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::bool(false)); @@ -502,7 +753,15 @@ pub fn object_entries(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(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(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(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); diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 948b2b98..60109278 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -148,7 +148,7 @@ pub fn regexp_test(vm: &mut H, args: &[u8]) -> NativeResult { // SAFETY: fn_ptr holds a Box 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(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) }; diff --git a/crates/oxide_builtins/src/set.rs b/crates/oxide_builtins/src/set.rs index ce4519dd..44392101 100644 --- a/crates/oxide_builtins/src/set.rs +++ b/crates/oxide_builtins/src/set.rs @@ -56,7 +56,7 @@ impl Hash for SetKey { } } -type SetInner = indexmap::IndexSet; +pub(crate) type SetInner = indexmap::IndexSet; /// Retrieve the `IndexSet` pointer stored in a Set object's native-data slot. /// @@ -226,3 +226,34 @@ pub fn set_size(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(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:: as *const (), + )) +} + +pub fn set_values(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:: as *const (), + )) +} + +pub fn set_keys(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:: as *const (), + )) +} diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 8d983496..6b394543 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -171,7 +171,7 @@ pub fn string_index_of(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string(result)) } +pub fn string_substr(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(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(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(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Ok(make_string_array(vm, &parts)); } }; - // SAFETY: fn_ptr holds a Box 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 = raw.iter().map(|p| p.to_string()).take(limit).collect(); + let mut parts: Vec = 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(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(vm: &mut H, args: &[u8]) -> NativeResult { Some(p) => p, None => return NativeResult::Ok(vm.new_string(&s)), }; - // SAFETY: fn_ptr holds a Box 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 = 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(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(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 pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; - let matches: Vec = 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 = 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 = 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(vm: &mut H, args: &[u8]) -> NativeResult { @@ -565,7 +724,7 @@ pub fn string_code_point_at(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(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(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(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 pointer stored by regexp_constructor. - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; - let matches: Vec = 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(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:: 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(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(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 = 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(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(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index bfc80dab..49d03a14 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -32,7 +32,7 @@ fn range_error(vm: &mut H, msg: &str) -> JsValue { } fn to_index(vm: &mut H, value: JsValue, msg: &str) -> Result { - 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(vm: &mut H, value: JsValue, msg: &str) -> Result(_vm: &mut H, value: JsValue, len: usize) -> usize { - let n = oxide_runtime_api::to_number(value); +fn normalize_index(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(_vm: &mut H, value: JsValue) -> f64 { - oxide_runtime_api::to_number(value) +fn numeric_value(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) { diff --git a/crates/oxide_builtins/tests/array_boundary_tests.rs b/crates/oxide_builtins/tests/array_boundary_tests.rs new file mode 100644 index 00000000..94f6b37f --- /dev/null +++ b/crates/oxide_builtins/tests/array_boundary_tests.rs @@ -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); +} diff --git a/crates/oxide_builtins/tests/builtins_json_tests.rs b/crates/oxide_builtins/tests/builtins_json_tests.rs new file mode 100644 index 00000000..9da44c7f --- /dev/null +++ b/crates/oxide_builtins/tests/builtins_json_tests.rs @@ -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()); +} diff --git a/crates/oxide_builtins/tests/number_methods_tests.rs b/crates/oxide_builtins/tests/number_methods_tests.rs new file mode 100644 index 00000000..43733823 --- /dev/null +++ b/crates/oxide_builtins/tests/number_methods_tests.rs @@ -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)); +} diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs new file mode 100644 index 00000000..c8f3a804 --- /dev/null +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -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()); +} diff --git a/crates/oxide_builtins/tests/object_integrity_tests.rs b/crates/oxide_builtins/tests/object_integrity_tests.rs new file mode 100644 index 00000000..ce1b08b2 --- /dev/null +++ b/crates/oxide_builtins/tests/object_integrity_tests.rs @@ -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()); +} diff --git a/crates/oxide_builtins/tests/runtime_api_tests.rs b/crates/oxide_builtins/tests/runtime_api_tests.rs new file mode 100644 index 00000000..a4a9e4c9 --- /dev/null +++ b/crates/oxide_builtins/tests/runtime_api_tests.rs @@ -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())); +} diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index de2be463..d4e35c4f 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -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 diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index af1a2dc6..796f3d53 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -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::new(KernelConfig::standard()) +fn make_kernel(verbose: bool, quiet: bool) -> Arc { + 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) -> Arc { @@ -121,6 +133,7 @@ fn eval(code: &str, kernel: &Arc, pool: &Arc) -> 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, pool: &Arc) -> 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, pool: &Arc) -> 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, pool: &Arc) -> 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, file: Option) -> 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 '' or a file path"); eprintln!("{}", Red.paint("compile requires -e '' or a file path")); return ExitCode::FAILURE; }; @@ -253,6 +271,7 @@ fn compile(expr: Option, file: Option) -> 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, file: Option) -> 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 } diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index e6fbcf38..5bd699c4 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -160,6 +160,10 @@ pub(crate) struct CompileCtx { pub(crate) static_block_this_reg: Option, pub(crate) after_super_insert: Option>, 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, /// 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 { diff --git a/crates/oxide_compiler/src/counter/expr/assignment.rs b/crates/oxide_compiler/src/counter/expr/assignment.rs index 675a8764..2ee9db1e 100644 --- a/crates/oxide_compiler/src/counter/expr/assignment.rs +++ b/crates/oxide_compiler/src/counter/expr/assignment.rs @@ -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 { diff --git a/crates/oxide_compiler/src/counter/expr/call.rs b/crates/oxide_compiler/src/counter/expr/call.rs index 9a1015a8..e7df509f 100644 --- a/crates/oxide_compiler/src/counter/expr/call.rs +++ b/crates/oxide_compiler/src/counter/expr/call.rs @@ -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; } diff --git a/crates/oxide_compiler/src/counter/expr/control.rs b/crates/oxide_compiler/src/counter/expr/control.rs index 9abedd5f..a77cb5a6 100644 --- a/crates/oxide_compiler/src/counter/expr/control.rs +++ b/crates/oxide_compiler/src/counter/expr/control.rs @@ -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); } diff --git a/crates/oxide_compiler/src/counter/helper/binding.rs b/crates/oxide_compiler/src/counter/helper/binding.rs index 9ac83567..a7c081a7 100644 --- a/crates/oxide_compiler/src/counter/helper/binding.rs +++ b/crates/oxide_compiler/src/counter/helper/binding.rs @@ -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); } } diff --git a/crates/oxide_compiler/src/counter/helper/class.rs b/crates/oxide_compiler/src/counter/helper/class.rs index bb121365..51b125b7 100644 --- a/crates/oxide_compiler/src/counter/helper/class.rs +++ b/crates/oxide_compiler/src/counter/helper/class.rs @@ -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 { diff --git a/crates/oxide_compiler/src/counter/stmt/exception.rs b/crates/oxide_compiler/src/counter/stmt/exception.rs index 408093e2..313282b5 100644 --- a/crates/oxide_compiler/src/counter/stmt/exception.rs +++ b/crates/oxide_compiler/src/counter/stmt/exception.rs @@ -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(_) = ¶m.pattern { + ctx.projected_pc += 1; // STORE_VAR + } } for cs in &catch.body.body { self.count_statement(cs, ctx); diff --git a/crates/oxide_compiler/src/counter/stmt/iteration.rs b/crates/oxide_compiler/src/counter/stmt/iteration.rs index 1a80e5ad..0eb6a0b9 100644 --- a/crates/oxide_compiler/src/counter/stmt/iteration.rs +++ b/crates/oxide_compiler/src/counter/stmt/iteration.rs @@ -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 } } diff --git a/crates/oxide_compiler/src/emitter/mod.rs b/crates/oxide_compiler/src/emitter/mod.rs index 83805fb6..a8a45de0 100644 --- a/crates/oxide_compiler/src/emitter/mod.rs +++ b/crates/oxide_compiler/src/emitter/mod.rs @@ -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 { diff --git a/crates/oxide_compiler/src/emitter/stmt/iteration.rs b/crates/oxide_compiler/src/emitter/stmt/iteration.rs index 484cbb4b..3cbe2cbc 100644 --- a/crates/oxide_compiler/src/emitter/stmt/iteration.rs +++ b/crates/oxide_compiler/src/emitter/stmt/iteration.rs @@ -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()); } } } diff --git a/crates/oxide_compiler/tests/compile_stmt_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs index 50138ba0..1e2632a4 100644 --- a/crates/oxide_compiler/tests/compile_stmt_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -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" ); } diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index be24cf1b..d6a1d1fb 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -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, pub sym_split: P, pub sym_iterator: P, + pub sym_to_primitive: P, + pub sym_has_instance: P, pub stub_objects: Vec>, } @@ -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, 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), ); } diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 467eeefb..207fdb21 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -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) diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 1f26cdc2..e7a40fe3 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -101,6 +101,9 @@ pub trait VmHost { // Call infrastructure fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result; + /// 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; // Error handling fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result, String>; @@ -118,11 +121,13 @@ pub trait VmHost { fn symbol_key_for_id(&self, idx: u32) -> Option; } -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(lhs: JsValue, rhs: JsValue, host: &mut H) -> Result { + // 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(val: JsValue, host: &mut H) -> Result "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(val: JsValue, hint: ToPrimitiveHint, host: &mut H) -> Result { + 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(val: JsValue, host: &mut H) -> Result { + 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(val: JsValue, host: &mut H) -> Result { + let primitive = to_primitive(val, ToPrimitiveHint::String, host)?; + Ok(to_string(primitive)) +} diff --git a/crates/oxide_test262/Cargo.toml b/crates/oxide_test262/Cargo.toml index 95ba05af..75592195 100644 --- a/crates/oxide_test262/Cargo.toml +++ b/crates/oxide_test262/Cargo.toml @@ -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" diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 075759f8..165c55d2 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -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 = 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); } } diff --git a/crates/oxide_test262/src/test262_log.rs b/crates/oxide_test262/src/test262_log.rs new file mode 100644 index 00000000..a3f8874d --- /dev/null +++ b/crates/oxide_test262/src/test262_log.rs @@ -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)*) + }; +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 72208698..e842ce8d 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -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 } diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index 3063e68f..1564a556 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -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, session: &KernelSession, global: &mut JsObject) { let _array_methods = ArrayMethods { @@ -46,6 +47,20 @@ pub fn bind_array(core: &Arc, 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:: 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::, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_date.rs b/crates/oxide_vm/src/bindings/bind_date.rs index 5b1d7324..ac5be310 100644 --- a/crates/oxide_vm/src/bindings/bind_date.rs +++ b/crates/oxide_vm/src/bindings/bind_date.rs @@ -83,12 +83,53 @@ pub fn bind_date(core: &Arc, session: &KernelSession, global: &mut J oxide_builtins::date::date_set_milliseconds:: as *const (), 1, ), + ( + "setUTCFullYear", + oxide_builtins::date::date_set_utc_full_year:: as *const (), + 3, + ), + ("setUTCMonth", oxide_builtins::date::date_set_utc_month:: as *const (), 2), + ("setUTCDate", oxide_builtins::date::date_set_utc_date:: as *const (), 1), + ("setUTCHours", oxide_builtins::date::date_set_utc_hours:: as *const (), 4), + ( + "setUTCMinutes", + oxide_builtins::date::date_set_utc_minutes:: as *const (), + 3, + ), + ( + "setUTCSeconds", + oxide_builtins::date::date_set_utc_seconds:: as *const (), + 2, + ), + ( + "setUTCMilliseconds", + oxide_builtins::date::date_set_utc_milliseconds:: as *const (), + 1, + ), + ("getYear", oxide_builtins::date::date_get_year:: as *const (), 0), + ("setYear", oxide_builtins::date::date_set_year:: as *const (), 1), ("toISOString", oxide_builtins::date::date_to_iso_string:: as *const (), 0), ("toJSON", oxide_builtins::date::date_to_json:: as *const (), 1), ("toString", oxide_builtins::date::date_to_string:: as *const (), 0), ("toDateString", oxide_builtins::date::date_to_date_string:: as *const (), 0), ("toTimeString", oxide_builtins::date::date_to_time_string:: as *const (), 0), ("toUTCString", oxide_builtins::date::date_to_utc_string:: as *const (), 0), + ("toGMTString", oxide_builtins::date::date_to_gmt_string:: as *const (), 0), + ( + "toLocaleDateString", + oxide_builtins::date::date_to_locale_date_string:: as *const (), + 0, + ), + ( + "toLocaleString", + oxide_builtins::date::date_to_locale_string:: as *const (), + 0, + ), + ( + "toLocaleTimeString", + oxide_builtins::date::date_to_locale_time_string:: as *const (), + 0, + ), ("valueOf", oxide_builtins::date::date_value_of:: as *const (), 0), ], ); diff --git a/crates/oxide_vm/src/bindings/bind_json.rs b/crates/oxide_vm/src/bindings/bind_json.rs index 01c4b864..2c8140e9 100644 --- a/crates/oxide_vm/src/bindings/bind_json.rs +++ b/crates/oxide_vm/src/bindings/bind_json.rs @@ -14,8 +14,8 @@ pub fn bind_json(core: &Arc, session: &KernelSession, global: &mut J json, core, &[ - ("parse", oxide_builtins::json::json_parse:: as *const (), 1), - ("stringify", oxide_builtins::json::json_stringify:: as *const (), 1), + ("parse", oxide_builtins::json::json_parse:: as *const (), 2), + ("stringify", oxide_builtins::json::json_stringify:: as *const (), 3), ], ); diff --git a/crates/oxide_vm/src/bindings/bind_map.rs b/crates/oxide_vm/src/bindings/bind_map.rs index c1bb13bc..9f4b3c69 100644 --- a/crates/oxide_vm/src/bindings/bind_map.rs +++ b/crates/oxide_vm/src/bindings/bind_map.rs @@ -24,6 +24,9 @@ pub fn bind_map(core: &Arc, session: &KernelSession, global: &mut Js ("delete", oxide_builtins::map::map_delete:: as *const (), 1), ("clear", oxide_builtins::map::map_clear:: as *const (), 0), ("size", oxide_builtins::map::map_size:: as *const (), 0), + ("entries", oxide_builtins::map::map_entries:: as *const (), 0), + ("values", oxide_builtins::map::map_values:: as *const (), 0), + ("keys", oxide_builtins::map::map_keys:: as *const (), 0), ], ); diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index e693de5c..8597063f 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -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, session: &KernelSession, global: &mut JsObject) { @@ -27,6 +27,8 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut oxide_builtins::number::number_is_safe_integer:: as *const (), 1, ), + ("parseInt", oxide_builtins::number::number_parse_int:: as *const (), 1), + ("parseFloat", oxide_builtins::number::number_parse_float:: as *const (), 1), ], ); @@ -62,9 +64,11 @@ pub fn bind_number(core: &Arc, 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::, 1, hash: true); diff --git a/crates/oxide_vm/src/bindings/bind_object.rs b/crates/oxide_vm/src/bindings/bind_object.rs index 12a190a7..712ec39d 100644 --- a/crates/oxide_vm/src/bindings/bind_object.rs +++ b/crates/oxide_vm/src/bindings/bind_object.rs @@ -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, session: &KernelSession, global: &mut JsObject) { let methods = ObjectMethods { @@ -36,6 +37,29 @@ pub fn bind_object(core: &Arc, 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:: as *const (), + 0, + ), + ("valueOf", oxide_builtins::object::object_proto_value_of:: 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, diff --git a/crates/oxide_vm/src/bindings/bind_set.rs b/crates/oxide_vm/src/bindings/bind_set.rs index 078d89c0..4e1c1fbc 100644 --- a/crates/oxide_vm/src/bindings/bind_set.rs +++ b/crates/oxide_vm/src/bindings/bind_set.rs @@ -23,6 +23,9 @@ pub fn bind_set(core: &Arc, session: &KernelSession, global: &mut Js ("delete", oxide_builtins::set::set_delete:: as *const (), 1), ("clear", oxide_builtins::set::set_clear:: as *const (), 0), ("size", oxide_builtins::set::set_size:: as *const (), 0), + ("entries", oxide_builtins::set::set_entries:: as *const (), 0), + ("values", oxide_builtins::set::set_values:: as *const (), 0), + ("keys", oxide_builtins::set::set_keys:: as *const (), 0), ], ); diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index c58f7b29..9a3f9f9b 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -35,6 +35,9 @@ pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut match_all: oxide_builtins::string::string_match_all:: as *const (), replace_all: oxide_builtins::string::string_replace_all:: as *const (), value_of: oxide_builtins::string::string_value_of:: as *const (), + substr: oxide_builtins::string::string_substr:: as *const (), + at: oxide_builtins::string::string_at:: as *const (), + last_index_of: oxide_builtins::string::string_last_index_of:: as *const (), }; session.builtin_world().bind_string_methods( &string_methods, diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 1b020df4..f56c837f 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -53,6 +53,14 @@ pub fn bind_symbol(core: &Arc, 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); } diff --git a/crates/oxide_vm/src/dispatch/comparison.rs b/crates/oxide_vm/src/dispatch/comparison.rs index c312dceb..dc831711 100644 --- a/crates/oxide_vm/src/dispatch/comparison.rs +++ b/crates/oxide_vm/src/dispatch/comparison.rs @@ -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)] diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 14258d62..68f8b055 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -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); diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index e86b9100..39a8b731 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -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); } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index c57f3d67..a1abb8ce 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -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, pub finally_pc: Option, 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, pub(crate) try_stack: Vec, pub(crate) exception_value: Option, + /// 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, pub(crate) pending_exception: Option, 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 { + self.last_uncaught_value.take() + } fn property_key_si(&mut self, val: JsValue) -> u32 { self.property_key_si(val) } diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 430dc991..5410395f 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -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(), }); } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index ee5ec836..dbaab0ad 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -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::>()); + 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::>()); 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]; diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 770c5f12..82bbd67b 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -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) { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index bbaac710..39f9529e 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -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, diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index 6be7816b..1af524fa 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -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); +} diff --git a/crates/oxide_vm/tests/builtins_error_tests.rs b/crates/oxide_vm/tests/builtins_error_tests.rs index 58eca22c..8be47cb6 100644 --- a/crates/oxide_vm/tests/builtins_error_tests.rs +++ b/crates/oxide_vm/tests/builtins_error_tests.rs @@ -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); +} diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index feb1e90b..4108930e 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -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); +} diff --git a/crates/oxide_vm/tests/builtins_symbol_tests.rs b/crates/oxide_vm/tests/builtins_symbol_tests.rs index 3f70562d..819ab586 100644 --- a/crates/oxide_vm/tests/builtins_symbol_tests.rs +++ b/crates/oxide_vm/tests/builtins_symbol_tests.rs @@ -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()); +} diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index 588cc720..e870e96f 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -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"); +} diff --git a/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs b/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs new file mode 100644 index 00000000..1e4ba3ca --- /dev/null +++ b/crates/oxide_vm/tests/compiler_alignment_binding_tests.rs @@ -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 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"); +} diff --git a/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs b/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs new file mode 100644 index 00000000..5d8a3e89 --- /dev/null +++ b/crates/oxide_vm/tests/compiler_alignment_expr_tests.rs @@ -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(c0){} v"), "2"); +} + +#[test] +fn compound_member_value_regression() { + assert_eq!(eval("var o={x:10}; o.x-=3; o.x"), "7"); +} diff --git a/crates/oxide_vm/tests/define_property_descriptor_tests.rs b/crates/oxide_vm/tests/define_property_descriptor_tests.rs new file mode 100644 index 00000000..fa1d5b7c --- /dev/null +++ b/crates/oxide_vm/tests/define_property_descriptor_tests.rs @@ -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 { + eval(source).map(|(_vm, v)| v) +} + +fn assert_err_contains(result: Result, 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"); +} diff --git a/crates/oxide_vm/tests/destructuring_tests.rs b/crates/oxide_vm/tests/destructuring_tests.rs index 285f59d8..5656ddc5 100644 --- a/crates/oxide_vm/tests/destructuring_tests.rs +++ b/crates/oxide_vm/tests/destructuring_tests.rs @@ -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); +} diff --git a/crates/oxide_vm/tests/for_in_null_undefined_tests.rs b/crates/oxide_vm/tests/for_in_null_undefined_tests.rs new file mode 100644 index 00000000..a205bde5 --- /dev/null +++ b/crates/oxide_vm/tests/for_in_null_undefined_tests.rs @@ -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}"); +} diff --git a/crates/oxide_vm/tests/for_in_ordering_tests.rs b/crates/oxide_vm/tests/for_in_ordering_tests.rs new file mode 100644 index 00000000..4e8c1f07 --- /dev/null +++ b/crates/oxide_vm/tests/for_in_ordering_tests.rs @@ -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']=2"), + "true", + "for-in still enumerates the object's own enumerable keys" + ); +} diff --git a/crates/oxide_vm/tests/for_of_abrupt_completion_tests.rs b/crates/oxide_vm/tests/for_of_abrupt_completion_tests.rs new file mode 100644 index 00000000..97702aa7 --- /dev/null +++ b/crates/oxide_vm/tests/for_of_abrupt_completion_tests.rs @@ -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"); +} diff --git a/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs b/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs new file mode 100644 index 00000000..a59fc330 --- /dev/null +++ b/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs @@ -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}"); +} diff --git a/crates/oxide_vm/tests/property_delete_tests.rs b/crates/oxide_vm/tests/property_delete_tests.rs new file mode 100644 index 00000000..dc02c921 --- /dev/null +++ b/crates/oxide_vm/tests/property_delete_tests.rs @@ -0,0 +1,95 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + 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 { + let source = lines.join("; "); + eval(&source) +} + +fn assert_err_contains(result: Result, 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 + ); +} diff --git a/crates/oxide_vm/tests/property_keys_order_tests.rs b/crates/oxide_vm/tests/property_keys_order_tests.rs new file mode 100644 index 00000000..1e6febfc --- /dev/null +++ b/crates/oxide_vm/tests/property_keys_order_tests.rs @@ -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 { + eval(source).map(|(_vm, v)| v) +} + +fn keys_to_strings(result: JsValue) -> Vec { + 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 = keys + .iter() + .enumerate() + .filter(|(_, k)| k.parse::().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'"); +} diff --git a/crates/oxide_vm/tests/property_receiver_tests.rs b/crates/oxide_vm/tests/property_receiver_tests.rs new file mode 100644 index 00000000..8efeb3de --- /dev/null +++ b/crates/oxide_vm/tests/property_receiver_tests.rs @@ -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 { + 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); +} diff --git a/crates/oxide_vm/tests/property_write_protection_tests.rs b/crates/oxide_vm/tests/property_write_protection_tests.rs new file mode 100644 index 00000000..de0a178c --- /dev/null +++ b/crates/oxide_vm/tests/property_write_protection_tests.rs @@ -0,0 +1,73 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + 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 { + let source = lines.join("; "); + eval(&source) +} + +fn assert_err_contains(result: Result, 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"); +}