use std::array;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use crate::builtins::symbol::{is_symbol_property_key, property_key_to_symbol};
use crate::objects::map::PropertyAttributes;
use crate::objects::value::JsValue;
pub const INTERNAL_PROTO_PROPERTY_KEY: &str = "\0stator.internal.proto";
const USER_VISIBLE_PROTO_PROPERTY_KEY: &str = "__proto__";
thread_local! {
static NEXT_SHAPE_ID: Cell<u64> = const { Cell::new(1) };
static GLOBAL_PROTO_MUTATION_EPOCH: Cell<u64> = const { Cell::new(0) };
}
const DEFAULT_ATTRS: PropertyAttributes = PropertyAttributes::from_bits_truncate(
PropertyAttributes::WRITABLE.bits()
| PropertyAttributes::ENUMERABLE.bits()
| PropertyAttributes::CONFIGURABLE.bits(),
);
const BUILTIN_ATTRS: PropertyAttributes = PropertyAttributes::from_bits_truncate(
PropertyAttributes::WRITABLE.bits() | PropertyAttributes::CONFIGURABLE.bits(),
);
const INLINE_CACHE_CAP: usize = 16;
const SMALL_PROPERTY_LINEAR_SCAN_CAP: usize = 6;
const PROPERTY_STORAGE_POOL_CAP: usize = 64;
const MAX_POOLED_PROPERTY_CAPACITY: usize = SMALL_PROPERTY_LINEAR_SCAN_CAP * 4;
const OBJECT_RC_POOL_CAP: usize = 1024;
thread_local! {
static PROPERTY_STORAGE_POOL: RefCell<Vec<PropertyStorageBuffers>> =
RefCell::new(Vec::with_capacity(PROPERTY_STORAGE_POOL_CAP));
static VALUES_VEC_POOL: RefCell<Vec<Vec<JsValue>>> =
RefCell::new(Vec::with_capacity(PROPERTY_STORAGE_POOL_CAP));
static OBJECT_RC_POOL: RefCell<Vec<Rc<RefCell<PropertyMap>>>> =
RefCell::new(Vec::with_capacity(64));
}
#[derive(Debug)]
struct PropertyStorageBuffers {
keys: Vec<Rc<str>>,
values: Vec<JsValue>,
attrs: Vec<PropertyAttributes>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
enum PropertyIndex {
#[default]
Inline,
Map(HashMap<Rc<str>, usize>),
}
#[derive(Debug)]
struct InlinePropertyCache {
hashes: [Cell<u64>; INLINE_CACHE_CAP],
slots: [Cell<u32>; INLINE_CACHE_CAP],
}
impl InlinePropertyCache {
fn new() -> Self {
Self {
hashes: Default::default(),
slots: array::from_fn(|_| Cell::new(u32::MAX)),
}
}
}
impl Clone for InlinePropertyCache {
fn clone(&self) -> Self {
Self {
hashes: array::from_fn(|i| Cell::new(self.hashes[i].get())),
slots: array::from_fn(|i| Cell::new(self.slots[i].get())),
}
}
}
#[inline]
fn next_shape_id() -> u64 {
NEXT_SHAPE_ID.with(|next| {
let shape_id = next.get();
next.set(shape_id.wrapping_add(1));
shape_id
})
}
#[inline]
fn current_global_proto_mutation_epoch() -> u64 {
GLOBAL_PROTO_MUTATION_EPOCH.with(Cell::get)
}
#[inline]
fn bump_global_proto_mutation_epoch() {
GLOBAL_PROTO_MUTATION_EPOCH.with(|epoch| {
epoch.set(epoch.get().wrapping_add(1));
});
}
fn acquire_storage_buffers(capacity: usize) -> PropertyStorageBuffers {
PROPERTY_STORAGE_POOL
.try_with(|pool| {
let mut pool = pool.borrow_mut();
if let Some(index) = pool.iter().position(|buffers| {
buffers.keys.capacity() >= capacity
&& buffers.values.capacity() >= capacity
&& buffers.attrs.capacity() >= capacity
}) {
let mut buffers = pool.swap_remove(index);
buffers.keys.clear();
buffers.values.clear();
buffers.attrs.clear();
buffers
} else {
PropertyStorageBuffers {
keys: Vec::with_capacity(capacity),
values: Vec::with_capacity(capacity),
attrs: Vec::with_capacity(capacity),
}
}
})
.unwrap_or_else(|_| PropertyStorageBuffers {
keys: Vec::with_capacity(capacity),
values: Vec::with_capacity(capacity),
attrs: Vec::with_capacity(capacity),
})
}
fn release_storage_buffers(mut buffers: PropertyStorageBuffers) {
if buffers.keys.capacity() > MAX_POOLED_PROPERTY_CAPACITY
|| buffers.values.capacity() > MAX_POOLED_PROPERTY_CAPACITY
|| buffers.attrs.capacity() > MAX_POOLED_PROPERTY_CAPACITY
{
return;
}
buffers.keys.clear();
buffers.values.clear();
buffers.attrs.clear();
let _ = PROPERTY_STORAGE_POOL.try_with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < PROPERTY_STORAGE_POOL_CAP {
pool.push(buffers);
}
});
}
fn acquire_values_vec(cap: usize) -> Vec<JsValue> {
VALUES_VEC_POOL
.try_with(|pool| {
let mut pool = pool.borrow_mut();
if let Some(idx) = pool.iter().position(|v| v.capacity() >= cap) {
let mut v = pool.swap_remove(idx);
v.clear();
v.resize(cap, JsValue::Undefined);
v
} else {
vec![JsValue::Undefined; cap]
}
})
.unwrap_or_else(|_| vec![JsValue::Undefined; cap])
}
fn release_values_vec(v: Vec<JsValue>) {
if v.capacity() > MAX_POOLED_PROPERTY_CAPACITY {
return;
}
let _ = VALUES_VEC_POOL.try_with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < PROPERTY_STORAGE_POOL_CAP {
pool.push(v);
}
});
}
#[inline]
pub(crate) fn acquire_object_rc_from_template(
template: &ObjectLiteralTemplate,
) -> Rc<RefCell<PropertyMap>> {
OBJECT_RC_POOL
.try_with(|pool| {
let pool = unsafe { &mut *pool.as_ptr() };
if let Some(rc) = pool.pop() {
let map = unsafe { &mut *rc.as_ptr() };
map.reinitialize_from_template(template);
rc
} else {
Rc::new(RefCell::new(template.instantiate()))
}
})
.unwrap_or_else(|_| Rc::new(RefCell::new(template.instantiate())))
}
#[inline]
pub(crate) fn acquire_object_rc_from_template_with_values(
template: &ObjectLiteralTemplate,
values: &[JsValue],
) -> Rc<RefCell<PropertyMap>> {
OBJECT_RC_POOL
.try_with(|pool| {
let pool = unsafe { &mut *pool.as_ptr() };
if let Some(rc) = pool.pop() {
let map = unsafe { &mut *rc.as_ptr() };
map.reinitialize_from_template_with_values(template, values);
rc
} else {
Rc::new(RefCell::new(
template.instantiate_with_values_from_slice(values),
))
}
})
.unwrap_or_else(|_| {
Rc::new(RefCell::new(
template.instantiate_with_values_from_slice(values),
))
})
}
#[cfg(stator_baseline_jit_x86_64)]
pub(crate) fn object_rc_pool_ptr() -> *const RefCell<Vec<Rc<RefCell<PropertyMap>>>> {
OBJECT_RC_POOL.with(|pool| pool as *const _)
}
#[cfg(stator_baseline_jit_x86_64)]
const POOL_BATCH_SIZE: usize = 32;
#[cfg(stator_baseline_jit_x86_64)]
#[inline(always)]
pub(crate) fn acquire_object_rc_from_template_with_values_cached(
template: &ObjectLiteralTemplate,
values: &[JsValue],
pool_ptr: *const RefCell<Vec<Rc<RefCell<PropertyMap>>>>,
) -> Rc<RefCell<PropertyMap>> {
if !pool_ptr.is_null() {
let pool = unsafe { &mut *(&*pool_ptr).as_ptr() };
if let Some(rc) = pool.pop() {
let map = unsafe { &mut *rc.as_ptr() };
if Rc::ptr_eq(&map.keys, &template.keys) && map.values.len() == values.len() {
map.reinitialize_values_inplace(values);
} else {
map.reinitialize_from_template_with_values(template, values);
}
return rc;
}
for _ in 0..POOL_BATCH_SIZE
.saturating_sub(1)
.min(OBJECT_RC_POOL_CAP - pool.len())
{
pool.push(Rc::new(RefCell::new(template.instantiate())));
}
}
Rc::new(RefCell::new(
template.instantiate_with_values_from_slice(values),
))
}
#[cfg(stator_baseline_jit_x86_64)]
#[inline(always)]
pub(crate) fn acquire_object_rc_from_template_cached(
template: &ObjectLiteralTemplate,
pool_ptr: *const RefCell<Vec<Rc<RefCell<PropertyMap>>>>,
) -> Rc<RefCell<PropertyMap>> {
if !pool_ptr.is_null() {
let pool = unsafe { &mut *(&*pool_ptr).as_ptr() };
if let Some(rc) = pool.pop() {
let map = unsafe { &mut *rc.as_ptr() };
map.reinitialize_from_template(template);
return rc;
}
for _ in 0..POOL_BATCH_SIZE
.saturating_sub(1)
.min(OBJECT_RC_POOL_CAP - pool.len())
{
pool.push(Rc::new(RefCell::new(template.instantiate())));
}
}
Rc::new(RefCell::new(template.instantiate()))
}
#[cfg(stator_baseline_jit_x86_64)]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn recycle_object_rc_cached(
rc: Rc<RefCell<PropertyMap>>,
pool_ptr: *const RefCell<Vec<Rc<RefCell<PropertyMap>>>>,
) {
if Rc::strong_count(&rc) != 1 {
return;
}
if pool_ptr.is_null() {
return;
}
let pool = unsafe { &mut *(&*pool_ptr).as_ptr() };
if pool.len() < OBJECT_RC_POOL_CAP {
pool.push(rc);
}
}
pub(crate) fn recycle_object_rc(rc: Rc<RefCell<PropertyMap>>) {
if Rc::strong_count(&rc) != 1 {
return;
}
#[cfg(stator_baseline_jit_x86_64)]
{
let pool_ptr = object_rc_pool_ptr();
if !pool_ptr.is_null() {
let pool = unsafe { &mut *(&*pool_ptr).as_ptr() };
if pool.len() < OBJECT_RC_POOL_CAP {
pool.push(rc);
}
return;
}
}
let _ = OBJECT_RC_POOL.try_with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < OBJECT_RC_POOL_CAP {
pool.push(rc);
}
});
}
pub fn clear_property_map_pools() {
OBJECT_RC_POOL.with(|pool| pool.borrow_mut().clear());
VALUES_VEC_POOL.with(|pool| pool.borrow_mut().clear());
PROPERTY_STORAGE_POOL.with(|pool| pool.borrow_mut().clear());
}
#[inline]
fn parse_integer_index(key: &str) -> Option<u32> {
if key.is_empty() || (key.len() > 1 && key.as_bytes()[0] == b'0') {
return None;
}
let n: u32 = key.parse().ok()?;
if n < u32::MAX { Some(n) } else { None }
}
#[inline]
fn name_hash(s: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3);
}
h
}
#[inline]
fn layout_hash(keys: &[Rc<str>], attrs: &[PropertyAttributes]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for (key, attr) in keys.iter().zip(attrs.iter()) {
for b in key.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3);
}
h ^= 0xff;
h = h.wrapping_mul(0x0100_0000_01b3);
for b in attr.bits().to_le_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3);
}
}
h ^= keys.len() as u64;
h.wrapping_mul(0x0100_0000_01b3)
}
#[derive(Debug)]
pub struct PropertyMap {
keys: Rc<Vec<Rc<str>>>,
values: Vec<JsValue>,
attrs: Rc<Vec<PropertyAttributes>>,
integer_key_count: usize,
index: PropertyIndex,
inline_cache: Option<Box<InlinePropertyCache>>,
shape_id: u64,
layout_id: u64,
proto_generation: Cell<u32>,
proto_epoch: Cell<u32>,
proto_global_epoch: Cell<u64>,
pub extensible: bool,
pub has_accessors: bool,
template_next_slot: usize,
capacity_hint: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct ObjectLiteralTemplate {
keys: Rc<Vec<Rc<str>>>,
attrs: Rc<Vec<PropertyAttributes>>,
integer_key_count: usize,
layout_id: u64,
extensible: bool,
has_accessors: bool,
capacity_hint: usize,
cached_index: PropertyIndex,
cached_shape_id: u64,
}
#[derive(Debug, Clone, Copy)]
struct ShapeMetadata {
integer_key_count: usize,
layout_id: u64,
extensible: bool,
has_accessors: bool,
capacity_hint: usize,
}
impl ObjectLiteralTemplate {
pub(crate) fn capture(map: &PropertyMap) -> Option<Self> {
(!map.is_empty()).then(|| {
let keys = Rc::clone(&map.keys);
let capacity_hint = map.capacity_hint;
let cached_index =
PropertyMap::build_index_from_keys(&keys, capacity_hint.max(keys.len()));
Self {
keys,
attrs: Rc::clone(&map.attrs),
integer_key_count: map.integer_key_count,
layout_id: map.layout_id,
extensible: map.extensible,
has_accessors: map.has_accessors,
capacity_hint,
cached_index,
cached_shape_id: map.shape_id,
}
})
}
pub(crate) fn instantiate(&self) -> PropertyMap {
let cap = self.keys.len();
let capacity_hint = self.capacity_hint.max(cap);
PropertyMap {
keys: Rc::clone(&self.keys),
values: acquire_values_vec(cap),
attrs: Rc::clone(&self.attrs),
integer_key_count: self.integer_key_count,
index: if cap <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
PropertyIndex::Inline
} else {
self.cached_index.clone()
},
inline_cache: None,
shape_id: self.cached_shape_id,
layout_id: self.layout_id,
proto_generation: Cell::new(0),
proto_epoch: Cell::new(0),
proto_global_epoch: Cell::new(0),
extensible: self.extensible,
has_accessors: self.has_accessors,
template_next_slot: 0,
capacity_hint,
}
}
pub(crate) fn instantiate_with_values(&self, mut values: Vec<JsValue>) -> PropertyMap {
let cap = self.keys.len();
let filled = values.len().min(cap);
if values.len() < cap {
values.resize(cap, JsValue::Undefined);
}
let capacity_hint = self.capacity_hint.max(cap);
PropertyMap {
keys: Rc::clone(&self.keys),
values,
attrs: Rc::clone(&self.attrs),
integer_key_count: self.integer_key_count,
index: if cap <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
PropertyIndex::Inline
} else {
self.cached_index.clone()
},
inline_cache: None,
shape_id: self.cached_shape_id,
layout_id: self.layout_id,
proto_generation: Cell::new(0),
proto_epoch: Cell::new(0),
proto_global_epoch: Cell::new(0),
extensible: self.extensible,
has_accessors: self.has_accessors,
template_next_slot: filled,
capacity_hint,
}
}
#[inline(always)]
pub(crate) fn instantiate_with_values_from_slice(&self, values: &[JsValue]) -> PropertyMap {
let cap = self.keys.len();
let filled = values.len().min(cap);
let mut vals = Vec::with_capacity(cap);
vals.extend_from_slice(&values[..filled]);
vals.resize(cap, JsValue::Undefined);
let capacity_hint = self.capacity_hint.max(cap);
PropertyMap {
keys: Rc::clone(&self.keys),
values: vals,
attrs: Rc::clone(&self.attrs),
integer_key_count: self.integer_key_count,
index: if cap <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
PropertyIndex::Inline
} else {
self.cached_index.clone()
},
inline_cache: None,
shape_id: self.cached_shape_id,
layout_id: self.layout_id,
proto_generation: Cell::new(0),
proto_epoch: Cell::new(0),
proto_global_epoch: Cell::new(0),
extensible: self.extensible,
has_accessors: self.has_accessors,
template_next_slot: filled,
capacity_hint,
}
}
pub(crate) fn pre_warm_pool(&self) {
const PRE_WARM_COUNT: usize = 32;
let _ = OBJECT_RC_POOL.try_with(|pool| {
let pool = unsafe { &mut *pool.as_ptr() };
let budget = PRE_WARM_COUNT.min(OBJECT_RC_POOL_CAP.saturating_sub(pool.len()));
pool.reserve(budget);
for _ in 0..budget {
pool.push(Rc::new(RefCell::new(self.instantiate())));
}
});
}
}
impl PartialEq for PropertyMap {
fn eq(&self, other: &Self) -> bool {
self.keys == other.keys
&& self.values == other.values
&& self.attrs == other.attrs
&& self.integer_key_count == other.integer_key_count
&& self.index == other.index
&& self.extensible == other.extensible
&& self.has_accessors == other.has_accessors
}
}
impl Clone for PropertyMap {
fn clone(&self) -> Self {
Self {
keys: Rc::clone(&self.keys),
values: self.values.clone(),
attrs: Rc::clone(&self.attrs),
integer_key_count: self.integer_key_count,
index: self.index.clone(),
inline_cache: self
.inline_cache
.as_ref()
.map(|cache| Box::new((**cache).clone())),
shape_id: self.shape_id,
layout_id: self.layout_id,
proto_generation: Cell::new(self.proto_generation.get()),
proto_epoch: Cell::new(self.proto_epoch.get()),
proto_global_epoch: Cell::new(self.proto_global_epoch.get()),
extensible: self.extensible,
has_accessors: self.has_accessors,
template_next_slot: self.template_next_slot,
capacity_hint: self.capacity_hint,
}
}
}
impl PropertyMap {
pub fn new() -> Self {
Self::with_capacity(0)
}
fn from_buffers(capacity: usize, buffers: PropertyStorageBuffers) -> Self {
let index = Self::build_index_from_keys(&[], capacity);
Self {
keys: Rc::new(buffers.keys),
values: buffers.values,
attrs: Rc::new(buffers.attrs),
integer_key_count: 0,
index,
inline_cache: None,
shape_id: next_shape_id(),
layout_id: layout_hash(&[], &[]),
proto_generation: Cell::new(0),
proto_epoch: Cell::new(0),
proto_global_epoch: Cell::new(0),
extensible: true,
has_accessors: false,
template_next_slot: usize::MAX,
capacity_hint: capacity,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self::from_buffers(capacity, acquire_storage_buffers(capacity))
}
#[inline(always)]
fn reinitialize_from_template(&mut self, template: &ObjectLiteralTemplate) {
let cap = template.keys.len();
if Rc::ptr_eq(&self.keys, &template.keys) && self.values.len() == cap {
if !Self::all_flat(&self.values[..cap]) {
for v in self.values.iter_mut() {
*v = JsValue::Undefined;
}
}
self.template_next_slot = 0;
self.proto_generation.set(0);
self.proto_epoch.set(0);
self.proto_global_epoch.set(0);
return;
}
let capacity_hint = template.capacity_hint.max(cap);
self.values.clear();
self.values.resize(cap, JsValue::Undefined);
self.keys = Rc::clone(&template.keys);
self.attrs = Rc::clone(&template.attrs);
self.integer_key_count = template.integer_key_count;
self.index = if cap <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
PropertyIndex::Inline
} else {
template.cached_index.clone()
};
self.inline_cache = None;
self.shape_id = template.cached_shape_id;
self.layout_id = template.layout_id;
self.proto_generation.set(0);
self.proto_epoch.set(0);
self.proto_global_epoch.set(0);
self.extensible = template.extensible;
self.has_accessors = template.has_accessors;
self.template_next_slot = 0;
self.capacity_hint = capacity_hint;
}
#[inline(always)]
fn reinitialize_from_template_with_values(
&mut self,
template: &ObjectLiteralTemplate,
values: &[JsValue],
) {
let cap = template.keys.len();
let filled = values.len().min(cap);
if Rc::ptr_eq(&self.keys, &template.keys) && self.values.len() == cap {
self.values[..filled].clone_from_slice(&values[..filled]);
for slot in &mut self.values[filled..cap] {
*slot = JsValue::Undefined;
}
self.template_next_slot = filled;
return;
}
let capacity_hint = template.capacity_hint.max(cap);
if self.values.len() == cap {
self.values[..filled].clone_from_slice(&values[..filled]);
for slot in &mut self.values[filled..cap] {
*slot = JsValue::Undefined;
}
} else {
self.values.clear();
self.values.extend_from_slice(&values[..filled]);
self.values.resize(cap, JsValue::Undefined);
}
self.keys = Rc::clone(&template.keys);
self.attrs = Rc::clone(&template.attrs);
self.integer_key_count = template.integer_key_count;
self.index = if cap <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
PropertyIndex::Inline
} else {
template.cached_index.clone()
};
self.inline_cache = None;
self.shape_id = template.cached_shape_id;
self.layout_id = template.layout_id;
self.proto_generation.set(0);
self.proto_epoch.set(0);
self.proto_global_epoch.set(0);
self.extensible = template.extensible;
self.has_accessors = template.has_accessors;
self.template_next_slot = filled;
self.capacity_hint = capacity_hint;
}
#[inline(always)]
pub(crate) fn try_reuse_with_values(
&mut self,
template: &ObjectLiteralTemplate,
values: &[JsValue],
) -> bool {
if Rc::ptr_eq(&self.keys, &template.keys) && self.values.len() == values.len() {
self.reinitialize_values_inplace(values);
true
} else {
false
}
}
#[inline(always)]
fn reinitialize_values_inplace(&mut self, values: &[JsValue]) {
let count = values.len();
debug_assert_eq!(self.values.len(), count);
if count <= 3 && Self::all_flat(&self.values[..count]) && Self::all_flat(values) {
unsafe {
std::ptr::copy_nonoverlapping(values.as_ptr(), self.values.as_mut_ptr(), count);
}
} else {
self.values[..count].clone_from_slice(&values[..count]);
}
self.template_next_slot = count;
self.proto_generation.set(0);
self.proto_epoch.set(0);
self.proto_global_epoch.set(0);
}
#[inline(always)]
fn all_flat(slice: &[JsValue]) -> bool {
slice.iter().all(|v| {
matches!(
v,
JsValue::Undefined
| JsValue::Null
| JsValue::TheHole
| JsValue::Boolean(_)
| JsValue::Smi(_)
| JsValue::HeapNumber(_)
| JsValue::Symbol(_)
| JsValue::Object(_)
)
})
}
pub fn from_boilerplate(
keys: &[Rc<str>],
attrs: &[crate::objects::map::PropertyAttributes],
) -> Self {
debug_assert_eq!(keys.len(), attrs.len());
Self::from_shape_parts(
keys,
attrs,
ShapeMetadata {
integer_key_count: keys
.iter()
.filter(|key| parse_integer_index(key).is_some())
.count(),
layout_id: layout_hash(keys, attrs),
extensible: true,
has_accessors: keys
.iter()
.any(|key| key.starts_with("__get_") || key.starts_with("__set_")),
capacity_hint: keys.len(),
},
0,
)
}
pub fn boilerplate_snapshot(
&self,
) -> (Vec<Rc<str>>, Vec<crate::objects::map::PropertyAttributes>) {
((*self.keys).clone(), (*self.attrs).clone())
}
pub fn clone_shape(&self) -> Self {
Self::from_shape_parts(
&self.keys,
&self.attrs,
ShapeMetadata {
integer_key_count: self.integer_key_count,
layout_id: self.layout_id,
extensible: self.extensible,
has_accessors: self.has_accessors,
capacity_hint: self.capacity_hint,
},
0,
)
}
#[inline]
pub fn clone_template(&self) -> Self {
self.clone_shape()
}
#[inline]
pub fn try_template_fill(&mut self, key: &str, value: JsValue) -> Result<usize, JsValue> {
let slot = self.template_next_slot;
if slot < self.keys.len() && slot < self.values.len() && self.keys[slot].as_ref() == key {
self.values[slot] = value;
self.template_next_slot = slot + 1;
Ok(slot)
} else {
self.template_next_slot = usize::MAX;
Err(value)
}
}
#[inline]
fn build_index_from_keys(keys: &[Rc<str>], capacity_hint: usize) -> PropertyIndex {
let capacity = capacity_hint.max(keys.len());
if capacity > SMALL_PROPERTY_LINEAR_SCAN_CAP {
let mut map = HashMap::with_capacity(capacity);
for (slot, key) in keys.iter().enumerate() {
map.insert(key.clone(), slot);
}
PropertyIndex::Map(map)
} else {
PropertyIndex::Inline
}
}
fn from_shape_parts(
keys: &[Rc<str>],
attrs: &[PropertyAttributes],
metadata: ShapeMetadata,
template_next_slot: usize,
) -> Self {
let cap = keys.len();
let mut buffers = acquire_storage_buffers(cap);
buffers.keys.extend_from_slice(keys);
buffers.values.resize(cap, JsValue::Undefined);
buffers.attrs.extend_from_slice(attrs);
let capacity_hint = metadata.capacity_hint.max(cap);
let index = Self::build_index_from_keys(&buffers.keys, capacity_hint);
Self {
keys: Rc::new(buffers.keys),
values: buffers.values,
attrs: Rc::new(buffers.attrs),
integer_key_count: metadata.integer_key_count,
index,
inline_cache: (cap > SMALL_PROPERTY_LINEAR_SCAN_CAP)
.then(|| Box::new(InlinePropertyCache::new())),
shape_id: next_shape_id(),
layout_id: metadata.layout_id,
proto_generation: Cell::new(0),
proto_epoch: Cell::new(0),
proto_global_epoch: Cell::new(0),
extensible: metadata.extensible,
has_accessors: metadata.has_accessors,
template_next_slot,
capacity_hint,
}
}
#[inline]
fn cache_probe(&self, key: &str) -> Option<usize> {
let cache = self.inline_cache.as_ref()?;
let h = name_hash(key);
let idx = (h as usize) & (INLINE_CACHE_CAP - 1);
if cache.hashes[idx].get() != h {
return None;
}
let slot = cache.slots[idx].get() as usize;
(slot < self.keys.len() && self.keys[slot].as_ref() == key).then_some(slot)
}
#[inline]
fn cache_record(&self, key: &str, slot: usize) {
let Some(cache) = self.inline_cache.as_ref() else {
return;
};
let h = name_hash(key);
let idx = (h as usize) & (INLINE_CACHE_CAP - 1);
cache.hashes[idx].set(h);
cache.slots[idx].set(slot as u32);
}
#[inline]
fn cache_invalidate(&self) {
let Some(cache) = self.inline_cache.as_ref() else {
return;
};
for entry in &cache.hashes {
entry.set(0);
}
for slot in &cache.slots {
slot.set(u32::MAX);
}
}
#[inline]
fn bump_shape_id(&mut self) {
self.shape_id = next_shape_id();
self.layout_id = layout_hash(&self.keys, &self.attrs);
}
#[inline]
fn touch_proto_generation(&self) {
self.proto_epoch.set(self.proto_epoch.get().wrapping_add(1));
bump_global_proto_mutation_epoch();
self.proto_global_epoch.set(u64::MAX);
}
#[inline]
pub fn shape_id(&self) -> u64 {
self.shape_id
}
#[inline]
pub fn layout_id(&self) -> u64 {
self.layout_id
}
#[inline]
pub fn proto_generation(&self) -> u32 {
let global_epoch = current_global_proto_mutation_epoch();
if self.proto_global_epoch.get() == global_epoch {
return self.proto_generation.get();
}
let inherited_generation = self
.get(INTERNAL_PROTO_PROPERTY_KEY)
.or_else(|| self.get(USER_VISIBLE_PROTO_PROPERTY_KEY))
.and_then(|value| match value {
JsValue::PlainObject(proto) => Some(proto.borrow().proto_generation()),
_ => None,
})
.unwrap_or(0);
let generation = self.proto_epoch.get().wrapping_add(inherited_generation);
self.proto_generation.set(generation);
self.proto_global_epoch.set(global_epoch);
generation
}
#[inline]
pub fn proto_generation_with_epoch(&self, global_epoch: u64) -> u32 {
if self.proto_global_epoch.get() == global_epoch {
return self.proto_generation.get();
}
let inherited_generation = self
.get(INTERNAL_PROTO_PROPERTY_KEY)
.or_else(|| self.get(USER_VISIBLE_PROTO_PROPERTY_KEY))
.and_then(|value| match value {
JsValue::PlainObject(proto) => Some(proto.borrow().proto_generation()),
_ => None,
})
.unwrap_or(0);
let generation = self.proto_epoch.get().wrapping_add(inherited_generation);
self.proto_generation.set(generation);
self.proto_global_epoch.set(global_epoch);
generation
}
#[inline]
pub fn global_proto_mutation_epoch() -> u64 {
current_global_proto_mutation_epoch()
}
#[inline]
pub fn offset_of(&self, key: &str) -> Option<usize> {
self.lookup_slot(key)
}
#[inline]
fn lookup_slot(&self, key: &str) -> Option<usize> {
match &self.index {
PropertyIndex::Inline => self
.keys
.iter()
.position(|candidate| candidate.as_ref() == key),
PropertyIndex::Map(index) => index.get(key).copied(),
}
}
#[inline]
fn lookup_slot_cached(&self, key: &str) -> Option<usize> {
if let Some(slot) = self.cache_probe(key) {
return Some(slot);
}
let slot = self.lookup_slot(key)?;
self.cache_record(key, slot);
Some(slot)
}
fn promote_index(&mut self) {
if matches!(self.index, PropertyIndex::Map(_)) {
return;
}
let mut index = HashMap::with_capacity(self.capacity_hint.max(self.keys.len()));
for (slot, key) in self.keys.iter().enumerate() {
index.insert(key.clone(), slot);
}
self.index = PropertyIndex::Map(index);
if self.inline_cache.is_none() {
self.inline_cache = Some(Box::new(InlinePropertyCache::new()));
}
}
fn refresh_index_mode(&mut self) {
if self.keys.len() <= SMALL_PROPERTY_LINEAR_SCAN_CAP {
self.index = PropertyIndex::Inline;
self.inline_cache = None;
} else {
self.promote_index();
}
}
#[inline]
pub fn get_by_offset(&self, offset: usize) -> Option<&JsValue> {
self.values.get(offset)
}
#[inline]
pub unsafe fn get_by_offset_unchecked(&self, offset: usize) -> &JsValue {
debug_assert!(offset < self.values.len());
unsafe { self.values.get_unchecked(offset) }
}
#[inline]
pub fn values_as_slice(&self) -> &[JsValue] {
&self.values
}
#[inline]
pub fn matches_key_at_offset(&self, offset: usize, key: &str) -> bool {
self.keys
.get(offset)
.is_some_and(|candidate| candidate.as_ref() == key)
}
#[inline]
pub fn set_by_offset(&mut self, offset: usize, value: JsValue) -> bool {
if let Some(slot) = self.values.get_mut(offset) {
*slot = value;
self.touch_proto_generation();
true
} else {
false
}
}
#[inline]
pub fn is_writable_by_offset(&self, offset: usize) -> bool {
self.attrs
.get(offset)
.is_some_and(|a| a.contains(PropertyAttributes::WRITABLE))
}
fn spec_insert_pos(&self, key: &str) -> usize {
if let Some(n) = parse_integer_index(key) {
self.keys[..self.integer_key_count]
.binary_search_by(|existing| {
parse_integer_index(existing).unwrap_or(u32::MAX).cmp(&n)
})
.unwrap_or_else(|pos| pos)
} else if is_symbol_property_key(key) {
self.keys.len()
} else {
let mut pos = self.keys.len();
while pos > 0 && is_symbol_property_key(&self.keys[pos - 1]) {
pos -= 1;
}
pos
}
}
fn insert_new(&mut self, key: Rc<str>, value: JsValue, attrs: PropertyAttributes, pos: usize) {
if key.starts_with("__get_") || key.starts_with("__set_") {
self.has_accessors = true;
}
let is_integer_key = parse_integer_index(&key).is_some();
self.index_shift_right(pos);
if let PropertyIndex::Map(index) = &mut self.index {
index.insert(key.clone(), pos);
}
Rc::make_mut(&mut self.keys).insert(pos, key);
if pos > self.values.len() {
self.values.resize(pos, JsValue::Undefined);
}
self.values.insert(pos, value);
let attrs_vec = Rc::make_mut(&mut self.attrs);
if pos > attrs_vec.len() {
attrs_vec.resize(pos, DEFAULT_ATTRS);
}
attrs_vec.insert(pos, attrs);
self.capacity_hint = self.capacity_hint.max(self.keys.len());
if is_integer_key {
self.integer_key_count += 1;
}
if self.keys.len() > SMALL_PROPERTY_LINEAR_SCAN_CAP {
self.promote_index();
}
}
fn index_shift_right(&mut self, pos: usize) {
if let PropertyIndex::Map(index) = &mut self.index {
for idx in index.values_mut() {
if *idx >= pos {
*idx += 1;
}
}
}
}
pub fn get(&self, key: &str) -> Option<&JsValue> {
if let Some(slot) = self.cache_probe(key) {
return self.values.get(slot);
}
let slot = self.lookup_slot(key)?;
if slot >= self.values.len() {
return None;
}
self.cache_record(key, slot);
Some(&self.values[slot])
}
pub fn get_by_rc(&self, key: &Rc<str>) -> Option<&JsValue> {
for (slot, candidate) in self.keys.iter().enumerate() {
if Rc::ptr_eq(candidate, key) {
if slot < self.values.len() {
self.cache_record(key.as_ref(), slot);
return Some(&self.values[slot]);
}
return None;
}
}
self.get(key.as_ref())
}
pub fn get_cloned(&self, key: &str) -> Option<JsValue> {
self.lookup_slot_cached(key)
.and_then(|slot| self.values.get(slot).cloned())
}
pub fn contains_key(&self, key: &str) -> bool {
self.lookup_slot_cached(key).is_some()
}
#[inline]
pub fn has_getter_for(&self, key: &str) -> bool {
if !self.has_accessors {
return false;
}
let prefix = "__get_";
let suffix = "__";
let total = prefix.len() + key.len() + suffix.len();
if total <= 128 {
let mut buf = [0u8; 128];
buf[..prefix.len()].copy_from_slice(prefix.as_bytes());
buf[prefix.len()..prefix.len() + key.len()].copy_from_slice(key.as_bytes());
buf[prefix.len() + key.len()..total].copy_from_slice(suffix.as_bytes());
let getter_key = unsafe { std::str::from_utf8_unchecked(&buf[..total]) };
self.contains_key(getter_key)
} else {
self.contains_key(&format!("__get_{key}__"))
}
}
#[inline]
pub fn has_setter_for(&self, key: &str) -> bool {
if !self.has_accessors {
return false;
}
let prefix = "__set_";
let suffix = "__";
let total = prefix.len() + key.len() + suffix.len();
if total <= 128 {
let mut buf = [0u8; 128];
buf[..prefix.len()].copy_from_slice(prefix.as_bytes());
buf[prefix.len()..prefix.len() + key.len()].copy_from_slice(key.as_bytes());
buf[prefix.len() + key.len()..total].copy_from_slice(suffix.as_bytes());
let setter_key = unsafe { std::str::from_utf8_unchecked(&buf[..total]) };
self.contains_key(setter_key)
} else {
self.contains_key(&format!("__set_{key}__"))
}
}
#[inline]
pub fn get_getter_for(&self, key: &str) -> Option<&JsValue> {
if !self.has_accessors {
return None;
}
let prefix = "__get_";
let suffix = "__";
let total = prefix.len() + key.len() + suffix.len();
if total <= 128 {
let mut buf = [0u8; 128];
buf[..prefix.len()].copy_from_slice(prefix.as_bytes());
buf[prefix.len()..prefix.len() + key.len()].copy_from_slice(key.as_bytes());
buf[prefix.len() + key.len()..total].copy_from_slice(suffix.as_bytes());
let getter_key = unsafe { std::str::from_utf8_unchecked(&buf[..total]) };
self.get(getter_key)
} else {
self.get(&format!("__get_{key}__"))
}
}
#[inline]
pub fn get_setter_for(&self, key: &str) -> Option<&JsValue> {
if !self.has_accessors {
return None;
}
let prefix = "__set_";
let suffix = "__";
let total = prefix.len() + key.len() + suffix.len();
if total <= 128 {
let mut buf = [0u8; 128];
buf[..prefix.len()].copy_from_slice(prefix.as_bytes());
buf[prefix.len()..prefix.len() + key.len()].copy_from_slice(key.as_bytes());
buf[prefix.len() + key.len()..total].copy_from_slice(suffix.as_bytes());
let setter_key = unsafe { std::str::from_utf8_unchecked(&buf[..total]) };
self.get(setter_key)
} else {
self.get(&format!("__set_{key}__"))
}
}
pub fn insert(&mut self, key: String, value: JsValue) {
self.insert_rc(key.into(), value);
}
pub fn insert_rc(&mut self, key: Rc<str>, value: JsValue) {
if let Some(i) = self.lookup_slot_cached(&key) {
if i >= self.values.len() {
self.values.resize(i + 1, JsValue::Undefined);
Rc::make_mut(&mut self.attrs).resize(i + 1, DEFAULT_ATTRS);
}
self.values[i] = value;
self.touch_proto_generation();
return;
}
if !self.extensible && !key.starts_with("__") {
return;
}
let attrs = if key.as_ref() == INTERNAL_PROTO_PROPERTY_KEY {
BUILTIN_ATTRS
} else {
DEFAULT_ATTRS
};
let pos = self.spec_insert_pos(&key);
self.insert_new(key, value, attrs, pos);
self.bump_shape_id();
if pos != self.keys.len() - 1 {
self.cache_invalidate();
}
self.touch_proto_generation();
}
pub fn insert_builtin(&mut self, key: String, value: JsValue) {
self.insert_builtin_rc(key.into(), value);
}
pub fn insert_builtin_rc(&mut self, key: Rc<str>, value: JsValue) {
if let Some(i) = self.lookup_slot_cached(&key) {
if i >= self.values.len() {
self.values.resize(i + 1, JsValue::Undefined);
Rc::make_mut(&mut self.attrs).resize(i + 1, BUILTIN_ATTRS);
}
self.values[i] = value;
Rc::make_mut(&mut self.attrs)[i] = BUILTIN_ATTRS;
self.touch_proto_generation();
return;
}
let pos = self.spec_insert_pos(&key);
self.insert_new(key, value, BUILTIN_ATTRS, pos);
self.bump_shape_id();
if pos != self.keys.len() - 1 {
self.cache_invalidate();
}
self.touch_proto_generation();
}
pub fn make_all_non_enumerable(&mut self) {
for attr in Rc::make_mut(&mut self.attrs).iter_mut() {
attr.remove(PropertyAttributes::ENUMERABLE);
}
if !self.attrs.is_empty() {
self.bump_shape_id();
self.touch_proto_generation();
}
}
pub fn remove(&mut self, key: &str) -> Option<JsValue> {
if let Some(i) = self.lookup_slot(key) {
if let PropertyIndex::Map(index) = &mut self.index {
index.remove(key);
}
if parse_integer_index(&self.keys[i]).is_some() {
self.integer_key_count -= 1;
}
let val = self.values[i].clone();
Rc::make_mut(&mut self.keys).remove(i);
self.values.remove(i);
Rc::make_mut(&mut self.attrs).remove(i);
if let PropertyIndex::Map(index) = &mut self.index {
for idx in index.values_mut() {
if *idx > i {
*idx -= 1;
}
}
}
self.refresh_index_mode();
self.bump_shape_id();
self.cache_invalidate();
self.touch_proto_generation();
Some(val)
} else {
None
}
}
pub fn keys(&self) -> impl Iterator<Item = &Rc<str>> {
self.keys.iter()
}
pub fn iter(&self) -> impl Iterator<Item = (&Rc<str>, &JsValue)> {
self.keys.iter().zip(self.values.iter())
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn get_with_attrs(&self, key: &str) -> Option<(&JsValue, PropertyAttributes)> {
self.lookup_slot_cached(key).and_then(|slot| {
let v = self.values.get(slot)?;
let a = self.attrs.get(slot).copied()?;
Some((v, a))
})
}
pub fn insert_with_attrs(&mut self, key: String, value: JsValue, attrs: PropertyAttributes) {
self.insert_with_attrs_rc(key.into(), value, attrs);
}
pub fn insert_with_attrs_rc(
&mut self,
key: Rc<str>,
value: JsValue,
attrs: PropertyAttributes,
) {
if let Some(i) = self.lookup_slot_cached(&key) {
if i >= self.values.len() {
self.values.resize(i + 1, JsValue::Undefined);
Rc::make_mut(&mut self.attrs).resize(i + 1, DEFAULT_ATTRS);
}
self.values[i] = value;
Rc::make_mut(&mut self.attrs)[i] = attrs;
self.touch_proto_generation();
} else {
let pos = self.spec_insert_pos(&key);
self.insert_new(key, value, attrs, pos);
self.bump_shape_id();
if pos != self.keys.len() - 1 {
self.cache_invalidate();
}
self.touch_proto_generation();
}
}
pub fn set_attrs(&mut self, key: &str, attrs: PropertyAttributes) -> bool {
if let Some(i) = self.lookup_slot_cached(key) {
let attrs_vec = Rc::make_mut(&mut self.attrs);
if i >= attrs_vec.len() {
attrs_vec.resize(i + 1, DEFAULT_ATTRS);
}
attrs_vec[i] = attrs;
self.bump_shape_id();
self.touch_proto_generation();
true
} else {
false
}
}
pub fn attrs(&self, key: &str) -> Option<PropertyAttributes> {
self.lookup_slot_cached(key)
.and_then(|slot| self.attrs.get(slot).copied())
}
pub fn is_writable(&self, key: &str) -> bool {
self.lookup_slot_cached(key)
.and_then(|i| self.attrs.get(i))
.map(|a| a.contains(PropertyAttributes::WRITABLE))
.unwrap_or(false)
}
pub fn is_configurable(&self, key: &str) -> bool {
self.lookup_slot_cached(key)
.and_then(|i| self.attrs.get(i))
.map(|a| a.contains(PropertyAttributes::CONFIGURABLE))
.unwrap_or(false)
}
pub fn is_enumerable(&self, key: &str) -> bool {
self.lookup_slot_cached(key)
.and_then(|i| self.attrs.get(i))
.map(|a| a.contains(PropertyAttributes::ENUMERABLE))
.unwrap_or(false)
}
pub fn set_writable(&mut self, key: &str, writable: bool) {
if let Some(i) = self.lookup_slot_cached(key) {
let attrs_vec = Rc::make_mut(&mut self.attrs);
if i >= attrs_vec.len() {
attrs_vec.resize(i + 1, DEFAULT_ATTRS);
}
let a = &mut attrs_vec[i];
if writable {
a.insert(PropertyAttributes::WRITABLE);
} else {
a.remove(PropertyAttributes::WRITABLE);
}
self.bump_shape_id();
self.touch_proto_generation();
}
}
pub fn set_enumerable(&mut self, key: &str, enumerable: bool) {
if let Some(i) = self.lookup_slot_cached(key) {
let attrs_vec = Rc::make_mut(&mut self.attrs);
if i >= attrs_vec.len() {
attrs_vec.resize(i + 1, DEFAULT_ATTRS);
}
let a = &mut attrs_vec[i];
if enumerable {
a.insert(PropertyAttributes::ENUMERABLE);
} else {
a.remove(PropertyAttributes::ENUMERABLE);
}
self.bump_shape_id();
self.touch_proto_generation();
}
}
pub fn set_configurable(&mut self, key: &str, configurable: bool) {
if let Some(i) = self.lookup_slot_cached(key) {
let attrs_vec = Rc::make_mut(&mut self.attrs);
if i >= attrs_vec.len() {
attrs_vec.resize(i + 1, DEFAULT_ATTRS);
}
let a = &mut attrs_vec[i];
if configurable {
a.insert(PropertyAttributes::CONFIGURABLE);
} else {
a.remove(PropertyAttributes::CONFIGURABLE);
}
self.bump_shape_id();
self.touch_proto_generation();
}
}
pub fn enumerable_keys(&self) -> impl Iterator<Item = &Rc<str>> {
self.keys
.iter()
.zip(self.attrs.iter())
.filter(|(k, a)| {
a.contains(PropertyAttributes::ENUMERABLE) && !is_symbol_property_key(k)
})
.map(|(k, _)| k)
}
pub fn enumerable_iter(&self) -> impl Iterator<Item = (&Rc<str>, &JsValue)> {
self.keys
.iter()
.zip(self.values.iter())
.zip(self.attrs.iter())
.filter(|((k, _), a)| {
a.contains(PropertyAttributes::ENUMERABLE) && !is_symbol_property_key(k)
})
.map(|((k, v), _)| (k, v))
}
pub fn iter_with_attrs(
&self,
) -> impl Iterator<Item = (&Rc<str>, &JsValue, PropertyAttributes)> {
self.keys
.iter()
.zip(self.values.iter())
.zip(self.attrs.iter())
.map(|((k, v), a)| (k, v, *a))
}
pub fn own_symbol_keys(&self) -> Vec<u64> {
self.keys
.iter()
.filter_map(|key| property_key_to_symbol(key))
.collect()
}
pub fn freeze(&mut self) {
for a in Rc::make_mut(&mut self.attrs).iter_mut() {
a.remove(PropertyAttributes::WRITABLE);
a.remove(PropertyAttributes::CONFIGURABLE);
}
self.extensible = false;
self.bump_shape_id();
self.touch_proto_generation();
}
pub fn is_frozen(&self) -> bool {
if self.extensible {
return false;
}
self.attrs.iter().all(|a| {
!a.contains(PropertyAttributes::WRITABLE)
&& !a.contains(PropertyAttributes::CONFIGURABLE)
})
}
pub fn seal(&mut self) {
for a in Rc::make_mut(&mut self.attrs).iter_mut() {
a.remove(PropertyAttributes::CONFIGURABLE);
}
self.extensible = false;
self.bump_shape_id();
self.touch_proto_generation();
}
pub fn is_sealed(&self) -> bool {
if self.extensible {
return false;
}
self.attrs
.iter()
.all(|a| !a.contains(PropertyAttributes::CONFIGURABLE))
}
}
impl Default for PropertyMap {
fn default() -> Self {
Self::new()
}
}
impl Drop for PropertyMap {
fn drop(&mut self) {
let keys_rc = std::mem::take(&mut self.keys);
let attrs_rc = std::mem::take(&mut self.attrs);
let values = std::mem::take(&mut self.values);
if let (Ok(keys), Ok(attrs)) = (Rc::try_unwrap(keys_rc), Rc::try_unwrap(attrs_rc)) {
release_storage_buffers(PropertyStorageBuffers {
keys,
values,
attrs,
});
} else {
release_values_vec(values);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_get_default_attrs() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(42));
assert_eq!(pm.get("x"), Some(&JsValue::Smi(42)));
let attrs = pm.attrs("x").unwrap();
assert!(attrs.contains(PropertyAttributes::WRITABLE));
assert!(attrs.contains(PropertyAttributes::ENUMERABLE));
assert!(attrs.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_insert_with_attrs() {
let mut pm = PropertyMap::new();
pm.insert_with_attrs(
"ro".to_string(),
JsValue::Smi(1),
PropertyAttributes::empty(),
);
assert!(!pm.is_writable("ro"));
assert!(!pm.is_enumerable("ro"));
assert!(!pm.is_configurable("ro"));
}
#[test]
fn test_insert_preserves_existing_attrs() {
let mut pm = PropertyMap::new();
pm.insert_with_attrs(
"p".to_string(),
JsValue::Smi(1),
PropertyAttributes::ENUMERABLE,
);
pm.insert("p".to_string(), JsValue::Smi(2));
assert_eq!(pm.get("p"), Some(&JsValue::Smi(2)));
let attrs = pm.attrs("p").unwrap();
assert_eq!(attrs, PropertyAttributes::ENUMERABLE);
}
#[test]
fn test_remove() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Boolean(true));
assert!(pm.contains_key("a"));
let removed = pm.remove("a");
assert_eq!(removed, Some(JsValue::Boolean(true)));
assert!(!pm.contains_key("a"));
}
#[test]
fn test_enumerable_keys() {
let mut pm = PropertyMap::new();
pm.insert("visible".to_string(), JsValue::Smi(1));
pm.insert_with_attrs(
"hidden".to_string(),
JsValue::Smi(2),
PropertyAttributes::WRITABLE,
);
let enum_keys: Vec<&str> = pm.enumerable_keys().map(|k| &**k).collect();
assert!(enum_keys.contains(&"visible"));
assert!(!enum_keys.contains(&"hidden"));
}
#[test]
fn test_len_and_is_empty() {
let mut pm = PropertyMap::new();
assert!(pm.is_empty());
assert_eq!(pm.len(), 0);
pm.insert("k".to_string(), JsValue::Null);
assert!(!pm.is_empty());
assert_eq!(pm.len(), 1);
}
#[test]
fn test_set_attrs() {
let mut pm = PropertyMap::new();
pm.insert("p".to_string(), JsValue::Smi(1));
assert!(pm.is_writable("p"));
pm.set_attrs("p", PropertyAttributes::ENUMERABLE);
assert!(!pm.is_writable("p"));
assert!(pm.is_enumerable("p"));
assert!(!pm.is_configurable("p"));
}
#[test]
fn test_get_with_attrs() {
let mut pm = PropertyMap::new();
pm.insert_with_attrs(
"x".to_string(),
JsValue::Smi(5),
PropertyAttributes::WRITABLE | PropertyAttributes::ENUMERABLE,
);
let (val, attrs) = pm.get_with_attrs("x").unwrap();
assert_eq!(val, &JsValue::Smi(5));
assert!(attrs.contains(PropertyAttributes::WRITABLE));
assert!(attrs.contains(PropertyAttributes::ENUMERABLE));
assert!(!attrs.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_iter_with_attrs() {
let mut pm = PropertyMap::new();
pm.insert_with_attrs(
"a".to_string(),
JsValue::Smi(1),
PropertyAttributes::WRITABLE,
);
let entries: Vec<_> = pm.iter_with_attrs().collect();
assert_eq!(entries.len(), 1);
assert_eq!(&**entries[0].0, "a");
assert_eq!(entries[0].1, &JsValue::Smi(1));
assert_eq!(entries[0].2, PropertyAttributes::WRITABLE);
}
#[test]
fn test_enumerable_keys_skip_symbol_keys() {
let mut pm = PropertyMap::new();
pm.insert("visible".to_string(), JsValue::Smi(1));
pm.insert(
crate::builtins::symbol::symbol_to_property_key(123),
JsValue::Smi(2),
);
let keys: Vec<&str> = pm.enumerable_keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["visible"]);
}
#[test]
fn test_own_symbol_keys_returns_symbols() {
let mut pm = PropertyMap::new();
pm.insert(
crate::builtins::symbol::symbol_to_property_key(321),
JsValue::Boolean(true),
);
assert_eq!(pm.own_symbol_keys(), vec![321]);
}
#[test]
fn test_missing_key_attr_queries() {
let pm = PropertyMap::new();
assert!(!pm.is_writable("nope"));
assert!(!pm.is_enumerable("nope"));
assert!(!pm.is_configurable("nope"));
assert!(pm.attrs("nope").is_none());
}
#[test]
fn test_with_capacity() {
let pm = PropertyMap::with_capacity(16);
assert!(pm.is_empty());
}
#[test]
fn test_small_maps_skip_inline_cache() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.insert("y".to_string(), JsValue::Smi(2));
assert_eq!(pm.get("x"), Some(&JsValue::Smi(1)));
assert!(pm.inline_cache.is_none());
}
#[test]
fn test_clone_shape_resets_values_and_preserves_layout() {
let mut pm = PropertyMap::with_capacity(SMALL_PROPERTY_LINEAR_SCAN_CAP + 4);
for idx in 0..(SMALL_PROPERTY_LINEAR_SCAN_CAP + 1) {
pm.insert(format!("k{idx}"), JsValue::Smi(idx as i32));
}
let shape_id = pm.shape_id();
let layout_id = pm.layout_id();
let cloned = pm.clone_shape();
assert_eq!(cloned.keys, pm.keys);
assert!(
cloned
.values
.iter()
.all(|value| matches!(value, JsValue::Undefined))
);
assert_eq!(cloned.attrs, pm.attrs);
assert_ne!(cloned.shape_id(), shape_id);
assert_eq!(cloned.layout_id(), layout_id);
assert!(matches!(cloned.index, PropertyIndex::Map(_)));
assert_eq!(cloned.template_next_slot, 0);
assert_eq!(cloned.capacity_hint, pm.capacity_hint);
}
#[test]
fn test_try_template_fill_returns_offset_and_disables_on_miss() {
let mut pm = PropertyMap::from_boilerplate(
&[Rc::from("first"), Rc::from("second")],
&[DEFAULT_ATTRS, DEFAULT_ATTRS],
);
assert_eq!(pm.try_template_fill("first", JsValue::Smi(1)), Ok(0));
assert_eq!(pm.try_template_fill("second", JsValue::Smi(2)), Ok(1));
assert_eq!(pm.get("first"), Some(&JsValue::Smi(1)));
assert_eq!(pm.get("second"), Some(&JsValue::Smi(2)));
let err = pm.try_template_fill("third", JsValue::Smi(3));
assert_eq!(err, Err(JsValue::Smi(3)));
assert_eq!(pm.template_next_slot, usize::MAX);
}
#[test]
fn test_cache_populated_on_get() {
let mut pm = PropertyMap::new();
for i in 0..=SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{i}"), JsValue::Smi(i as i32));
}
assert_eq!(pm.get("k0"), Some(&JsValue::Smi(0)));
assert_eq!(pm.get("k0"), Some(&JsValue::Smi(0)));
assert_eq!(pm.get("k1"), Some(&JsValue::Smi(1)));
assert!(pm.inline_cache.is_some());
}
#[test]
fn test_cache_invalidated_on_remove() {
let mut pm = PropertyMap::new();
for i in 0..=SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{i}"), JsValue::Smi(i as i32));
}
assert_eq!(pm.get("k0"), Some(&JsValue::Smi(0)));
pm.remove("k0");
}
#[test]
fn test_cache_wraps_around() {
let mut pm = PropertyMap::new();
for i in 0..(INLINE_CACHE_CAP as i32 + 2) {
pm.insert(format!("k{i}"), JsValue::Smi(i));
}
for i in 0..(INLINE_CACHE_CAP as i32 + 2) {
assert_eq!(pm.get(&format!("k{i}")), Some(&JsValue::Smi(i)));
}
for i in 0..(INLINE_CACHE_CAP as i32 + 2) {
assert_eq!(pm.get(&format!("k{i}")), Some(&JsValue::Smi(i)));
}
}
#[test]
fn test_cache_hit_moves_entry_to_front() {
let mut pm = PropertyMap::new();
for i in 0..=SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{i}"), JsValue::Smi(i as i32));
}
assert_eq!(pm.get("k0"), Some(&JsValue::Smi(0)));
assert_eq!(pm.get("k1"), Some(&JsValue::Smi(1)));
let hash_a = name_hash("k0");
let hash_b = name_hash("k1");
let idx_a = (hash_a as usize) & (INLINE_CACHE_CAP - 1);
let idx_b = (hash_b as usize) & (INLINE_CACHE_CAP - 1);
let cache = pm
.inline_cache
.as_ref()
.expect("large map should allocate cache");
assert_eq!(cache.hashes[idx_a].get(), hash_a);
assert_eq!(cache.hashes[idx_b].get(), hash_b);
assert_eq!(pm.get("k1"), Some(&JsValue::Smi(1)));
assert_eq!(cache.hashes[idx_b].get(), hash_b);
}
#[test]
fn test_cache_contains_key_fast_path() {
let mut pm = PropertyMap::new();
for i in 0..=SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{i}"), JsValue::Smi(i as i32));
}
let _ = pm.get("k0");
assert!(pm.contains_key("k0"));
assert!(!pm.contains_key("missing"));
}
#[test]
fn test_cache_get_with_attrs_fast_path() {
let mut pm = PropertyMap::new();
for i in 0..SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{i}"), JsValue::Smi(i as i32));
}
pm.insert_with_attrs(
format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}"),
JsValue::Smi(42),
PropertyAttributes::WRITABLE | PropertyAttributes::ENUMERABLE,
);
let (val, attrs) = pm
.get_with_attrs(&format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}"))
.unwrap();
assert_eq!(val, &JsValue::Smi(42));
assert!(attrs.contains(PropertyAttributes::WRITABLE));
let (val2, attrs2) = pm
.get_with_attrs(&format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}"))
.unwrap();
assert_eq!(val2, &JsValue::Smi(42));
assert_eq!(attrs, attrs2);
}
#[test]
fn test_cache_equality_ignores_cache_state() {
let mut pm1 = PropertyMap::new();
let mut pm2 = PropertyMap::new();
pm1.insert("x".to_string(), JsValue::Smi(1));
pm2.insert("x".to_string(), JsValue::Smi(1));
let _ = pm1.get("x");
assert_eq!(pm1, pm2);
}
#[test]
fn test_parse_integer_index() {
assert_eq!(parse_integer_index("0"), Some(0));
assert_eq!(parse_integer_index("1"), Some(1));
assert_eq!(parse_integer_index("42"), Some(42));
assert_eq!(parse_integer_index("4294967294"), Some(u32::MAX - 1));
assert_eq!(parse_integer_index("4294967295"), None);
assert_eq!(parse_integer_index("01"), None);
assert_eq!(parse_integer_index("007"), None);
assert_eq!(parse_integer_index(""), None);
assert_eq!(parse_integer_index("abc"), None);
assert_eq!(parse_integer_index("-1"), None);
assert_eq!(parse_integer_index("1.5"), None);
}
#[test]
fn test_integer_indices_sorted_before_strings() {
let mut pm = PropertyMap::new();
pm.insert("b".to_string(), JsValue::Smi(1));
pm.insert("2".to_string(), JsValue::Smi(2));
pm.insert("a".to_string(), JsValue::Smi(3));
pm.insert("0".to_string(), JsValue::Smi(4));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, vec!["0", "2", "b", "a"]);
}
#[test]
fn test_integer_indices_ascending_numeric_order() {
let mut pm = PropertyMap::new();
pm.insert("10".to_string(), JsValue::Smi(10));
pm.insert("2".to_string(), JsValue::Smi(2));
pm.insert("1".to_string(), JsValue::Smi(1));
pm.insert("20".to_string(), JsValue::Smi(20));
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["1", "2", "10", "20"]);
assert_eq!(pm.integer_key_count, 4);
}
#[test]
fn test_string_and_symbol_inserts_do_not_change_integer_key_count() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
pm.insert("2".to_string(), JsValue::Smi(2));
pm.insert("name".to_string(), JsValue::Smi(1));
pm.insert(symbol_to_property_key(symbol_create(None)), JsValue::Smi(3));
assert_eq!(pm.integer_key_count, 1);
}
#[test]
fn test_string_keys_preserve_insertion_order() {
let mut pm = PropertyMap::new();
pm.insert("z".to_string(), JsValue::Smi(1));
pm.insert("a".to_string(), JsValue::Smi(2));
pm.insert("m".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["z", "a", "m"]);
}
#[test]
fn test_mixed_integer_and_string_order() {
let mut pm = PropertyMap::new();
pm.insert("z".to_string(), JsValue::Smi(1));
pm.insert("5".to_string(), JsValue::Smi(2));
pm.insert("a".to_string(), JsValue::Smi(3));
pm.insert("1".to_string(), JsValue::Smi(4));
pm.insert("m".to_string(), JsValue::Smi(5));
pm.insert("3".to_string(), JsValue::Smi(6));
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["1", "3", "5", "z", "a", "m"]);
}
#[test]
fn test_remove_preserves_order() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
pm.insert("b".to_string(), JsValue::Smi(2));
pm.insert("c".to_string(), JsValue::Smi(3));
pm.remove("b");
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["a", "c"]);
assert_eq!(pm.get("a"), Some(&JsValue::Smi(1)));
assert_eq!(pm.get("c"), Some(&JsValue::Smi(3)));
}
#[test]
fn test_remove_preserves_spec_order() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.insert("3".to_string(), JsValue::Smi(2));
pm.insert("y".to_string(), JsValue::Smi(3));
pm.insert("1".to_string(), JsValue::Smi(4));
pm.remove("3");
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["1", "x", "y"]);
assert_eq!(pm.integer_key_count, 1);
}
#[test]
fn test_enumerable_keys_spec_order() {
let mut pm = PropertyMap::new();
pm.insert("b".to_string(), JsValue::Smi(1));
pm.insert("2".to_string(), JsValue::Smi(2));
pm.insert_with_attrs(
"hidden".to_string(),
JsValue::Smi(99),
PropertyAttributes::WRITABLE, );
pm.insert("0".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.enumerable_keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["0", "2", "b"]);
}
#[test]
fn test_iter_values_match_spec_ordered_keys() {
let mut pm = PropertyMap::new();
pm.insert("b".to_string(), JsValue::Smi(10));
pm.insert("1".to_string(), JsValue::Smi(20));
pm.insert("0".to_string(), JsValue::Smi(30));
let pairs: Vec<(&str, &JsValue)> = pm.iter().map(|(k, v)| (&**k, v)).collect();
assert_eq!(
pairs,
vec![
("0", &JsValue::Smi(30)),
("1", &JsValue::Smi(20)),
("b", &JsValue::Smi(10)),
]
);
}
#[test]
fn test_insert_existing_integer_key_no_reorder() {
let mut pm = PropertyMap::new();
pm.insert("1".to_string(), JsValue::Smi(10));
pm.insert("a".to_string(), JsValue::Smi(20));
pm.insert("1".to_string(), JsValue::Smi(99));
let keys: Vec<&str> = pm.keys().map(|s| &**s).collect();
assert_eq!(keys, vec!["1", "a"]);
assert_eq!(pm.get("1"), Some(&JsValue::Smi(99)));
}
#[test]
fn test_shape_id_stable_on_value_update() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
let id_after_insert = pm.shape_id();
pm.insert("x".to_string(), JsValue::Smi(2));
assert_eq!(pm.shape_id(), id_after_insert);
}
#[test]
fn test_shape_id_changes_on_new_property() {
let mut pm = PropertyMap::new();
let id0 = pm.shape_id();
pm.insert("x".to_string(), JsValue::Smi(1));
assert_ne!(pm.shape_id(), id0);
}
#[test]
fn test_shape_id_changes_on_remove() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
let id1 = pm.shape_id();
pm.remove("x");
assert_ne!(pm.shape_id(), id1);
}
#[test]
fn test_shape_id_changes_on_attr_change() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
let id1 = pm.shape_id();
pm.set_writable("x", false);
assert_ne!(pm.shape_id(), id1);
}
#[test]
fn test_layout_id_shared_for_identical_property_sequences() {
let mut first = PropertyMap::new();
first.insert("x".to_string(), JsValue::Smi(1));
first.insert("y".to_string(), JsValue::Smi(2));
let mut second = PropertyMap::new();
second.insert("x".to_string(), JsValue::Smi(10));
second.insert("y".to_string(), JsValue::Smi(20));
assert_eq!(first.layout_id(), second.layout_id());
}
#[test]
fn test_layout_id_changes_for_different_layouts() {
let mut attrs = PropertyMap::new();
attrs.insert("x".to_string(), JsValue::Smi(1));
attrs.set_writable("x", false);
let mut order = PropertyMap::new();
order.insert("y".to_string(), JsValue::Smi(1));
order.insert("x".to_string(), JsValue::Smi(2));
let mut baseline = PropertyMap::new();
baseline.insert("x".to_string(), JsValue::Smi(3));
assert_ne!(baseline.layout_id(), attrs.layout_id());
assert_ne!(baseline.layout_id(), order.layout_id());
}
#[test]
fn test_offset_of_and_get_by_offset() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(10));
pm.insert("b".to_string(), JsValue::Smi(20));
let off_a = pm.offset_of("a").unwrap();
let off_b = pm.offset_of("b").unwrap();
assert_eq!(pm.get_by_offset(off_a), Some(&JsValue::Smi(10)));
assert_eq!(pm.get_by_offset(off_b), Some(&JsValue::Smi(20)));
assert!(pm.offset_of("missing").is_none());
assert!(pm.get_by_offset(999).is_none());
}
#[test]
fn test_set_by_offset() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
let off = pm.offset_of("x").unwrap();
assert!(pm.set_by_offset(off, JsValue::Smi(42)));
assert_eq!(pm.get("x"), Some(&JsValue::Smi(42)));
assert!(!pm.set_by_offset(999, JsValue::Null));
}
#[test]
fn test_is_writable_by_offset() {
let mut pm = PropertyMap::new();
pm.insert("w".to_string(), JsValue::Smi(1));
pm.insert_with_attrs(
"ro".to_string(),
JsValue::Smi(2),
PropertyAttributes::ENUMERABLE,
);
let off_w = pm.offset_of("w").unwrap();
let off_ro = pm.offset_of("ro").unwrap();
assert!(pm.is_writable_by_offset(off_w));
assert!(!pm.is_writable_by_offset(off_ro));
assert!(!pm.is_writable_by_offset(999));
}
#[test]
fn test_unique_shape_ids_across_maps() {
let pm1 = PropertyMap::new();
let pm2 = PropertyMap::new();
assert_ne!(pm1.shape_id(), pm2.shape_id());
}
#[test]
fn test_freeze_makes_all_non_writable_non_configurable() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
pm.insert("b".to_string(), JsValue::Smi(2));
pm.freeze();
assert!(!pm.is_writable("a"));
assert!(!pm.is_configurable("a"));
assert!(!pm.is_writable("b"));
assert!(!pm.is_configurable("b"));
assert!(!pm.extensible);
}
#[test]
fn test_seal_preserves_writable_removes_configurable() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
pm.seal();
assert!(pm.is_writable("a"), "seal should preserve writable");
assert!(!pm.is_configurable("a"), "seal should remove configurable");
assert!(!pm.extensible);
}
#[test]
fn test_is_frozen_empty_non_extensible() {
let mut pm = PropertyMap::new();
pm.extensible = false;
assert!(pm.is_frozen(), "empty non-extensible map should be frozen");
}
#[test]
fn test_is_frozen_with_writable_property() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.extensible = false;
assert!(
!pm.is_frozen(),
"non-extensible map with writable prop is not frozen"
);
}
#[test]
fn test_is_sealed_with_configurable_property() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.extensible = false;
assert!(
!pm.is_sealed(),
"non-extensible map with configurable prop is not sealed"
);
}
#[test]
fn test_non_extensible_insert_rejected() {
let mut pm = PropertyMap::new();
pm.extensible = false;
pm.insert("newkey".to_string(), JsValue::Smi(42));
assert!(
!pm.contains_key("newkey"),
"non-extensible map should reject new property"
);
}
#[test]
fn test_non_extensible_allows_existing_update() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.extensible = false;
pm.insert("x".to_string(), JsValue::Smi(2));
assert_eq!(
pm.get("x"),
Some(&JsValue::Smi(2)),
"updating existing property should succeed on non-extensible"
);
}
#[test]
fn test_enumerable_keys_skips_non_enumerable() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1)); pm.insert_with_attrs(
"b".to_string(),
JsValue::Smi(2),
PropertyAttributes::WRITABLE | PropertyAttributes::CONFIGURABLE,
); pm.insert("c".to_string(), JsValue::Smi(3)); let keys: Vec<&str> = pm.enumerable_keys().map(|k| &**k).collect();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0], "a");
assert_eq!(keys[1], "c");
}
#[test]
fn test_enumerable_iter_returns_only_enumerable_pairs() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(10));
pm.insert_with_attrs(
"hidden".to_string(),
JsValue::Smi(99),
PropertyAttributes::empty(),
);
pm.insert("y".to_string(), JsValue::Smi(20));
let pairs: Vec<(&str, &JsValue)> = pm.enumerable_iter().map(|(k, v)| (&**k, v)).collect();
assert_eq!(pairs.len(), 2);
assert_eq!(pairs[0].0, "x");
assert_eq!(pairs[1].0, "y");
}
#[test]
fn test_iter_with_attrs_returns_all_triples() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
pm.insert_with_attrs(
"b".to_string(),
JsValue::Smi(2),
PropertyAttributes::WRITABLE,
);
let triples: Vec<(&str, &JsValue, PropertyAttributes)> =
pm.iter_with_attrs().map(|(k, v, a)| (&**k, v, a)).collect();
assert_eq!(triples.len(), 2);
assert!(triples[0].2.contains(PropertyAttributes::ENUMERABLE));
assert!(!triples[1].2.contains(PropertyAttributes::ENUMERABLE));
}
#[test]
fn test_freeze_makes_non_writable_non_configurable() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.freeze();
assert!(!pm.is_writable("x"));
assert!(!pm.is_configurable("x"));
assert!(!pm.extensible);
assert!(pm.is_frozen());
}
#[test]
fn test_seal_makes_non_configurable() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.seal();
assert!(pm.is_writable("x")); assert!(!pm.is_configurable("x"));
assert!(!pm.extensible);
assert!(pm.is_sealed());
}
#[test]
fn test_proto_key_non_enumerable() {
let mut pm = PropertyMap::new();
pm.insert(INTERNAL_PROTO_PROPERTY_KEY.to_string(), JsValue::Null);
pm.insert("visible".to_string(), JsValue::Smi(1));
let enum_keys: Vec<&str> = pm.enumerable_keys().map(|k| &**k).collect();
assert_eq!(enum_keys.len(), 1);
assert_eq!(enum_keys[0], "visible");
}
#[test]
fn test_make_all_non_enumerable() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
pm.insert("b".to_string(), JsValue::Smi(2));
assert!(pm.is_enumerable("a"));
pm.make_all_non_enumerable();
assert!(!pm.is_enumerable("a"));
assert!(!pm.is_enumerable("b"));
}
#[test]
fn test_proto_generation_tracks_prototype_mutations() {
use std::cell::RefCell;
use std::rc::Rc;
let proto = Rc::new(RefCell::new(PropertyMap::new()));
proto.borrow_mut().insert("x".to_string(), JsValue::Smi(1));
let child = Rc::new(RefCell::new(PropertyMap::new()));
child.borrow_mut().insert(
"__proto__".to_string(),
JsValue::PlainObject(Rc::clone(&proto)),
);
let before = child.borrow().proto_generation();
proto.borrow_mut().insert("x".to_string(), JsValue::Smi(2));
let after = child.borrow().proto_generation();
assert_ne!(after, before);
}
#[test]
fn test_set_writable_toggle() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
assert!(pm.is_writable("x"));
pm.set_writable("x", false);
assert!(!pm.is_writable("x"));
pm.set_writable("x", true);
assert!(pm.is_writable("x"));
}
#[test]
fn test_set_enumerable_toggle() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
assert!(pm.is_enumerable("x"));
pm.set_enumerable("x", false);
assert!(!pm.is_enumerable("x"));
}
#[test]
fn test_set_configurable_toggle() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
assert!(pm.is_configurable("x"));
pm.set_configurable("x", false);
assert!(!pm.is_configurable("x"));
}
#[test]
fn test_enum_order_integer_indices_sorted_ascending() {
let mut pm = PropertyMap::new();
pm.insert("2".to_string(), JsValue::Smi(2));
pm.insert("0".to_string(), JsValue::Smi(0));
pm.insert("1".to_string(), JsValue::Smi(1));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "1", "2"]);
}
#[test]
fn test_enum_order_strings_after_integers() {
let mut pm = PropertyMap::new();
pm.insert("b".to_string(), JsValue::Smi(1));
pm.insert("1".to_string(), JsValue::Smi(2));
pm.insert("a".to_string(), JsValue::Smi(3));
pm.insert("0".to_string(), JsValue::Smi(4));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "1", "b", "a"]);
}
#[test]
fn test_enum_order_symbols_after_strings() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
let sym = symbol_create(Some("s".into()));
let sym_key = symbol_to_property_key(sym);
pm.insert(sym_key.clone(), JsValue::Smi(2));
pm.insert("b".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["a", "b", sym_key.as_str()]);
}
#[test]
fn test_enum_order_integers_strings_symbols_combined() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
let sym = symbol_create(Some("sym".into()));
let sym_key = symbol_to_property_key(sym);
pm.insert("z".to_string(), JsValue::Smi(1));
pm.insert(sym_key.clone(), JsValue::Smi(2));
pm.insert("5".to_string(), JsValue::Smi(3));
pm.insert("a".to_string(), JsValue::Smi(4));
pm.insert("0".to_string(), JsValue::Smi(5));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "5", "z", "a", sym_key.as_str()]);
}
#[test]
fn test_enum_order_sparse_array_indices() {
let mut pm = PropertyMap::new();
pm.insert("2".to_string(), JsValue::String("c".into()));
pm.insert("0".to_string(), JsValue::String("a".into()));
pm.insert("1".to_string(), JsValue::String("b".into()));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "1", "2"]);
}
#[test]
fn test_enum_order_large_integer_indices() {
let mut pm = PropertyMap::new();
pm.insert("100".to_string(), JsValue::Smi(1));
pm.insert("5".to_string(), JsValue::Smi(2));
pm.insert("42".to_string(), JsValue::Smi(3));
pm.insert("3".to_string(), JsValue::Smi(4));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["3", "5", "42", "100"]);
}
#[test]
fn test_enum_order_u32_max_not_array_index() {
let mut pm = PropertyMap::new();
let max_str = u32::MAX.to_string();
pm.insert("0".to_string(), JsValue::Smi(1));
pm.insert(max_str.clone(), JsValue::Smi(2));
pm.insert("a".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", max_str.as_str(), "a"]);
}
#[test]
fn test_enum_order_leading_zero_not_array_index() {
let mut pm = PropertyMap::new();
pm.insert("01".to_string(), JsValue::Smi(1));
pm.insert("0".to_string(), JsValue::Smi(2));
pm.insert("1".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "1", "01"]);
}
#[test]
fn test_enum_order_string_insertion_order_preserved() {
let mut pm = PropertyMap::new();
pm.insert("c".to_string(), JsValue::Smi(1));
pm.insert("a".to_string(), JsValue::Smi(2));
pm.insert("b".to_string(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["c", "a", "b"]);
}
#[test]
fn test_enum_order_multiple_symbols_insertion_order() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
let s1 = symbol_create(Some("s1".into()));
let s2 = symbol_create(Some("s2".into()));
let k1 = symbol_to_property_key(s1);
let k2 = symbol_to_property_key(s2);
pm.insert(k1.clone(), JsValue::Smi(1));
pm.insert("a".to_string(), JsValue::Smi(2));
pm.insert(k2.clone(), JsValue::Smi(3));
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["a", k1.as_str(), k2.as_str()]);
}
#[test]
fn test_enumerable_keys_skips_symbols() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
let sym = symbol_create(Some("hidden".into()));
let sym_key = symbol_to_property_key(sym);
pm.insert(sym_key, JsValue::Smi(2));
pm.insert("b".to_string(), JsValue::Smi(3));
let enumerable: Vec<&str> = pm.enumerable_keys().map(|k| &**k).collect();
assert_eq!(enumerable, &["a", "b"]);
}
#[test]
fn test_enumerable_keys_skips_non_enumerable_v2() {
let mut pm = PropertyMap::new();
pm.insert("visible".to_string(), JsValue::Smi(1));
pm.insert_with_attrs(
"hidden".to_string(),
JsValue::Smi(2),
PropertyAttributes::WRITABLE | PropertyAttributes::CONFIGURABLE,
);
let enumerable: Vec<&str> = pm.enumerable_keys().map(|k| &**k).collect();
assert_eq!(enumerable, &["visible"]);
}
#[test]
fn test_enumerable_iter_follows_spec_order() {
let mut pm = PropertyMap::new();
pm.insert("z".to_string(), JsValue::Smi(1));
pm.insert("3".to_string(), JsValue::Smi(2));
pm.insert("1".to_string(), JsValue::Smi(3));
pm.insert("a".to_string(), JsValue::Smi(4));
let pairs: Vec<(&str, &JsValue)> = pm.enumerable_iter().map(|(k, v)| (&**k, v)).collect();
let keys: Vec<&str> = pairs.iter().map(|(k, _)| *k).collect();
assert_eq!(keys, &["1", "3", "z", "a"]);
}
#[test]
fn test_own_symbol_keys_returns_symbols_only() {
use crate::builtins::symbol::{symbol_create, symbol_to_property_key};
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(1));
let s1 = symbol_create(None);
let s2 = symbol_create(None);
let k1 = symbol_to_property_key(s1);
let k2 = symbol_to_property_key(s2);
pm.insert(k1, JsValue::Smi(2));
pm.insert(k2, JsValue::Smi(3));
pm.insert("b".to_string(), JsValue::Smi(4));
let syms = pm.own_symbol_keys();
assert_eq!(syms.len(), 2);
assert_eq!(syms[0], s1);
assert_eq!(syms[1], s2);
}
#[test]
fn test_remove_preserves_spec_order_v2() {
let mut pm = PropertyMap::new();
pm.insert("1".to_string(), JsValue::Smi(1));
pm.insert("0".to_string(), JsValue::Smi(2));
pm.insert("a".to_string(), JsValue::Smi(3));
pm.insert("b".to_string(), JsValue::Smi(4));
pm.remove("a");
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["0", "1", "b"]);
}
#[test]
fn test_iter_with_attrs_follows_spec_order() {
let mut pm = PropertyMap::new();
pm.insert("b".to_string(), JsValue::Smi(1));
pm.insert("10".to_string(), JsValue::Smi(2));
pm.insert("2".to_string(), JsValue::Smi(3));
pm.insert("a".to_string(), JsValue::Smi(4));
let keys: Vec<&str> = pm.iter_with_attrs().map(|(k, _, _)| &**k).collect();
assert_eq!(keys, &["2", "10", "b", "a"]);
}
#[test]
fn test_insert_with_attrs_follows_spec_order() {
let mut pm = PropertyMap::new();
pm.insert_with_attrs(
"b".to_string(),
JsValue::Smi(1),
PropertyAttributes::ENUMERABLE,
);
pm.insert_with_attrs(
"3".to_string(),
JsValue::Smi(2),
PropertyAttributes::ENUMERABLE,
);
pm.insert_with_attrs(
"1".to_string(),
JsValue::Smi(3),
PropertyAttributes::ENUMERABLE,
);
let keys: Vec<&str> = pm.keys().map(|k| &**k).collect();
assert_eq!(keys, &["1", "3", "b"]);
}
#[test]
fn test_small_maps_stay_inline_until_threshold() {
let mut pm = PropertyMap::with_capacity(SMALL_PROPERTY_LINEAR_SCAN_CAP);
for idx in 0..SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{idx}"), JsValue::Smi(idx as i32));
}
assert!(matches!(pm.index, PropertyIndex::Inline));
let last_key = format!("k{}", SMALL_PROPERTY_LINEAR_SCAN_CAP - 1);
assert_eq!(
pm.get(&last_key),
Some(&JsValue::Smi((SMALL_PROPERTY_LINEAR_SCAN_CAP - 1) as i32))
);
pm.insert(
format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}"),
JsValue::Smi(SMALL_PROPERTY_LINEAR_SCAN_CAP as i32),
);
assert!(matches!(pm.index, PropertyIndex::Map(_)));
assert_eq!(
pm.get(&format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}")),
Some(&JsValue::Smi(SMALL_PROPERTY_LINEAR_SCAN_CAP as i32))
);
}
#[test]
fn test_small_map_remove_demotes_to_inline() {
let mut pm = PropertyMap::with_capacity(SMALL_PROPERTY_LINEAR_SCAN_CAP + 1);
for idx in 0..=SMALL_PROPERTY_LINEAR_SCAN_CAP {
pm.insert(format!("k{idx}"), JsValue::Smi(idx as i32));
}
assert!(matches!(pm.index, PropertyIndex::Map(_)));
assert_eq!(
pm.remove(&format!("k{SMALL_PROPERTY_LINEAR_SCAN_CAP}")),
Some(JsValue::Smi(SMALL_PROPERTY_LINEAR_SCAN_CAP as i32))
);
assert!(matches!(pm.index, PropertyIndex::Inline));
assert_eq!(pm.get("k0"), Some(&JsValue::Smi(0)));
}
#[test]
fn test_reinitialize_same_template_fast_path() {
let mut pm = PropertyMap::new();
pm.insert("x".to_string(), JsValue::Smi(1));
pm.insert("y".to_string(), JsValue::Smi(2));
pm.insert("z".to_string(), JsValue::Smi(3));
let template = ObjectLiteralTemplate::capture(&pm).unwrap();
pm.reinitialize_from_template(&template);
assert!(Rc::ptr_eq(&pm.keys, &template.keys));
assert_eq!(pm.template_next_slot, 0);
pm.values[0] = JsValue::Smi(10);
pm.values[1] = JsValue::Smi(20);
pm.values[2] = JsValue::Smi(30);
let keys_ptr_before = Rc::as_ptr(&pm.keys);
pm.reinitialize_from_template(&template);
assert_eq!(Rc::as_ptr(&pm.keys), keys_ptr_before);
assert_eq!(pm.template_next_slot, 0);
}
#[test]
fn test_reinitialize_same_template_with_values_fast_path() {
let mut pm = PropertyMap::new();
pm.insert("a".to_string(), JsValue::Smi(0));
pm.insert("b".to_string(), JsValue::Smi(0));
let template = ObjectLiteralTemplate::capture(&pm).unwrap();
let vals = [JsValue::Smi(7), JsValue::Smi(8)];
pm.reinitialize_from_template_with_values(&template, &vals);
assert_eq!(pm.values[0], JsValue::Smi(7));
assert_eq!(pm.values[1], JsValue::Smi(8));
assert!(Rc::ptr_eq(&pm.keys, &template.keys));
let keys_ptr_before = Rc::as_ptr(&pm.keys);
let vals2 = [JsValue::Smi(42), JsValue::Smi(99)];
pm.reinitialize_from_template_with_values(&template, &vals2);
assert_eq!(Rc::as_ptr(&pm.keys), keys_ptr_before);
assert_eq!(pm.values[0], JsValue::Smi(42));
assert_eq!(pm.values[1], JsValue::Smi(99));
}
}