mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 18:51:13 +08:00
feat: Temporal 最小子集(Temporal.Now/Instant/PlainDate/PlainTime)
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bindings::{apply_binding_table, bind_accessor_getter, bind_global_value, configure_native_constructor};
|
||||
use oxide_kernel::kernel::{KernelCore, KernelSession};
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
/// 把 Temporal 命名空间及其 Now/Instant/PlainDate/PlainTime 子对象绑定到 global。
|
||||
pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
|
||||
let world = session.builtin_world();
|
||||
let temporal_ptr = world.temporal_object.as_ptr() as *mut JsObject;
|
||||
let temporal = unsafe { &mut *temporal_ptr };
|
||||
|
||||
// Temporal.Now:非构造器命名空间,方法直接绑定。
|
||||
let now_ptr = world.temporal_now_object.as_ptr() as *mut JsObject;
|
||||
let now = unsafe { &mut *now_ptr };
|
||||
apply_binding_table(
|
||||
world,
|
||||
now,
|
||||
core,
|
||||
&[
|
||||
("instant", oxide_builtins::temporal::now_instant::<crate::vm::Vm> as *const (), 0),
|
||||
(
|
||||
"timeZoneId",
|
||||
oxide_builtins::temporal::now_time_zone_id::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Temporal.Instant:构造器 + from 静态方法 + 原型 getter 与 toString/valueOf。
|
||||
let instant_ctor_ptr = world.instant_constructor.as_ptr() as *mut JsObject;
|
||||
let instant_ctor = unsafe { &mut *instant_ctor_ptr };
|
||||
configure_native_constructor(
|
||||
instant_ctor,
|
||||
oxide_builtins::temporal::instant_constructor::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
);
|
||||
apply_binding_table(
|
||||
world,
|
||||
instant_ctor,
|
||||
core,
|
||||
&[("from", oxide_builtins::temporal::instant_from::<crate::vm::Vm> as *const (), 1)],
|
||||
);
|
||||
let instant_proto_ptr = world.instant_proto.as_ptr() as *mut JsObject;
|
||||
let instant_proto = unsafe { &mut *instant_proto_ptr };
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
instant_proto,
|
||||
"epochSeconds",
|
||||
oxide_builtins::temporal::instant_epoch_seconds::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
instant_proto,
|
||||
"epochMilliseconds",
|
||||
oxide_builtins::temporal::instant_epoch_milliseconds::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
instant_proto,
|
||||
"epochMicroseconds",
|
||||
oxide_builtins::temporal::instant_epoch_microseconds::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
instant_proto,
|
||||
"epochNanoseconds",
|
||||
oxide_builtins::temporal::instant_epoch_nanoseconds::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
apply_binding_table(
|
||||
world,
|
||||
instant_proto,
|
||||
core,
|
||||
&[
|
||||
("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),
|
||||
],
|
||||
);
|
||||
|
||||
// Temporal.PlainDate:构造器 + from 静态方法 + year/month/day getter 与 toString。
|
||||
let plain_date_ctor_ptr = world.plain_date_constructor.as_ptr() as *mut JsObject;
|
||||
let plain_date_ctor = unsafe { &mut *plain_date_ctor_ptr };
|
||||
configure_native_constructor(
|
||||
plain_date_ctor,
|
||||
oxide_builtins::temporal::plain_date_constructor::<crate::vm::Vm> as *const (),
|
||||
3,
|
||||
);
|
||||
apply_binding_table(
|
||||
world,
|
||||
plain_date_ctor,
|
||||
core,
|
||||
&[("from", oxide_builtins::temporal::plain_date_from::<crate::vm::Vm> as *const (), 1)],
|
||||
);
|
||||
let plain_date_proto_ptr = world.plain_date_proto.as_ptr() as *mut JsObject;
|
||||
let plain_date_proto = unsafe { &mut *plain_date_proto_ptr };
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_date_proto,
|
||||
"year",
|
||||
oxide_builtins::temporal::plain_date_year::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_date_proto,
|
||||
"month",
|
||||
oxide_builtins::temporal::plain_date_month::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_date_proto,
|
||||
"day",
|
||||
oxide_builtins::temporal::plain_date_day::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
apply_binding_table(
|
||||
world,
|
||||
plain_date_proto,
|
||||
core,
|
||||
&[("toString", oxide_builtins::temporal::plain_date_to_string::<crate::vm::Vm> as *const (), 0)],
|
||||
);
|
||||
|
||||
// Temporal.PlainTime:构造器 + 6 个分量 getter 与 toString。
|
||||
let plain_time_ctor_ptr = world.plain_time_constructor.as_ptr() as *mut JsObject;
|
||||
let plain_time_ctor = unsafe { &mut *plain_time_ctor_ptr };
|
||||
configure_native_constructor(
|
||||
plain_time_ctor,
|
||||
oxide_builtins::temporal::plain_time_constructor::<crate::vm::Vm> as *const (),
|
||||
6,
|
||||
);
|
||||
let plain_time_proto_ptr = world.plain_time_proto.as_ptr() as *mut JsObject;
|
||||
let plain_time_proto = unsafe { &mut *plain_time_proto_ptr };
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"hour",
|
||||
oxide_builtins::temporal::plain_time_hour::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"minute",
|
||||
oxide_builtins::temporal::plain_time_minute::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"second",
|
||||
oxide_builtins::temporal::plain_time_second::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"millisecond",
|
||||
oxide_builtins::temporal::plain_time_millisecond::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"microsecond",
|
||||
oxide_builtins::temporal::plain_time_microsecond::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
bind_accessor_getter(
|
||||
core,
|
||||
session,
|
||||
plain_time_proto,
|
||||
"nanosecond",
|
||||
oxide_builtins::temporal::plain_time_nanosecond::<crate::vm::Vm> as *const (),
|
||||
);
|
||||
apply_binding_table(
|
||||
world,
|
||||
plain_time_proto,
|
||||
core,
|
||||
&[("toString", oxide_builtins::temporal::plain_time_to_string::<crate::vm::Vm> as *const (), 0)],
|
||||
);
|
||||
|
||||
// 把子对象挂到 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));
|
||||
bind_global_value(core, temporal, "PlainDate", JsValue::from_js_object(plain_date_ctor_ptr));
|
||||
bind_global_value(core, temporal, "PlainTime", JsValue::from_js_object(plain_time_ctor_ptr));
|
||||
bind_global_value(core, global, "Temporal", JsValue::from_js_object(temporal_ptr));
|
||||
}
|
||||
@@ -42,6 +42,8 @@ pub mod bind_string;
|
||||
pub mod bind_stubs;
|
||||
/// Symbol 构造器与原型的 native 方法绑定。
|
||||
pub mod bind_symbol;
|
||||
/// Temporal 命名空间对象(Now/Instant/PlainDate/PlainTime)绑定。
|
||||
pub mod bind_temporal;
|
||||
/// 各 TypedArray 构造器与共享原型的 native 方法绑定。
|
||||
pub mod bind_typed_array;
|
||||
|
||||
@@ -483,6 +485,7 @@ pub fn bind_global_builtin_slots(core: &Arc<KernelCore>, session: &KernelSession
|
||||
("Symbol", JsValue::from_js_object(world.symbol_constructor.as_ptr() as *mut JsObject)),
|
||||
("Math", JsValue::from_js_object(world.math_object.as_ptr() as *mut JsObject)),
|
||||
("JSON", JsValue::from_js_object(world.json_object.as_ptr() as *mut JsObject)),
|
||||
("Temporal", JsValue::from_js_object(world.temporal_object.as_ptr() as *mut JsObject)),
|
||||
] {
|
||||
bind_existing_global(core, global, name, value);
|
||||
}
|
||||
@@ -665,6 +668,9 @@ pub fn rebind_dirty_builtins(core: &Arc<KernelCore>, session: &mut KernelSession
|
||||
if dirty.map_or(true, |d| d.symbol_family) {
|
||||
bind_symbol::bind_symbol(core, session, global);
|
||||
}
|
||||
if dirty.map_or(true, |d| d.temporal) {
|
||||
bind_temporal::bind_temporal(core, session, global);
|
||||
}
|
||||
if dirty.map_or(true, |d| d.stubs) {
|
||||
bind_stubs::bind_stubs(core, session, global);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(vm: &mut Vm, source: &str) -> Result<oxide_types::value::JsValue, String> {
|
||||
let allocator = oxide_parser::Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
fn str_val(vm: &Vm, val: oxide_types::value::JsValue) -> String {
|
||||
vm.lookup_str(val).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn num(vm: &mut Vm, source: &str) -> f64 {
|
||||
let r = eval(vm, source).unwrap();
|
||||
if r.is_double() {
|
||||
r.as_double()
|
||||
} else {
|
||||
r.as_int() as f64
|
||||
}
|
||||
}
|
||||
|
||||
// -- Temporal 命名空间 --
|
||||
|
||||
#[test]
|
||||
fn temporal_is_object() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "typeof Temporal").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "object");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporal_now_time_zone_id_is_utc() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "Temporal.Now.timeZoneId()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "UTC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporal_now_instant_returns_instant() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "Temporal.Now.instant() instanceof Temporal.Instant").unwrap();
|
||||
assert!(r.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporal_now_instant_epoch_is_positive() {
|
||||
let mut vm = Vm::new();
|
||||
let r = num(&mut vm, "Temporal.Now.instant().epochMilliseconds");
|
||||
assert!(r > 1_500_000_000_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporal_now_is_namespace_not_constructor() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "typeof Temporal.Now").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "object");
|
||||
}
|
||||
|
||||
// -- Temporal.Instant --
|
||||
|
||||
#[test]
|
||||
fn instant_from_iso_string() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "2024-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_epoch_seconds() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(num(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochSeconds"), 1704067200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_epoch_milliseconds() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(
|
||||
num(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochMilliseconds"),
|
||||
1704067200000.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_epoch_nanoseconds() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(
|
||||
num(&mut vm, "Temporal.Instant.from('2024-01-01T00:00:00Z').epochNanoseconds"),
|
||||
1704067200000000000.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_constructor_requires_new() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { Temporal.Instant(0); 'no' } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_value_of_throws() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"try { Temporal.Instant.prototype.valueOf.call(Temporal.Now.instant()) } catch (e) { e.constructor.name }",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_method_on_non_instant_throws() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { Temporal.Instant.prototype.toString.call({}) } catch (e) { e.constructor.name }")
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instant_from_invalid_throws_range_error() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { Temporal.Instant.from('nope') } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
|
||||
// -- Temporal.PlainDate --
|
||||
|
||||
#[test]
|
||||
fn plain_date_from_string() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').year"), 2024.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').month"), 1.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.PlainDate.from('2024-01-15').day"), 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_from_object() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "Temporal.PlainDate.from({year:2023, month:3, day:5}).toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "2023-03-05");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_constructor_month_day() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).month"), 2.0);
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).day"), 29.0);
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainDate(2024, 2, 29).year"), 2024.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_leap_year_validation() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { new Temporal.PlainDate(2024, 2, 30) } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_invalid_month() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { new Temporal.PlainDate(2024, 13, 1) } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_to_string() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 5).toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "2024-01-05");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_requires_new() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { Temporal.PlainDate(2024, 1, 1); 'no' } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_method_on_non_date_throws() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"try { Temporal.PlainDate.prototype.year.get.call({}) } catch (e) { e.constructor.name }",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "TypeError");
|
||||
}
|
||||
|
||||
// -- Temporal.PlainTime --
|
||||
|
||||
#[test]
|
||||
fn plain_time_constructor_components() {
|
||||
let mut vm = Vm::new();
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).hour"), 12.0);
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).minute"), 30.0);
|
||||
assert_eq!(num(&mut vm, "new Temporal.PlainTime(12, 30, 5).second"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_time_full_precision_components() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"let t = new Temporal.PlainTime(23, 59, 59, 999, 999, 999); t.hour + ',' + t.minute + ',' + t.second + ',' + t.millisecond + ',' + t.microsecond + ',' + t.nanosecond",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "23,59,59,999,999,999");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_time_to_string() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5).toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "12:30:05");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_time_to_string_with_subseconds() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "new Temporal.PlainTime(12, 30, 5, 500, 250, 1).toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "12:30:05.500250001");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_time_missing_components_default_zero() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "new Temporal.PlainTime().toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "00:00:00");
|
||||
let r = eval(&mut vm, "new Temporal.PlainTime(12).toString()").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "12:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_time_range_validation() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "try { new Temporal.PlainTime(24, 0, 0) } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
let r = eval(&mut vm, "try { new Temporal.PlainTime(0, 60, 0) } catch (e) { e.constructor.name }").unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
Reference in New Issue
Block a user