From e6627b02f1538f51891c86e6c3b2a5f99ba222fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Tue, 11 Aug 2026 21:32:15 +0800 Subject: [PATCH] feat: implement Temporal PlainDateTime foundation --- crates/oxide_builtins/src/temporal.rs | 272 +++++++++++++++++- crates/oxide_kernel/src/builtin.rs | 22 +- crates/oxide_kernel/src/kernel.rs | 10 +- crates/oxide_types/src/object.rs | 7 + crates/oxide_vm/src/bindings/bind_temporal.rs | 124 ++++++++ .../oxide_vm/tests/builtins_temporal_tests.rs | 70 +++++ 6 files changed, 500 insertions(+), 5 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index ae4330f..4075481 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -6,9 +6,11 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; // Temporal 命名空间的最小实现子集:Temporal.Now / Temporal.Instant / -// Temporal.PlainDate / Temporal.PlainTime / Temporal.ZonedDateTime。内部数据按对象类型存入 prop 槽: +// Temporal.PlainDate / Temporal.PlainTime / Temporal.PlainDateTime / Temporal.ZonedDateTime。 +// 内部数据按对象类型存入 prop 槽: // Instant 存纪元纳秒(BigInt,prop 0)、PlainDate 存年/月/日(prop 0-2)、 -// PlainTime 存午夜后纳秒(f64,prop 0),ZonedDateTime 存纪元纳秒、时区 ID、日历 ID(prop 0-2)。 +// PlainTime 存午夜后纳秒(f64,prop 0)、PlainDateTime 存年/月/日与午夜后纳秒(prop 0-3), +// ZonedDateTime 存纪元纳秒、时区 ID、日历 ID(prop 0-2)。 const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000; @@ -62,6 +64,13 @@ fn ensure_plain_time(vm: &mut H, obj: &JsObject) -> Result<(), JsValu Ok(()) } +fn ensure_plain_date_time(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> { + if !obj.is_plain_date_time_obj() { + return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + Ok(()) +} + fn ensure_duration(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> { if !obj.is_duration_obj() { return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); @@ -1326,6 +1335,14 @@ fn make_plain_date(vm: &mut H, year: i32, month: u32, day: u32) -> Na NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) } +fn make_plain_time(vm: &mut H, total_ns: f64) -> NativeResult { + let proto = JsValue::from_js_object(vm.session().builtin_world().plain_time_proto.as_ptr() as *mut JsObject); + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); + obj.type_tag = JsObject::OBJ_TYPE_PLAIN_TIME; + obj.set_prop_at(0, JsValue::float(total_ns)); + NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) +} + fn make_duration(vm: &mut H, values: [f64; 10]) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().duration_proto.as_ptr() as *mut JsObject); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); @@ -2072,6 +2089,257 @@ pub fn plain_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult } } +// ───────────────────── PlainDateTime 基础方法 ───────────────────── + +/// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。 +pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_date_time_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainDateTime cannot be invoked without 'new'", + )); + } + + let mut component = |index: usize, default: f64| -> Result { + if args.len() > index { + let raw = vm.reg(args[index]); + temporal_option_number(vm, raw).map(f64::trunc) + } else { + Ok(default) + } + }; + let year_value = native_try!(component(1, f64::NAN)); + let month_value = native_try!(component(2, f64::NAN)); + let day_value = native_try!(component(3, f64::NAN)); + let hour_value = native_try!(component(4, 0.0)); + let minute_value = native_try!(component(5, 0.0)); + let second_value = native_try!(component(6, 0.0)); + let millisecond_value = native_try!(component(7, 0.0)); + let microsecond_value = native_try!(component(8, 0.0)); + let nanosecond_value = native_try!(component(9, 0.0)); + let components = [ + year_value, + month_value, + day_value, + hour_value, + minute_value, + second_value, + millisecond_value, + microsecond_value, + nanosecond_value, + ]; + if components.iter().any(|value| !value.is_finite()) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); + } + + let year = year_value as i32; + let month = month_value as u32; + let day = day_value as u32; + let hour = hour_value as u32; + let minute = minute_value as u32; + let second = second_value as u32; + let millisecond = millisecond_value as u32; + let microsecond = microsecond_value as u32; + let nanosecond = nanosecond_value as u32; + let non_negative = components[1..].iter().all(|value| *value >= 0.0); + if !non_negative + || !valid_iso_date(year, month, day) + || !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) + { + 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 total_ns = hour as f64 * 3_600_000_000_000.0 + + minute as f64 * 60_000_000_000.0 + + second as f64 * 1_000_000_000.0 + + millisecond as f64 * 1_000_000.0 + + microsecond as f64 * 1_000.0 + + nanosecond as f64; + initialize_temporal_receiver( + vm, + args, + JsObject::OBJ_TYPE_PLAIN_DATE_TIME, + [ + JsValue::float(year as f64), + JsValue::float(month as f64), + JsValue::float(day as f64), + JsValue::float(total_ns), + ], + ) +} + +fn plain_date_time_parts(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_date_time(vm, obj)?; + Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + get_double_prop(obj, 3), + )) +} + +macro_rules! plain_date_time_date_getter { + ($name:ident, $index:tt) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let parts = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(parts.$index as f64)) + } + }; +} + +plain_date_time_date_getter!(plain_date_time_year, 0); +plain_date_time_date_getter!(plain_date_time_month, 1); +plain_date_time_date_getter!(plain_date_time_day, 2); + +fn plain_date_time_naive(vm: &mut H, args: &[u8]) -> Result { + let (year, month, day, _) = plain_date_time_parts(vm, args)?; + NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) +} + +/// `Temporal.PlainDateTime.prototype.dayOfWeek` getter。 +pub fn plain_date_time_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.dayOfYear` getter。 +pub fn plain_date_time_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.ordinal() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.daysInMonth` getter。 +pub fn plain_date_time_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.daysInWeek` getter。 +pub fn plain_date_time_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(7.0)) +} + +/// `Temporal.PlainDateTime.prototype.daysInYear` getter。 +pub fn plain_date_time_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) +} + +/// `Temporal.PlainDateTime.prototype.monthsInYear` getter。 +pub fn plain_date_time_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(12.0)) +} + +/// `Temporal.PlainDateTime.prototype.inLeapYear` getter。 +pub fn plain_date_time_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) +} + +/// `Temporal.PlainDateTime.prototype.weekOfYear` getter。 +pub fn plain_date_time_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.yearOfWeek` getter。 +pub fn plain_date_time_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.monthCode` getter。 +pub fn plain_date_time_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, month, _, _) = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) +} + +/// ISO 日历没有可观察的 era 字段。 +pub fn plain_date_time_era(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// ISO 日历没有可观察的 eraYear 字段。 +pub fn plain_date_time_era_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +fn plain_date_time_time_get( + vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32, +) -> NativeResult { + let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); + let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns); + NativeResult::Ok(JsValue::float(select(hour, minute, second, millisecond, microsecond, nanosecond) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.hour` getter。 +pub fn plain_date_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |hour, _, _, _, _, _| hour) +} + +/// `Temporal.PlainDateTime.prototype.minute` getter。 +pub fn plain_date_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, minute, _, _, _, _| minute) +} + +/// `Temporal.PlainDateTime.prototype.second` getter。 +pub fn plain_date_time_second(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, second, _, _, _| second) +} + +/// `Temporal.PlainDateTime.prototype.millisecond` getter。 +pub fn plain_date_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, millisecond, _, _| millisecond) +} + +/// `Temporal.PlainDateTime.prototype.microsecond` getter。 +pub fn plain_date_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, _, microsecond, _| microsecond) +} + +/// `Temporal.PlainDateTime.prototype.nanosecond` getter。 +pub fn plain_date_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, _, _, nanosecond| nanosecond) +} + +/// `Temporal.PlainDateTime.prototype.calendarId` getter。 +pub fn plain_date_time_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(vm.new_string("iso8601")) +} + +/// 返回仅保留日期分量的新 `Temporal.PlainDate`。 +pub fn plain_date_time_to_plain_date(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); + make_plain_date(vm, year, month, day) +} + +/// 返回仅保留时间分量的新 `Temporal.PlainTime`。 +pub fn plain_date_time_to_plain_time(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); + make_plain_time(vm, total_ns) +} + +/// Temporal 对象禁止隐式转换为原始值。 +pub fn plain_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf")) +} + // ───────────────────── PlainDate 扩展方法 ───────────────────── /// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。 diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index e9edfe2..950569e 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -256,6 +256,8 @@ pub struct BuiltinWorld { pub duration_proto: P, pub zoned_date_time_constructor: P, pub zoned_date_time_proto: P, + pub plain_date_time_constructor: P, + pub plain_date_time_proto: P, pub bigint_constructor: P, pub bigint_proto: P, pub stub_objects: Vec>, @@ -511,10 +513,11 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { wire_ctor_proto(&world.plain_time_constructor, &world.plain_time_proto); wire_ctor_proto(&world.duration_constructor, &world.duration_proto); wire_ctor_proto(&world.zoned_date_time_constructor, &world.zoned_date_time_proto); + wire_ctor_proto(&world.plain_date_time_constructor, &world.plain_date_time_proto); wire_ctor_proto(&world.bigint_constructor, &world.bigint_proto); let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let non_object_protos: [&P; 20] = [ + let non_object_protos: [&P; 21] = [ &world.array_proto, &world.function_proto, &world.string_proto, @@ -534,6 +537,7 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { &world.plain_time_proto, &world.duration_proto, &world.zoned_date_time_proto, + &world.plain_date_time_proto, &world.bigint_proto, ]; for proto in &non_object_protos { @@ -674,6 +678,8 @@ impl BuiltinWorld { BuiltinId::DurationProto => &self.duration_proto, BuiltinId::ZonedDateTimeConstructor => &self.zoned_date_time_constructor, BuiltinId::ZonedDateTimeProto => &self.zoned_date_time_proto, + BuiltinId::PlainDateTimeConstructor => &self.plain_date_time_constructor, + BuiltinId::PlainDateTimeProto => &self.plain_date_time_proto, BuiltinId::BigIntConstructor => &self.bigint_constructor, BuiltinId::BigIntProto => &self.bigint_proto, } @@ -728,6 +734,8 @@ impl BuiltinWorld { let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration"); let (zoned_date_time_proto, zoned_date_time_constructor) = make_named_pair(string_forge, shape_forge, labels, "ZonedDateTime"); + let (plain_date_time_proto, plain_date_time_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainDateTime"); let (bigint_proto, bigint_constructor) = make_named_pair(string_forge, shape_forge, labels, "BigInt"); let stub_objects = Vec::new(); @@ -814,6 +822,8 @@ impl BuiltinWorld { duration_proto, zoned_date_time_constructor, zoned_date_time_proto, + plain_date_time_constructor, + plain_date_time_proto, bigint_constructor, bigint_proto, stub_objects, @@ -1006,6 +1016,8 @@ impl BuiltinWorld { duration_constructor, zoned_date_time_proto, zoned_date_time_constructor, + plain_date_time_proto, + plain_date_time_constructor, ) = if dirty.temporal { let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant"); let (plain_date_proto, plain_date_constructor) = @@ -1015,6 +1027,8 @@ impl BuiltinWorld { let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration"); let (zoned_date_time_proto, zoned_date_time_constructor) = make_named_pair(string_forge, shape_forge, labels, "ZonedDateTime"); + let (plain_date_time_proto, plain_date_time_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainDateTime"); ( P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -1028,6 +1042,8 @@ impl BuiltinWorld { duration_constructor, zoned_date_time_proto, zoned_date_time_constructor, + plain_date_time_proto, + plain_date_time_constructor, ) } else { ( @@ -1043,6 +1059,8 @@ impl BuiltinWorld { current.duration_constructor.clone(), current.zoned_date_time_proto.clone(), current.zoned_date_time_constructor.clone(), + current.plain_date_time_proto.clone(), + current.plain_date_time_constructor.clone(), ) }; let (bigint_proto, bigint_constructor) = if dirty.stubs { @@ -1135,6 +1153,8 @@ impl BuiltinWorld { duration_proto, zoned_date_time_constructor, zoned_date_time_proto, + plain_date_time_constructor, + plain_date_time_proto, bigint_constructor, bigint_proto, stub_objects, diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 107a402..b646e47 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -225,7 +225,7 @@ pub struct KernelSession { /// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及 /// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的 /// builtin 家族。 -pub const NUM_BUILTINS: usize = 83; +pub const NUM_BUILTINS: usize = 85; /// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。 /// @@ -317,6 +317,8 @@ pub enum BuiltinId { SymToStringTag = 80, ZonedDateTimeConstructor = 81, ZonedDateTimeProto = 82, + PlainDateTimeConstructor = 83, + PlainDateTimeProto = 84, } impl BuiltinId { @@ -405,6 +407,8 @@ impl BuiltinId { BuiltinId::SymToStringTag, BuiltinId::ZonedDateTimeConstructor, BuiltinId::ZonedDateTimeProto, + BuiltinId::PlainDateTimeConstructor, + BuiltinId::PlainDateTimeProto, ]; } @@ -642,7 +646,9 @@ impl KernelSession { || gen(BuiltinId::DurationConstructor) != snap(BuiltinId::DurationConstructor) || gen(BuiltinId::DurationProto) != snap(BuiltinId::DurationProto) || gen(BuiltinId::ZonedDateTimeConstructor) != snap(BuiltinId::ZonedDateTimeConstructor) - || gen(BuiltinId::ZonedDateTimeProto) != snap(BuiltinId::ZonedDateTimeProto), + || gen(BuiltinId::ZonedDateTimeProto) != snap(BuiltinId::ZonedDateTimeProto) + || gen(BuiltinId::PlainDateTimeConstructor) != snap(BuiltinId::PlainDateTimeConstructor) + || gen(BuiltinId::PlainDateTimeProto) != snap(BuiltinId::PlainDateTimeProto), stubs: world.stub_objects.len() != snapshot.stub_objects_len || stub_generations_dirty || gen(BuiltinId::BigIntConstructor) != snap(BuiltinId::BigIntConstructor) diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 04a393e..9972d9a 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -447,6 +447,8 @@ impl JsObject { pub const OBJ_TYPE_DURATION: u8 = 17; /// Temporal.ZonedDateTime 对象:纪元纳秒、时区 ID、日历 ID 存于 prop 0-2。 pub const OBJ_TYPE_ZONED_DATE_TIME: u8 = 18; + /// Temporal.PlainDateTime 对象:ISO 年/月/日与午夜后纳秒存于 prop 0-3。 + pub const OBJ_TYPE_PLAIN_DATE_TIME: u8 = 19; /// `is_session_epoch` 字段中的 session 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -537,6 +539,11 @@ impl JsObject { pub fn is_zoned_date_time_obj(&self) -> bool { self.type_tag == Self::OBJ_TYPE_ZONED_DATE_TIME } + /// 是否 Temporal.PlainDateTime 对象。 + #[inline] + pub fn is_plain_date_time_obj(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_PLAIN_DATE_TIME + } /// 构造无属性、可扩展的空对象(`new Object()` 的基础对象)。 pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index c256199..e29a165 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -377,6 +377,129 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m )], ); + // Temporal.PlainDateTime:ISO 日期时间分量、拆分转换与禁止隐式原始值转换。 + let plain_date_time_ctor_ptr = world.plain_date_time_constructor.as_ptr() as *mut JsObject; + let plain_date_time_ctor = unsafe { &mut *plain_date_time_ctor_ptr }; + configure_native_constructor( + plain_date_time_ctor, + oxide_builtins::temporal::plain_date_time_constructor:: as *const (), + 3, + ); + let length_si = core.perm_interner().intern("length").0; + let length_shape = core.shape_forge().make_shape(plain_date_time_ctor.shape_id(), length_si); + plain_date_time_ctor.set_shape_id(length_shape); + plain_date_time_ctor.ensure_hash_props().push(JsValue::int(3)); + let length_pos = plain_date_time_ctor + .hash_props_vec() + .map_or(0, |props| props.len() as u32) + .saturating_sub(1); + plain_date_time_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true)); + let plain_date_time_proto_ptr = world.plain_date_time_proto.as_ptr() as *mut JsObject; + let plain_date_time_proto = unsafe { &mut *plain_date_time_proto_ptr }; + for (name, getter) in [ + ("year", oxide_builtins::temporal::plain_date_time_year:: as *const ()), + ("month", oxide_builtins::temporal::plain_date_time_month:: as *const ()), + ("day", oxide_builtins::temporal::plain_date_time_day:: as *const ()), + ( + "dayOfWeek", + oxide_builtins::temporal::plain_date_time_day_of_week:: as *const (), + ), + ( + "dayOfYear", + oxide_builtins::temporal::plain_date_time_day_of_year:: as *const (), + ), + ( + "daysInMonth", + oxide_builtins::temporal::plain_date_time_days_in_month:: as *const (), + ), + ( + "daysInWeek", + oxide_builtins::temporal::plain_date_time_days_in_week:: as *const (), + ), + ( + "daysInYear", + oxide_builtins::temporal::plain_date_time_days_in_year:: as *const (), + ), + ( + "monthsInYear", + oxide_builtins::temporal::plain_date_time_months_in_year:: as *const (), + ), + ( + "inLeapYear", + oxide_builtins::temporal::plain_date_time_in_leap_year:: as *const (), + ), + ( + "weekOfYear", + oxide_builtins::temporal::plain_date_time_week_of_year:: as *const (), + ), + ( + "yearOfWeek", + oxide_builtins::temporal::plain_date_time_year_of_week:: as *const (), + ), + ( + "monthCode", + oxide_builtins::temporal::plain_date_time_month_code:: as *const (), + ), + ("era", oxide_builtins::temporal::plain_date_time_era:: as *const ()), + ( + "eraYear", + oxide_builtins::temporal::plain_date_time_era_year:: as *const (), + ), + ("hour", oxide_builtins::temporal::plain_date_time_hour:: as *const ()), + ("minute", oxide_builtins::temporal::plain_date_time_minute:: as *const ()), + ("second", oxide_builtins::temporal::plain_date_time_second:: as *const ()), + ( + "millisecond", + oxide_builtins::temporal::plain_date_time_millisecond:: as *const (), + ), + ( + "microsecond", + oxide_builtins::temporal::plain_date_time_microsecond:: as *const (), + ), + ( + "nanosecond", + oxide_builtins::temporal::plain_date_time_nanosecond:: as *const (), + ), + ( + "calendarId", + oxide_builtins::temporal::plain_date_time_calendar_id:: as *const (), + ), + ] { + bind_accessor_getter(core, session, plain_date_time_proto, name, getter); + } + apply_binding_table( + world, + plain_date_time_proto, + core, + &[ + ( + "toPlainDate", + oxide_builtins::temporal::plain_date_time_to_plain_date:: as *const (), + 0, + ), + ( + "toPlainTime", + oxide_builtins::temporal::plain_date_time_to_plain_time:: as *const (), + 0, + ), + ( + "valueOf", + oxide_builtins::temporal::plain_date_time_value_of:: as *const (), + 0, + ), + ], + ); + bind_well_known_data_property( + core, + plain_date_time_proto, + 9, + JsValue::perm_string( + core.perm_interner() + .string_ptr(core.perm_interner().intern("Temporal.PlainDateTime").0), + ), + PropAttributes::new(false, false, true), + ); + // Temporal.Duration:构造器、from、分量 getter 与 ISO 字符串转换。 let duration_ctor_ptr = world.duration_constructor.as_ptr() as *mut JsObject; let duration_ctor = unsafe { &mut *duration_ctor_ptr }; @@ -435,6 +558,7 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m bind_global_value(core, temporal, "Instant", JsValue::from_js_object(instant_ctor_ptr)); bind_global_value(core, temporal, "PlainDate", JsValue::from_js_object(plain_date_ctor_ptr)); bind_global_value(core, temporal, "PlainTime", JsValue::from_js_object(plain_time_ctor_ptr)); + bind_global_value(core, temporal, "PlainDateTime", JsValue::from_js_object(plain_date_time_ctor_ptr)); bind_global_value(core, temporal, "Duration", JsValue::from_js_object(duration_ctor_ptr)); bind_global_value(core, temporal, "ZonedDateTime", JsValue::from_js_object(zoned_date_time_ctor_ptr)); bind_global_value(core, global, "Temporal", JsValue::from_js_object(temporal_ptr)); diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index d22c3ad..84cfe16 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -616,3 +616,73 @@ fn plain_time_range_validation() { let r = eval(&mut vm, "try { new Temporal.PlainTime(0, 60, 0) } catch (e) { e.constructor.name }").unwrap(); assert_eq!(str_val(&vm, r), "RangeError"); } + +// -- Temporal.PlainDateTime -- + +#[test] +fn plain_date_time_constructor_and_components() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "let dt = new Temporal.PlainDateTime(2024, 2, 29, 23, 58, 57, 987, 654, 321); + dt.year + ',' + dt.month + ',' + dt.day + ',' + dt.hour + ',' + dt.minute + ',' + + dt.second + ',' + dt.millisecond + ',' + dt.microsecond + ',' + dt.nanosecond + ',' + dt.calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "2024,2,29,23,58,57,987,654,321,iso8601"); +} + +#[test] +fn plain_date_time_defaults_time_to_midnight() { + let mut vm = Vm::new(); + assert_eq!( + num( + &mut vm, + "let dt = new Temporal.PlainDateTime(2024, 7, 6); dt.hour + dt.minute + dt.second + dt.nanosecond", + ), + 0.0 + ); +} + +#[test] +fn plain_date_time_splits_into_plain_date_and_time() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "let dt = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 789, 123, 456); + dt.toPlainDate().toString() + 'T' + dt.toPlainTime().toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "2000-05-02T12:34:56.789123456"); +} + +#[test] +fn plain_date_time_validates_brand_and_ranges() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "let getter = Object.getOwnPropertyDescriptor(Temporal.PlainDateTime.prototype, 'year').get; + let a; let b; let c; + try { getter.call({}) } catch (e) { a = e.constructor.name } + try { new Temporal.PlainDateTime(2023, 2, 29) } catch (e) { b = e.constructor.name } + try { new Temporal.PlainDateTime(2024, 1, 1, 24) } catch (e) { c = e.constructor.name } + a + ',' + b + ',' + c", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError,RangeError,RangeError"); +} + +#[test] +fn plain_date_time_metadata_and_value_of() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "let dt = new Temporal.PlainDateTime(2024, 1, 2); + let error; + try { dt.valueOf() } catch (e) { error = e.constructor.name } + Temporal.PlainDateTime.length + ',' + (dt instanceof Temporal.PlainDateTime) + ',' + + Object.prototype.toString.call(dt) + ',' + error", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "3,true,[object Temporal.PlainDateTime],TypeError"); +}