From e33e7a1eb7f22a324c12a9b42fd8b16138a743d4 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 03:42:48 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20PlainMonthDay/PlainYearMonth=20?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=9C=B0=E5=9F=BA=E2=80=94=E2=80=94=E6=9E=84?= =?UTF-8?q?=E9=80=A0=E5=99=A8/getter/toString/toJSON=20=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E4=B8=8E=20BuiltinWorld=20=E6=8E=A5=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 421 +++++++++++++++++- crates/oxide_kernel/src/builtin.rs | 50 ++- crates/oxide_kernel/src/kernel.rs | 16 +- crates/oxide_types/src/object.rs | 14 + crates/oxide_vm/src/bindings/bind_temporal.rs | 146 ++++++ .../oxide_vm/tests/builtins_temporal_tests.rs | 193 ++++++++ 6 files changed, 834 insertions(+), 6 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 0ff5de06..e8b4b2e0 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -7,10 +7,13 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; // Temporal 命名空间的最小实现子集:Temporal.Now / Temporal.Instant / -// Temporal.PlainDate / Temporal.PlainTime / Temporal.PlainDateTime / Temporal.ZonedDateTime。 +// Temporal.PlainDate / Temporal.PlainTime / Temporal.PlainDateTime / +// Temporal.PlainMonthDay / Temporal.PlainYearMonth / Temporal.ZonedDateTime。 // 内部数据按对象类型存入 prop 槽: // Instant 存纪元纳秒(BigInt,prop 0)、PlainDate 存年/月/日/日历 ID(prop 0-3)、 -// PlainTime 存午夜后纳秒(f64,prop 0)、PlainDateTime 存年/月/日/午夜后纳秒/日历 ID(prop 0-4), +// PlainTime 存午夜后纳秒(f64,prop 0)、PlainDateTime 存年/月/日/午夜后纳秒/日历 ID(prop 0-4)、 +// PlainMonthDay 存月/日/参考年/日历 ID(prop 0-3)、 +// PlainYearMonth 存年/月/参考日/日历 ID(prop 0-3)、 // ZonedDateTime 存纪元纳秒、时区 ID、日历 ID(prop 0-2)。 // 日历 ID 未显式给定时统一取 "iso8601"。 @@ -2089,6 +2092,8 @@ fn reject_partial_object_with_calendar_or_time_zone(vm: &mut H, value || obj.is_plain_date_time_obj() || obj.is_plain_time_obj() || obj.is_zoned_date_time_obj() + || obj.is_plain_month_day_obj() + || obj.is_plain_year_month_obj() { return Err(crate::error::create_type_error(vm, "invalid argument")); } @@ -5563,6 +5568,9 @@ fn temporal_calendar_id(vm: &mut H, value: JsValue) -> Result(vm: &mut H, args: &[u8]) -> NativeResult { ) } +// Temporal.PlainMonthDay / Temporal.PlainYearMonth 对象地基: +// 数字分量转换、槽对象构造、构造器与 getter/toString/toJSON。 + +/// Temporal 数字分量转换(number-only 路径):先 ToPrimitive(Number)——对象经 +/// valueOf/toString 取值;Symbol/BigInt → TypeError;NaN/±Inf → RangeError +/// (undefined 与不可解析串都归 NaN,由此统一抛 RangeError);其余截断取整。 +fn temporal_number_component(vm: &mut H, value: JsValue) -> Result { + let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::Number, vm) + .map_err(|error| native_engine_error(vm, &error))?; + if primitive.is_symbol() || primitive.is_bigint() { + return Err(crate::error::create_type_error(vm, "invalid number")); + } + let number = to_number(primitive); + if number.is_infinite() || number.is_nan() { + return Err(crate::error::create_range_error(vm, "invalid number")); + } + Ok(number.trunc()) +} + +/// ISO 年-月是否落在 PlainYearMonth 可表示范围(ISOYearMonthWithinLimits): +/// 仅 -271821 年 3 月及以前、+275760 年 10 月及以后越界。 +fn iso_year_month_within_limits(year: i32, month: u32) -> bool { + !((year == -271821 && month < 4) || (year == 275760 && month > 9)) +} + +/// 构造 PlainMonthDay 实例对象(槽 0-3 = 月/日/参考年/日历 ID)。 +/// from/toPlainDate 等返回新对象的成员使用;构造器走 receiver 初始化路径。 +#[expect(dead_code)] +fn make_plain_month_day(vm: &mut H, month: u32, day: u32, ref_year: i32, calendar: &str) -> NativeResult { + let proto = JsValue::from_js_object(vm.session().builtin_world().plain_month_day_proto.as_ptr() as *mut JsObject); + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); + obj.type_tag = JsObject::OBJ_TYPE_PLAIN_MONTH_DAY; + obj.set_prop_at(0, JsValue::float(month as f64)); + obj.set_prop_at(1, JsValue::float(day as f64)); + obj.set_prop_at(2, JsValue::float(ref_year as f64)); + obj.set_prop_at(3, vm.new_string(calendar)); + NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) +} + +/// 构造 PlainYearMonth 实例对象(槽 0-3 = 年/月/参考日/日历 ID)。 +/// from/toPlainDate 等返回新对象的成员使用;构造器走 receiver 初始化路径。 +#[expect(dead_code)] +fn make_plain_year_month(vm: &mut H, year: i32, month: u32, ref_day: u32, calendar: &str) -> NativeResult { + let proto = JsValue::from_js_object(vm.session().builtin_world().plain_year_month_proto.as_ptr() as *mut JsObject); + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); + obj.type_tag = JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH; + obj.set_prop_at(0, JsValue::float(year as f64)); + obj.set_prop_at(1, JsValue::float(month as f64)); + obj.set_prop_at(2, JsValue::float(ref_day as f64)); + obj.set_prop_at(3, vm.new_string(calendar)); + NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) +} + +fn ensure_plain_month_day(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> { + if !obj.is_plain_month_day_obj() { + return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + Ok(()) +} + +fn ensure_plain_year_month(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> { + if !obj.is_plain_year_month_obj() { + return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + Ok(()) +} + +/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。 +fn plain_month_day_mdy(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_month_day(vm, obj)?; + Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) +} + +/// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。 +fn plain_year_month_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_year_month(vm, obj)?; + Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) +} + +/// 构造器日历参数解析(ToTemporalCalendarSlotValue,缺省 "iso8601"): +/// undefined → 缺省;字符串 → 严格白名单(非法 RangeError);Temporal 日期实例 → +/// 日历槽直读(不触发属性 getter);函数对象 → 缺省(无日历行为); +/// 其他对象与原始值 → TypeError。 +fn temporal_constructor_calendar_id(vm: &mut H, value: JsValue) -> Result { + if value.is_undefined() { + return Ok("iso8601".to_string()); + } + if value.is_string() { + return temporal_calendar_id_strict(vm, value) + .map(|calendar| calendar.unwrap_or_else(|| "iso8601".to_string())); + } + if value.is_object() { + let ptr = value.as_js_object_ptr(); + if !ptr.is_null() { + let obj = unsafe { &*ptr }; + if obj.is_function() { + return Ok("iso8601".to_string()); + } + let slot = if obj.is_plain_date_obj() { + 3 + } else if obj.is_plain_date_time_obj() { + 4 + } else if obj.is_zoned_date_time_obj() { + 2 + } else if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() { + 3 + } else { + return Err(crate::error::create_type_error(vm, "invalid calendar")); + }; + return Ok(get_calendar_id(obj, slot)); + } + } + Err(crate::error::create_type_error(vm, "invalid calendar")) +} + +/// `Temporal.PlainMonthDay` 构造器:`new PlainMonthDay(month, day[, calendar[, referenceISOYear]])`。 +pub fn plain_month_day_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_month_day_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainMonthDay cannot be invoked without 'new'", + )); + } + // 转换序:month → day → calendar → referenceISOYear(后两者有缺省值)。 + let month = native_try!(temporal_number_component( + vm, + if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }, + )); + let day = native_try!(temporal_number_component( + vm, + if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }, + )); + let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() }; + let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw)); + let ref_year = if args.len() > 4 && !vm.reg(args[4]).is_undefined() { + native_try!(temporal_number_component(vm, vm.reg(args[4]))) as i32 + } else { + 1972 + }; + if !valid_iso_date(ref_year, month as u32, day as u32) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 + let day_count = days_from_civil(i128::from(ref_year), i128::from(month as i32), i128::from(day as i32)); + 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_value = vm.new_string(&calendar); + initialize_temporal_receiver( + vm, + args, + JsObject::OBJ_TYPE_PLAIN_MONTH_DAY, + [ + JsValue::float(month), + JsValue::float(day), + JsValue::float(ref_year as f64), + calendar_value, + ], + ) +} + +/// `Temporal.PlainYearMonth` 构造器:`new PlainYearMonth(year, month[, calendar[, referenceISODay]])`。 +pub fn plain_year_month_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_year_month_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainYearMonth cannot be invoked without 'new'", + )); + } + // 转换序:year → month → calendar → referenceISODay(后两者有缺省值)。 + let year = native_try!(temporal_number_component( + vm, + if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }, + )) as i32; + let month = native_try!(temporal_number_component( + vm, + if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }, + )); + let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() }; + let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw)); + let ref_day = if args.len() > 4 && !vm.reg(args[4]).is_undefined() { + native_try!(temporal_number_component(vm, vm.reg(args[4]))) + } else { + 1.0 + }; + let month_i = month as i32; + if !(1..=12).contains(&month_i) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + let ref_day_i = ref_day as i32; + if ref_day_i < 1 || ref_day_i > days_in_month_iso(year, month_i as u32) as i32 { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + if !iso_year_month_within_limits(year, month_i as u32) { + return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); + } + let calendar_value = vm.new_string(&calendar); + initialize_temporal_receiver( + vm, + args, + JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH, + [ + JsValue::float(year as f64), + JsValue::float(month), + JsValue::float(ref_day), + calendar_value, + ], + ) +} + +/// `Temporal.PlainMonthDay.prototype.day` getter(槽 1)。 +pub fn plain_month_day_day(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, day, _) = native_try!(plain_month_day_mdy(vm, args)); + NativeResult::Ok(JsValue::float(day)) +} + +/// `Temporal.PlainMonthDay.prototype.monthCode` getter:`M01`..`M12`。 +pub fn plain_month_day_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let (month, _, _) = native_try!(plain_month_day_mdy(vm, args)); + NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32))) +} + +/// `Temporal.PlainMonthDay.prototype.calendarId` getter:读日历槽(槽 3)。 +pub fn plain_month_day_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_month_day(vm, obj)); + NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3))) +} + +/// `Temporal.PlainYearMonth.prototype.year` getter(槽 0)。 +pub fn plain_year_month_year(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, _, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::float(year)) +} + +/// `Temporal.PlainYearMonth.prototype.month` getter(槽 1)。 +pub fn plain_year_month_month(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, month, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::float(month)) +} + +/// `Temporal.PlainYearMonth.prototype.monthCode` getter:`M01`..`M12`。 +pub fn plain_year_month_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, month, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32))) +} + +/// `Temporal.PlainYearMonth.prototype.calendarId` getter:读日历槽(槽 3)。 +pub fn plain_year_month_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_year_month(vm, obj)); + NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3))) +} + +/// `Temporal.PlainYearMonth.prototype.daysInMonth` getter:按年月的 ISO 月长。 +pub fn plain_year_month_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, month, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::float(days_in_month_iso(year as i32, month as u32) as f64)) +} + +/// `Temporal.PlainYearMonth.prototype.daysInYear` getter:366(闰年)或 365。 +pub fn plain_year_month_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, _, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::float(if is_leap_year_iso(year as i32) { 366.0 } else { 365.0 })) +} + +/// `Temporal.PlainYearMonth.prototype.monthsInYear` getter:恒 12(ISO 日历)。 +pub fn plain_year_month_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::float(12.0)) +} + +/// `Temporal.PlainYearMonth.prototype.inLeapYear` getter。 +pub fn plain_year_month_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, _, _) = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::bool(is_leap_year_iso(year as i32))) +} + +/// `Temporal.PlainYearMonth.prototype.era` getter:ISO 日历无纪元,恒 undefined。 +pub fn plain_year_month_era(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// `Temporal.PlainYearMonth.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。 +pub fn plain_year_month_era_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// toString 的 calendarName 选项取值(auto/never 同默认形)。 +enum ShowCalendar { + Omitted, + Always, + Critical, +} + +/// 读并校验 toString 的 calendarName 选项:options 非对象 → TypeError; +/// 值非字符串先 ToPrimitive(String) 转换(Symbol → TypeError); +/// 仅 auto/always/never/critical 合法(大小写敏感),否则 RangeError。 +fn temporal_to_show_calendar(vm: &mut H, args: &[u8]) -> Result { + let options = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if options.is_undefined() { + return Ok(ShowCalendar::Omitted); + } + if !options.is_object() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let ptr = options.as_js_object_ptr(); + if ptr.is_null() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let raw = temporal_option_value(vm, unsafe { &*ptr }, options, "calendarName")?; + if raw.is_undefined() { + return Ok(ShowCalendar::Omitted); + } + let value = temporal_option_string(vm, raw)?; + match value.as_str() { + "always" => Ok(ShowCalendar::Always), + "critical" => Ok(ShowCalendar::Critical), + "auto" | "never" => Ok(ShowCalendar::Omitted), + _ => Err(crate::error::create_range_error(vm, "invalid calendarName")), + } +} + +/// PlainMonthDay 默认形串(`MM-DD`),含 receiver 校验,不读 options。 +fn plain_month_day_default_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_month_day(vm, obj)); + let month = get_double_prop(obj, 0) as i32; + let day = get_double_prop(obj, 1) as i32; + NativeResult::Ok(vm.new_string_owned(format!("{month:02}-{day:02}"))) +} + +/// `Temporal.PlainMonthDay.prototype.toString([options])`: +/// 默认 `MM-DD`;always/critical 补参考年与 `[u-ca=…]`/`[!u-ca=…]` 注解。 +pub fn plain_month_day_to_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_month_day(vm, obj)); + let show = native_try!(temporal_to_show_calendar(vm, args)); + if matches!(show, ShowCalendar::Omitted) { + return plain_month_day_default_string(vm, args); + } + let month = get_double_prop(obj, 0) as i32; + let day = get_double_prop(obj, 1) as i32; + let ref_year = get_double_prop(obj, 2) as i32; + let calendar = get_calendar_id(obj, 3); + let annotation = match show { + ShowCalendar::Always => format!("[u-ca={calendar}]"), + _ => format!("[!u-ca={calendar}]"), + }; + NativeResult::Ok( + vm.new_string_owned(format!("{}-{month:02}-{day:02}{annotation}", format_iso_year(ref_year as i128))), + ) +} + +/// `Temporal.PlainMonthDay.prototype.toJSON()`:默认形串,忽略参数。 +pub fn plain_month_day_to_json(vm: &mut H, args: &[u8]) -> NativeResult { + plain_month_day_default_string(vm, args) +} + +/// PlainYearMonth 默认形串(`±YYYY-MM`),含 receiver 校验,不读 options。 +fn plain_year_month_default_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_year_month(vm, obj)); + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as i32; + NativeResult::Ok(vm.new_string_owned(format!("{}-{month:02}", format_iso_year(year as i128)))) +} + +/// `Temporal.PlainYearMonth.prototype.toString([options])`: +/// 默认 `±YYYY-MM`;always/critical 补参考日与 `[u-ca=…]`/`[!u-ca=…]` 注解。 +pub fn plain_year_month_to_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_year_month(vm, obj)); + let show = native_try!(temporal_to_show_calendar(vm, args)); + if matches!(show, ShowCalendar::Omitted) { + return plain_year_month_default_string(vm, args); + } + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as i32; + let ref_day = get_double_prop(obj, 2) as i32; + let calendar = get_calendar_id(obj, 3); + let annotation = match show { + ShowCalendar::Always => format!("[u-ca={calendar}]"), + _ => format!("[!u-ca={calendar}]"), + }; + NativeResult::Ok( + vm.new_string_owned(format!("{}-{month:02}-{ref_day:02}{annotation}", format_iso_year(year as i128))), + ) +} + +/// `Temporal.PlainYearMonth.prototype.toJSON()`:默认形串,忽略参数。 +pub fn plain_year_month_to_json(vm: &mut H, args: &[u8]) -> NativeResult { + plain_year_month_default_string(vm, args) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 9f56dc34..ce488dcf 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -285,6 +285,10 @@ pub struct BuiltinWorld { pub zoned_date_time_proto: P, pub plain_date_time_constructor: P, pub plain_date_time_proto: P, + pub plain_month_day_constructor: P, + pub plain_month_day_proto: P, + pub plain_year_month_constructor: P, + pub plain_year_month_proto: P, pub bigint_constructor: P, pub bigint_proto: P, /// `%IteratorPrototype%`:各集合迭代器原型的公共祖先,持有 `@@iterator`(返回自身)。 @@ -600,6 +604,8 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { 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.plain_month_day_constructor, &world.plain_month_day_proto); + wire_ctor_proto(&world.plain_year_month_constructor, &world.plain_year_month_proto); wire_ctor_proto(&world.bigint_constructor, &world.bigint_proto); // 所有内置构造器的 [[Prototype]] 指向 %FunctionPrototype%(ECMA-262 §17: @@ -627,12 +633,14 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { &world.duration_constructor, &world.zoned_date_time_constructor, &world.plain_date_time_constructor, + &world.plain_month_day_constructor, + &world.plain_year_month_constructor, ] { set_proto_if_changed(ctor, fn_proto_val); } let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let non_object_protos: [&P; 23] = [ + let non_object_protos: [&P; 25] = [ &world.array_proto, &world.function_proto, &world.string_proto, @@ -653,6 +661,8 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { &world.duration_proto, &world.zoned_date_time_proto, &world.plain_date_time_proto, + &world.plain_month_day_proto, + &world.plain_year_month_proto, &world.bigint_proto, &world.disposable_stack_proto, &world.async_disposable_stack_proto, @@ -911,7 +921,7 @@ impl BuiltinWorld { /// session 收尾(`teardown_heap_data`)与选择性重建收尾(`retire_replaced`) /// 的 P 字段枚举唯一入口:`BuiltinWorld` 新增 P 字段须在此同步补一行,否则 /// 收尾时该字段属性区永久泄漏、重建重指/释放漏掉该字段。 - pub(crate) fn all_p_fields(&self) -> [&P; 100] { + pub(crate) fn all_p_fields(&self) -> [&P; 104] { [ &self.object_proto, &self.array_proto, @@ -1001,6 +1011,10 @@ impl BuiltinWorld { &self.zoned_date_time_proto, &self.plain_date_time_constructor, &self.plain_date_time_proto, + &self.plain_month_day_constructor, + &self.plain_month_day_proto, + &self.plain_year_month_constructor, + &self.plain_year_month_proto, &self.bigint_constructor, &self.bigint_proto, &self.iterator_proto, @@ -1148,6 +1162,10 @@ impl BuiltinWorld { BuiltinId::ZonedDateTimeProto => &self.zoned_date_time_proto, BuiltinId::PlainDateTimeConstructor => &self.plain_date_time_constructor, BuiltinId::PlainDateTimeProto => &self.plain_date_time_proto, + BuiltinId::PlainMonthDayConstructor => &self.plain_month_day_constructor, + BuiltinId::PlainMonthDayProto => &self.plain_month_day_proto, + BuiltinId::PlainYearMonthConstructor => &self.plain_year_month_constructor, + BuiltinId::PlainYearMonthProto => &self.plain_year_month_proto, BuiltinId::BigIntConstructor => &self.bigint_constructor, BuiltinId::BigIntProto => &self.bigint_proto, BuiltinId::Console => &self.console_object, @@ -1210,6 +1228,10 @@ impl BuiltinWorld { 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 (plain_month_day_proto, plain_month_day_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainMonthDay"); + let (plain_year_month_proto, plain_year_month_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainYearMonth"); let (bigint_proto, bigint_constructor) = make_named_pair(string_forge, shape_forge, labels, "BigInt"); let stub_objects = Vec::new(); @@ -1314,6 +1336,10 @@ impl BuiltinWorld { zoned_date_time_proto, plain_date_time_constructor, plain_date_time_proto, + plain_month_day_constructor, + plain_month_day_proto, + plain_year_month_constructor, + plain_year_month_proto, bigint_constructor, bigint_proto, iterator_proto, @@ -1548,6 +1574,10 @@ impl BuiltinWorld { zoned_date_time_constructor, plain_date_time_proto, plain_date_time_constructor, + plain_month_day_proto, + plain_month_day_constructor, + plain_year_month_proto, + plain_year_month_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) = @@ -1559,6 +1589,10 @@ impl BuiltinWorld { 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 (plain_month_day_proto, plain_month_day_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainMonthDay"); + let (plain_year_month_proto, plain_year_month_constructor) = + make_named_pair(string_forge, shape_forge, labels, "PlainYearMonth"); ( P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -1574,6 +1608,10 @@ impl BuiltinWorld { zoned_date_time_constructor, plain_date_time_proto, plain_date_time_constructor, + plain_month_day_proto, + plain_month_day_constructor, + plain_year_month_proto, + plain_year_month_constructor, ) } else { ( @@ -1591,6 +1629,10 @@ impl BuiltinWorld { current.zoned_date_time_constructor.clone(), current.plain_date_time_proto.clone(), current.plain_date_time_constructor.clone(), + current.plain_month_day_proto.clone(), + current.plain_month_day_constructor.clone(), + current.plain_year_month_proto.clone(), + current.plain_year_month_constructor.clone(), ) }; let (bigint_proto, bigint_constructor) = if dirty.stubs { @@ -1732,6 +1774,10 @@ impl BuiltinWorld { zoned_date_time_proto, plain_date_time_constructor, plain_date_time_proto, + plain_month_day_constructor, + plain_month_day_proto, + plain_year_month_constructor, + plain_year_month_proto, bigint_constructor, bigint_proto, iterator_proto, diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 89f04ecf..87400417 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -318,7 +318,7 @@ pub struct KernelSession { /// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及 /// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的 /// builtin 家族。 -pub const NUM_BUILTINS: usize = 90; +pub const NUM_BUILTINS: usize = 94; /// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。 /// @@ -418,6 +418,10 @@ pub enum BuiltinId { SymDispose = 87, SuppressedErrorProto = 88, Console = 89, + PlainMonthDayConstructor = 90, + PlainMonthDayProto = 91, + PlainYearMonthConstructor = 92, + PlainYearMonthProto = 93, } impl BuiltinId { @@ -515,6 +519,10 @@ impl BuiltinId { BuiltinId::SymDispose, BuiltinId::SuppressedErrorProto, BuiltinId::Console, + BuiltinId::PlainMonthDayConstructor, + BuiltinId::PlainMonthDayProto, + BuiltinId::PlainYearMonthConstructor, + BuiltinId::PlainYearMonthProto, ]; } @@ -776,7 +784,11 @@ impl KernelSession { || gen(BuiltinId::ZonedDateTimeConstructor) != snap(BuiltinId::ZonedDateTimeConstructor) || gen(BuiltinId::ZonedDateTimeProto) != snap(BuiltinId::ZonedDateTimeProto) || gen(BuiltinId::PlainDateTimeConstructor) != snap(BuiltinId::PlainDateTimeConstructor) - || gen(BuiltinId::PlainDateTimeProto) != snap(BuiltinId::PlainDateTimeProto), + || gen(BuiltinId::PlainDateTimeProto) != snap(BuiltinId::PlainDateTimeProto) + || gen(BuiltinId::PlainMonthDayConstructor) != snap(BuiltinId::PlainMonthDayConstructor) + || gen(BuiltinId::PlainMonthDayProto) != snap(BuiltinId::PlainMonthDayProto) + || gen(BuiltinId::PlainYearMonthConstructor) != snap(BuiltinId::PlainYearMonthConstructor) + || gen(BuiltinId::PlainYearMonthProto) != snap(BuiltinId::PlainYearMonthProto), 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 a540b36e..9cbdc592 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -661,6 +661,10 @@ impl JsObject { pub const OBJ_TYPE_DISPOSABLE_STACK: u8 = 24; /// AsyncDisposableStack 对象(异步资源栈):状态盒与同步栈同构,预留 type_tag。 pub const OBJ_TYPE_ASYNC_DISPOSABLE_STACK: u8 = 25; + /// Temporal.PlainMonthDay 对象:月/日/参考年/日历 ID 存于 prop 0-3。 + pub const OBJ_TYPE_PLAIN_MONTH_DAY: u8 = 26; + /// Temporal.PlainYearMonth 对象:年/月/参考日/日历 ID 存于 prop 0-3。 + pub const OBJ_TYPE_PLAIN_YEAR_MONTH: u8 = 27; /// `is_session_epoch` 字段中的 session 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -764,6 +768,16 @@ impl JsObject { pub fn is_plain_date_time_obj(&self) -> bool { self.type_tag == Self::OBJ_TYPE_PLAIN_DATE_TIME } + /// 是否 Temporal.PlainMonthDay 对象。 + #[inline] + pub fn is_plain_month_day_obj(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_PLAIN_MONTH_DAY + } + /// 是否 Temporal.PlainYearMonth 对象。 + #[inline] + pub fn is_plain_year_month_obj(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_PLAIN_YEAR_MONTH + } #[inline] pub fn is_arguments_obj(&self) -> bool { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index 005d7f4e..2636ec37 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -957,6 +957,150 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m PropAttributes::new(false, false, true), ); + // Temporal.PlainMonthDay:构造器、3 个分量 getter 与 toString/toJSON。 + let plain_month_day_ctor_ptr = world.plain_month_day_constructor.as_ptr() as *mut JsObject; + let plain_month_day_ctor = unsafe { &mut *plain_month_day_ctor_ptr }; + configure_native_constructor( + plain_month_day_ctor, + oxide_builtins::temporal::plain_month_day_constructor:: as *const (), + 2, + ); + let length_si = core.perm_interner().intern("length").0; + let length_shape = core.shape_forge().make_shape(plain_month_day_ctor.shape_id(), length_si); + plain_month_day_ctor.set_shape_id(length_shape); + plain_month_day_ctor.ensure_hash_props().push(JsValue::int(2)); + let length_pos = plain_month_day_ctor + .hash_props_vec() + .map_or(0, |props| props.len() as u32) + .saturating_sub(1); + plain_month_day_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true)); + + let plain_month_day_proto_ptr = world.plain_month_day_proto.as_ptr() as *mut JsObject; + let plain_month_day_proto = unsafe { &mut *plain_month_day_proto_ptr }; + for (name, getter) in [ + ("day", oxide_builtins::temporal::plain_month_day_day:: as *const ()), + ( + "monthCode", + oxide_builtins::temporal::plain_month_day_month_code:: as *const (), + ), + ( + "calendarId", + oxide_builtins::temporal::plain_month_day_calendar_id:: as *const (), + ), + ] { + bind_accessor_getter(core, session, plain_month_day_proto, name, getter); + } + apply_binding_table( + world, + plain_month_day_proto, + core, + &[ + ( + "toString", + oxide_builtins::temporal::plain_month_day_to_string:: as *const (), + 0, + ), + ( + "toJSON", + oxide_builtins::temporal::plain_month_day_to_json:: as *const (), + 0, + ), + ], + ); + bind_well_known_data_property( + core, + plain_month_day_proto, + 9, + JsValue::perm_string( + core.perm_interner() + .string_ptr(core.perm_interner().intern("Temporal.PlainMonthDay").0), + ), + PropAttributes::new(false, false, true), + ); + + // Temporal.PlainYearMonth:构造器、10 个 getter 与 toString/toJSON。 + let plain_year_month_ctor_ptr = world.plain_year_month_constructor.as_ptr() as *mut JsObject; + let plain_year_month_ctor = unsafe { &mut *plain_year_month_ctor_ptr }; + configure_native_constructor( + plain_year_month_ctor, + oxide_builtins::temporal::plain_year_month_constructor:: as *const (), + 2, + ); + let length_si = core.perm_interner().intern("length").0; + let length_shape = core.shape_forge().make_shape(plain_year_month_ctor.shape_id(), length_si); + plain_year_month_ctor.set_shape_id(length_shape); + plain_year_month_ctor.ensure_hash_props().push(JsValue::int(2)); + let length_pos = plain_year_month_ctor + .hash_props_vec() + .map_or(0, |props| props.len() as u32) + .saturating_sub(1); + plain_year_month_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true)); + + let plain_year_month_proto_ptr = world.plain_year_month_proto.as_ptr() as *mut JsObject; + let plain_year_month_proto = unsafe { &mut *plain_year_month_proto_ptr }; + for (name, getter) in [ + ("year", oxide_builtins::temporal::plain_year_month_year:: as *const ()), + ("month", oxide_builtins::temporal::plain_year_month_month:: as *const ()), + ( + "monthCode", + oxide_builtins::temporal::plain_year_month_month_code:: as *const (), + ), + ( + "calendarId", + oxide_builtins::temporal::plain_year_month_calendar_id:: as *const (), + ), + ( + "daysInMonth", + oxide_builtins::temporal::plain_year_month_days_in_month:: as *const (), + ), + ( + "daysInYear", + oxide_builtins::temporal::plain_year_month_days_in_year:: as *const (), + ), + ( + "monthsInYear", + oxide_builtins::temporal::plain_year_month_months_in_year:: as *const (), + ), + ( + "inLeapYear", + oxide_builtins::temporal::plain_year_month_in_leap_year:: as *const (), + ), + ("era", oxide_builtins::temporal::plain_year_month_era:: as *const ()), + ( + "eraYear", + oxide_builtins::temporal::plain_year_month_era_year:: as *const (), + ), + ] { + bind_accessor_getter(core, session, plain_year_month_proto, name, getter); + } + apply_binding_table( + world, + plain_year_month_proto, + core, + &[ + ( + "toString", + oxide_builtins::temporal::plain_year_month_to_string:: as *const (), + 0, + ), + ( + "toJSON", + oxide_builtins::temporal::plain_year_month_to_json:: as *const (), + 0, + ), + ], + ); + bind_well_known_data_property( + core, + plain_year_month_proto, + 9, + JsValue::perm_string( + core.perm_interner() + .string_ptr(core.perm_interner().intern("Temporal.PlainYearMonth").0), + ), + PropAttributes::new(false, false, true), + ); + // 把子对象挂到 Temporal 命名空间对象上,再把 Temporal 挂到 global。 bind_global_value(core, temporal, "Now", JsValue::from_js_object(now_ptr)); bind_global_value(core, temporal, "Instant", JsValue::from_js_object(instant_ctor_ptr)); @@ -965,5 +1109,7 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m 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, temporal, "PlainMonthDay", JsValue::from_js_object(plain_month_day_ctor_ptr)); + bind_global_value(core, temporal, "PlainYearMonth", JsValue::from_js_object(plain_year_month_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 ca97f09d..9651db1d 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -2604,3 +2604,196 @@ fn duration_total_relative_zdt() { .unwrap(); assert_eq!(r.as_double(), 1.0); } + +// -- Temporal.PlainMonthDay / PlainYearMonth(E 批)-- + +#[test] +fn plain_month_day_ctor_basic_and_instanceof() { + let mut vm = Vm::new(); + // 构造成功:instanceof / length / name / 槽值 / 参考年默认 1972 经 always 形回读。 + let r = eval( + &mut vm, + "(() => { + const md = new Temporal.PlainMonthDay(5, 2); + return [ + md instanceof Temporal.PlainMonthDay, + Temporal.PlainMonthDay.length, + Temporal.PlainMonthDay.name, + Temporal.PlainYearMonth.length, + md.monthCode, + md.day, + md.calendarId, + md.toString({calendarName: 'always'}), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "true|2|PlainMonthDay|2|M05|2|iso8601|1972-05-02[u-ca=iso8601]"); +} + +#[test] +fn plain_month_day_ctor_rejects() { + let mut vm = Vm::new(); + // BigInt/Symbol 分量 → TypeError;NaN/±Inf → RangeError;缺参 → RangeError; + // 非闰参考年 2/29 → RangeError,闰参考年 1972 合法;非 new 调用 → TypeError。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + kind(() => new Temporal.PlainMonthDay(1n, 1)), + kind(() => new Temporal.PlainMonthDay(Symbol(), 1)), + kind(() => new Temporal.PlainMonthDay(NaN, 1)), + kind(() => new Temporal.PlainMonthDay(1, Infinity)), + kind(() => new Temporal.PlainMonthDay()), + kind(() => new Temporal.PlainMonthDay(2, 29, 'iso8601', 2023)), + new Temporal.PlainMonthDay(2, 29).day, + kind(() => Temporal.PlainMonthDay(1, 2)), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "TypeError|TypeError|RangeError|RangeError|RangeError|RangeError|29|TypeError" + ); +} + +#[test] +fn plain_month_day_ctor_calendar_argument() { + let mut vm = Vm::new(); + // 日历参数:{} → TypeError;函数对象按缺省 iso8601;大小写不敏感白名单; + // 未知字符串 → RangeError。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + kind(() => new Temporal.PlainMonthDay(1, 1, {})), + new Temporal.PlainMonthDay(1, 1, () => 'iso8601').calendarId, + new Temporal.PlainMonthDay(1, 1, 'iSo8601').calendarId, + kind(() => new Temporal.PlainMonthDay(1, 1, 'local')), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError|iso8601|iso8601|RangeError"); +} + +#[test] +fn plain_year_month_ctor_limits_and_ref_day() { + let mut vm = Vm::new(); + // 年月边界:-271821-03 与 275760-10 越界抛 RangeError,相邻月合法; + // refISODay 缺省 1,须落在目标月长内;非 new 调用 → TypeError; + // NaN 分量(undefined/不可解析串)→ RangeError,null → 0。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const ym = new Temporal.PlainYearMonth(-271821, 4, 'iso8601', 18); + return [ + kind(() => new Temporal.PlainYearMonth(-271821, 3)), + kind(() => new Temporal.PlainYearMonth(275760, 10)), + new Temporal.PlainYearMonth(-271821, 4).monthCode, + new Temporal.PlainYearMonth(275760, 9).monthCode, + ym.toString({calendarName: 'always'}), + new Temporal.PlainYearMonth(2021, 2).toString({calendarName: 'always'}), + kind(() => new Temporal.PlainYearMonth(2021, 2, 'iso8601', 0)), + kind(() => new Temporal.PlainYearMonth(2021, 2, 'iso8601', 29)), + kind(() => new Temporal.PlainYearMonth(undefined, 11)), + kind(() => new Temporal.PlainYearMonth('invalid', 11)), + new Temporal.PlainYearMonth(null, 11).year, + kind(() => new Temporal.PlainMonthDay(undefined, 24)), + kind(() => Temporal.PlainYearMonth(1970, 1)), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "RangeError|RangeError|M04|M09|-271821-04-18[u-ca=iso8601]|2021-02-01[u-ca=iso8601]|RangeError|RangeError|RangeError|RangeError|0|RangeError|TypeError" + ); +} + +#[test] +fn plain_year_month_getters() { + let mut vm = Vm::new(); + // 槽值 getter + 日历直读 + ISO 日历无纪元(era/eraYear 恒 undefined)。 + let r = eval( + &mut vm, + "(() => { + const ym = new Temporal.PlainYearMonth(-1, 8); + return [ + ym.year, ym.month, ym.monthCode, ym.calendarId, + ym.daysInMonth, ym.daysInYear, ym.monthsInYear, ym.inLeapYear, + String(ym.era), String(ym.eraYear), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "-1|8|M08|iso8601|31|365|12|false|undefined|undefined"); +} + +#[test] +fn plain_month_day_to_string_and_to_json() { + let mut vm = Vm::new(); + // 默认/auto/never 同形;critical 带 ! 注解;toJSON = 默认形且忽略参数; + // 非法 calendarName → RangeError;options 非对象 → TypeError。 + let r = eval( + &mut vm, + "(() => { + const md = new Temporal.PlainMonthDay(5, 2); + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + md.toString(), + md.toString({calendarName: 'auto'}), + md.toString({calendarName: 'never'}), + md.toString({calendarName: 'critical'}), + md.toJSON(), + kind(() => md.toString({calendarName: 'ALWAYS'})), + kind(() => md.toString(1)), + md.toString({}), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "05-02|05-02|05-02|1972-05-02[!u-ca=iso8601]|05-02|RangeError|TypeError|05-02" + ); +} + +#[test] +fn plain_year_month_to_string_year_format() { + let mut vm = Vm::new(); + // 年份格式化:-1 → -000001(6 位带符号);always 形补参考日。 + let r = eval( + &mut vm, + "(() => { + const ym = new Temporal.PlainYearMonth(-1, 8); + return [ym.toString(), ym.toString({calendarName: 'always'}), ym.toJSON()].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "-000001-08|-000001-08-01[u-ca=iso8601]|-000001-08"); +} + +#[test] +fn plain_month_day_and_year_month_branding() { + let mut vm = Vm::new(); + // receiver 非实例 → TypeError(toString / getter 均校验品牌)。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + kind(() => Temporal.PlainMonthDay.prototype.toString.call(1)), + kind(() => Temporal.PlainMonthDay.prototype.day.call({})), + kind(() => Temporal.PlainYearMonth.prototype.toString.call('x')), + kind(() => Temporal.PlainYearMonth.prototype.year.call(null)), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError|TypeError|TypeError|TypeError"); +}