diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 7f5d4b3d..b646e242 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -2,7 +2,8 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::PermInterner; use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; 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; @@ -110,7 +111,8 @@ fn decode_symbol_key(vm: &H, key: u32) -> JsValue { 5 => world.sym_to_primitive.as_ptr(), 6 => world.sym_has_instance.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); } @@ -643,29 +645,13 @@ pub fn object_get_own_property_descriptor(vm: &mut H, args: &[u8]) -> Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)), }; let obj_ptr = obj_val.as_js_object_ptr(); - let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2])); - let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; - - 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 { + let key = vm.property_key_si(vm.reg(args[2])); + let obj = unsafe { &*obj_ptr }; + let Some(offset) = vm.get_own_property_slot(obj, key) else { 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 sh_ptr = vm.kernel_core().shape_forge().as_ref() as *const ShapeForge; @@ -1029,8 +1015,16 @@ pub fn object_proto_to_string(vm: &mut H, args: &[u8]) -> NativeResul "Object" }; // @@toStringTag 为字符串时覆盖内置标签;getter 抛错透传原异常。 - let tag_si = vm.kernel_core().perm_interner().intern("@@toStringTag").0; - match vm.ordinary_get(obj, tag_si, this_val) { + let tag_key = make_well_known_symbol_key(9); + 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) { Some(s) => return NativeResult::Ok(vm.new_string(&format!("[object {s}]"))), None => builtin, diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 264a6a31..ae4330fe 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -6,9 +6,9 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; // 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)、 -// 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; @@ -69,6 +69,13 @@ fn ensure_duration(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> Ok(()) } +fn ensure_zoned_date_time(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(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> { let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); if !raw.is_object() { @@ -127,6 +134,19 @@ fn make_instant(vm: &mut H, epoch_ns: i128) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) } +fn make_zoned_date_time(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 { let end = cursor.checked_add(count)?; let digits = bytes.get(*cursor..end)?; @@ -986,13 +1006,13 @@ fn parse_offset_minutes(value: &str) -> Option { Some(if bytes[0] == b'-' { -magnitude } else { magnitude }) } -fn instant_time_zone_offset(value: &str) -> Option { +fn canonical_time_zone(value: &str) -> Option<(String, i32)> { let input = value.trim(); 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) { - return Some(offset); + return Some((input.to_string(), offset)); } if input.starts_with("-000000") { return None; @@ -1005,21 +1025,26 @@ fn instant_time_zone_offset(value: &str) -> Option { } let annotation = &input[open + 1..input.len() - 1]; 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 = &input[time_start + 1..]; if time.ends_with(['Z', 'z']) { - return Some(0); + return Some(("UTC".to_string(), 0)); } let offset_start = time .char_indices() .rev() .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 { + canonical_time_zone(value).map(|(_, offset)| offset) } fn civil_from_days(days: i128) -> (i128, i128, i128) { @@ -1199,11 +1224,98 @@ pub fn instant_to_locale_string(vm: &mut H, args: &[u8]) -> NativeRes instant_default_string(vm, args) } +/// `Temporal.Instant.prototype.toZonedDateTimeISO(timeZone)`:以同一纪元纳秒创建 ISO 日历 ZonedDateTime。 +pub fn instant_to_zoned_date_time_iso(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 对象禁止转原始值。 pub fn instant_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf")) } +/// `Temporal.ZonedDateTime` 构造器:保存纪元纳秒、固定偏移或 UTC 时区以及 ISO 日历。 +pub fn zoned_date_time_constructor(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(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(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(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(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 mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs index 02ee479c..aaa0f382 100644 --- a/crates/oxide_builtins/tests/object_boundary_tests.rs +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -226,6 +226,30 @@ fn test_get_own_property_descriptor_no_args_type_error() { 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] fn test_has_own_property_boxes_primitive_this() { // ToObject 装箱 this:原始值上 hasOwnProperty 返回 false 而非抛错。 diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index a066c8fd..e9edfe23 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -243,6 +243,7 @@ pub struct BuiltinWorld { pub sym_has_instance: P, pub sym_match_all: P, pub sym_async_iterator: P, + pub sym_to_string_tag: P, pub temporal_object: P, pub temporal_now_object: P, pub instant_constructor: P, @@ -253,6 +254,8 @@ pub struct BuiltinWorld { pub plain_time_proto: P, pub duration_constructor: P, pub duration_proto: P, + pub zoned_date_time_constructor: P, + pub zoned_date_time_proto: P, pub bigint_constructor: P, pub bigint_proto: P, pub stub_objects: Vec>, @@ -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_time_constructor, &world.plain_time_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); let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let non_object_protos: [&P; 19] = [ + let non_object_protos: [&P; 20] = [ &world.array_proto, &world.function_proto, &world.string_proto, @@ -529,6 +533,7 @@ fn wire_builtin_world_links(world: &BuiltinWorld) { &world.plain_date_proto, &world.plain_time_proto, &world.duration_proto, + &world.zoned_date_time_proto, &world.bigint_proto, ]; for proto in &non_object_protos { @@ -656,6 +661,7 @@ impl BuiltinWorld { BuiltinId::SymHasInstance => &self.sym_has_instance, BuiltinId::SymMatchAll => &self.sym_match_all, BuiltinId::SymAsyncIterator => &self.sym_async_iterator, + BuiltinId::SymToStringTag => &self.sym_to_string_tag, BuiltinId::TemporalObject => &self.temporal_object, BuiltinId::TemporalNowObject => &self.temporal_now_object, BuiltinId::InstantConstructor => &self.instant_constructor, @@ -666,6 +672,8 @@ impl BuiltinWorld { BuiltinId::PlainTimeProto => &self.plain_time_proto, BuiltinId::DurationConstructor => &self.duration_constructor, 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::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_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_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_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"); @@ -717,6 +726,8 @@ impl BuiltinWorld { let (plain_time_proto, plain_time_constructor) = make_named_pair(string_forge, shape_forge, labels, "PlainTime"); 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 stub_objects = Vec::new(); @@ -790,6 +801,7 @@ impl BuiltinWorld { sym_has_instance, sym_match_all, sym_async_iterator, + sym_to_string_tag, temporal_object, temporal_now_object, instant_constructor, @@ -800,6 +812,8 @@ impl BuiltinWorld { plain_time_proto, duration_constructor, duration_proto, + zoned_date_time_constructor, + zoned_date_time_proto, bigint_constructor, bigint_proto, stub_objects, @@ -875,6 +889,7 @@ impl BuiltinWorld { sym_has_instance, sym_match_all, sym_async_iterator, + sym_to_string_tag, ) = if dirty.symbol_family { 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())), ) } else { ( @@ -903,6 +919,7 @@ impl BuiltinWorld { current.sym_has_instance.clone(), current.sym_match_all.clone(), current.sym_async_iterator.clone(), + current.sym_to_string_tag.clone(), ) }; @@ -987,6 +1004,8 @@ impl BuiltinWorld { plain_time_constructor, duration_proto, duration_constructor, + zoned_date_time_proto, + zoned_date_time_constructor, ) = if dirty.temporal { let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant"); let (plain_date_proto, plain_date_constructor) = @@ -994,6 +1013,8 @@ impl BuiltinWorld { let (plain_time_proto, plain_time_constructor) = make_named_pair(string_forge, shape_forge, labels, "PlainTime"); 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())), @@ -1005,6 +1026,8 @@ impl BuiltinWorld { plain_time_constructor, duration_proto, duration_constructor, + zoned_date_time_proto, + zoned_date_time_constructor, ) } else { ( @@ -1018,6 +1041,8 @@ impl BuiltinWorld { current.plain_time_constructor.clone(), current.duration_proto.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 { @@ -1097,6 +1122,7 @@ impl BuiltinWorld { sym_has_instance, sym_match_all, sym_async_iterator, + sym_to_string_tag, temporal_object, temporal_now_object, instant_constructor, @@ -1107,6 +1133,8 @@ impl BuiltinWorld { plain_time_proto, duration_constructor, duration_proto, + zoned_date_time_constructor, + zoned_date_time_proto, bigint_constructor, bigint_proto, stub_objects, diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 82d07a49..107a402d 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -225,7 +225,7 @@ pub struct KernelSession { /// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及 /// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的 /// builtin 家族。 -pub const NUM_BUILTINS: usize = 80; +pub const NUM_BUILTINS: usize = 83; /// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。 /// @@ -314,6 +314,9 @@ pub enum BuiltinId { BigIntProto = 77, DurationConstructor = 78, DurationProto = 79, + SymToStringTag = 80, + ZonedDateTimeConstructor = 81, + ZonedDateTimeProto = 82, } impl BuiltinId { @@ -399,6 +402,9 @@ impl BuiltinId { BuiltinId::BigIntProto, BuiltinId::DurationConstructor, BuiltinId::DurationProto, + BuiltinId::SymToStringTag, + BuiltinId::ZonedDateTimeConstructor, + BuiltinId::ZonedDateTimeProto, ]; } @@ -586,7 +592,8 @@ impl KernelSession { || gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive) || gen(BuiltinId::SymHasInstance) != snap(BuiltinId::SymHasInstance) || 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), json: gen(BuiltinId::JsonObject) != snap(BuiltinId::JsonObject), date: gen(BuiltinId::DateConstructor) != snap(BuiltinId::DateConstructor) @@ -633,7 +640,9 @@ impl KernelSession { || gen(BuiltinId::PlainTimeConstructor) != snap(BuiltinId::PlainTimeConstructor) || gen(BuiltinId::PlainTimeProto) != snap(BuiltinId::PlainTimeProto) || 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 || stub_generations_dirty || gen(BuiltinId::BigIntConstructor) != snap(BuiltinId::BigIntConstructor) diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 62aa9cf6..04a393ec 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -445,6 +445,8 @@ impl JsObject { pub const OBJ_TYPE_PLAIN_TIME: u8 = 16; /// Temporal.Duration 对象:十个时长分量依次存于 prop 0-9。 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 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -530,6 +532,11 @@ impl JsObject { pub fn is_duration_obj(&self) -> bool { 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()` 的基础对象)。 pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self { diff --git a/crates/oxide_types/src/private_key.rs b/crates/oxide_types/src/private_key.rs index fc6db39f..2c8f659a 100644 --- a/crates/oxide_types/src/private_key.rs +++ b/crates/oxide_types/src/private_key.rs @@ -17,7 +17,7 @@ pub const PRIVATE_NAME_BASE: u32 = 0x8000_0000; pub const SYMBOL_KEY_BASE: u32 = 0xA000_0000; /// 保留给 well-known symbol 的键槽位数(位于 Symbol 键区间最低端)。 -pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 9; +pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 10; /// 用户 symbol 下标的保留位掩码(低 28 位)。 const SYMBOL_INDEX_MASK: u32 = 0x0FFF_FFFF; diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index f1e8247d..fca89525 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -70,6 +70,10 @@ pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut "asyncIterator", 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); } diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index d2e89516..c2561995 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -1,8 +1,11 @@ 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_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; /// 把 Temporal 命名空间及其子对象绑定到 global。 @@ -98,10 +101,67 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m 0, ), ("toString", oxide_builtins::temporal::instant_to_string:: as *const (), 0), + ( + "toZonedDateTimeISO", + oxide_builtins::temporal::instant_to_zoned_date_time_iso:: as *const (), + 1, + ), ("until", oxide_builtins::temporal::instant_until:: as *const (), 1), ("valueOf", oxide_builtins::temporal::instant_value_of:: 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:: 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:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "timeZoneId", + oxide_builtins::temporal::zoned_date_time_time_zone_id:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "calendarId", + oxide_builtins::temporal::zoned_date_time_calendar_id:: 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。 let plain_date_ctor_ptr = world.plain_date_constructor.as_ptr() as *mut JsObject; @@ -376,5 +436,6 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m 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, "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)); } diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index b4666fb5..2badc469 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -222,6 +222,18 @@ pub(crate) fn bind_well_known_method_alias( proto.bump_generation(); } +/// 在对象上按 well-known symbol 键绑定数据属性。 +pub(crate) fn bind_well_known_data_property( + core: &Arc, 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, global: &mut JsObject, name: &str, value: JsValue) { let si = core.perm_interner().intern(name).0; let shape = core.shape_forge().make_shape(global.shape_id(), si); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 3507b80d..ddf041a0 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -905,6 +905,8 @@ impl Vm { Some(7) } else if std::ptr::eq(ptr, world.sym_async_iterator.as_ptr()) { Some(8) + } else if std::ptr::eq(ptr, world.sym_to_string_tag.as_ptr()) { + Some(9) } else { None }; @@ -985,7 +987,7 @@ impl Vm { // 数组属性存储索引 = array_prop_count + shape 槽位(与元素区分)。 let idx = obj.array_prop_count as usize + pos as usize; let val = obj.get_prop_at(idx); - if !val.is_undefined() { + if !val.is_undefined() || obj.prop_vec_len() > idx { Some(idx as u32) } else { None diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 10085413..d22c3ad0 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -323,6 +323,43 @@ fn instant_json_and_locale_string_use_default_iso_output() { 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] fn instant_epoch_factories_validate_input_and_range() { let mut vm = Vm::new();