mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
feat: Map 内部表改墓碑槽模型闭合活表序位
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user