diff --git a/Cargo.lock b/Cargo.lock index 2b8152e7..ae84eb1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.19.13" +version = "0.19.14" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.19.13" +version = "0.19.14" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.19.13" +version = "0.19.14" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.19.13" +version = "0.19.14" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.19.13" +version = "0.19.14" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.19.13" +version = "0.19.14" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.19.13" +version = "0.19.14" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.19.13" +version = "0.19.14" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.19.13" +version = "0.19.14" [[package]] name = "oxide_parser" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.19.13" +version = "0.19.14" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.19.13" +version = "0.19.14" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.19.13" +version = "0.19.14" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.19.13" +version = "0.19.14" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ebef44fe..d921d42c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.13" +version = "0.19.14" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/temporal/instant.rs b/crates/oxide_builtins/src/temporal/instant.rs index 3ac0395b..5ebdf4d7 100644 --- a/crates/oxide_builtins/src/temporal/instant.rs +++ b/crates/oxide_builtins/src/temporal/instant.rs @@ -2,6 +2,7 @@ use chrono::Utc; use num_traits::ToPrimitive; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -937,6 +938,71 @@ pub fn instant_to_zoned_date_time_iso(vm: &mut H, args: &[u8]) -> Nat make_zoned_date_time(vm, epoch_ns, &time_zone_id, "iso8601") } +/// ToNumeric 语义的纪元纳秒取值:number 须为有限整数(否则 RangeError), +/// BigInt 直读 i128;其余原始值 → TypeError。 +fn to_numeric_epoch_ns(vm: &mut H, value: JsValue) -> Result { + if value.is_number() { + let number = to_number(value); + if !number.is_finite() || number.fract() != 0.0 { + return Err(crate::error::create_range_error(vm, "invalid epochNanoseconds")); + } + return Ok(number as i128); + } + if value.is_bigint() { + return Ok(vm.bigint_value(value).to_i128().unwrap_or(i128::MAX)); + } + Err(crate::error::create_type_error(vm, "invalid epochNanoseconds")) +} + +/// `Temporal.Instant.prototype.with(properties)`:properties.epochNanoseconds +/// (number 或 BigInt)须过 Instant 范围,返回新 Instant;options 不读取。 +/// +/// # 步骤 +/// 1. receiver branding(TypeError)。 +/// 2. properties 非对象 → TypeError。 +/// 3. Get epochNanoseconds:undefined → TypeError;ToNumeric 取值。 +/// 4. 绝对值超 MAX_INSTANT_NS → RangeError;make_instant 重建。 +pub fn instant_with(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_instant(vm, obj)); + let properties = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; + if !properties.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "properties must be an object")); + } + let pptr = properties.as_js_object_ptr(); + if pptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "properties must be an object")); + } + let properties_obj = unsafe { &*pptr }; + let raw = native_try!(temporal_option_value(vm, properties_obj, properties, "epochNanoseconds")); + if raw.is_undefined() { + return NativeResult::Err(crate::error::create_type_error(vm, "epochNanoseconds is required")); + } + let epoch_ns = native_try!(to_numeric_epoch_ns(vm, raw)); + if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { + return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); + } + make_instant(vm, epoch_ns) +} + +/// `Temporal.Instant.prototype.toDate()`:纪元纳秒按毫秒取整(floor)后 +/// 产出 Date 对象(同一时刻,亚毫秒精度丢失,与 epochMilliseconds getter 同口径)。 +pub fn instant_to_date(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_instant(vm, obj)); + let Some(epoch_ns) = get_instant_epoch_ns(obj) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); + }; + let epoch_ms = epoch_ns.div_euclid(1_000_000) as f64; + let mut date = + JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(vm.builtin_proto(ProtoKind::DateProto))); + date.type_tag = JsObject::OBJ_TYPE_DATE; + date.set_prop_at(0, JsValue::float(epoch_ms)); + NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(date))) +} + /// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。 pub fn instant_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf")) diff --git a/crates/oxide_builtins/src/temporal/plain_month_day.rs b/crates/oxide_builtins/src/temporal/plain_month_day.rs index e256da78..d2eef4f4 100644 --- a/crates/oxide_builtins/src/temporal/plain_month_day.rs +++ b/crates/oxide_builtins/src/temporal/plain_month_day.rs @@ -6,12 +6,12 @@ use oxide_types::value::JsValue; use super::common::native_try; use super::{ - calendar_annotation, days_from_civil, days_in_month_iso, ensure_plain_month_day, format_iso_year, get_calendar_id, - get_double_prop, initialize_temporal_receiver, is_ctor_call, make_plain_date, make_plain_month_day, - month_day_bag_calendar_id, read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, - strip_iso_offset, temporal_calendar_id_strict, temporal_number_component, temporal_option_string, - temporal_option_value, temporal_overflow, temporal_to_show_calendar, valid_iso_date, valid_plain_time, - ShowCalendar, + calendar_annotation, days_from_civil, days_in_month_iso, duration_like_values, ensure_plain_month_day, + format_iso_year, get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call, + make_plain_date, make_plain_month_day, month_day_bag_calendar_id, parse_round_options, read_iso2, receiver_obj, + reject_partial_object_with_calendar_or_time_zone, strip_iso_offset, temporal_calendar_id_strict, + temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow, + temporal_to_show_calendar, valid_iso_date, valid_plain_time, ShowCalendar, }; /// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。 @@ -746,3 +746,109 @@ pub fn plain_month_day_to_plain_date(vm: &mut H, args: &[u8]) -> Nati let calendar = get_calendar_id(obj, 3); make_plain_date(vm, year, month, day, &calendar) } + +// ==================== PlainMonthDay add/subtract/round ==================== + +/// `Temporal.PlainMonthDay.prototype.add/subtract(durationLike [, options])` 核心。 +/// +/// # 步骤 +/// 1. receiver branding(TypeError)。 +/// 2. ToTemporalDuration(串 / bag / 实例;TypeError、RangeError 各自语义)。 +/// 3. 读 overflow(缺省 constrain)。 +/// 4. years / weeks / days / 时间分量任一非零 → RangeError(PMD 无年与日分量)。 +/// 5. (年, 月) 按参考年(槽 2)折算绝对月数后平衡,日按 overflow 规整 +/// (constrain 钳到目标年月长,reject 非法即抛)。 +/// 6. 结果 (年, 月, 日) 过 ISODateWithinLimits(day 级)→ 越界 RangeError。 +/// +/// # 边界与副作用 +/// - 结果参考年为算术后的年,日历标识取自 receiver 槽 3。 +/// - subtract 与 add 共用本函数,sign 参数取反月分量。 +fn plain_month_day_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> 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 i64; + let day = get_double_prop(obj, 1) as i64; + let ref_year = get_double_prop(obj, 2) as i64; + let calendar = get_calendar_id(obj, 3); + let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let values = native_try!(duration_like_values(vm, val)); + let constrain = native_try!(temporal_overflow(vm, args)); + // PMD 无年分量:years 与 weeks/days/时间分量均须为零。 + if values[0] != 0.0 || values[2..].iter().any(|value| *value != 0.0) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "month-day cannot add years, weeks, days, or time units", + )); + } + // BalanceISOYearMonth:月折算绝对月数后平衡(days 恒 0,日不参与)。 + let months = sign * values[1] as i64; + let absolute = ref_year * 12 + month - 1 + months; + let year = absolute.div_euclid(12); + let month = absolute.rem_euclid(12) + 1; + // RegulateISODate:constrain 钳日到目标年月长,reject 非法即抛。 + let day = if constrain { + day.min(days_in_month_iso(year as i32, month as u32) as i64) + } else { + if !valid_iso_date(year as i32, month as u32, day as u32) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + day + }; + // ISODateWithinLimits(day 级):-100_000_001..=100_000_000。 + let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range")); + } + make_plain_month_day(vm, month as u32, day as u32, year as i32, &calendar) +} + +/// `Temporal.PlainMonthDay.prototype.add(durationLike)`。 +pub fn plain_month_day_add(vm: &mut H, args: &[u8]) -> NativeResult { + plain_month_day_apply_duration(vm, args, 1) +} + +/// `Temporal.PlainMonthDay.prototype.subtract(durationLike)`。 +pub fn plain_month_day_subtract(vm: &mut H, args: &[u8]) -> NativeResult { + plain_month_day_apply_duration(vm, args, -1) +} + +/// `Temporal.PlainMonthDay.prototype.round(roundTo)`:smallestUnit 仅 "day", +/// 增量须为 1(日已是整数,舍入为恒等),日钳到参考年月长后过表示范围。 +/// +/// # 步骤 +/// 1. receiver 品牌校验;读槽 0-3 入本地(参考年与日历槽继承 receiver)。 +/// 2. roundTo 经 parse_round_options(undefined → TypeError;字符串简写;对象固定读序)。 +/// 3. smallestUnit 缺失 → RangeError;非 day → RangeError;增量 > 1 → RangeError。 +/// 4. 日钳到参考年月长(constrain 语义);valid_iso_date 与 day 级表示范围检查。 +/// 5. make_plain_month_day 重建(参考年与日历槽继承 receiver)。 +pub fn plain_month_day_round(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 u32; + let day = get_double_prop(obj, 1) as u32; + let ref_year = get_double_prop(obj, 2) as i32; + let calendar = get_calendar_id(obj, 3); + let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (increment, _mode, unit_value) = native_try!(parse_round_options(vm, round_to)); + let Some(unit_value) = unit_value else { + return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); + }; + if unit_value != "day" && unit_value != "days" { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); + } + if increment > 1 { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + // 日已是整数且增量恒 1,舍入为恒等;钳到参考年月长。 + let day = day.min(days_in_month_iso(ref_year, month)); + if !valid_iso_date(ref_year, month, day) { + return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range")); + } + let day_count = days_from_civil(i128::from(ref_year), i128::from(month), i128::from(day)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range")); + } + make_plain_month_day(vm, month, day, ref_year, &calendar) +} diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index 0c74a6f0..bf4c6339 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -149,6 +149,8 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) { ), ("until", oxide_builtins::temporal::instant_until:: as *const (), 1), ("valueOf", oxide_builtins::temporal::instant_value_of:: as *const (), 0), + ("with", oxide_builtins::temporal::instant_with:: as *const (), 1), + ("toDate", oxide_builtins::temporal::instant_to_date:: as *const (), 0), ], ); bind_well_known_data_property( @@ -1063,6 +1065,13 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) { oxide_builtins::temporal::plain_month_day_to_plain_date:: as *const (), 1, ), + ("add", oxide_builtins::temporal::plain_month_day_add:: as *const (), 1), + ( + "subtract", + oxide_builtins::temporal::plain_month_day_subtract:: as *const (), + 1, + ), + ("round", oxide_builtins::temporal::plain_month_day_round:: as *const (), 1), ], ); bind_well_known_data_property( diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 4c0e6ffb..46552ce2 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -412,6 +412,61 @@ fn instant_from_invalid_throws_range_error() { assert_eq!(str_val(&vm, r), "RangeError"); } +#[test] +fn instant_with_epoch_nanoseconds() { + let mut vm = Vm::new(); + // with:epochNanoseconds 按 ToNumeric 取值(number 须有限整数、BigInt 直读); + // 越界 → RangeError;缺失/非对象 → TypeError;非整数/NaN → RangeError; + // 边界值 ±MAX_INSTANT_NS 合法。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const i = Temporal.Instant.fromEpochNanoseconds(0n); + return [ + i.with({epochNanoseconds: 123456789n}).epochNanoseconds, + i.with({epochNanoseconds: 5000000}).epochNanoseconds, + kind(() => i.with({epochNanoseconds: 8640000000000000000001n})), + kind(() => i.with({epochNanoseconds: -8640000000000000000001n})), + i.with({epochNanoseconds: 8640000000000000000000n}).epochNanoseconds, + i.with({epochNanoseconds: -8640000000000000000000n}).epochNanoseconds, + kind(() => i.with({})), + kind(() => i.with(5)), + kind(() => i.with({epochNanoseconds: 1.5})), + kind(() => i.with({epochNanoseconds: NaN})), + kind(() => i.with({epochNanoseconds: '1'})), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "123456789|5000000|RangeError|RangeError|8640000000000000000000|-8640000000000000000000|TypeError|TypeError|RangeError|RangeError|TypeError" + ); +} + +#[test] +fn instant_to_date() { + let mut vm = Vm::new(); + // toDate:纳秒按毫秒向下取整(与 epochMilliseconds getter 同口径), + // 负值向 -Inf 取整;结果是真实 Date(instanceof + getTime)。 + let r = eval( + &mut vm, + "(() => { + const d = Temporal.Instant.fromEpochNanoseconds(1234567890123456789n).toDate(); + return [ + d instanceof Date, + d.getTime(), + Temporal.Instant.fromEpochNanoseconds(-1n).toDate().getTime(), + Temporal.Instant.fromEpochNanoseconds(-1000001n).toDate().getTime(), + Temporal.Instant.fromEpochNanoseconds(1000000000n).toDate().getTime(), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "true|1234567890123|-1|-2|1000"); +} + // -- Temporal.PlainDate -- #[test] @@ -3166,6 +3221,101 @@ fn plain_month_day_to_plain_date_matrix() { ); } +#[test] +fn plain_month_day_add_balance_and_clamp() { + let mut vm = Vm::new(); + // add:月按(参考年, 月)平衡,负年经 div_euclid 处理;日钳到目标年月长 + // (constrain 缺省,reject 非法即抛);years/weeks/days/时间分量 → RangeError; + // 无参 → TypeError;day 级表示范围 ±100_000_00x。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const md = Temporal.PlainMonthDay.from('01-31'); + return [ + md.add('P1M').toString(), + kind(() => md.add('P1M', {overflow: 'reject'})), + Temporal.PlainMonthDay.from('02-29').add('P1M').toString(), + Temporal.PlainMonthDay.from('12-15').add('P1M').toString(), + Temporal.PlainMonthDay.from('01-15').add('P13M').toString(), + Temporal.PlainMonthDay.from('01-15').add({months: -1}).toString(), + new Temporal.PlainMonthDay(1, 15, 'iso8601', -1).add({months: -1}).toString(), + Temporal.PlainMonthDay.from('01-15').add({months: 1}).toString(), + kind(() => Temporal.PlainMonthDay.from('01-15').add('P1Y')), + kind(() => Temporal.PlainMonthDay.from('01-15').add('P1W')), + kind(() => Temporal.PlainMonthDay.from('01-15').add('P1D')), + kind(() => Temporal.PlainMonthDay.from('01-15').add('PT1H')), + kind(() => Temporal.PlainMonthDay.from('01-15').add()), + kind(() => new Temporal.PlainMonthDay(10, 31, 'iso8601', 275760).add('P1M')), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "02-29|RangeError|03-29|01-15|02-15|12-15|12-15|02-15|RangeError|RangeError|RangeError|RangeError|TypeError|RangeError" + ); +} + +#[test] +fn plain_month_day_subtract_balance_and_clamp() { + let mut vm = Vm::new(); + // subtract:月分量反向(sign 取反);日钳到目标年月长; + // years/weeks/days/时间分量 → RangeError;day 级表示范围下界。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + Temporal.PlainMonthDay.from('03-31').subtract('P1M').toString(), + kind(() => Temporal.PlainMonthDay.from('03-31').subtract('P1M', {overflow: 'reject'})), + Temporal.PlainMonthDay.from('01-15').subtract('P13M').toString(), + new Temporal.PlainMonthDay(1, 15, 'iso8601', -1).subtract('P1M').toString(), + kind(() => Temporal.PlainMonthDay.from('01-15').subtract('P1Y')), + kind(() => Temporal.PlainMonthDay.from('01-15').subtract('P1D')), + kind(() => Temporal.PlainMonthDay.from('01-15').subtract('PT1H')), + kind(() => new Temporal.PlainMonthDay(3, 10, 'iso8601', -271821).subtract('P1M')), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "02-29|RangeError|12-15|12-15|RangeError|RangeError|RangeError|RangeError" + ); +} + +#[test] +fn plain_month_day_round_identity_and_validation() { + let mut vm = Vm::new(); + // round:日已是整数且增量恒 1,舍入为恒等;日钳到参考年月长 + // (from('02-31') 串路径不校验日,可造出超长按日);smallestUnit 须 day + // (缺失/其他 → RangeError);增量 > 1 → RangeError;非串/对象 → TypeError。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const leap = Temporal.PlainMonthDay.from('02-29'); + return [ + leap.round('day').toString(), + leap.round({smallestUnit: 'days'}).toString(), + Temporal.PlainMonthDay.from('02-31').round('day').toString(), + kind(() => leap.round('month')), + kind(() => leap.round({})), + kind(() => leap.round({smallestUnit: 'day', roundingIncrement: 2})), + kind(() => leap.round(5)), + kind(() => leap.round()), + leap.round('day').day, + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "02-29|02-29|02-29|RangeError|RangeError|RangeError|TypeError|TypeError|29" + ); +} + #[test] fn plain_year_month_from_string_matrix() { let mut vm = Vm::new();