feat: implement Temporal Instant zoned conversion

This commit is contained in:
爱特每羊队
2026-08-11 20:54:25 +08:00
parent a9e194f2c6
commit 5ec0dc2b7d
12 changed files with 333 additions and 43 deletions
+20 -26
View File
@@ -2,7 +2,8 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
use oxide_kernel::string_forge::PermInterner; use oxide_kernel::string_forge::PermInterner;
use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry};
use oxide_types::private_key::{ use oxide_types::private_key::{
is_private_name_key, is_symbol_key, symbol_index_from_key, well_known_symbol_id_from_key, is_private_name_key, is_symbol_key, make_well_known_symbol_key, symbol_index_from_key,
well_known_symbol_id_from_key,
}; };
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
@@ -110,7 +111,8 @@ fn decode_symbol_key<H: VmHost>(vm: &H, key: u32) -> JsValue {
5 => world.sym_to_primitive.as_ptr(), 5 => world.sym_to_primitive.as_ptr(),
6 => world.sym_has_instance.as_ptr(), 6 => world.sym_has_instance.as_ptr(),
7 => world.sym_match_all.as_ptr(), 7 => world.sym_match_all.as_ptr(),
_ => world.sym_async_iterator.as_ptr(), 8 => world.sym_async_iterator.as_ptr(),
_ => world.sym_to_string_tag.as_ptr(),
}; };
return JsValue::from_js_object(ptr as *mut JsObject); return JsValue::from_js_object(ptr as *mut JsObject);
} }
@@ -643,29 +645,13 @@ pub fn object_get_own_property_descriptor<H: VmHost>(vm: &mut H, args: &[u8]) ->
Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)), Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)),
}; };
let obj_ptr = obj_val.as_js_object_ptr(); let obj_ptr = obj_val.as_js_object_ptr();
let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2])); let key = vm.property_key_si(vm.reg(args[2]));
let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; let obj = unsafe { &*obj_ptr };
let Some(offset) = vm.get_own_property_slot(obj, key) else {
let (found_value, found_meta, found) = {
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
let mut found_value = JsValue::undefined();
let mut found_meta = None;
let mut found = false;
for (prop_si, offset) in keys {
if prop_si == si {
found_value = obj.get_prop_at(offset);
found_meta = obj.prop_meta_at(offset);
found = true;
break;
}
}
(found_value, found_meta, found)
};
if !found {
return NativeResult::Ok(JsValue::undefined()); return NativeResult::Ok(JsValue::undefined());
} };
let found_value = obj.get_prop_at(offset);
let found_meta = obj.prop_meta_at(offset);
let sf_ptr = vm.kernel_core().perm_interner().as_ref() as *const PermInterner; let sf_ptr = vm.kernel_core().perm_interner().as_ref() as *const PermInterner;
let sh_ptr = vm.kernel_core().shape_forge().as_ref() as *const ShapeForge; let sh_ptr = vm.kernel_core().shape_forge().as_ref() as *const ShapeForge;
@@ -1029,8 +1015,16 @@ pub fn object_proto_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
"Object" "Object"
}; };
// @@toStringTag 为字符串时覆盖内置标签;getter 抛错透传原异常。 // @@toStringTag 为字符串时覆盖内置标签;getter 抛错透传原异常。
let tag_si = vm.kernel_core().perm_interner().intern("@@toStringTag").0; let tag_key = make_well_known_symbol_key(9);
match vm.ordinary_get(obj, tag_si, this_val) { let tag_value = match vm.ordinary_get(obj, tag_key, this_val) {
Ok(value) if !value.is_undefined() => Ok(value),
Ok(_) => {
let legacy_key = vm.kernel_core().perm_interner().intern("@@toStringTag").0;
vm.ordinary_get(obj, legacy_key, this_val)
}
Err(err) => Err(err),
};
match tag_value {
Ok(v) => match vm.lookup_str(v) { Ok(v) => match vm.lookup_str(v) {
Some(s) => return NativeResult::Ok(vm.new_string(&format!("[object {s}]"))), Some(s) => return NativeResult::Ok(vm.new_string(&format!("[object {s}]"))),
None => builtin, None => builtin,
+121 -9
View File
@@ -6,9 +6,9 @@ use oxide_types::object::JsObject;
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
// Temporal 命名空间的最小实现子集:Temporal.Now / Temporal.Instant / // Temporal 命名空间的最小实现子集:Temporal.Now / Temporal.Instant /
// Temporal.PlainDate / Temporal.PlainTime。内部数据按对象类型存入 prop 槽: // Temporal.PlainDate / Temporal.PlainTime / Temporal.ZonedDateTime。内部数据按对象类型存入 prop 槽:
// Instant 存纪元纳秒(BigInt,prop 0)、PlainDate 存年/月/日(prop 0-2)、 // Instant 存纪元纳秒(BigInt,prop 0)、PlainDate 存年/月/日(prop 0-2)、
// PlainTime 存午夜后纳秒(f64,prop 0)。 // PlainTime 存午夜后纳秒(f64,prop 0),ZonedDateTime 存纪元纳秒、时区 ID、日历 ID(prop 0-2)。
const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000; const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000;
@@ -69,6 +69,13 @@ fn ensure_duration<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue>
Ok(()) Ok(())
} }
fn ensure_zoned_date_time<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_zoned_date_time_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
fn receiver_obj<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> { fn receiver_obj<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); let raw = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !raw.is_object() { if !raw.is_object() {
@@ -127,6 +134,19 @@ fn make_instant<H: VmHost>(vm: &mut H, epoch_ns: i128) -> NativeResult {
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
} }
fn make_zoned_date_time<H: VmHost>(vm: &mut H, epoch_ns: i128, time_zone_id: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().zoned_date_time_proto.as_ptr() as *mut JsObject);
let epoch_value = vm.new_bigint(epoch_ns);
let time_zone_value = vm.new_string(time_zone_id);
let calendar_value = vm.new_string("iso8601");
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_ZONED_DATE_TIME;
obj.set_prop_at(0, epoch_value);
obj.set_prop_at(1, time_zone_value);
obj.set_prop_at(2, calendar_value);
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option<i128> { fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option<i128> {
let end = cursor.checked_add(count)?; let end = cursor.checked_add(count)?;
let digits = bytes.get(*cursor..end)?; let digits = bytes.get(*cursor..end)?;
@@ -986,13 +1006,13 @@ fn parse_offset_minutes(value: &str) -> Option<i32> {
Some(if bytes[0] == b'-' { -magnitude } else { magnitude }) Some(if bytes[0] == b'-' { -magnitude } else { magnitude })
} }
fn instant_time_zone_offset(value: &str) -> Option<i32> { fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
let input = value.trim(); let input = value.trim();
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") { if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
return Some(0); return Some(("UTC".to_string(), 0));
} }
if let Some(offset) = parse_offset_minutes(input) { if let Some(offset) = parse_offset_minutes(input) {
return Some(offset); return Some((input.to_string(), offset));
} }
if input.starts_with("-000000") { if input.starts_with("-000000") {
return None; return None;
@@ -1005,21 +1025,26 @@ fn instant_time_zone_offset(value: &str) -> Option<i32> {
} }
let annotation = &input[open + 1..input.len() - 1]; let annotation = &input[open + 1..input.len() - 1];
if annotation.eq_ignore_ascii_case("UTC") { if annotation.eq_ignore_ascii_case("UTC") {
return Some(0); return Some(("UTC".to_string(), 0));
} }
return parse_offset_minutes(annotation); return parse_offset_minutes(annotation).map(|offset| (annotation.to_string(), offset));
} }
let time_start = input.find(['T', 't', ' '])?; let time_start = input.find(['T', 't', ' '])?;
let time = &input[time_start + 1..]; let time = &input[time_start + 1..];
if time.ends_with(['Z', 'z']) { if time.ends_with(['Z', 'z']) {
return Some(0); return Some(("UTC".to_string(), 0));
} }
let offset_start = time let offset_start = time
.char_indices() .char_indices()
.rev() .rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?; .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
parse_offset_minutes(&time[offset_start..]) let offset_id = &time[offset_start..];
parse_offset_minutes(offset_id).map(|offset| (offset_id.to_string(), offset))
}
fn instant_time_zone_offset(value: &str) -> Option<i32> {
canonical_time_zone(value).map(|(_, offset)| offset)
} }
fn civil_from_days(days: i128) -> (i128, i128, i128) { fn civil_from_days(days: i128) -> (i128, i128, i128) {
@@ -1199,11 +1224,98 @@ pub fn instant_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRes
instant_default_string(vm, args) instant_default_string(vm, args)
} }
/// `Temporal.Instant.prototype.toZonedDateTimeISO(timeZone)`:以同一纪元纳秒创建 ISO 日历 ZonedDateTime。
pub fn instant_to_zoned_date_time_iso<H: VmHost>(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"));
};
if args.len() < 2 {
return NativeResult::Err(crate::error::create_type_error(vm, "timeZone is required"));
}
let raw = vm.reg(args[1]);
if !raw.is_string() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
}
let input = to_string(raw);
let Some((time_zone_id, _)) = canonical_time_zone(&input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
};
make_zoned_date_time(vm, epoch_ns, &time_zone_id)
}
/// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。 /// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。
pub fn instant_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult { pub fn instant_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf")) NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf"))
} }
/// `Temporal.ZonedDateTime` 构造器:保存纪元纳秒、固定偏移或 UTC 时区以及 ISO 日历。
pub fn zoned_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ctor_proto = vm.session().builtin_world().zoned_date_time_proto.as_ptr();
if !is_ctor_call(vm, args, ctor_proto) {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Class constructor Temporal.ZonedDateTime cannot be invoked without 'new'",
));
}
let epoch_raw = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
let epoch_ns = native_try!(primitive_to_bigint(vm, epoch_raw));
if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range"));
}
if args.len() < 3 || !vm.reg(args[2]).is_string() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
}
let time_zone_input = to_string(vm.reg(args[2]));
let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
};
if args.len() >= 4 && !vm.reg(args[3]).is_undefined() {
let calendar = vm.reg(args[3]);
if !calendar.is_string() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid calendar"));
}
if !to_string(calendar).eq_ignore_ascii_case("iso8601") {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendar"));
}
}
let epoch_value = vm.new_bigint(epoch_ns);
let time_zone_value = vm.new_string(&time_zone_id);
let calendar_value = vm.new_string("iso8601");
initialize_temporal_receiver(
vm,
args,
JsObject::OBJ_TYPE_ZONED_DATE_TIME,
[epoch_value, time_zone_value, calendar_value],
)
}
/// `Temporal.ZonedDateTime.prototype.epochNanoseconds`:返回精确纪元纳秒。
pub fn zoned_date_time_epoch_nanoseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj));
NativeResult::Ok(obj.get_prop_at(0))
}
/// `Temporal.ZonedDateTime.prototype.timeZoneId`:返回规范化时区标识符。
pub fn zoned_date_time_time_zone_id<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj));
NativeResult::Ok(obj.get_prop_at(1))
}
/// `Temporal.ZonedDateTime.prototype.calendarId`:最小实现固定返回 ISO 8601 日历。
pub fn zoned_date_time_calendar_id<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj));
NativeResult::Ok(obj.get_prop_at(2))
}
fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32) -> NativeResult { fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject); let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
@@ -226,6 +226,30 @@ fn test_get_own_property_descriptor_no_args_type_error() {
assert!(eval("Object.getOwnPropertyDescriptor()").is_err()); assert!(eval("Object.getOwnPropertyDescriptor()").is_err());
} }
#[test]
fn test_get_own_property_descriptor_supports_symbol_keys() {
let (_vm, result) = eval(
"var key = Symbol('key');
var object = {};
Object.defineProperty(object, key, { value: 7, writable: false });
var descriptor = Object.getOwnPropertyDescriptor(object, key);
descriptor.value === 7 && descriptor.writable === false",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn test_get_own_property_descriptor_keeps_undefined_array_properties() {
let (_vm, result) = eval(
"var array = [];
Object.defineProperty(array, 'named', { value: undefined });
Object.getOwnPropertyDescriptor(array, 'named').value === undefined",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test] #[test]
fn test_has_own_property_boxes_primitive_this() { fn test_has_own_property_boxes_primitive_this() {
// ToObject 装箱 this:原始值上 hasOwnProperty 返回 false 而非抛错。 // ToObject 装箱 this:原始值上 hasOwnProperty 返回 false 而非抛错。
+29 -1
View File
@@ -243,6 +243,7 @@ pub struct BuiltinWorld {
pub sym_has_instance: P<JsObject>, pub sym_has_instance: P<JsObject>,
pub sym_match_all: P<JsObject>, pub sym_match_all: P<JsObject>,
pub sym_async_iterator: P<JsObject>, pub sym_async_iterator: P<JsObject>,
pub sym_to_string_tag: P<JsObject>,
pub temporal_object: P<JsObject>, pub temporal_object: P<JsObject>,
pub temporal_now_object: P<JsObject>, pub temporal_now_object: P<JsObject>,
pub instant_constructor: P<JsObject>, pub instant_constructor: P<JsObject>,
@@ -253,6 +254,8 @@ pub struct BuiltinWorld {
pub plain_time_proto: P<JsObject>, pub plain_time_proto: P<JsObject>,
pub duration_constructor: P<JsObject>, pub duration_constructor: P<JsObject>,
pub duration_proto: P<JsObject>, pub duration_proto: P<JsObject>,
pub zoned_date_time_constructor: P<JsObject>,
pub zoned_date_time_proto: P<JsObject>,
pub bigint_constructor: P<JsObject>, pub bigint_constructor: P<JsObject>,
pub bigint_proto: P<JsObject>, pub bigint_proto: P<JsObject>,
pub stub_objects: Vec<P<JsObject>>, pub stub_objects: Vec<P<JsObject>>,
@@ -507,10 +510,11 @@ fn wire_builtin_world_links(world: &BuiltinWorld) {
wire_ctor_proto(&world.plain_date_constructor, &world.plain_date_proto); wire_ctor_proto(&world.plain_date_constructor, &world.plain_date_proto);
wire_ctor_proto(&world.plain_time_constructor, &world.plain_time_proto); wire_ctor_proto(&world.plain_time_constructor, &world.plain_time_proto);
wire_ctor_proto(&world.duration_constructor, &world.duration_proto); wire_ctor_proto(&world.duration_constructor, &world.duration_proto);
wire_ctor_proto(&world.zoned_date_time_constructor, &world.zoned_date_time_proto);
wire_ctor_proto(&world.bigint_constructor, &world.bigint_proto); wire_ctor_proto(&world.bigint_constructor, &world.bigint_proto);
let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject);
let non_object_protos: [&P<JsObject>; 19] = [ let non_object_protos: [&P<JsObject>; 20] = [
&world.array_proto, &world.array_proto,
&world.function_proto, &world.function_proto,
&world.string_proto, &world.string_proto,
@@ -529,6 +533,7 @@ fn wire_builtin_world_links(world: &BuiltinWorld) {
&world.plain_date_proto, &world.plain_date_proto,
&world.plain_time_proto, &world.plain_time_proto,
&world.duration_proto, &world.duration_proto,
&world.zoned_date_time_proto,
&world.bigint_proto, &world.bigint_proto,
]; ];
for proto in &non_object_protos { for proto in &non_object_protos {
@@ -656,6 +661,7 @@ impl BuiltinWorld {
BuiltinId::SymHasInstance => &self.sym_has_instance, BuiltinId::SymHasInstance => &self.sym_has_instance,
BuiltinId::SymMatchAll => &self.sym_match_all, BuiltinId::SymMatchAll => &self.sym_match_all,
BuiltinId::SymAsyncIterator => &self.sym_async_iterator, BuiltinId::SymAsyncIterator => &self.sym_async_iterator,
BuiltinId::SymToStringTag => &self.sym_to_string_tag,
BuiltinId::TemporalObject => &self.temporal_object, BuiltinId::TemporalObject => &self.temporal_object,
BuiltinId::TemporalNowObject => &self.temporal_now_object, BuiltinId::TemporalNowObject => &self.temporal_now_object,
BuiltinId::InstantConstructor => &self.instant_constructor, BuiltinId::InstantConstructor => &self.instant_constructor,
@@ -666,6 +672,8 @@ impl BuiltinWorld {
BuiltinId::PlainTimeProto => &self.plain_time_proto, BuiltinId::PlainTimeProto => &self.plain_time_proto,
BuiltinId::DurationConstructor => &self.duration_constructor, BuiltinId::DurationConstructor => &self.duration_constructor,
BuiltinId::DurationProto => &self.duration_proto, BuiltinId::DurationProto => &self.duration_proto,
BuiltinId::ZonedDateTimeConstructor => &self.zoned_date_time_constructor,
BuiltinId::ZonedDateTimeProto => &self.zoned_date_time_proto,
BuiltinId::BigIntConstructor => &self.bigint_constructor, BuiltinId::BigIntConstructor => &self.bigint_constructor,
BuiltinId::BigIntProto => &self.bigint_proto, BuiltinId::BigIntProto => &self.bigint_proto,
} }
@@ -709,6 +717,7 @@ impl BuiltinWorld {
let sym_has_instance = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_has_instance = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_match_all = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_match_all = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_async_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_async_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let sym_to_string_tag = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let temporal_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let temporal_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let temporal_now_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let temporal_now_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant"); let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant");
@@ -717,6 +726,8 @@ impl BuiltinWorld {
let (plain_time_proto, plain_time_constructor) = let (plain_time_proto, plain_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "PlainTime"); make_named_pair(string_forge, shape_forge, labels, "PlainTime");
let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration"); let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration");
let (zoned_date_time_proto, zoned_date_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "ZonedDateTime");
let (bigint_proto, bigint_constructor) = make_named_pair(string_forge, shape_forge, labels, "BigInt"); let (bigint_proto, bigint_constructor) = make_named_pair(string_forge, shape_forge, labels, "BigInt");
let stub_objects = Vec::new(); let stub_objects = Vec::new();
@@ -790,6 +801,7 @@ impl BuiltinWorld {
sym_has_instance, sym_has_instance,
sym_match_all, sym_match_all,
sym_async_iterator, sym_async_iterator,
sym_to_string_tag,
temporal_object, temporal_object,
temporal_now_object, temporal_now_object,
instant_constructor, instant_constructor,
@@ -800,6 +812,8 @@ impl BuiltinWorld {
plain_time_proto, plain_time_proto,
duration_constructor, duration_constructor,
duration_proto, duration_proto,
zoned_date_time_constructor,
zoned_date_time_proto,
bigint_constructor, bigint_constructor,
bigint_proto, bigint_proto,
stub_objects, stub_objects,
@@ -875,6 +889,7 @@ impl BuiltinWorld {
sym_has_instance, sym_has_instance,
sym_match_all, sym_match_all,
sym_async_iterator, sym_async_iterator,
sym_to_string_tag,
) = if dirty.symbol_family { ) = if dirty.symbol_family {
let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol"); let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol");
( (
@@ -889,6 +904,7 @@ impl BuiltinWorld {
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
) )
} else { } else {
( (
@@ -903,6 +919,7 @@ impl BuiltinWorld {
current.sym_has_instance.clone(), current.sym_has_instance.clone(),
current.sym_match_all.clone(), current.sym_match_all.clone(),
current.sym_async_iterator.clone(), current.sym_async_iterator.clone(),
current.sym_to_string_tag.clone(),
) )
}; };
@@ -987,6 +1004,8 @@ impl BuiltinWorld {
plain_time_constructor, plain_time_constructor,
duration_proto, duration_proto,
duration_constructor, duration_constructor,
zoned_date_time_proto,
zoned_date_time_constructor,
) = if dirty.temporal { ) = if dirty.temporal {
let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant"); let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant");
let (plain_date_proto, plain_date_constructor) = let (plain_date_proto, plain_date_constructor) =
@@ -994,6 +1013,8 @@ impl BuiltinWorld {
let (plain_time_proto, plain_time_constructor) = let (plain_time_proto, plain_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "PlainTime"); make_named_pair(string_forge, shape_forge, labels, "PlainTime");
let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration"); let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration");
let (zoned_date_time_proto, zoned_date_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "ZonedDateTime");
( (
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
@@ -1005,6 +1026,8 @@ impl BuiltinWorld {
plain_time_constructor, plain_time_constructor,
duration_proto, duration_proto,
duration_constructor, duration_constructor,
zoned_date_time_proto,
zoned_date_time_constructor,
) )
} else { } else {
( (
@@ -1018,6 +1041,8 @@ impl BuiltinWorld {
current.plain_time_constructor.clone(), current.plain_time_constructor.clone(),
current.duration_proto.clone(), current.duration_proto.clone(),
current.duration_constructor.clone(), current.duration_constructor.clone(),
current.zoned_date_time_proto.clone(),
current.zoned_date_time_constructor.clone(),
) )
}; };
let (bigint_proto, bigint_constructor) = if dirty.stubs { let (bigint_proto, bigint_constructor) = if dirty.stubs {
@@ -1097,6 +1122,7 @@ impl BuiltinWorld {
sym_has_instance, sym_has_instance,
sym_match_all, sym_match_all,
sym_async_iterator, sym_async_iterator,
sym_to_string_tag,
temporal_object, temporal_object,
temporal_now_object, temporal_now_object,
instant_constructor, instant_constructor,
@@ -1107,6 +1133,8 @@ impl BuiltinWorld {
plain_time_proto, plain_time_proto,
duration_constructor, duration_constructor,
duration_proto, duration_proto,
zoned_date_time_constructor,
zoned_date_time_proto,
bigint_constructor, bigint_constructor,
bigint_proto, bigint_proto,
stub_objects, stub_objects,
+12 -3
View File
@@ -225,7 +225,7 @@ pub struct KernelSession {
/// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及 /// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及
/// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的 /// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的
/// builtin 家族。 /// builtin 家族。
pub const NUM_BUILTINS: usize = 80; pub const NUM_BUILTINS: usize = 83;
/// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。 /// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。
/// ///
@@ -314,6 +314,9 @@ pub enum BuiltinId {
BigIntProto = 77, BigIntProto = 77,
DurationConstructor = 78, DurationConstructor = 78,
DurationProto = 79, DurationProto = 79,
SymToStringTag = 80,
ZonedDateTimeConstructor = 81,
ZonedDateTimeProto = 82,
} }
impl BuiltinId { impl BuiltinId {
@@ -399,6 +402,9 @@ impl BuiltinId {
BuiltinId::BigIntProto, BuiltinId::BigIntProto,
BuiltinId::DurationConstructor, BuiltinId::DurationConstructor,
BuiltinId::DurationProto, BuiltinId::DurationProto,
BuiltinId::SymToStringTag,
BuiltinId::ZonedDateTimeConstructor,
BuiltinId::ZonedDateTimeProto,
]; ];
} }
@@ -586,7 +592,8 @@ impl KernelSession {
|| gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive) || gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive)
|| gen(BuiltinId::SymHasInstance) != snap(BuiltinId::SymHasInstance) || gen(BuiltinId::SymHasInstance) != snap(BuiltinId::SymHasInstance)
|| gen(BuiltinId::SymMatchAll) != snap(BuiltinId::SymMatchAll) || gen(BuiltinId::SymMatchAll) != snap(BuiltinId::SymMatchAll)
|| gen(BuiltinId::SymAsyncIterator) != snap(BuiltinId::SymAsyncIterator), || gen(BuiltinId::SymAsyncIterator) != snap(BuiltinId::SymAsyncIterator)
|| gen(BuiltinId::SymToStringTag) != snap(BuiltinId::SymToStringTag),
math: gen(BuiltinId::MathObject) != snap(BuiltinId::MathObject), math: gen(BuiltinId::MathObject) != snap(BuiltinId::MathObject),
json: gen(BuiltinId::JsonObject) != snap(BuiltinId::JsonObject), json: gen(BuiltinId::JsonObject) != snap(BuiltinId::JsonObject),
date: gen(BuiltinId::DateConstructor) != snap(BuiltinId::DateConstructor) date: gen(BuiltinId::DateConstructor) != snap(BuiltinId::DateConstructor)
@@ -633,7 +640,9 @@ impl KernelSession {
|| gen(BuiltinId::PlainTimeConstructor) != snap(BuiltinId::PlainTimeConstructor) || gen(BuiltinId::PlainTimeConstructor) != snap(BuiltinId::PlainTimeConstructor)
|| gen(BuiltinId::PlainTimeProto) != snap(BuiltinId::PlainTimeProto) || gen(BuiltinId::PlainTimeProto) != snap(BuiltinId::PlainTimeProto)
|| gen(BuiltinId::DurationConstructor) != snap(BuiltinId::DurationConstructor) || gen(BuiltinId::DurationConstructor) != snap(BuiltinId::DurationConstructor)
|| gen(BuiltinId::DurationProto) != snap(BuiltinId::DurationProto), || gen(BuiltinId::DurationProto) != snap(BuiltinId::DurationProto)
|| gen(BuiltinId::ZonedDateTimeConstructor) != snap(BuiltinId::ZonedDateTimeConstructor)
|| gen(BuiltinId::ZonedDateTimeProto) != snap(BuiltinId::ZonedDateTimeProto),
stubs: world.stub_objects.len() != snapshot.stub_objects_len stubs: world.stub_objects.len() != snapshot.stub_objects_len
|| stub_generations_dirty || stub_generations_dirty
|| gen(BuiltinId::BigIntConstructor) != snap(BuiltinId::BigIntConstructor) || gen(BuiltinId::BigIntConstructor) != snap(BuiltinId::BigIntConstructor)
+7
View File
@@ -445,6 +445,8 @@ impl JsObject {
pub const OBJ_TYPE_PLAIN_TIME: u8 = 16; pub const OBJ_TYPE_PLAIN_TIME: u8 = 16;
/// Temporal.Duration 对象:十个时长分量依次存于 prop 0-9。 /// Temporal.Duration 对象:十个时长分量依次存于 prop 0-9。
pub const OBJ_TYPE_DURATION: u8 = 17; pub const OBJ_TYPE_DURATION: u8 = 17;
/// Temporal.ZonedDateTime 对象:纪元纳秒、时区 ID、日历 ID 存于 prop 0-2。
pub const OBJ_TYPE_ZONED_DATE_TIME: u8 = 18;
/// `is_session_epoch` 字段中的 session 标记位。 /// `is_session_epoch` 字段中的 session 标记位。
pub const SESSION_EPOCH_BIT: u8 = 0x01; pub const SESSION_EPOCH_BIT: u8 = 0x01;
/// `is_session_epoch` 字段中的 GC 标记位。 /// `is_session_epoch` 字段中的 GC 标记位。
@@ -530,6 +532,11 @@ impl JsObject {
pub fn is_duration_obj(&self) -> bool { pub fn is_duration_obj(&self) -> bool {
self.type_tag == Self::OBJ_TYPE_DURATION self.type_tag == Self::OBJ_TYPE_DURATION
} }
/// 是否 Temporal.ZonedDateTime 对象。
#[inline]
pub fn is_zoned_date_time_obj(&self) -> bool {
self.type_tag == Self::OBJ_TYPE_ZONED_DATE_TIME
}
/// 构造无属性、可扩展的空对象(`new Object()` 的基础对象)。 /// 构造无属性、可扩展的空对象(`new Object()` 的基础对象)。
pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self { pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self {
+1 -1
View File
@@ -17,7 +17,7 @@ pub const PRIVATE_NAME_BASE: u32 = 0x8000_0000;
pub const SYMBOL_KEY_BASE: u32 = 0xA000_0000; pub const SYMBOL_KEY_BASE: u32 = 0xA000_0000;
/// 保留给 well-known symbol 的键槽位数(位于 Symbol 键区间最低端)。 /// 保留给 well-known symbol 的键槽位数(位于 Symbol 键区间最低端)。
pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 9; pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 10;
/// 用户 symbol 下标的保留位掩码(低 28 位)。 /// 用户 symbol 下标的保留位掩码(低 28 位)。
const SYMBOL_INDEX_MASK: u32 = 0x0FFF_FFFF; const SYMBOL_INDEX_MASK: u32 = 0x0FFF_FFFF;
@@ -70,6 +70,10 @@ pub fn bind_symbol(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
"asyncIterator", "asyncIterator",
JsValue::from_js_object(session.builtin_world().sym_async_iterator.as_ptr() as *mut JsObject), JsValue::from_js_object(session.builtin_world().sym_async_iterator.as_ptr() as *mut JsObject),
), ),
(
"toStringTag",
JsValue::from_js_object(session.builtin_world().sym_to_string_tag.as_ptr() as *mut JsObject),
),
] { ] {
bind_well_known_symbol(core, ctor, name, val); bind_well_known_symbol(core, ctor, name, val);
} }
+63 -2
View File
@@ -1,8 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use crate::bindings::{apply_binding_table, bind_accessor_getter, bind_global_value, configure_native_constructor}; use crate::bindings::{
apply_binding_table, bind_accessor_getter, bind_global_value, bind_well_known_data_property,
configure_native_constructor,
};
use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject; use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
/// 把 Temporal 命名空间及其子对象绑定到 global。 /// 把 Temporal 命名空间及其子对象绑定到 global。
@@ -98,10 +101,67 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
0, 0,
), ),
("toString", oxide_builtins::temporal::instant_to_string::<crate::vm::Vm> as *const (), 0), ("toString", oxide_builtins::temporal::instant_to_string::<crate::vm::Vm> as *const (), 0),
(
"toZonedDateTimeISO",
oxide_builtins::temporal::instant_to_zoned_date_time_iso::<crate::vm::Vm> as *const (),
1,
),
("until", oxide_builtins::temporal::instant_until::<crate::vm::Vm> as *const (), 1), ("until", oxide_builtins::temporal::instant_until::<crate::vm::Vm> as *const (), 1),
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0), ("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
], ],
); );
bind_well_known_data_property(
core,
instant_proto,
9,
JsValue::perm_string(
core.perm_interner()
.string_ptr(core.perm_interner().intern("Temporal.Instant").0),
),
PropAttributes::new(false, false, true),
);
// Temporal.ZonedDateTime:最小构造器与三个稳定内部槽 getter。
let zoned_date_time_ctor_ptr = world.zoned_date_time_constructor.as_ptr() as *mut JsObject;
let zoned_date_time_ctor = unsafe { &mut *zoned_date_time_ctor_ptr };
configure_native_constructor(
zoned_date_time_ctor,
oxide_builtins::temporal::zoned_date_time_constructor::<crate::vm::Vm> as *const (),
2,
);
let zoned_date_time_proto_ptr = world.zoned_date_time_proto.as_ptr() as *mut JsObject;
let zoned_date_time_proto = unsafe { &mut *zoned_date_time_proto_ptr };
bind_accessor_getter(
core,
session,
zoned_date_time_proto,
"epochNanoseconds",
oxide_builtins::temporal::zoned_date_time_epoch_nanoseconds::<crate::vm::Vm> as *const (),
);
bind_accessor_getter(
core,
session,
zoned_date_time_proto,
"timeZoneId",
oxide_builtins::temporal::zoned_date_time_time_zone_id::<crate::vm::Vm> as *const (),
);
bind_accessor_getter(
core,
session,
zoned_date_time_proto,
"calendarId",
oxide_builtins::temporal::zoned_date_time_calendar_id::<crate::vm::Vm> as *const (),
);
bind_well_known_data_property(
core,
zoned_date_time_proto,
9,
JsValue::perm_string(
core.perm_interner()
.string_ptr(core.perm_interner().intern("Temporal.ZonedDateTime").0),
),
PropAttributes::new(false, false, true),
);
// Temporal.PlainDate:构造器 + from 静态方法 + year/month/day getter 与 toString。 // 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_ptr = world.plain_date_constructor.as_ptr() as *mut JsObject;
@@ -376,5 +436,6 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
bind_global_value(core, temporal, "PlainDate", JsValue::from_js_object(plain_date_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, temporal, "PlainTime", JsValue::from_js_object(plain_time_ctor_ptr));
bind_global_value(core, temporal, "Duration", JsValue::from_js_object(duration_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, global, "Temporal", JsValue::from_js_object(temporal_ptr)); bind_global_value(core, global, "Temporal", JsValue::from_js_object(temporal_ptr));
} }
+12
View File
@@ -222,6 +222,18 @@ pub(crate) fn bind_well_known_method_alias(
proto.bump_generation(); proto.bump_generation();
} }
/// 在对象上按 well-known symbol 键绑定数据属性。
pub(crate) fn bind_well_known_data_property(
core: &Arc<KernelCore>, target: &mut JsObject, well_known_id: u32, value: JsValue, attributes: PropAttributes,
) {
let key = oxide_types::private_key::make_well_known_symbol_key(well_known_id);
let new_shape = core.shape_forge().make_shape(target.shape_id(), key);
target.set_shape_id(new_shape);
let pos = target.push_prop(value);
target.set_data_meta(pos, attributes);
target.bump_generation();
}
pub(crate) fn bind_global_value(core: &Arc<KernelCore>, global: &mut JsObject, name: &str, value: JsValue) { pub(crate) fn bind_global_value(core: &Arc<KernelCore>, global: &mut JsObject, name: &str, value: JsValue) {
let si = core.perm_interner().intern(name).0; let si = core.perm_interner().intern(name).0;
let shape = core.shape_forge().make_shape(global.shape_id(), si); let shape = core.shape_forge().make_shape(global.shape_id(), si);
+3 -1
View File
@@ -905,6 +905,8 @@ impl Vm {
Some(7) Some(7)
} else if std::ptr::eq(ptr, world.sym_async_iterator.as_ptr()) { } else if std::ptr::eq(ptr, world.sym_async_iterator.as_ptr()) {
Some(8) Some(8)
} else if std::ptr::eq(ptr, world.sym_to_string_tag.as_ptr()) {
Some(9)
} else { } else {
None None
}; };
@@ -985,7 +987,7 @@ impl Vm {
// 数组属性存储索引 = array_prop_count + shape 槽位(与元素区分)。 // 数组属性存储索引 = array_prop_count + shape 槽位(与元素区分)。
let idx = obj.array_prop_count as usize + pos as usize; let idx = obj.array_prop_count as usize + pos as usize;
let val = obj.get_prop_at(idx); let val = obj.get_prop_at(idx);
if !val.is_undefined() { if !val.is_undefined() || obj.prop_vec_len() > idx {
Some(idx as u32) Some(idx as u32)
} else { } else {
None None
@@ -323,6 +323,43 @@ fn instant_json_and_locale_string_use_default_iso_output() {
assert!(r.as_bool()); assert!(r.as_bool());
} }
#[test]
fn instant_to_string_tag_has_temporal_descriptor() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let descriptor = Object.getOwnPropertyDescriptor(Temporal.Instant.prototype, Symbol.toStringTag);
Object.prototype.toString.call(Temporal.Instant.prototype) === '[object Temporal.Instant]'
&& descriptor.value === 'Temporal.Instant'
&& descriptor.writable === false
&& descriptor.enumerable === false
&& descriptor.configurable === true",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_to_zoned_date_time_iso_preserves_epoch_and_time_zone() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let instant = new Temporal.Instant(1000000000000000000n);
let utc = instant.toZonedDateTimeISO('uTc');
let offset = instant.toZonedDateTimeISO('2021-08-19T17:30-07:00');
let constructed = new Temporal.ZonedDateTime(0n, '+01:30');
utc.epochNanoseconds === instant.epochNanoseconds
&& utc.timeZoneId === 'UTC'
&& utc.calendarId === 'iso8601'
&& offset.timeZoneId === '-07:00'
&& constructed.epochNanoseconds === 0n
&& constructed.timeZoneId === '+01:30'
&& Object.prototype.toString.call(constructed) === '[object Temporal.ZonedDateTime]'",
)
.unwrap();
assert!(r.as_bool());
}
#[test] #[test]
fn instant_epoch_factories_validate_input_and_range() { fn instant_epoch_factories_validate_input_and_range() {
let mut vm = Vm::new(); let mut vm = Vm::new();