feat: implement precise Temporal Instant arithmetic

This commit is contained in:
爱特每羊队
2026-08-11 15:55:05 +08:00
parent 4c42112c51
commit 756d6b8e1b
3 changed files with 120 additions and 23 deletions
+79 -23
View File
@@ -81,7 +81,19 @@ fn receiver_obj<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsV
Ok(ptr)
}
/// 读 receiver 是否为构造调用:`new` 时 this 的原型是相应构造器的 prototype。
fn initialize_temporal_receiver<H: VmHost, const N: usize>(
vm: &mut H, args: &[u8], type_tag: u8, values: [JsValue; N],
) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &mut *ptr };
obj.type_tag = type_tag;
for (index, value) in values.into_iter().enumerate() {
obj.set_prop_at(index, value);
}
NativeResult::Ok(JsValue::undefined())
}
/// 读 receiver 是否为构造调用:`new` 时 this 的原型链包含相应构造器的 prototype。
fn is_ctor_call<H: VmHost>(vm: &mut H, args: &[u8], proto_ptr: *const JsObject) -> bool {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
@@ -92,8 +104,18 @@ fn is_ctor_call<H: VmHost>(vm: &mut H, args: &[u8], proto_ptr: *const JsObject)
return false;
}
let obj = unsafe { &*ptr };
let this_proto = obj.proto().as_js_object_ptr();
!this_proto.is_null() && std::ptr::eq(this_proto, proto_ptr)
let mut this_proto = obj.proto();
while this_proto.is_object() {
let this_proto_ptr = this_proto.as_js_object_ptr();
if this_proto_ptr.is_null() {
return false;
}
if std::ptr::eq(this_proto_ptr, proto_ptr) {
return true;
}
this_proto = unsafe { &*this_proto_ptr }.proto();
}
false
}
fn make_instant<H: VmHost>(vm: &mut H, epoch_ns: i128) -> NativeResult {
@@ -439,7 +461,8 @@ pub fn instant_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
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)
let epoch_value = vm.new_bigint(epoch_ns);
initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_INSTANT, [epoch_value])
}
/// `Temporal.Instant.from(value)`:接受 Instant、ISO 字符串或可转换为字符串的对象。
@@ -563,6 +586,44 @@ pub fn instant_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::bool(epoch_ns == other_ns))
}
fn instant_add_duration<H: VmHost>(vm: &mut H, args: &[u8], direction: i128) -> 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 duration_like = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
let values = native_try!(duration_like_values(vm, duration_like));
if values[..4].iter().any(|value| *value != 0.0) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"Instant arithmetic does not support date units",
));
}
let Some(delta_ns) = duration_time_nanoseconds(&values) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration"));
};
let Some(result_ns) = delta_ns.checked_mul(direction).and_then(|delta| epoch_ns.checked_add(delta)) else {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
};
if result_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
}
make_instant(vm, result_ns)
}
/// `Temporal.Instant.prototype.add(durationLike)`:精确增加仅含时间单位的时长。
pub fn instant_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
instant_add_duration(vm, args, 1)
}
/// `Temporal.Instant.prototype.subtract(durationLike)`:精确减去仅含时间单位的时长。
pub fn instant_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
instant_add_duration(vm, args, -1)
}
/// `Temporal.Instant.prototype.toString()`:输出 ISO 8601 UTC(如 `2024-01-01T00:00:00Z`)。
pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
@@ -854,7 +915,7 @@ pub fn duration_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
*value = number;
}
native_try!(validate_duration_values(vm, &values));
make_duration(vm, values)
initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_DURATION, values.map(JsValue::float))
}
/// `Temporal.Duration.from(value)`,复制 Duration 或读取同名字段。
@@ -1148,7 +1209,12 @@ pub fn plain_date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
if !valid_iso_date(year, month, day) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
}
make_plain_date(vm, year, month, day)
initialize_temporal_receiver(
vm,
args,
JsObject::OBJ_TYPE_PLAIN_DATE,
[JsValue::float(year as f64), JsValue::float(month as f64), JsValue::float(day as f64)],
)
}
/// `Temporal.PlainDate.from(value)`:接受 ISO 日期字符串或 `{year, month, day}` 对象。
@@ -1226,22 +1292,6 @@ pub fn plain_date_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}")))
}
fn make_plain_time<H: VmHost>(
vm: &mut H, hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32,
) -> NativeResult {
let total_ns = hour as f64 * 3.6e12
+ minute as f64 * 6e10
+ second as f64 * 1e9
+ ms as f64 * 1e6
+ us as f64 * 1e3
+ ns as f64;
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_time_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_TIME;
obj.set_prop_at(0, JsValue::float(total_ns));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn valid_plain_time(hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> bool {
hour <= 23 && minute <= 59 && second <= 59 && ms <= 999 && us <= 999 && ns <= 999
}
@@ -1271,7 +1321,13 @@ pub fn plain_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
if !valid_plain_time(hour, minute, second, ms, us, ns) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component"));
}
make_plain_time(vm, hour, minute, second, ms, us, ns)
let total_ns = hour as f64 * 3.6e12
+ minute as f64 * 6e10
+ second as f64 * 1e9
+ ms as f64 * 1e6
+ us as f64 * 1e3
+ ns as f64;
initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_PLAIN_TIME, [JsValue::float(total_ns)])
}
/// 拆解午夜后纳秒为各分量。
@@ -86,7 +86,9 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
instant_proto,
core,
&[
("add", oxide_builtins::temporal::instant_add::<crate::vm::Vm> as *const (), 1),
("equals", oxide_builtins::temporal::instant_equals::<crate::vm::Vm> as *const (), 1),
("subtract", oxide_builtins::temporal::instant_subtract::<crate::vm::Vm> as *const (), 1),
("toString", oxide_builtins::temporal::instant_to_string::<crate::vm::Vm> as *const (), 0),
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
],
@@ -141,6 +141,45 @@ fn instant_compare_accepts_instances_strings_and_annotations() {
assert!(r.as_bool());
}
#[test]
fn instant_add_and_subtract_are_exact() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let i = Temporal.Instant.fromEpochNanoseconds(1582966647747612578n);
i.add({hours: 1, microseconds: 9007199254740991}).epochNanoseconds === 10590169502488603578n
&& i.subtract('PT1.03125H').epochNanoseconds === 1582962935247612578n",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_add_ignores_receiver_subclass_for_result() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"class Sub extends Temporal.Instant { constructor(value) { super(value); } }
let result = new Sub(10n).add({nanoseconds: 5});
result instanceof Temporal.Instant && !(result instanceof Sub) && result.epochNanoseconds === 15n",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_arithmetic_rejects_date_units_and_out_of_range_results() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let max = Temporal.Instant.fromEpochNanoseconds(8640000000000000000000n);
(() => { try { max.add({nanoseconds: 1}); return false } catch (e) { return e instanceof RangeError } })()
&& (() => { try { max.subtract({days: 1}); return false } catch (e) { return e instanceof RangeError } })()",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_epoch_factories_validate_input_and_range() {
let mut vm = Vm::new();