From a78056f2fff7824381c05153ed4b03843402f340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Sat, 27 Jun 2026 17:49:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Map/Set=20=E6=94=AF=E6=8C=81=20for-of?= =?UTF-8?q?=20=E8=BF=AD=E4=BB=A3=E4=B8=8E=20entries/values/keys=EF=BC=88?= =?UTF-8?q?=E5=86=85=E9=83=A8=E6=A7=BD=E6=96=B9=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/iterator.rs | 150 +++++++++++++++++- crates/oxide_builtins/src/map.rs | 32 +++- crates/oxide_builtins/src/set.rs | 33 +++- crates/oxide_vm/src/bindings/bind_map.rs | 3 + crates/oxide_vm/src/bindings/bind_set.rs | 3 + .../tests/for_of_builtin_iter_tests.rs | 119 ++++++++++++++ 6 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 crates/oxide_vm/tests/for_of_builtin_iter_tests.rs diff --git a/crates/oxide_builtins/src/iterator.rs b/crates/oxide_builtins/src/iterator.rs index ffe9884..19e7862 100644 --- a/crates/oxide_builtins/src/iterator.rs +++ b/crates/oxide_builtins/src/iterator.rs @@ -74,7 +74,7 @@ pub fn iterator_wrapper_next(vm: &mut H, args: &[u8]) -> NativeResult } fn get_iterator(vm: &mut H, value: JsValue) -> Result { - if value.is_string() || is_array_value(value) { + if value.is_string() || is_array_value(value) || is_map_value(value) || is_set_value(value) { return Ok(value); } @@ -115,6 +115,14 @@ fn next_array_like(vm: &mut H, wrapper: &mut JsObject, inner: JsValue return Some(make_iter_result(vm, JsValue::undefined(), true)); } + // for-of loop default iterators: Map yields [key, value] entries, Set yields values. + if is_map_value(inner) { + return Some(map_set_step(vm, wrapper, inner, index_si, MapSetMode::MapEntries)); + } + if is_set_value(inner) { + return Some(map_set_step(vm, wrapper, inner, index_si, MapSetMode::SetValues)); + } + None } @@ -169,3 +177,143 @@ fn is_callable(value: JsValue) -> bool { let ptr = value.as_js_object_ptr(); !ptr.is_null() && unsafe { &*ptr }.is_function() } + +fn is_map_value(value: JsValue) -> bool { + if !value.is_object() { + return false; + } + let ptr = value.as_js_object_ptr(); + !ptr.is_null() && unsafe { &*ptr }.is_map() +} + +fn is_set_value(value: JsValue) -> bool { + if !value.is_object() { + return false; + } + let ptr = value.as_js_object_ptr(); + !ptr.is_null() && unsafe { &*ptr }.is_set() +} + +fn make_map_set_pair(vm: &mut H, a: JsValue, b: JsValue) -> JsValue { + let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; + let pair = vm.alloc_object(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(array_proto), + 2, + vm.epoch().bump(), + )); + // SAFETY: pair is a freshly allocated array JsObject in the current epoch. + unsafe { + (*pair).set_prop_at(0, a); + (*pair).set_prop_at(1, b); + (*pair).set_prop_count(2); + } + JsValue::from_js_object(pair) +} + +#[derive(Clone, Copy)] +enum MapSetMode { + MapEntries, + MapValues, + MapKeys, + SetEntries, + SetValues, +} + +/// Advance a Map/Set iterator wrapper one step, yielding the {value, done} result for +/// the requested mode. Entries live in the indexmap stored in the collection's +/// native-data slot; the (a, b) pair is copied out before any allocation so no borrow +/// of the native collection is held across a `vm` call. +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 obj_ptr = inner.as_js_object_ptr(); + if obj_ptr.is_null() { + None + } else if is_map { + let p = (*obj_ptr).native_data() as *const crate::map::MapInner; + if p.is_null() { + None + } else { + (*p).get_index(index).map(|(key, value)| (key.0, *value)) + } + } else { + let p = (*obj_ptr).native_data() as *const crate::set::SetInner; + if p.is_null() { + None + } else { + (*p).get_index(index).map(|elem| (elem.0, elem.0)) + } + } + }; + + match entry { + Some((a, b)) => { + vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((index + 1) as i32)); + let value = match mode { + MapSetMode::MapEntries | MapSetMode::SetEntries => make_map_set_pair(vm, a, b), + MapSetMode::MapValues => b, + MapSetMode::MapKeys | MapSetMode::SetValues => a, + }; + make_iter_result(vm, value, false) + } + None => make_iter_result(vm, JsValue::undefined(), true), + } +} + +fn map_set_next_dispatch(vm: &mut H, args: &[u8], mode: MapSetMode) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "iterator next called on non-object")); + } + let wrapper = unsafe { &mut *this_val.as_js_object_ptr() }; + let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0; + let index_si = vm.kernel_core().perm_interner().intern(INDEX_PROP).0; + let inner = match vm.ordinary_get(wrapper, inner_si, this_val) { + Ok(inner) if !inner.is_undefined() => inner, + _ => return NativeResult::Err(crate::error::create_type_error(vm, "iterator has no inner collection")), + }; + NativeResult::Ok(map_set_step(vm, wrapper, inner, index_si, mode)) +} + +pub(crate) fn map_entries_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapEntries) +} + +pub(crate) fn map_values_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapValues) +} + +pub(crate) fn map_keys_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::MapKeys) +} + +pub(crate) fn set_values_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::SetValues) +} + +pub(crate) fn set_entries_iter_next(vm: &mut H, args: &[u8]) -> NativeResult { + map_set_next_dispatch::(vm, args, MapSetMode::SetEntries) +} + +/// Build an iterator wrapper whose `next` delegates to a caller-supplied mode-specific +/// native function. Mirrors `make_iterator_for_value` but lets Map/Set prototype methods +/// pick the values/keys/entries variant. +pub(crate) fn make_mode_iterator(vm: &mut H, inner: JsValue, next_fn: *const ()) -> JsValue { + let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; + let wrapper = vm + .epoch() + .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto))); + let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0; + let index_si = vm.kernel_core().perm_interner().intern(INDEX_PROP).0; + let next_si = vm.kernel_core().perm_interner().intern("next").0; + let wrapper_obj = unsafe { &mut *wrapper }; + vm.set_or_create_prop_value(wrapper_obj, inner_si, inner); + vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(0)); + let next = make_native_function(vm, "next", next_fn, 0); + vm.set_or_create_prop_value(wrapper_obj, next_si, next); + JsValue::from_js_object(wrapper) +} diff --git a/crates/oxide_builtins/src/map.rs b/crates/oxide_builtins/src/map.rs index 49c8674..8057cf7 100644 --- a/crates/oxide_builtins/src/map.rs +++ b/crates/oxide_builtins/src/map.rs @@ -15,7 +15,7 @@ macro_rules! native_try { }; } -type MapInner = indexmap::IndexMap; +pub(crate) type MapInner = indexmap::IndexMap; /// Retrieve the `IndexMap` pointer stored in a Map object's native-data slot. /// @@ -201,3 +201,33 @@ pub fn map_size(vm: &mut H, args: &[u8]) -> NativeResult { let inner = native_try!(get_map_inner(vm, this_val)); NativeResult::Ok(JsValue::float(unsafe { (*inner).len() } as f64)) } + +pub fn map_entries(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)); + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::map_entries_iter_next:: as *const (), + )) +} + +pub fn map_values(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)); + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::map_values_iter_next:: as *const (), + )) +} + +pub fn map_keys(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)); + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::map_keys_iter_next:: as *const (), + )) +} diff --git a/crates/oxide_builtins/src/set.rs b/crates/oxide_builtins/src/set.rs index ce4519d..4439210 100644 --- a/crates/oxide_builtins/src/set.rs +++ b/crates/oxide_builtins/src/set.rs @@ -56,7 +56,7 @@ impl Hash for SetKey { } } -type SetInner = indexmap::IndexSet; +pub(crate) type SetInner = indexmap::IndexSet; /// Retrieve the `IndexSet` pointer stored in a Set object's native-data slot. /// @@ -226,3 +226,34 @@ pub fn set_size(vm: &mut H, args: &[u8]) -> NativeResult { let inner = native_try!(get_set_inner(vm, this_val)); NativeResult::Ok(JsValue::float(unsafe { (*inner).len() } as f64)) } + +pub fn set_entries(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let _inner = native_try!(get_set_inner(vm, this_val)); + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::set_entries_iter_next:: as *const (), + )) +} + +pub fn set_values(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let _inner = native_try!(get_set_inner(vm, this_val)); + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::set_values_iter_next:: as *const (), + )) +} + +pub fn set_keys(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let _inner = native_try!(get_set_inner(vm, this_val)); + // Set keys() is an alias for values() — same per-element iterator. + NativeResult::Ok(crate::iterator::make_mode_iterator( + vm, + this_val, + crate::iterator::set_values_iter_next:: as *const (), + )) +} diff --git a/crates/oxide_vm/src/bindings/bind_map.rs b/crates/oxide_vm/src/bindings/bind_map.rs index c1bb13b..9f4b3c6 100644 --- a/crates/oxide_vm/src/bindings/bind_map.rs +++ b/crates/oxide_vm/src/bindings/bind_map.rs @@ -24,6 +24,9 @@ pub fn bind_map(core: &Arc, session: &KernelSession, global: &mut Js ("delete", oxide_builtins::map::map_delete:: as *const (), 1), ("clear", oxide_builtins::map::map_clear:: as *const (), 0), ("size", oxide_builtins::map::map_size:: as *const (), 0), + ("entries", oxide_builtins::map::map_entries:: as *const (), 0), + ("values", oxide_builtins::map::map_values:: as *const (), 0), + ("keys", oxide_builtins::map::map_keys:: as *const (), 0), ], ); diff --git a/crates/oxide_vm/src/bindings/bind_set.rs b/crates/oxide_vm/src/bindings/bind_set.rs index 078d89c..4e1c1fb 100644 --- a/crates/oxide_vm/src/bindings/bind_set.rs +++ b/crates/oxide_vm/src/bindings/bind_set.rs @@ -23,6 +23,9 @@ pub fn bind_set(core: &Arc, session: &KernelSession, global: &mut Js ("delete", oxide_builtins::set::set_delete:: as *const (), 1), ("clear", oxide_builtins::set::set_clear:: as *const (), 0), ("size", oxide_builtins::set::set_size:: as *const (), 0), + ("entries", oxide_builtins::set::set_entries:: as *const (), 0), + ("values", oxide_builtins::set::set_values:: as *const (), 0), + ("keys", oxide_builtins::set::set_keys:: as *const (), 0), ], ); diff --git a/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs b/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs new file mode 100644 index 0000000..a59fc33 --- /dev/null +++ b/crates/oxide_vm/tests/for_of_builtin_iter_tests.rs @@ -0,0 +1,119 @@ +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&module) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +// Assertions stay inside JS and resolve to a boolean/number, because JsValue Display +// reveals numbers/bools but not string/object contents. + +// ---- for-of loop over Map/Set (default iterators) ---- + +#[test] +fn for_of_map_yields_key_value_entries() { + assert_eq!( + eval("var m=new Map();m.set('a',1);m.set('b',2);var r='';for(var e of m){r=r+e[0]+e[1];}r==='a1b2'"), + "true", + "for-of over Map yields [key, value] entries in insertion order" + ); +} + +#[test] +fn for_of_set_yields_values() { + assert_eq!( + eval("var s=new Set();s.add(10);s.add(20);var r=0;for(var v of s){r=r+v;}r===30"), + "true", + "for-of over Set yields values" + ); +} + +#[test] +fn for_of_empty_map_runs_zero_times() { + assert_eq!(eval("var m=new Map();var r=0;for(var e of m){r=r+1;}r===0"), "true"); +} + +#[test] +fn for_of_empty_set_runs_zero_times() { + assert_eq!(eval("var s=new Set();var r=0;for(var v of s){r=r+1;}r===0"), "true"); +} + +// ---- Map.prototype.{entries,values,keys} ---- + +#[test] +fn map_values_iterator_yields_values() { + assert_eq!( + eval("var m=new Map();m.set('a',5);var it=m.values();var r=it.next();r.value===5 && r.done===false"), + "true" + ); +} + +#[test] +fn map_entries_iterator_yields_pairs() { + assert_eq!( + eval("var m=new Map();m.set('a',5);var e=m.entries().next().value;e[0]==='a' && e[1]===5"), + "true" + ); +} + +#[test] +fn map_keys_iterator_yields_keys() { + assert_eq!(eval("var m=new Map();m.set('a',5);m.keys().next().value==='a'"), "true"); +} + +#[test] +fn map_iterator_reports_done_at_end() { + assert_eq!(eval("var m=new Map();var it=m.values();it.next().done===true"), "true"); +} + +// ---- Set.prototype.{entries,values,keys} ---- + +#[test] +fn set_values_iterator_yields_values() { + assert_eq!(eval("var s=new Set();s.add(7);s.values().next().value===7"), "true"); +} + +#[test] +fn set_entries_iterator_yields_value_value_pairs() { + assert_eq!( + eval("var s=new Set();s.add(7);var e=s.entries().next().value;e[0]===7 && e[1]===7"), + "true" + ); +} + +#[test] +fn set_keys_iterator_is_values_alias() { + assert_eq!(eval("var s=new Set();s.add(7);s.keys().next().value===7"), "true"); +} + +// ---- regression: existing iterables still work; non-iterables still throw ---- + +#[test] +fn for_of_array_still_iterates() { + assert_eq!(eval("var r=0;for(var v of [1,2,3]){r=r+v;}r===6"), "true"); +} + +#[test] +fn for_of_string_still_iterates() { + assert_eq!(eval("var r=0;for(var c of 'abc'){r=r+1;}r===3"), "true"); +} + +#[test] +fn for_of_non_iterable_throws() { + let out = eval("for(var v of 42){}"); + assert!(out.contains("TypeError"), "expected TypeError for non-iterable, got: {out}"); +}