feat: Map 内部表改墓碑槽模型闭合活表序位

This commit is contained in:
2026-09-24 04:49:34 +00:00
parent 834ef9e2d8
commit d8ea3d8a65
3 changed files with 268 additions and 31 deletions
+29 -9
View File
@@ -2000,37 +2000,57 @@ impl MapSetMode {
}
/// 把 Map/Set 迭代器包装器推进一步,按指定模式产出 `{value, done}` 结果。
/// 条目存放在集合 native-data 槽的 indexmap 中;(a, b) 对在任意分配之前拷出,
/// 使对 native 集合的借用不会跨越 `vm` 调用保持。
/// 条目存放在集合 native-data 槽中(Map 为槽表:空槽跳过、下标口径为"下一待检槽";
/// Set 为插入序表);(a, b) 对在任意分配之前拷出,使对 native 集合的借用不会
/// 跨越 `vm` 调用保持。
fn map_set_step<H: VmHost>(
vm: &mut H, wrapper: &mut JsObject, inner: JsValue, index_si: u32, mode: MapSetMode,
) -> JsValue {
let index = current_index(vm, wrapper, index_si);
let is_map = matches!(mode, MapSetMode::MapEntries | MapSetMode::MapValues | MapSetMode::MapKeys);
let entry: Option<(JsValue, JsValue)> = unsafe {
let (entry, next_index): (Option<(JsValue, JsValue)>, usize) = unsafe {
let obj_ptr = inner.as_js_object_ptr();
if obj_ptr.is_null() {
None
(None, 0)
} else if is_map {
let p = (*obj_ptr).native_data() as *const crate::map::MapInner;
if p.is_null() {
None
(None, 0)
} else {
(*p).get_index(index).map(|(key, value)| (key.0, *value))
// 从当前下标起扫首个活槽;命中的写回值为"下一待检槽",
// 耗尽(含 done 哨兵后的下标)恒 done。
let mut found = None;
let mut i = index;
while i < (*p).slot_count() {
match (*p).slot(i) {
Some((key, value)) => {
found = Some((key.0, value));
break;
}
None => i += 1,
}
}
match found {
Some(e) => (Some(e), i + 1),
None => (None, 0),
}
}
} else {
let p = (*obj_ptr).native_data() as *const crate::set::SetInner;
if p.is_null() {
None
(None, 0)
} else {
(*p).get_index(index).map(|elem| (elem.0, elem.0))
match (*p).get_index(index) {
Some(elem) => (Some((elem.0, elem.0)), index + 1),
None => (None, 0),
}
}
}
};
match entry {
Some((a, b)) => {
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((index + 1) as i32));
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(next_index as i32));
let value = match mode {
MapSetMode::MapEntries | MapSetMode::SetEntries => make_map_set_pair(vm, a, b),
MapSetMode::MapValues => b,
+123 -22
View File
@@ -1,3 +1,5 @@
use std::collections::HashMap;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
@@ -17,15 +19,103 @@ macro_rules! native_try {
};
}
pub(crate) type MapInner = indexmap::IndexMap<SetKey, JsValue>;
/// Map 内部槽表:`slots` 按插入序存放槽(`None` = 空槽,键已删除且未重加),
/// `index` 是活键到槽下标的辅助索引(只索引活槽)。
///
/// 语义对齐规范 `[[MapData]]` List + `~empty~` 空槽:删除原位留洞、后续槽
/// 不左移,重加/新增键追加 List 末尾。活扫(forEach、迭代器)跳空槽且每轮
/// 重读总槽数,故"删当前键后重加"在末尾再访问、"删未来键"跳过、"新增键"
/// 在末尾被访问。
///
/// 不变式:`index` 与 `slots` 活槽一一对应,remove/clear 同步除名。
pub(crate) struct MapInner {
slots: Vec<Option<(SetKey, JsValue)>>,
index: HashMap<SetKey, usize>,
}
/// 取出 Map 对象 native-data 槽中存储的 `IndexMap` 指针。
impl MapInner {
pub(crate) fn new() -> Self {
Self {
slots: Vec::new(),
index: HashMap::new(),
}
}
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self {
slots: Vec::with_capacity(capacity),
index: HashMap::with_capacity(capacity),
}
}
/// 活表条目数(规范 SetDataSize 口径,空槽不计)。
pub(crate) fn len(&self) -> usize {
self.index.len()
}
/// 总槽数(含空槽):活扫的重读口径,每轮重读以捕获迭代期追加。
pub(crate) fn slot_count(&self) -> usize {
self.slots.len()
}
/// 读槽:`None` 为空槽,`Some` 为活条目。
pub(crate) fn slot(&self, i: usize) -> Option<(SetKey, JsValue)> {
self.slots.get(i).copied().flatten()
}
/// 活条目按插入序迭代(空槽不产边、不入枚举)。
pub(crate) fn iter(&self) -> impl Iterator<Item = (SetKey, JsValue)> + '_ {
self.slots.iter().filter_map(|slot| *slot)
}
pub(crate) fn get(&self, key: &SetKey) -> Option<JsValue> {
self.index
.get(key)
.map(|&i| self.slots[i].as_ref().expect("辅助索引指向活槽").1)
}
pub(crate) fn get_mut(&mut self, key: &SetKey) -> Option<&mut JsValue> {
let i = *self.index.get(key)?;
self.slots[i].as_mut().map(|entry| &mut entry.1)
}
/// 活槽原位更值(保留原键位,如 -0/+0 区分不丢);缺失末尾追加。
pub(crate) fn insert(&mut self, key: SetKey, value: JsValue) {
match self.index.get(&key) {
Some(&i) => self.slots[i].as_mut().expect("辅助索引指向活槽").1 = value,
None => {
self.index.insert(key, self.slots.len());
self.slots.push(Some((key, value)));
}
}
}
/// 删除 = 置洞:槽原位留空槽(不左移),辅助索引同步除名。
pub(crate) fn remove(&mut self, key: &SetKey) -> bool {
let Some(i) = self.index.remove(key) else {
return false;
};
self.slots[i] = None;
true
}
pub(crate) fn contains(&self, key: &SetKey) -> bool {
self.index.contains_key(key)
}
pub(crate) fn clear(&mut self) {
self.slots.clear();
self.index.clear();
}
}
/// 取出 Map 对象 native-data 槽中存储的 `MapInner` 指针。
///
/// # 调用方维护的安全性契约
///
/// 指针在 Map `JsObject` 存活期间有效:`JsObject` 分配于当前 `Epoch` arena,
/// native builtin 执行期间不会调用 `Epoch::reset()`。持有分配的
/// `Box<IndexMap>` 由 `new_map_inner()` 创建,进程退出前不释放(生命周期与
/// `Box<MapInner>` 由 `new_map_inner()` 创建,进程退出前不释放(生命周期与
/// epoch 绑定,属有意为之)。native 调用为单线程,同一 Map 对象同时至多
/// 存在一个活 `*mut` 别名。
fn get_map_inner<H: VmHost>(vm: &mut H, this_val: JsValue) -> Result<*mut MapInner, JsValue> {
@@ -46,7 +136,7 @@ fn get_map_inner<H: VmHost>(vm: &mut H, this_val: JsValue) -> Result<*mut MapInn
));
}
// SAFETY: native_data 持有 `alloc_map` 写入的裸指针,即有效的
// 堆分配 `Box<IndexMap<SetKey, JsValue>>`;IndexMap 至多要求 8 字节对齐,
// 堆分配 `Box<MapInner>`;MapInner 至多要求 8 字节对齐,
// 全局分配器满足该要求。
let inner_ptr = map_obj.native_data() as *mut MapInner;
if inner_ptr.is_null() {
@@ -78,7 +168,7 @@ pub fn map_native_edges(obj: &JsObject) -> Vec<JsValue> {
if inner.is_null() {
return Vec::new();
}
unsafe { (*inner).iter().flat_map(|(key, value)| [key.0, *value]).collect() }
unsafe { (*inner).iter().flat_map(|(key, value)| [key.0, value]).collect() }
}
/// 克隆 Map 的 native 数据到新对象,用 `rewrite` 改写其中的对象引用
@@ -98,8 +188,8 @@ where
let mut cloned = MapInner::new();
unsafe {
for (key, value) in (*inner).iter() {
let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key };
let new_value = if value.is_object() { rewrite(*value) } else { *value };
let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { key };
let new_value = if value.is_object() { rewrite(value) } else { value };
cloned.insert(new_key, new_value);
}
}
@@ -121,8 +211,8 @@ where
unsafe {
let mut rewritten = MapInner::with_capacity((*inner).len());
for (key, value) in (*inner).iter() {
let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key };
let new_value = if value.is_object() { rewrite(*value) } else { *value };
let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { key };
let new_value = if value.is_object() { rewrite(value) } else { value };
rewritten.insert(new_key, new_value);
}
*inner = rewritten;
@@ -130,7 +220,8 @@ where
}
/// 只读核算 Map 的 native 数据字节(不释放)。
/// 与 `drop_map_native` 释放口径一致(capacity),供 GC 账目核算。
/// 槽表与辅助索引均按 capacity 核算,与 `drop_map_native` 释放口径一致,
/// 供 GC 账目核算。
pub fn map_native_size(obj: &JsObject) -> u64 {
if !obj.is_map() {
return 0;
@@ -139,7 +230,12 @@ pub fn map_native_size(obj: &JsObject) -> u64 {
if inner.is_null() {
return 0;
}
unsafe { (std::mem::size_of::<MapInner>() + (*inner).capacity() * std::mem::size_of::<(SetKey, JsValue)>()) as u64 }
unsafe {
let inner = &*inner;
(std::mem::size_of::<MapInner>()
+ inner.slots.capacity() * std::mem::size_of::<Option<(SetKey, JsValue)>>()
+ inner.index.capacity() * std::mem::size_of::<(SetKey, usize)>()) as u64
}
}
/// 释放 Map 的 native 数据(IndexMap),返回释放的字节数供泄漏统计。
@@ -264,7 +360,7 @@ pub fn map_get<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let inner = native_try!(get_map_inner(vm, this_val));
let key = vm.reg(if args.len() > 1 { args[1] } else { 0 });
let found = unsafe { (*inner).get(&SetKey(key)).copied() };
let found = unsafe { (*inner).get(&SetKey(key)) };
NativeResult::Ok(found.unwrap_or(JsValue::undefined()))
}
@@ -279,7 +375,7 @@ pub fn map_get_or_insert<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let inner = native_try!(get_map_inner(vm, this_val));
let key = vm.reg(if args.len() > 1 { args[1] } else { 0 });
let val = vm.reg(if args.len() > 2 { args[2] } else { 0 });
let found = unsafe { (*inner).get(&SetKey(key)).copied() };
let found = unsafe { (*inner).get(&SetKey(key)) };
if let Some(existing) = found {
return NativeResult::Ok(existing);
}
@@ -324,7 +420,7 @@ pub fn map_get_or_insert_computed<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeR
} else {
key
};
let found = unsafe { (*inner).get(&SetKey(key)).copied() };
let found = unsafe { (*inner).get(&SetKey(key)) };
if let Some(existing) = found {
return NativeResult::Ok(existing);
}
@@ -352,7 +448,7 @@ pub fn map_has<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let inner = native_try!(get_map_inner(vm, this_val));
let key = vm.reg(if args.len() > 1 { args[1] } else { 0 });
let found = unsafe { (*inner).contains_key(&SetKey(key)) };
let found = unsafe { (*inner).contains(&SetKey(key)) };
NativeResult::Ok(JsValue::bool(found))
}
@@ -361,8 +457,8 @@ pub fn map_delete<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let inner = native_try!(get_map_inner(vm, this_val));
let key = vm.reg(if args.len() > 1 { args[1] } else { 0 });
let removed = unsafe { (*inner).shift_remove(&SetKey(key)) };
NativeResult::Ok(JsValue::bool(removed.is_some()))
let removed = unsafe { (*inner).remove(&SetKey(key)) };
NativeResult::Ok(JsValue::bool(removed))
}
/// `Map.prototype.clear()`:清空全部键值对,返回 undefined。
@@ -376,7 +472,8 @@ pub fn map_clear<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
/// `Map.prototype.forEach(callbackfn, thisArg)`:按插入序对每个键值对调用回调,
/// 回调参数为 `(value, key, map)`。迭代期间新增的键值对也会被访问。
/// 回调参数为 `(value, key, map)`。活表扫描:空槽跳过,迭代期间新增键值对被访问,
/// 访问后删除且完成前重加的键再访问。
pub fn map_for_each<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let inner = native_try!(get_map_inner(vm, this_val));
@@ -385,13 +482,17 @@ pub fn map_for_each<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Err(crate::error::create_type_error(vm, "callback is not a function"));
}
let this_arg = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
// 按下标迭代:每次回调后重新读当前下标(支持迭代期间插入)。
// 活扫槽:每轮重读总槽数(捕获迭代期追加),空槽不回调直接推进;
// 下标口径恒为"下一待检槽",重加条目落在末尾槽由后续步进访问。
let mut index = 0usize;
loop {
let entry = unsafe { (*inner).get_index(index).map(|(key, value)| (key.0, *value)) };
let Some((key, value)) = entry else { break };
if index >= unsafe { (*inner).slot_count() } {
break;
}
let entry = unsafe { (*inner).slot(index) };
index += 1;
if let Err(err) = vm.call_function_sync(callback, this_arg, &[value, key, this_val]) {
let Some((key, value)) = entry else { continue };
if let Err(err) = vm.call_function_sync(callback, this_arg, &[value, key.0, this_val]) {
return NativeResult::Err(crate::iterator::engine_error(vm, &err));
}
}
+116
View File
@@ -399,3 +399,119 @@ fn map_get_or_insert_computed_length_name_and_descriptor() {
.unwrap();
assert!(r.as_bool());
}
// ── 活表序位语义:删除留洞、重加/新增末尾追加、空槽跳过、每轮重读槽数 ──
#[test]
fn map_for_each_delete_current_and_readd_visits_tail() {
// 删当前键+重加:原槽留洞跳过、重加条目末尾再访问,count=3、size 不变。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('a', 0); m.set('b', 1); var r = []; \
m.forEach(function (v, k) { if (r.length === 0) { m.delete('a'); m.set('a', 'x'); } r.push(k + '=' + v); }); \
r.join('|') + '|' + r.length + '|' + m.size",
)
.unwrap();
assert_eq!(str_val(&vm, r), "a=0|b=1|a=x|3|2");
}
#[test]
fn map_for_each_delete_future_key_skips() {
// 删未来键不重加:该键不访问,其后的键不被连带跳过。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('a', 0); m.set('b', 1); m.set('c', 2); var r = []; \
m.forEach(function (v, k) { if (k === 'a') m.delete('b'); r.push(k); }); \
r.join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "a,c");
}
#[test]
fn map_for_each_new_key_during_iteration_visited() {
// 迭代期新增键:末尾追加并被访问,count=初始+1。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('a', 0); m.set('b', 1); var r = []; \
m.forEach(function (v, k) { if (k === 'b') m.set('c', 2); r.push(k); }); \
r.join(',') + '|' + m.size",
)
.unwrap();
assert_eq!(str_val(&vm, r), "a,b,c|3");
}
#[test]
fn map_for_each_delete_after_visit_readd_visits_again() {
// 访问后删+完成前重加:再访问(a, b, c, a′ 形),size 不变。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('a', 0); m.set('b', 1); m.set('c', 2); var r = []; \
m.forEach(function (v, k) { if (k === 'a' && v === 0) { m.delete('a'); m.set('a', 'x'); } r.push(k + '=' + v); }); \
r.join('|') + '|' + m.size",
)
.unwrap();
assert_eq!(str_val(&vm, r), "a=0|b=1|c=2|a=x|3");
}
#[test]
fn map_for_of_delete_and_readd_visits_readded() {
// for..of 迭代期 delete(2)+set(2):重加条目末尾再访问,键序 1,2,3,2。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set(1, 10); m.set(2, 20); m.set(3, 30); var r = []; \
for (var e of m) { if (e[0] === 2 && e[1] === 20) { m.delete(2); m.set(2, 'x'); } r.push(e[0]); } \
r.join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "1,2,3,2");
}
#[test]
fn map_entries_exhausted_then_set_stays_done() {
// entries 迭代器耗尽后 set 新键不复活:后续 next() 恒 done。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set(1, 1); m.set(2, 2); var it = m.entries(); \
var a = it.next(); var b = it.next(); var c = it.next(); \
m.set(3, 3); var d = it.next(); \
a.value[0] + ',' + b.value[0] + ',' + c.done + ',' + d.done",
)
.unwrap();
assert_eq!(str_val(&vm, r), "1,2,true,true");
}
#[test]
fn map_get_or_insert_computed_second_scan_keeps_order() {
// 二扫原位覆盖保序位:键值更新不移动序位,forEach 序不变、size 不变。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('first', 'f'); \
var v = m.getOrInsertComputed('k', function () { m.set('k', 0); return 3; }); \
var ord = []; m.forEach(function (val, key) { ord.push(key); }); \
v + '/' + m.get('k') + '/' + m.size + '/' + ord.join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "3/3/2/first,k");
}
#[test]
fn map_delete_then_set_same_key_moves_to_tail() {
// delete 后 set 同键:原槽留洞、重加条目末尾追加,forEach 序 [1, 3, 2]。
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set(1, 1); m.set(2, 2); m.set(3, 3); \
m.delete(2); m.set(2, 'x'); var r = []; \
m.forEach(function (v, k) { r.push(k + ':' + v); }); r.join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "1:1,3:3,2:x");
}