diff --git a/crates/oxide_builtins/src/iterator.rs b/crates/oxide_builtins/src/iterator.rs index 3146f4ef..c9ad56fb 100644 --- a/crates/oxide_builtins/src/iterator.rs +++ b/crates/oxide_builtins/src/iterator.rs @@ -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( 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, diff --git a/crates/oxide_builtins/src/map.rs b/crates/oxide_builtins/src/map.rs index b0fa722e..68339084 100644 --- a/crates/oxide_builtins/src/map.rs +++ b/crates/oxide_builtins/src/map.rs @@ -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; +/// Map 内部槽表:`slots` 按插入序存放槽(`None` = 空槽,键已删除且未重加), +/// `index` 是活键到槽下标的辅助索引(只索引活槽)。 +/// +/// 语义对齐规范 `[[MapData]]` List + `~empty~` 空槽:删除原位留洞、后续槽 +/// 不左移,重加/新增键追加 List 末尾。活扫(forEach、迭代器)跳空槽且每轮 +/// 重读总槽数,故"删当前键后重加"在末尾再访问、"删未来键"跳过、"新增键" +/// 在末尾被访问。 +/// +/// 不变式:`index` 与 `slots` 活槽一一对应,remove/clear 同步除名。 +pub(crate) struct MapInner { + slots: Vec>, + index: HashMap, +} -/// 取出 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 + '_ { + self.slots.iter().filter_map(|slot| *slot) + } + + pub(crate) fn get(&self, key: &SetKey) -> Option { + 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` 由 `new_map_inner()` 创建,进程退出前不释放(生命周期与 +/// `Box` 由 `new_map_inner()` 创建,进程退出前不释放(生命周期与 /// epoch 绑定,属有意为之)。native 调用为单线程,同一 Map 对象同时至多 /// 存在一个活 `*mut` 别名。 fn get_map_inner(vm: &mut H, this_val: JsValue) -> Result<*mut MapInner, JsValue> { @@ -46,7 +136,7 @@ fn get_map_inner(vm: &mut H, this_val: JsValue) -> Result<*mut MapInn )); } // SAFETY: native_data 持有 `alloc_map` 写入的裸指针,即有效的 - // 堆分配 `Box>`;IndexMap 至多要求 8 字节对齐, + // 堆分配 `Box`;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 { 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::() + (*inner).capacity() * std::mem::size_of::<(SetKey, JsValue)>()) as u64 } + unsafe { + let inner = &*inner; + (std::mem::size_of::() + + inner.slots.capacity() * std::mem::size_of::>() + + inner.index.capacity() * std::mem::size_of::<(SetKey, usize)>()) as u64 + } } /// 释放 Map 的 native 数据(IndexMap),返回释放的字节数供泄漏统计。 @@ -264,7 +360,7 @@ pub fn map_get(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { } /// `Map.prototype.forEach(callbackfn, thisArg)`:按插入序对每个键值对调用回调, -/// 回调参数为 `(value, key, map)`。迭代期间新增的键值对也会被访问。 +/// 回调参数为 `(value, key, map)`。活表扫描:空槽跳过,迭代期间新增键值对被访问, +/// 访问后删除且完成前重加的键再访问。 pub fn map_for_each(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(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)); } } diff --git a/crates/oxide_vm/tests/builtins_map_tests.rs b/crates/oxide_vm/tests/builtins_map_tests.rs index ce37fdc1..9530b612 100644 --- a/crates/oxide_vm/tests/builtins_map_tests.rs +++ b/crates/oxide_vm/tests/builtins_map_tests.rs @@ -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"); +}