From 4906e1502df8a7dea35d6c719e97d44b974ca5ca Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 16 Aug 2026 17:30:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Temporal=20=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E4=B8=A5=E6=A0=BC=E6=A0=A1=E9=AA=8C=E6=97=A5=E5=8E=86=E5=B9=B6?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E5=AD=98=E6=A7=BD=EF=BC=88=E5=90=AB=20fast?= =?UTF-8?q?=20path=20=E4=B8=8E=E6=97=A5=E5=8E=86=E4=BC=A0=E6=92=AD?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 135 ++++++++++++------ .../oxide_vm/tests/builtins_temporal_tests.rs | 121 ++++++++++++++++ 2 files changed, 213 insertions(+), 43 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index c0b7c4f..8af31dc 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -1295,18 +1295,14 @@ pub fn zoned_date_time_constructor(vm: &mut H, args: &[u8]) -> Native let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); }; - if args.len() >= 4 && !vm.reg(args[3]).is_undefined() { - let calendar = vm.reg(args[3]); - if !calendar.is_string() { - return NativeResult::Err(crate::error::create_type_error(vm, "invalid calendar")); - } - if !to_string(calendar).eq_ignore_ascii_case("iso8601") { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendar")); - } - } + let calendar = if args.len() >= 4 && !vm.reg(args[3]).is_undefined() { + native_try!(temporal_calendar_id_strict(vm, vm.reg(args[3]))).unwrap_or_else(|| "iso8601".to_string()) + } else { + "iso8601".to_string() + }; let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns)); let time_zone_value = vm.new_string(&time_zone_id); - let calendar_value = vm.new_string("iso8601"); + let calendar_value = vm.new_string(&calendar); initialize_temporal_receiver( vm, args, @@ -2384,18 +2380,29 @@ pub fn plain_date_constructor(vm: &mut H, args: &[u8]) -> NativeResul if !(-100_000_001..=100_000_000).contains(&day_count) { return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); } + let calendar = if args.len() > 4 && !vm.reg(args[4]).is_undefined() { + native_try!(temporal_calendar_id_strict(vm, vm.reg(args[4]))).unwrap_or_else(|| "iso8601".to_string()) + } else { + "iso8601".to_string() + }; + let calendar_value = vm.new_string(&calendar); initialize_temporal_receiver( vm, args, JsObject::OBJ_TYPE_PLAIN_DATE, - [JsValue::float(year as f64), JsValue::float(month as f64), JsValue::float(day as f64)], + [ + JsValue::float(year as f64), + JsValue::float(month as f64), + JsValue::float(day as f64), + calendar_value, + ], ) } /// `Temporal.PlainDate.from(value, options)`:接受 ISO 日期字符串或 `{year, month, day}` 对象。 pub fn plain_date_from(vm: &mut H, args: &[u8]) -> NativeResult { let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (year, month, day) = if val.is_string() { + let (year, month, day, calendar) = if val.is_string() { // 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。 let ymd = match parse_plain_date_string(&to_string(val)) { Ok(ymd) => ymd, @@ -2404,7 +2411,7 @@ pub fn plain_date_from(vm: &mut H, args: &[u8]) -> NativeResult { } }; native_try!(temporal_overflow(vm, args)); - ymd + (ymd.0, ymd.1, ymd.2, None) } else { let constrain = native_try!(temporal_overflow(vm, args)); match object_date_ymd(vm, val, constrain) { @@ -2420,7 +2427,7 @@ pub fn plain_date_from(vm: &mut H, args: &[u8]) -> NativeResult { if !(-100_000_001..=100_000_000).contains(&day_count) { return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); } - make_plain_date(vm, year, month, day, "iso8601") + make_plain_date(vm, year, month, day, calendar.as_deref().unwrap_or("iso8601")) } fn plain_date_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { @@ -2644,13 +2651,11 @@ pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> Native return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); } - if args.len() > 10 && !vm.reg(args[10]).is_undefined() { - let calendar = vm.reg(args[10]); - let calendar = native_try!(temporal_option_string(vm, calendar)); - if !calendar.eq_ignore_ascii_case("iso8601") { - return NativeResult::Err(crate::error::create_range_error(vm, "unsupported calendar")); - } - } + let calendar = if args.len() > 10 && !vm.reg(args[10]).is_undefined() { + native_try!(temporal_calendar_id_strict(vm, vm.reg(args[10]))).unwrap_or_else(|| "iso8601".to_string()) + } else { + "iso8601".to_string() + }; let total_ns = hour as f64 * 3_600_000_000_000.0 + minute as f64 * 60_000_000_000.0 @@ -2661,6 +2666,7 @@ pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> Native if !valid_plain_date_time_range(year, month, day, total_ns) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); } + let calendar_value = vm.new_string(&calendar); initialize_temporal_receiver( vm, args, @@ -2670,6 +2676,7 @@ pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> Native JsValue::float(month as f64), JsValue::float(day as f64), JsValue::float(total_ns), + calendar_value, ], ) } @@ -3037,8 +3044,8 @@ fn parse_partial_calendar_date(input: &str) -> Result<(), String> { } /// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None; -/// string → 宽松解析(白名单 ID 或 ISO 串);其他类型 → TypeError。 -/// 日历对象的内部槽 fast path 在日历槽接入后补充。 +/// string → 宽松解析(白名单 ID 或 ISO 串);Temporal 实例 → 读日历槽(不触发属性 getter); +/// 其他类型 → TypeError。 fn temporal_calendar_id(vm: &mut H, value: JsValue) -> Result, JsValue> { if value.is_undefined() { return Ok(None); @@ -3048,12 +3055,26 @@ fn temporal_calendar_id(vm: &mut H, value: JsValue) -> Result(vm: &mut H, value: JsValue) -> Result, JsValue> { if value.is_string() { return parse_temporal_calendar_id_strict(&to_string(value)) @@ -3264,7 +3285,7 @@ fn plain_date_time_like_parts( get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, get_double_prop(obj, 3), - None, // 实例日历传播在日历槽接入后补齐 + Some(get_calendar_id(obj, 4)), )); } if obj.is_plain_date_obj() { @@ -3273,12 +3294,12 @@ fn plain_date_time_like_parts( get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, 0.0, - None, // 实例日历传播在日历槽接入后补齐 + Some(get_calendar_id(obj, 3)), )); } if obj.is_zoned_date_time_obj() { return zoned_date_time_plain_parts(vm, obj) - .map(|(year, month, day, total_ns)| (year, month, day, total_ns, None)); + .map(|(year, month, day, total_ns)| (year, month, day, total_ns, Some(get_calendar_id(obj, 2)))); } plain_date_time_object_parts(vm, value, obj, constrain, false) } @@ -3322,8 +3343,8 @@ pub fn plain_date_time_from(vm: &mut H, args: &[u8]) -> NativeResult make_plain_date_time(vm, year, month, day, total_ns, "iso8601") } else { let constrain = native_try!(temporal_overflow(vm, args)); - let (year, month, day, total_ns, _) = native_try!(plain_date_time_like_parts(vm, value, constrain)); - make_plain_date_time(vm, year, month, day, total_ns, "iso8601") + let (year, month, day, total_ns, calendar) = native_try!(plain_date_time_like_parts(vm, value, constrain)); + make_plain_date_time(vm, year, month, day, total_ns, calendar.as_deref().unwrap_or("iso8601")) } } @@ -3504,10 +3525,12 @@ pub fn plain_date_time_calendar_id(vm: &mut H, args: &[u8]) -> Native NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4))) } -/// 返回仅保留日期分量的新 `Temporal.PlainDate`。 +/// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。 pub fn plain_date_time_to_plain_date(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); - make_plain_date(vm, year, month, day, "iso8601") + make_plain_date(vm, year, month, day, &get_calendar_id(obj, 4)) } /// 返回仅保留时间分量的新 `Temporal.PlainTime`。 @@ -3651,6 +3674,8 @@ pub fn plain_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeRe /// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天), /// 再按年/月/周/日推进日期,最后校验范围。 fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) { Ok(parts) => parts, Err(error) => return NativeResult::Err(error), @@ -3701,7 +3726,7 @@ fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); } - make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, "iso8601") + make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4)) } /// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。 @@ -4471,9 +4496,11 @@ fn plain_date_ymd_checked(vm: &mut H, args: &[u8]) -> Result<(i32, u3 Ok(ymd) } -/// 从对象式日期字段(`{year, month, day}`)读三字段;PlainDate 对象直接读内部槽; -/// 缺字段返回 None。 -fn object_date_ymd(vm: &mut H, val: JsValue, constrain: bool) -> Result<(i32, u32, u32), JsValue> { +/// 从对象式日期字段(`{year, month, day}`)读三字段与日历;PlainDate/PlainDateTime 实例 +/// 直接读内部槽;缺字段返回 None。 +fn object_date_ymd( + vm: &mut H, val: JsValue, constrain: bool, +) -> Result<(i32, u32, u32, Option), JsValue> { if !val.is_object() { return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); } @@ -4482,25 +4509,35 @@ fn object_date_ymd(vm: &mut H, val: JsValue, constrain: bool) -> Resu return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); } let obj = unsafe { &*ptr }; - if obj.is_plain_date_obj() || obj.is_plain_date_time_obj() { + if obj.is_plain_date_time_obj() { return Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, + Some(get_calendar_id(obj, 4)), + )); + } + if obj.is_plain_date_obj() { + return Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + Some(get_calendar_id(obj, 3)), )); } if obj.is_zoned_date_time_obj() { let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; - return Ok((year, month, day)); + return Ok((year, month, day, Some(get_calendar_id(obj, 2)))); } - let (year, month, day, _, _) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; - Ok((year, month, day)) + let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; + Ok((year, month, day, calendar)) } -fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> { +fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option), JsValue> { let ymd = if val.is_string() { - parse_plain_date_string(&to_string(val)) - .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))? + let (y, m, d) = parse_plain_date_string(&to_string(val)) + .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?; + (y, m, d, None) } else { object_date_ymd(vm, val, true)? }; @@ -4702,6 +4739,8 @@ pub fn plain_date_to_json(vm: &mut H, args: &[u8]) -> NativeResult { /// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。 fn date_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), @@ -4741,7 +4780,7 @@ fn date_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeR return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); } } - make_plain_date(vm, result.year(), result.month(), result.day(), "iso8601") + make_plain_date(vm, result.year(), result.month(), result.day(), &get_calendar_id(obj, 3)) } /// `Temporal.PlainDate.prototype.add(durationLike)`。 @@ -4882,4 +4921,14 @@ mod tests { assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][u-ca=discord]").is_ok()); assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][!u-ca=iso8601]").is_err()); } + + #[test] + fn calendar_id_slot_fallback() { + // 字符串槽原值返回由 VM 层构造器/getter 测试覆盖;此处验证 undefined 与非 string 槽兜底 iso8601。 + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()); + obj.set_prop_at(3, JsValue::undefined()); + assert_eq!(get_calendar_id(&obj, 3), "iso8601"); + obj.set_prop_at(3, JsValue::int(7)); + assert_eq!(get_calendar_id(&obj, 3), "iso8601"); + } } diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 77c518b..9b88fcc 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -844,3 +844,124 @@ fn plain_date_time_add_balances_months_days_and_time() { .unwrap(); assert_eq!(str_val(&vm, r), "2001:6:18:12:34:0:3:0:1997:11:30:2:29:true"); } + +#[test] +fn plain_date_constructor_stores_real_calendar() { + let mut vm = Vm::new(); + // 规范日历 ID 存槽:大小写折叠为规范小写。 + let r = eval( + &mut vm, + "new Temporal.PlainDate(2000, 5, 2, 'gregory').calendarId + ',' + + new Temporal.PlainDate(2000, 5, 2, 'iSo8601').calendarId + ',' + + new Temporal.PlainDate(2000, 5, 2).calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "gregory,iso8601,iso8601"); +} + +#[test] +fn plain_date_constructor_rejects_invalid_calendar() { + let mut vm = Vm::new(); + // 未在白名单 → RangeError;ISO 串拒绝;对象(非 Temporal 实例)→ TypeError。 + let r = eval( + &mut vm, + "(() => { try { new Temporal.PlainDate(2000, 5, 2, 'notacal'); return false; } + catch (e) { return e instanceof RangeError; } })() + ',' + + (() => { try { new Temporal.PlainDate(2000, 5, 2, '1997-12-04[u-ca=iso8601]'); return false; } + catch (e) { return e instanceof RangeError; } })() + ',' + + (() => { try { new Temporal.PlainDate(2000, 5, 2, {}); return false; } + catch (e) { return e instanceof TypeError; } })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "true,true,true"); +} + +#[test] +fn plain_date_time_constructor_stores_real_calendar() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew').calendarId + ',' + + new Temporal.PlainDateTime(2000, 5, 2).calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "hebrew,iso8601"); +} + +#[test] +fn plain_date_time_constructor_wrong_calendar_is_type_error() { + let mut vm = Vm::new(); + // 规范要求 `{}` 日历 → TypeError(strict 走日历 ID 校验而非宽松 to-option-string)。 + let r = eval( + &mut vm, + "(() => { try { new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, {}); return false; } + catch (e) { return e instanceof TypeError; } })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn zoned_date_time_constructor_stores_real_calendar() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "new Temporal.ZonedDateTime(0n, 'UTC', 'hebrew').calendarId + ',' + + new Temporal.ZonedDateTime(0n, 'UTC').calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "hebrew,iso8601"); +} + +#[test] +fn zoned_date_time_constructor_wrong_calendar_is_type_error() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(() => { try { new Temporal.ZonedDateTime(0n, 'UTC', {}); return false; } + catch (e) { return e instanceof TypeError; } })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn temporal_instance_as_calendar_fast_path_reads_slot() { + let mut vm = Vm::new(); + // Temporal 实例作 property bag 的 calendar:直接读内部槽,不触发 calendar 属性 getter。 + let r = eval( + &mut vm, + "const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory'); + const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew'); + const zdt = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese'); + Object.defineProperty(pd, 'calendar', { get() { throw new Error('getter'); } }); + Object.defineProperty(pdt, 'calendar', { get() { throw new Error('getter'); } }); + Object.defineProperty(zdt, 'calendar', { get() { throw new Error('getter'); } }); + const a = Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: pd }).calendarId; + const b = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: pdt }).calendarId; + const c = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: zdt }).calendarId; + a + ',' + b + ',' + c", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "gregory,hebrew,japanese"); +} + +#[test] +fn plain_date_from_instance_preserves_calendar() { + let mut vm = Vm::new(); + // from(实例) 复制日历槽;add/subtract 与 toPlainDate 传播日历。 + let r = eval( + &mut vm, + "const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory'); + const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew'); + Temporal.PlainDate.from(pd).calendarId + ',' + + Temporal.PlainDateTime.from(pd).calendarId + ',' + + Temporal.PlainDateTime.from(pdt).calendarId + ',' + + pdt.toPlainDate().calendarId + ',' + + pd.add({ days: 1 }).calendarId + ',' + + pd.subtract({ days: 1 }).calendarId + ',' + + pdt.add({ days: 1 }).calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "gregory,gregory,hebrew,hebrew,gregory,gregory,hebrew"); +}