feat: PlainMonthDay/PlainYearMonth 对象地基——构造器/getter/toString/toJSON 注册与 BuiltinWorld 接线

This commit is contained in:
2026-09-12 03:42:48 +00:00
parent 390b72e3bd
commit e33e7a1eb7
6 changed files with 834 additions and 6 deletions
@@ -957,6 +957,150 @@ pub fn bind_temporal(core: &Arc<KernelCore>, 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::<crate::vm::Vm> 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::<crate::vm::Vm> as *const ()),
(
"monthCode",
oxide_builtins::temporal::plain_month_day_month_code::<crate::vm::Vm> as *const (),
),
(
"calendarId",
oxide_builtins::temporal::plain_month_day_calendar_id::<crate::vm::Vm> 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::<crate::vm::Vm> as *const (),
0,
),
(
"toJSON",
oxide_builtins::temporal::plain_month_day_to_json::<crate::vm::Vm> 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::<crate::vm::Vm> 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::<crate::vm::Vm> as *const ()),
("month", oxide_builtins::temporal::plain_year_month_month::<crate::vm::Vm> as *const ()),
(
"monthCode",
oxide_builtins::temporal::plain_year_month_month_code::<crate::vm::Vm> as *const (),
),
(
"calendarId",
oxide_builtins::temporal::plain_year_month_calendar_id::<crate::vm::Vm> as *const (),
),
(
"daysInMonth",
oxide_builtins::temporal::plain_year_month_days_in_month::<crate::vm::Vm> as *const (),
),
(
"daysInYear",
oxide_builtins::temporal::plain_year_month_days_in_year::<crate::vm::Vm> as *const (),
),
(
"monthsInYear",
oxide_builtins::temporal::plain_year_month_months_in_year::<crate::vm::Vm> as *const (),
),
(
"inLeapYear",
oxide_builtins::temporal::plain_year_month_in_leap_year::<crate::vm::Vm> as *const (),
),
("era", oxide_builtins::temporal::plain_year_month_era::<crate::vm::Vm> as *const ()),
(
"eraYear",
oxide_builtins::temporal::plain_year_month_era_year::<crate::vm::Vm> 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::<crate::vm::Vm> as *const (),
0,
),
(
"toJSON",
oxide_builtins::temporal::plain_year_month_to_json::<crate::vm::Vm> 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<KernelCore>, 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));
}
@@ -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");
}