From d1d85ffe4f19b8e2f9181a75a6d104ff0c701481 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 23:07:41 +0800 Subject: [PATCH] feat: implement PlainDateTime toString options and ISO range parsing --- crates/oxide_builtins/src/temporal.rs | 432 ++++++++++++++---- crates/oxide_emit/src/class/emit.rs | 7 +- .../oxide_vm/tests/builtins_temporal_tests.rs | 91 ++++ 3 files changed, 441 insertions(+), 89 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 8b32089..fb34cdc 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -1791,9 +1791,28 @@ duration_getter!(duration_milliseconds, 7); duration_getter!(duration_microseconds, 8); duration_getter!(duration_nanoseconds, 9); -/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年)。 +/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。 fn valid_iso_date(year: i32, month: u32, day: u32) -> bool { - NaiveDate::from_ymd_opt(year, month, day).is_some() + match days_in_month(i128::from(year), i128::from(month)) { + Some(days) => day != 0 && day as i128 <= days, + None => false, + } +} + +/// 计算 ISO 日期时间的纪元纳秒;分量须已通过 `valid_iso_date`。 +fn iso_date_time_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64) -> Option { + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + days.checked_mul(86_400_000_000_000)?.checked_add(time_ns as i128) +} + +/// 校验日期时间落在 PlainDateTime 表示范围(约 ±(10^8 + 1) 天,边界互斥)。 +fn valid_plain_date_time_range(year: i32, month: u32, day: u32, time_ns: f64) -> bool { + const MAX_ISO_DAY: i128 = 100_000_000; + let limit = (MAX_ISO_DAY + 1) * 86_400_000_000_000; + match iso_date_time_epoch_ns(year, month, day, time_ns) { + Some(epoch_ns) => epoch_ns > -limit && epoch_ns < limit, + None => false, + } } /// 读两位数字(basic 格式的月/日等紧凑分量)。 @@ -1879,7 +1898,7 @@ fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> { }; let rest = &t[i..]; if !rest.is_empty() { - let ok = rest.starts_with(['T', 't', '[', '+', '-', 'Z', 'z']); + let ok = rest.starts_with(['T', 't', '[']); if !ok { return Err("invalid trailing content".into()); } @@ -2177,6 +2196,9 @@ pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> Native + millisecond as f64 * 1_000_000.0 + microsecond as f64 * 1_000.0 + nanosecond as f64; + if !valid_plain_date_time_range(year, month, day, total_ns) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); + } initialize_temporal_receiver( vm, args, @@ -2192,6 +2214,7 @@ pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> Native fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> { let mut calendar_seen = false; + let mut saw_critical_calendar = false; let mut time_zone_seen = false; while !suffix.is_empty() { if !suffix.starts_with('[') { @@ -2210,15 +2233,21 @@ fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> { return Err("annotation keys must be lowercase".into()); } if key == "u-ca" { - if calendar_seen || !value.eq_ignore_ascii_case("iso8601") { + if calendar_seen { + if critical || saw_critical_calendar { + return Err("invalid calendar annotation".into()); + } + } else if !value.eq_ignore_ascii_case("iso8601") { return Err("invalid calendar annotation".into()); + } else { + calendar_seen = true; + saw_critical_calendar = critical; } - calendar_seen = true; } else if critical { return Err("unknown critical annotation".into()); } } else { - if critical || time_zone_seen { + if time_zone_seen { return Err("invalid time-zone annotation".into()); } time_zone_seen = true; @@ -2228,6 +2257,78 @@ fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> { Ok(()) } +/// 剥离 ISO 偏移(±HH、±HHMM、±HH:MM、±HHMMSS、±HH:MM:SS,秒可带小数),返回剩余部分。 +fn strip_iso_offset(input: &str) -> Result<&str, String> { + let bytes = input.as_bytes(); + if bytes.len() < 3 || !matches!(bytes[0], b'+' | b'-') { + return Err("invalid ISO offset".into()); + } + let colon = bytes.len() > 3 && bytes[3] == b':'; + let mut i = 1usize; + let take_digits = |count: usize, cursor: &mut usize| -> Result<(), String> { + if *cursor + count > bytes.len() || !bytes[*cursor..*cursor + count].iter().all(u8::is_ascii_digit) { + return Err("invalid ISO offset".into()); + } + *cursor += count; + Ok(()) + }; + take_digits(2, &mut i)?; + if i < bytes.len() { + if colon { + if bytes[i] != b':' { + return Err("invalid ISO offset".into()); + } + i += 1; + } + if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) { + i += 2; + if i < bytes.len() { + if colon { + if bytes[i] != b':' { + return Err("invalid ISO offset".into()); + } + i += 1; + } + if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) { + i += 2; + if i < bytes.len() && matches!(bytes[i], b'.' | b',') { + i += 1; + let start = i; + while i < bytes.len() && bytes[i].is_ascii_digit() { + i += 1; + } + if i == start { + return Err("invalid ISO offset fraction".into()); + } + if i - start > 9 { + return Err("invalid ISO offset fraction".into()); + } + } + } + } + } + } + let digits: String = input[1..i].chars().filter(|ch| ch.is_ascii_digit()).take(6).collect(); + if digits.len() < 2 { + return Err("invalid ISO offset".into()); + } + let hour: u32 = digits[..2].parse().map_err(|_| "invalid ISO offset".to_string())?; + let minute: u32 = if digits.len() >= 4 { + digits[2..4].parse().map_err(|_| "invalid ISO offset".to_string())? + } else { + 0 + }; + let second: u32 = if digits.len() >= 6 { + digits[4..6].parse().map_err(|_| "invalid ISO offset".to_string())? + } else { + 0 + }; + if hour > 23 || minute > 59 || second > 59 { + return Err("invalid ISO offset".into()); + } + Ok(&input[i..]) +} + fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> { let trimmed = input.trim(); if trimmed.contains('\u{2212}') { @@ -2240,13 +2341,14 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str if text.contains('Z') || text.contains('z') { return Err("UTC designator is not valid for PlainDateTime".into()); } - let (year, month, day) = parse_iso_date(text)?; let separator = text.find(['T', 't', ' ']); + let date_part = match separator { + Some(index) => &text[..index], + None => text, + }; + let (year, month, day) = parse_iso_date(date_part)?; let Some(separator) = separator else { - if text.contains('+') { - return Err("offset without time is not valid for PlainDateTime".into()); - } - if !valid_iso_date(year, month, day) { + if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, 0.0) { return Err("invalid ISO date".into()); } return Ok((year, month, day, 0.0)); @@ -2255,30 +2357,10 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str let time_and_suffix = &text[separator + 1..]; let time_end = time_and_suffix .char_indices() - .find_map(|(index, ch)| matches!(ch, '+' | '-' | '[').then_some(index)) + .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index)) .unwrap_or(time_and_suffix.len()); - if time_end < time_and_suffix.len() { - let suffix = &time_and_suffix[time_end..]; - let annotations = if suffix.starts_with('[') { - suffix - } else { - let offset_end = if suffix.len() >= 6 && suffix.as_bytes().get(3) == Some(&b':') { - 6 - } else if suffix.len() >= 5 { - 5 - } else if suffix.len() >= 3 { - 3 - } else { - 0 - }; - if offset_end == 0 { - return Err("invalid ISO offset".into()); - } - &suffix[offset_end..] - }; - if annotations.starts_with(':') || validate_temporal_annotation_suffix(annotations).is_err() { - return Err("invalid ISO offset".into()); - } + if time_end < time_and_suffix.len() && !strip_iso_offset(&time_and_suffix[time_end..])?.is_empty() { + return Err("invalid ISO offset".into()); } let time = &time_and_suffix[..time_end]; if time.is_empty() { @@ -2334,13 +2416,13 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) { return Err("invalid ISO time".into()); } - if !valid_iso_date(year, month, day) { - return Err("invalid ISO date".into()); - } 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 + subsecond as f64; + if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, total_ns) { + return Err("invalid ISO date".into()); + } Ok((year, month, day, total_ns)) } @@ -2352,59 +2434,106 @@ fn plain_date_time_object_parts( return Err(crate::error::create_range_error(vm, "unsupported calendar")); } - let (day, hour, microsecond, millisecond, minute, month) = { - let mut number_field = |name: &str| -> Result, JsValue> { - let raw = temporal_option_value(vm, obj, value, name)?; - if raw.is_undefined() { - Ok(None) - } else { - temporal_option_number(vm, raw).map(|number| Some(number.trunc())) - } - }; - ( - number_field("day")?, - number_field("hour")?.unwrap_or(0.0), - number_field("microsecond")?.unwrap_or(0.0), - number_field("millisecond")?.unwrap_or(0.0), - number_field("minute")?.unwrap_or(0.0), - number_field("month")?, - ) - }; - let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?; + // 先按规范顺序读取全部原始字段,暂不转换类型。 + let ( + day_raw, + hour_raw, + microsecond_raw, + millisecond_raw, + minute_raw, + month_raw, + month_code_raw, + nanosecond_raw, + second_raw, + year_raw, + ) = ( + temporal_option_value(vm, obj, value, "day")?, + temporal_option_value(vm, obj, value, "hour")?, + temporal_option_value(vm, obj, value, "microsecond")?, + temporal_option_value(vm, obj, value, "millisecond")?, + temporal_option_value(vm, obj, value, "minute")?, + temporal_option_value(vm, obj, value, "month")?, + temporal_option_value(vm, obj, value, "monthCode")?, + temporal_option_value(vm, obj, value, "nanosecond")?, + temporal_option_value(vm, obj, value, "second")?, + temporal_option_value(vm, obj, value, "year")?, + ); + + // 缺失必填字段先抛 TypeError(先于任何 RangeError 值校验)。 + if day_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "day is required")); + } + if year_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "year is required")); + } + if month_raw.is_undefined() && month_code_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "month is required")); + } + + // monthCode 必须是 string 类型,否则 TypeError。 let month_code = if month_code_raw.is_undefined() { None } else { - let code = temporal_option_string(vm, month_code_raw)?; - let parsed = code - .strip_prefix('M') - .filter(|digits| digits.len() == 2) - .and_then(|digits| digits.parse::().ok()) - .filter(|number| (1..=12).contains(number)) - .ok_or_else(|| crate::error::create_range_error(vm, "invalid monthCode"))?; - Some(parsed as f64) + if !month_code_raw.is_string() { + return Err(crate::error::create_type_error(vm, "monthCode must be a string")); + } + let code = to_string(month_code_raw); + // 语法(well-formed):M 后两位数字,可选 L 后缀。 + let digits_ok = + code.len() >= 3 && code.starts_with('M') && code.as_bytes()[1..3].iter().all(u8::is_ascii_digit); + let well_formed = digits_ok && (code.len() == 3 || (code.len() == 4 && code.ends_with('L'))); + if !well_formed { + return Err(crate::error::create_range_error(vm, "invalid monthCode")); + } + Some((code.clone(), code.ends_with('L'))) }; - let mut number_field = |name: &str| -> Result, JsValue> { - let raw = temporal_option_value(vm, obj, value, name)?; + + // 数值字段类型转换:Symbol/BigInt 抛 TypeError。 + let mut convert_number = |_name: &str, raw: JsValue| -> Result, JsValue> { if raw.is_undefined() { Ok(None) } else { temporal_option_number(vm, raw).map(|number| Some(number.trunc())) } }; - let nanosecond = number_field("nanosecond")?.unwrap_or(0.0); - let second = number_field("second")?.unwrap_or(0.0); - let year = number_field("year")?; + let (day, hour, microsecond, millisecond, minute, month, nanosecond, second, year) = ( + convert_number("day", day_raw)?, + convert_number("hour", hour_raw)?.unwrap_or(0.0), + convert_number("microsecond", microsecond_raw)?.unwrap_or(0.0), + convert_number("millisecond", millisecond_raw)?.unwrap_or(0.0), + convert_number("minute", minute_raw)?.unwrap_or(0.0), + convert_number("month", month_raw)?, + convert_number("nanosecond", nanosecond_raw)?.unwrap_or(0.0), + convert_number("second", second_raw)?.unwrap_or(0.0), + convert_number("year", year_raw)?, + ); - let day = day.ok_or_else(|| crate::error::create_type_error(vm, "day is required"))?; - let year = year.ok_or_else(|| crate::error::create_type_error(vm, "year is required"))?; + // monthCode 适配 ISO 日历:仅 M01-M12,L 后缀不支持;无效抛 RangeError。 + let month_code = match month_code { + Some((code, leap)) => { + if leap { + return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar")); + } + let number = code[1..3] + .parse::() + .map_err(|_| crate::error::create_range_error(vm, "invalid monthCode"))?; + if !(1.0..=12.0).contains(&number) { + return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar")); + } + Some(number) + } + None => None, + }; let month = match (month, month_code) { (Some(month), Some(code)) if month != code => { return Err(crate::error::create_range_error(vm, "month and monthCode disagree")); } (Some(month), _) => month, (None, Some(code)) => code, - (None, None) => return Err(crate::error::create_type_error(vm, "month is required")), + (None, None) => unreachable!(), }; + let day = day.ok_or_else(|| crate::error::create_type_error(vm, "day is required"))?; + let year = year.ok_or_else(|| crate::error::create_type_error(vm, "year is required"))?; let mut values = [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond]; if values.iter().any(|number| !number.is_finite()) || values[1..].iter().any(|number| *number < 0.0) { return Err(crate::error::create_range_error(vm, "invalid date-time component")); @@ -2434,9 +2563,32 @@ fn plain_date_time_object_parts( + millisecond as f64 * 1_000_000.0 + microsecond as f64 * 1_000.0 + nanosecond as f64; + if !valid_plain_date_time_range(year, month, day, total_ns) { + return Err(crate::error::create_range_error(vm, "invalid date-time component")); + } Ok((year, month, day, total_ns)) } +/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。 +fn zoned_date_time_plain_parts(vm: &mut H, obj: &JsObject) -> Result<(i32, u32, u32, f64), JsValue> { + let Some(epoch_ns) = get_instant_epoch_ns(obj) else { + return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); + }; + let time_zone_id = to_string(obj.get_prop_at(1)); + let Some(offset_minutes) = instant_time_zone_offset(&time_zone_id) else { + return Err(crate::error::create_range_error(vm, "invalid time zone")); + }; + const DAY_NS: i128 = 86_400_000_000_000; + let local_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; + let days = local_ns.div_euclid(DAY_NS); + let time_ns = local_ns.rem_euclid(DAY_NS) as f64; + let (year, month, day) = civil_from_days(days); + if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns) { + return Err(crate::error::create_range_error(vm, "invalid date-time")); + } + Ok((year as i32, month as u32, day as u32, time_ns)) +} + fn plain_date_time_like_parts( vm: &mut H, value: JsValue, constrain: bool, ) -> Result<(i32, u32, u32, f64), JsValue> { @@ -2468,6 +2620,9 @@ fn plain_date_time_like_parts( 0.0, )); } + if obj.is_zoned_date_time_obj() { + return zoned_date_time_plain_parts(vm, obj); + } plain_date_time_object_parts(vm, value, obj, constrain) } @@ -2680,28 +2835,131 @@ pub fn plain_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeRe NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf")) } +/// 格式化 PlainDateTime 本地 ISO 日期时间(无偏移),可裁秒与小数位并追加日历注解。 +fn format_plain_date_time_iso( + year: i32, month: u32, day: u32, time_ns: i128, include_seconds: bool, fractional_digits: Option, + calendar_name: &str, +) -> String { + let hour = time_ns / 3_600_000_000_000; + let minute = time_ns / 60_000_000_000 % 60; + let second = time_ns / 1_000_000_000 % 60; + let subsecond = time_ns % 1_000_000_000; + let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(i128::from(year))); + if include_seconds { + output.push_str(&format!(":{second:02}")); + match fractional_digits { + Some(0) => {} + Some(digits) => { + let fraction = format!("{subsecond:09}"); + output.push('.'); + output.push_str(&fraction[..digits]); + } + None if subsecond != 0 => { + output.push('.'); + output.push_str(format!("{subsecond:09}").trim_end_matches('0')); + } + None => {} + } + } + match calendar_name { + "always" => output.push_str("[u-ca=iso8601]"), + "critical" => output.push_str("[!u-ca=iso8601]"), + _ => {} + } + output +} + fn plain_date_time_iso_string(vm: &mut H, args: &[u8]) -> NativeResult { let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); - let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns); - let fraction = millisecond as u64 * 1_000_000 + microsecond as u64 * 1_000 + nanosecond as u64; - let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}", format_iso_year(year as i128)); - if fraction != 0 { - let mut digits = format!("{fraction:09}"); - while digits.ends_with('0') { - digits.pop(); - } - output.push('.'); - output.push_str(&digits); - } + let output = format_plain_date_time_iso(year, month, day, total_ns as i128, true, None, "never"); NativeResult::Ok(vm.new_string(&output)) } -/// `Temporal.PlainDateTime.prototype.toString()`:输出本地 ISO 日期时间。 +/// `Temporal.PlainDateTime.prototype.toString(options)`:按精度、舍入模式与日历显示输出 ISO 8601。 pub fn plain_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { - if args.len() > 1 && !vm.reg(args[1]).is_undefined() && !vm.reg(args[1]).is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); + let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; + let (calendar_name, fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { + ("auto".to_string(), FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) + } else { + if !options_value.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options_ptr = options_value.as_js_object_ptr(); + if options_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options = unsafe { &*options_ptr }; + let calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName")); + let calendar_name = if calendar_raw.is_undefined() { + "auto".to_string() + } else { + native_try!(temporal_option_string(vm, calendar_raw)) + }; + let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); + let fractional_input = if fractional_raw.is_undefined() { + FractionalSecondDigitsInput::Auto + } else if fractional_raw.is_int() || fractional_raw.is_double() { + FractionalSecondDigitsInput::Number(to_number(fractional_raw)) + } else { + FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) + }; + let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); + let mode_value = if mode_raw.is_undefined() { + "trunc".to_string() + } else { + native_try!(temporal_option_string(vm, mode_raw)) + }; + let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); + let smallest_value = if smallest_raw.is_undefined() { + None + } else { + Some(native_try!(temporal_option_string(vm, smallest_raw))) + }; + (calendar_name, fractional_input, mode_value, smallest_value) + }; + + match calendar_name.as_str() { + "auto" | "always" | "never" | "critical" => {} + _ => return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName")), } - plain_date_time_iso_string(vm, args) + let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); + let Some(mode) = instant_rounding_mode(&mode_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); + }; + let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { + Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), + Some("second" | "seconds") => (1_000_000_000, true, Some(0)), + Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), + Some("microsecond" | "microseconds") => (1_000, true, Some(6)), + Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), + Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), + None => match fractional_digits { + Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), + None => (1, true, None), + }, + }; + const DAY_NS: i128 = 86_400_000_000_000; + let Some(rounded_ns) = round_instant_ns(total_ns as i128, quantum_ns, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + }; + let extra_days = rounded_ns / DAY_NS; + let time_ns = rounded_ns % DAY_NS; + let total_days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + extra_days; + let (year, month, day) = civil_from_days(total_days); + if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + } + let output = format_plain_date_time_iso( + year as i32, + month as u32, + day as u32, + time_ns, + include_seconds, + output_digits, + &calendar_name, + ); + NativeResult::Ok(vm.new_string(&output)) } /// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。 diff --git a/crates/oxide_emit/src/class/emit.rs b/crates/oxide_emit/src/class/emit.rs index 8fea5c5..6162d54 100644 --- a/crates/oxide_emit/src/class/emit.rs +++ b/crates/oxide_emit/src/class/emit.rs @@ -229,8 +229,11 @@ impl Emitter { if is_derived { module.insts.clear(); module.constants.clear(); - module.n_registers = 1; - module.insts.push(Inst::super_call(Operand::None, Operand::None, 0)); + module.n_registers = 2; + // 隐式派生构造器等价于 `constructor(...args) { super(...args); }`: + // 收集全部实参为数组,再展开传给 super。 + module.insts.push(Inst::create_rest_array(Operand::Reg(1), 0)); + module.insts.push(Inst::super_call_spread(Operand::Reg(0), &[0x8000_0000 | 1])); // 字段初始化直接重发:构造器 upvalue/内置槽引用须与首轮编译产物对齐。 let mut field_ctx = CompileCtx::new(); field_ctx.scopes.private_name_map = diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 38841cf..b245e6b 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -719,6 +719,81 @@ fn plain_date_time_compare_uses_internal_slots() { assert_eq!(str_val(&vm, r), "-1,1,0"); } +#[test] +fn plain_date_time_from_accepts_variants_and_full_range() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "const lower = Temporal.PlainDateTime.from('1976-11-18t15:23').toString(); + const space = Temporal.PlainDateTime.from('1976-11-18 15:23').toString(); + const offset = Temporal.PlainDateTime.from('1976-11-18T15:23+00:00:00,0[UTC]').toString(); + const criticalZone = Temporal.PlainDateTime.from('1976-11-18T15:23[!Europe/Vienna]').toString(); + const secondCalendar = Temporal.PlainDateTime.from('1976-11-18T15:23[u-ca=iso8601][u-ca=discord]').toString(); + const minOk = Temporal.PlainDateTime.from('-271821-04-19T00:00:00.000000001').toString(); + const maxOk = Temporal.PlainDateTime.from('+275760-09-13').toString(); + const minBad = (() => { try { Temporal.PlainDateTime.from('-271821-04-19T00:00'); return false } + catch (e) { return e instanceof RangeError } })(); + const maxBad = (() => { try { Temporal.PlainDateTime.from('+275760-09-14'); return false } + catch (e) { return e instanceof RangeError } })(); + const dateOffsetBad = (() => { try { Temporal.PlainDateTime.from('2022-09-15+00:00'); return false } + catch (e) { return e instanceof RangeError } })(); + const badCalendar = (() => { try { Temporal.PlainDateTime.from('1997-12-04[u-ca=notacal]'); return false } + catch (e) { return e instanceof RangeError } })(); + const duplicateCritical = (() => { try { Temporal.PlainDateTime.from('1970-01-01[u-ca=iso8601][!u-ca=iso8601]'); return false } + catch (e) { return e instanceof RangeError } })(); + [lower, space, offset, criticalZone, secondCalendar, minOk, maxOk, + minBad, maxBad, dateOffsetBad, badCalendar, duplicateCritical].join('|')", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + concat!( + "1976-11-18T15:23:00|1976-11-18T15:23:00|1976-11-18T15:23:00|", + "1976-11-18T15:23:00|1976-11-18T15:23:00|-271821-04-19T00:00:00.000000001|", + "+275760-09-13T00:00:00|true|true|true|true|true" + ) + ); +} +#[test] +fn plain_date_time_to_string_honors_options() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(() => { + const dt = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 650, 0); + const minute = dt.toString({ smallestUnit: 'minute' }); + const seconds = dt.toString({ smallestUnit: 'second' }); + const micros = dt.toString({ smallestUnit: 'microsecond' }); + const two = dt.toString({ fractionalSecondDigits: 2.5 }); + const nine = dt.toString({ fractionalSecondDigits: 9.7 }); + const midnight = new Temporal.PlainDateTime(1999, 12, 31, 23, 59, 59, 999, 999, 999) + .toString({ fractionalSecondDigits: 8, roundingMode: 'ceil' }); + const always = dt.toString({ calendarName: 'always' }); + const critical = dt.toString({ calendarName: 'critical' }); + const never = dt.toString({ calendarName: 'never' }); + const badCalendar = (() => { try { dt.toString({ calendarName: 'ALWAYS' }); return false } + catch (e) { return e instanceof RangeError } })(); + const badUnit = (() => { try { dt.toString({ smallestUnit: 'hour' }); return false } + catch (e) { return e instanceof RangeError } })(); + const badDigits = (() => { try { dt.toString({ fractionalSecondDigits: -0.6 }); return false } + catch (e) { return e instanceof RangeError } })(); + const badOptions = (() => { try { dt.toString(null); return false } + catch (e) { return e instanceof TypeError } })(); + return [minute, seconds, micros, two, nine, midnight, always, critical, never, + badCalendar, badUnit, badDigits, badOptions].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + concat!( + "2000-05-02T12:34|2000-05-02T12:34:56|2000-05-02T12:34:56.987650|", + "2000-05-02T12:34:56.98|2000-05-02T12:34:56.987650000|2000-01-01T00:00:00.00000000|", + "2000-05-02T12:34:56.98765[u-ca=iso8601]|2000-05-02T12:34:56.98765[!u-ca=iso8601]|", + "2000-05-02T12:34:56.98765|true|true|true|true" + ) + ); +} #[test] fn plain_date_time_serializes_extended_years() { let mut vm = Vm::new(); @@ -730,3 +805,19 @@ fn plain_date_time_serializes_extended_years() { .unwrap(); assert_eq!(str_val(&vm, r), "-000001-08-07T06:54:32.1,+010000-06-07T08:09:10.987"); } + +#[test] +fn zdt_subclass_compare_debug() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "class AvoidGettersDateTime extends Temporal.PlainDateTime { + get year() { throw new Error('getter'); } + } + const one = new AvoidGettersDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321); + const two = new AvoidGettersDateTime(2006, 3, 25, 6, 54, 32, 123, 456, 789); + typeof Temporal.PlainDateTime.compare(one, two) + ':' + Temporal.PlainDateTime.compare(one, two)", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "number:-1"); +}