diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 766d10a8..88f2b731 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -2133,315 +2133,4 @@ impl JsObject { } #[cfg(test)] -mod tests { - use super::*; - use crate::shape::EMPTY_SHAPE_ID; - - #[test] - fn object_size_bounds() { - let sz = std::mem::size_of::(); - assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); - } - - #[test] - fn js_string_cons_basic() { - // 左右子 Flat 构造 Cons:utf16_len/is_empty/文本视图全部按拼接语义。 - let left = Box::new(JsString::new("ab".to_string())); - let right = Box::new(JsString::new("cd".to_string())); - let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); - assert_eq!(cons.utf16_len(), 4); - assert!(!cons.is_empty()); - assert_eq!(cons.as_lossy_str(), "abcd"); - assert_eq!(cons.to_owned_string(), "abcd"); - assert!(cons.is_cons()); - // 扁平化产物缓存命中:内容仍一致(units 视图同样走缓存)。 - assert_eq!(cons.as_lossy_str(), "abcd"); - assert_eq!(cons.units().as_ref(), &['a' as u16, 'b' as u16, 'c' as u16, 'd' as u16]); - } - - #[test] - fn js_string_layout_anchor_and_tags() { - // 32B 锚 + 三形态 tag/utf16_len 预写口径(布局变更先同步头注释再改断言)。 - let flat = JsString::new("ab".to_string()); - assert_eq!(std::mem::size_of::(), 32); - assert!(flat.is_flat() && !flat.is_cons() && !flat.is_flat_u16()); - assert_eq!(flat.tag, TAG_FLAT); - assert_eq!(flat.utf16_len(), 2); - assert_eq!(flat.payload_bytes(), 2); - - let u16 = JsString::new_flat_u16(vec![0xD800, 0xDC00]); - assert!(u16.is_flat_u16()); - assert_eq!(u16.tag, TAG_FLAT_U16); - assert_eq!(u16.utf16_len(), 2); - assert_eq!(u16.payload_bytes(), 4); - assert_eq!(u16.units_borrowed(), Some(&[0xD800, 0xDC00][..])); - // 代理对 well-formed:smart 路由落 Flat 且内容等价。 - let smart = JsString::from_units(vec![0xD800, 0xDC00]); - assert!(smart.is_flat()); - assert_eq!(smart.as_str(), "\u{10000}"); - // 孤立 surrogate:smart 路由落 FlatU16。 - let lone = JsString::from_units(vec![0xD800]); - assert!(lone.is_flat_u16()); - assert!(lone.has_lone_surrogate()); - assert_eq!(lone.units().as_ref(), &[0xD800]); - assert_eq!(lone.as_lossy_str(), "\u{FFFD}"); - - let left = Box::new(JsString::new("ab".to_string())); - let right = Box::new(JsString::new("cd".to_string())); - let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); - assert!(cons.is_cons()); - assert_eq!(cons.tag, TAG_CONS); - assert_eq!(cons.utf16_len(), 4, "Cons 单元长构造期 eager 归纳"); - assert_eq!(cons.payload_bytes(), 8); - assert!(!cons.has_lone_surrogate()); - // Cons 扁平化产物恒 FlatU16:units 零拷贝借用来自缓存。 - let u16_node = cons.units().into_owned(); - assert_eq!(u16_node, vec![0x61, 0x62, 0x63, 0x64]); - assert!(!cons.flat_cache_ptr().is_null()); - unsafe { JsString::drop_cons_node(cons.cons_node_ptr()) }; - } - - #[test] - fn js_string_cons_deep_chain_flattens_iteratively() { - // 2000 层左倾链:显式栈展开不爆栈,产物与逐段拼接一致。 - let mut nodes: Vec> = Vec::new(); - nodes.push(Box::new(JsString::new("x".to_string()))); - for _ in 0..2000 { - nodes.push(Box::new(JsString::new("y".to_string()))); - } - let mut chain_ptr = nodes[0].as_ref() as *const JsString; - for i in 1..nodes.len() { - let cons = Box::new(unsafe { JsString::new_cons(chain_ptr, nodes[i].as_ref() as *const JsString) }); - chain_ptr = cons.as_ref() as *const JsString; - nodes.push(cons); - } - let text = nodes.last().unwrap().as_lossy_str(); - assert_eq!(text.len(), 1 + 2000); - assert!(text.starts_with('x')); - assert!(text.ends_with('y')); - } - - #[test] - fn js_string_cons_empty_parts() { - // 双空串 Cons:空判定与长度均为 0,扁平化为空文本。 - let left = Box::new(JsString::new(String::new())); - let right = Box::new(JsString::new(String::new())); - let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); - assert!(cons.is_empty()); - assert_eq!(cons.utf16_len(), 0); - assert_eq!(cons.as_lossy_str(), ""); - } - - #[test] - fn new_empty_defaults() { - let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert_eq!(obj.shape_id(), EMPTY_SHAPE_ID); - assert_eq!(obj.prop_count(), 0); - assert!(obj.is_extensible()); - assert!(!obj.is_array()); - assert!(!obj.is_function()); - assert!(!obj.is_session_epoch()); - assert_eq!(obj.generation(), 1); - assert!(obj.hash_props_vec().is_none()); - assert!(!obj.has_prop_meta()); - } - - #[test] - fn session_epoch_marker_roundtrip() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert!(!obj.is_session_epoch()); - obj.set_session_epoch(true); - assert!(obj.is_session_epoch()); - obj.set_session_epoch(false); - assert!(!obj.is_session_epoch()); - } - - #[test] - fn session_epoch_marker_preserves_gc_mark_bit() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert!(!obj.is_gc_marked()); - obj.set_gc_mark(true); - assert!(obj.is_gc_marked()); - assert!(!obj.is_session_epoch()); - - obj.set_session_epoch(true); - assert!(obj.is_session_epoch()); - assert!(obj.is_gc_marked()); - - obj.set_gc_mark(false); - assert!(!obj.is_gc_marked()); - } - - #[test] - fn session_epoch_marker_keeps_object_size_bound() { - let sz = std::mem::size_of::(); - assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); - } - - #[test] - fn clone_for_session_epoch_marks_clone_and_does_not_alias_hash_props() { - let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - source.set_prop_at(0, JsValue::int(1)); - - let mut clone = source.clone_for_session_epoch(); - assert!(clone.is_session_epoch()); - clone.set_prop_at(0, JsValue::int(2)); - - assert_eq!(source.get_prop_at(0), JsValue::int(1)); - assert_eq!(clone.get_prop_at(0), JsValue::int(2)); - } - - #[test] - fn clone_for_session_epoch_does_not_alias_prop_meta() { - let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - source.set_prop_at(0, JsValue::undefined()); - source.set_accessor_meta(0, JsValue::int(10), JsValue::int(11), PropAttributes::DEFAULT_DATA); - - let mut clone = source.clone_for_session_epoch(); - clone.set_accessor_meta(0, JsValue::int(20), JsValue::int(21), PropAttributes::DEFAULT_DATA); - - let source_meta = source.prop_meta_at(0).expect("source meta"); - let clone_meta = clone.prop_meta_at(0).expect("clone meta"); - assert_eq!(source_meta.get, JsValue::int(10)); - assert_eq!(source_meta.set, JsValue::int(11)); - assert_eq!(clone_meta.get, JsValue::int(20)); - assert_eq!(clone_meta.set, JsValue::int(21)); - } - - #[test] - fn shape_id_roundtrip() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - obj.set_shape_id(0x00AB_CDEF); - assert_eq!(obj.shape_id(), 0x00AB_CDEF); - } - - #[test] - fn prop_count_roundtrip() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert_eq!(obj.prop_count(), 0); - obj.ensure_hash_props().push(JsValue::int(17)); - assert_eq!(obj.prop_count(), 1); - } - - #[test] - fn flags_individual() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert!(obj.is_extensible()); - obj.set_extensible(false); - assert!(!obj.is_extensible()); - } - - #[test] - fn hash_prop_read_write() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - obj.set_prop_at(0, JsValue::int(42)); - assert_eq!(obj.get_prop_at(0), JsValue::int(42)); - } - - #[test] - fn new_array_flags() { - let bump = bumpalo::Bump::new(); - let obj = JsObject::new_array(5, JsValue::null(), 3, &bump); - assert!(obj.is_array()); - assert_eq!(obj.shape_id(), 5); - assert_eq!(obj.prop_count(), 3); - } - - #[test] - fn generation_bump() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert_eq!(obj.generation(), 1); - obj.bump_generation(); - assert_eq!(obj.generation(), 2); - } - - #[test] - fn hash_props_lazy_init() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - assert!(obj.hash_props_vec().is_none()); - assert_eq!(obj.prop_count(), 0); - obj.set_prop_at(0, JsValue::int(1)); - assert!(obj.hash_props_vec().is_some()); - assert_eq!(obj.prop_count(), 1); - } - - #[test] - fn hash_props_flat_storage_roundtrip() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - obj.set_prop_at(0, JsValue::int(100)); - obj.set_prop_at(1, JsValue::int(200)); - assert_eq!(obj.get_prop_at(0), JsValue::int(100)); - assert_eq!(obj.get_prop_at(1), JsValue::int(200)); - } - - #[test] - fn prop_meta_lazy_init() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - obj.set_prop_at(0, JsValue::int(1)); - assert!(!obj.has_prop_meta()); - - obj.set_data_meta(0, PropAttributes::new(false, true, false)); - assert!(obj.has_prop_meta()); - let meta = obj.prop_meta_at(0).expect("meta"); - assert!(!meta.is_accessor); - assert!(!meta.attributes.writable()); - assert!(meta.attributes.enumerable()); - assert!(!meta.attributes.configurable()); - } - - #[test] - fn accessor_meta_roundtrip_and_alignment() { - let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - obj.set_prop_at(0, JsValue::int(1)); - obj.set_accessor_meta(2, JsValue::int(10), JsValue::int(11), PropAttributes::new(false, false, true)); - - assert_eq!(obj.prop_count(), 3); - assert!(obj.is_accessor_meta(2)); - let meta = obj.prop_meta_at(2).expect("accessor meta"); - assert_eq!(meta.get, JsValue::int(10)); - assert_eq!(meta.set, JsValue::int(11)); - assert!(!meta.attributes.writable()); - assert!(!meta.attributes.enumerable()); - assert!(meta.attributes.configurable()); - - obj.push_prop(JsValue::int(4)); - assert_eq!(obj.prop_meta_vec().expect("meta").len(), obj.prop_vec_len()); - } - - #[test] - fn array_element_write_preserves_props_after_element_growth() { - // 先写属性再 push 元素:元素区增长必须整体搬移属性区,不覆盖属性。 - let bump = bumpalo::Bump::new(); - let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 3, &bump); - obj.set_prop_shape(0, JsValue::int(99)); - obj.set_prop_at(3, JsValue::int(4)); - assert_eq!(obj.prop_count(), 4); - assert_eq!(obj.get_prop_at(3), JsValue::int(4)); - assert_eq!(obj.get_prop_shape(0), JsValue::int(99)); - } - - #[test] - fn array_element_write_beyond_count_relocates_prop_zone() { - // 稀疏写入(越界索引)把属性区推到新元素之后,属性读取仍命中。 - let bump = bumpalo::Bump::new(); - let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 2, &bump); - obj.set_prop_shape(0, JsValue::int(7)); - obj.set_prop_at(5, JsValue::int(50)); - assert_eq!(obj.prop_count(), 6); - assert_eq!(obj.get_prop_at(5), JsValue::int(50)); - assert_eq!(obj.get_prop_shape(0), JsValue::int(7)); - } - - #[test] - fn array_prop_count_truncate_keeps_prop_zone() { - // pop 截断元素区时属性区不得被删(meta 同步 insert/drain 对齐)。 - let bump = bumpalo::Bump::new(); - let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 3, &bump); - obj.set_prop_shape(0, JsValue::int(5)); - obj.set_data_meta(3, PropAttributes::new(true, false, true)); - obj.set_prop_count_fast(2); - assert_eq!(obj.prop_count(), 2); - assert_eq!(obj.get_prop_shape(0), JsValue::int(5)); - assert!(obj.prop_meta_at(2).is_some()); - } -} +mod tests; diff --git a/crates/oxide_types/src/object/tests.rs b/crates/oxide_types/src/object/tests.rs new file mode 100644 index 00000000..ca4463f7 --- /dev/null +++ b/crates/oxide_types/src/object/tests.rs @@ -0,0 +1,310 @@ +use super::*; +use crate::shape::EMPTY_SHAPE_ID; + +#[test] +fn object_size_bounds() { + let sz = std::mem::size_of::(); + assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); +} + +#[test] +fn js_string_cons_basic() { + // 左右子 Flat 构造 Cons:utf16_len/is_empty/文本视图全部按拼接语义。 + let left = Box::new(JsString::new("ab".to_string())); + let right = Box::new(JsString::new("cd".to_string())); + let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); + assert_eq!(cons.utf16_len(), 4); + assert!(!cons.is_empty()); + assert_eq!(cons.as_lossy_str(), "abcd"); + assert_eq!(cons.to_owned_string(), "abcd"); + assert!(cons.is_cons()); + // 扁平化产物缓存命中:内容仍一致(units 视图同样走缓存)。 + assert_eq!(cons.as_lossy_str(), "abcd"); + assert_eq!(cons.units().as_ref(), &['a' as u16, 'b' as u16, 'c' as u16, 'd' as u16]); +} + +#[test] +fn js_string_layout_anchor_and_tags() { + // 32B 锚 + 三形态 tag/utf16_len 预写口径(布局变更先同步头注释再改断言)。 + let flat = JsString::new("ab".to_string()); + assert_eq!(std::mem::size_of::(), 32); + assert!(flat.is_flat() && !flat.is_cons() && !flat.is_flat_u16()); + assert_eq!(flat.tag, TAG_FLAT); + assert_eq!(flat.utf16_len(), 2); + assert_eq!(flat.payload_bytes(), 2); + + let u16 = JsString::new_flat_u16(vec![0xD800, 0xDC00]); + assert!(u16.is_flat_u16()); + assert_eq!(u16.tag, TAG_FLAT_U16); + assert_eq!(u16.utf16_len(), 2); + assert_eq!(u16.payload_bytes(), 4); + assert_eq!(u16.units_borrowed(), Some(&[0xD800, 0xDC00][..])); + // 代理对 well-formed:smart 路由落 Flat 且内容等价。 + let smart = JsString::from_units(vec![0xD800, 0xDC00]); + assert!(smart.is_flat()); + assert_eq!(smart.as_str(), "\u{10000}"); + // 孤立 surrogate:smart 路由落 FlatU16。 + let lone = JsString::from_units(vec![0xD800]); + assert!(lone.is_flat_u16()); + assert!(lone.has_lone_surrogate()); + assert_eq!(lone.units().as_ref(), &[0xD800]); + assert_eq!(lone.as_lossy_str(), "\u{FFFD}"); + + let left = Box::new(JsString::new("ab".to_string())); + let right = Box::new(JsString::new("cd".to_string())); + let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); + assert!(cons.is_cons()); + assert_eq!(cons.tag, TAG_CONS); + assert_eq!(cons.utf16_len(), 4, "Cons 单元长构造期 eager 归纳"); + assert_eq!(cons.payload_bytes(), 8); + assert!(!cons.has_lone_surrogate()); + // Cons 扁平化产物恒 FlatU16:units 零拷贝借用来自缓存。 + let u16_node = cons.units().into_owned(); + assert_eq!(u16_node, vec![0x61, 0x62, 0x63, 0x64]); + assert!(!cons.flat_cache_ptr().is_null()); + unsafe { JsString::drop_cons_node(cons.cons_node_ptr()) }; +} + +#[test] +fn js_string_cons_deep_chain_flattens_iteratively() { + // 2000 层左倾链:显式栈展开不爆栈,产物与逐段拼接一致。 + let mut nodes: Vec> = Vec::new(); + nodes.push(Box::new(JsString::new("x".to_string()))); + for _ in 0..2000 { + nodes.push(Box::new(JsString::new("y".to_string()))); + } + let mut chain_ptr = nodes[0].as_ref() as *const JsString; + for i in 1..nodes.len() { + let cons = Box::new(unsafe { JsString::new_cons(chain_ptr, nodes[i].as_ref() as *const JsString) }); + chain_ptr = cons.as_ref() as *const JsString; + nodes.push(cons); + } + let text = nodes.last().unwrap().as_lossy_str(); + assert_eq!(text.len(), 1 + 2000); + assert!(text.starts_with('x')); + assert!(text.ends_with('y')); +} + +#[test] +fn js_string_cons_empty_parts() { + // 双空串 Cons:空判定与长度均为 0,扁平化为空文本。 + let left = Box::new(JsString::new(String::new())); + let right = Box::new(JsString::new(String::new())); + let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); + assert!(cons.is_empty()); + assert_eq!(cons.utf16_len(), 0); + assert_eq!(cons.as_lossy_str(), ""); +} + +#[test] +fn new_empty_defaults() { + let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert_eq!(obj.shape_id(), EMPTY_SHAPE_ID); + assert_eq!(obj.prop_count(), 0); + assert!(obj.is_extensible()); + assert!(!obj.is_array()); + assert!(!obj.is_function()); + assert!(!obj.is_session_epoch()); + assert_eq!(obj.generation(), 1); + assert!(obj.hash_props_vec().is_none()); + assert!(!obj.has_prop_meta()); +} + +#[test] +fn session_epoch_marker_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(!obj.is_session_epoch()); + obj.set_session_epoch(true); + assert!(obj.is_session_epoch()); + obj.set_session_epoch(false); + assert!(!obj.is_session_epoch()); +} + +#[test] +fn session_epoch_marker_preserves_gc_mark_bit() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(!obj.is_gc_marked()); + obj.set_gc_mark(true); + assert!(obj.is_gc_marked()); + assert!(!obj.is_session_epoch()); + + obj.set_session_epoch(true); + assert!(obj.is_session_epoch()); + assert!(obj.is_gc_marked()); + + obj.set_gc_mark(false); + assert!(!obj.is_gc_marked()); +} + +#[test] +fn session_epoch_marker_keeps_object_size_bound() { + let sz = std::mem::size_of::(); + assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); +} + +#[test] +fn clone_for_session_epoch_marks_clone_and_does_not_alias_hash_props() { + let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + source.set_prop_at(0, JsValue::int(1)); + + let mut clone = source.clone_for_session_epoch(); + assert!(clone.is_session_epoch()); + clone.set_prop_at(0, JsValue::int(2)); + + assert_eq!(source.get_prop_at(0), JsValue::int(1)); + assert_eq!(clone.get_prop_at(0), JsValue::int(2)); +} + +#[test] +fn clone_for_session_epoch_does_not_alias_prop_meta() { + let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + source.set_prop_at(0, JsValue::undefined()); + source.set_accessor_meta(0, JsValue::int(10), JsValue::int(11), PropAttributes::DEFAULT_DATA); + + let mut clone = source.clone_for_session_epoch(); + clone.set_accessor_meta(0, JsValue::int(20), JsValue::int(21), PropAttributes::DEFAULT_DATA); + + let source_meta = source.prop_meta_at(0).expect("source meta"); + let clone_meta = clone.prop_meta_at(0).expect("clone meta"); + assert_eq!(source_meta.get, JsValue::int(10)); + assert_eq!(source_meta.set, JsValue::int(11)); + assert_eq!(clone_meta.get, JsValue::int(20)); + assert_eq!(clone_meta.set, JsValue::int(21)); +} + +#[test] +fn shape_id_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_shape_id(0x00AB_CDEF); + assert_eq!(obj.shape_id(), 0x00AB_CDEF); +} + +#[test] +fn prop_count_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert_eq!(obj.prop_count(), 0); + obj.ensure_hash_props().push(JsValue::int(17)); + assert_eq!(obj.prop_count(), 1); +} + +#[test] +fn flags_individual() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(obj.is_extensible()); + obj.set_extensible(false); + assert!(!obj.is_extensible()); +} + +#[test] +fn hash_prop_read_write() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(42)); + assert_eq!(obj.get_prop_at(0), JsValue::int(42)); +} + +#[test] +fn new_array_flags() { + let bump = bumpalo::Bump::new(); + let obj = JsObject::new_array(5, JsValue::null(), 3, &bump); + assert!(obj.is_array()); + assert_eq!(obj.shape_id(), 5); + assert_eq!(obj.prop_count(), 3); +} + +#[test] +fn generation_bump() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert_eq!(obj.generation(), 1); + obj.bump_generation(); + assert_eq!(obj.generation(), 2); +} + +#[test] +fn hash_props_lazy_init() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(obj.hash_props_vec().is_none()); + assert_eq!(obj.prop_count(), 0); + obj.set_prop_at(0, JsValue::int(1)); + assert!(obj.hash_props_vec().is_some()); + assert_eq!(obj.prop_count(), 1); +} + +#[test] +fn hash_props_flat_storage_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(100)); + obj.set_prop_at(1, JsValue::int(200)); + assert_eq!(obj.get_prop_at(0), JsValue::int(100)); + assert_eq!(obj.get_prop_at(1), JsValue::int(200)); +} + +#[test] +fn prop_meta_lazy_init() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(1)); + assert!(!obj.has_prop_meta()); + + obj.set_data_meta(0, PropAttributes::new(false, true, false)); + assert!(obj.has_prop_meta()); + let meta = obj.prop_meta_at(0).expect("meta"); + assert!(!meta.is_accessor); + assert!(!meta.attributes.writable()); + assert!(meta.attributes.enumerable()); + assert!(!meta.attributes.configurable()); +} + +#[test] +fn accessor_meta_roundtrip_and_alignment() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(1)); + obj.set_accessor_meta(2, JsValue::int(10), JsValue::int(11), PropAttributes::new(false, false, true)); + + assert_eq!(obj.prop_count(), 3); + assert!(obj.is_accessor_meta(2)); + let meta = obj.prop_meta_at(2).expect("accessor meta"); + assert_eq!(meta.get, JsValue::int(10)); + assert_eq!(meta.set, JsValue::int(11)); + assert!(!meta.attributes.writable()); + assert!(!meta.attributes.enumerable()); + assert!(meta.attributes.configurable()); + + obj.push_prop(JsValue::int(4)); + assert_eq!(obj.prop_meta_vec().expect("meta").len(), obj.prop_vec_len()); +} + +#[test] +fn array_element_write_preserves_props_after_element_growth() { + // 先写属性再 push 元素:元素区增长必须整体搬移属性区,不覆盖属性。 + let bump = bumpalo::Bump::new(); + let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 3, &bump); + obj.set_prop_shape(0, JsValue::int(99)); + obj.set_prop_at(3, JsValue::int(4)); + assert_eq!(obj.prop_count(), 4); + assert_eq!(obj.get_prop_at(3), JsValue::int(4)); + assert_eq!(obj.get_prop_shape(0), JsValue::int(99)); +} + +#[test] +fn array_element_write_beyond_count_relocates_prop_zone() { + // 稀疏写入(越界索引)把属性区推到新元素之后,属性读取仍命中。 + let bump = bumpalo::Bump::new(); + let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 2, &bump); + obj.set_prop_shape(0, JsValue::int(7)); + obj.set_prop_at(5, JsValue::int(50)); + assert_eq!(obj.prop_count(), 6); + assert_eq!(obj.get_prop_at(5), JsValue::int(50)); + assert_eq!(obj.get_prop_shape(0), JsValue::int(7)); +} + +#[test] +fn array_prop_count_truncate_keeps_prop_zone() { + // pop 截断元素区时属性区不得被删(meta 同步 insert/drain 对齐)。 + let bump = bumpalo::Bump::new(); + let mut obj = JsObject::new_array(EMPTY_SHAPE_ID, JsValue::null(), 3, &bump); + obj.set_prop_shape(0, JsValue::int(5)); + obj.set_data_meta(3, PropAttributes::new(true, false, true)); + obj.set_prop_count_fast(2); + assert_eq!(obj.prop_count(), 2); + assert_eq!(obj.get_prop_shape(0), JsValue::int(5)); + assert!(obj.prop_meta_at(2).is_some()); +}