diff --git a/crates/oxide_builtins/src/date.rs b/crates/oxide_builtins/src/date.rs index c723495..91fa984 100644 --- a/crates/oxide_builtins/src/date.rs +++ b/crates/oxide_builtins/src/date.rs @@ -734,6 +734,14 @@ pub fn date_get_year(vm: &mut H, args: &[u8]) -> NativeResult { 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)); } @@ -741,7 +749,7 @@ pub fn date_set_year(vm: &mut H, args: &[u8]) -> NativeResult { Some(d) => d, None => return NativeResult::Ok(JsValue::float(f64::NAN)), }; - let y = oxide_runtime_api::to_number(vm.reg(args[1])).trunc() as i32; + 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; diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index aa3927e..ad29001 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -345,3 +345,40 @@ fn date_set_utc_invalid_returns_nan() { 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); +}