mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: Map/Set 支持 for-of 迭代与 entries/values/keys(内部槽方式)
This commit is contained in:
@@ -74,7 +74,7 @@ pub fn iterator_wrapper_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
||||
}
|
||||
|
||||
fn get_iterator<H: VmHost>(vm: &mut H, value: JsValue) -> Result<JsValue, JsValue> {
|
||||
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<H: VmHost>(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<H: VmHost>(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<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 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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapEntries)
|
||||
}
|
||||
|
||||
pub(crate) fn map_values_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapValues)
|
||||
}
|
||||
|
||||
pub(crate) fn map_keys_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
map_set_next_dispatch::<H>(vm, args, MapSetMode::MapKeys)
|
||||
}
|
||||
|
||||
pub(crate) fn set_values_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
map_set_next_dispatch::<H>(vm, args, MapSetMode::SetValues)
|
||||
}
|
||||
|
||||
pub(crate) fn set_entries_iter_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
map_set_next_dispatch::<H>(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<H: VmHost>(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)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ macro_rules! native_try {
|
||||
};
|
||||
}
|
||||
|
||||
type MapInner = indexmap::IndexMap<SetKey, JsValue>;
|
||||
pub(crate) type MapInner = indexmap::IndexMap<SetKey, JsValue>;
|
||||
|
||||
/// Retrieve the `IndexMap` pointer stored in a Map object's native-data slot.
|
||||
///
|
||||
@@ -201,3 +201,33 @@ pub fn map_size<H: VmHost>(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<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));
|
||||
NativeResult::Ok(crate::iterator::make_mode_iterator(
|
||||
vm,
|
||||
this_val,
|
||||
crate::iterator::map_entries_iter_next::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn map_values<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));
|
||||
NativeResult::Ok(crate::iterator::make_mode_iterator(
|
||||
vm,
|
||||
this_val,
|
||||
crate::iterator::map_values_iter_next::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn map_keys<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));
|
||||
NativeResult::Ok(crate::iterator::make_mode_iterator(
|
||||
vm,
|
||||
this_val,
|
||||
crate::iterator::map_keys_iter_next::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Hash for SetKey {
|
||||
}
|
||||
}
|
||||
|
||||
type SetInner = indexmap::IndexSet<SetKey>;
|
||||
pub(crate) type SetInner = indexmap::IndexSet<SetKey>;
|
||||
|
||||
/// Retrieve the `IndexSet` pointer stored in a Set object's native-data slot.
|
||||
///
|
||||
@@ -226,3 +226,34 @@ pub fn set_size<H: VmHost>(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<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_set_inner(vm, this_val));
|
||||
NativeResult::Ok(crate::iterator::make_mode_iterator(
|
||||
vm,
|
||||
this_val,
|
||||
crate::iterator::set_entries_iter_next::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_values<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_set_inner(vm, this_val));
|
||||
NativeResult::Ok(crate::iterator::make_mode_iterator(
|
||||
vm,
|
||||
this_val,
|
||||
crate::iterator::set_values_iter_next::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_keys<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_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::<H> as *const (),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ pub fn bind_map(core: &Arc<KernelCore>, session: &KernelSession, global: &mut Js
|
||||
("delete", oxide_builtins::map::map_delete::<crate::vm::Vm> as *const (), 1),
|
||||
("clear", oxide_builtins::map::map_clear::<crate::vm::Vm> as *const (), 0),
|
||||
("size", oxide_builtins::map::map_size::<crate::vm::Vm> as *const (), 0),
|
||||
("entries", oxide_builtins::map::map_entries::<crate::vm::Vm> as *const (), 0),
|
||||
("values", oxide_builtins::map::map_values::<crate::vm::Vm> as *const (), 0),
|
||||
("keys", oxide_builtins::map::map_keys::<crate::vm::Vm> as *const (), 0),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ pub fn bind_set(core: &Arc<KernelCore>, session: &KernelSession, global: &mut Js
|
||||
("delete", oxide_builtins::set::set_delete::<crate::vm::Vm> as *const (), 1),
|
||||
("clear", oxide_builtins::set::set_clear::<crate::vm::Vm> as *const (), 0),
|
||||
("size", oxide_builtins::set::set_size::<crate::vm::Vm> as *const (), 0),
|
||||
("entries", oxide_builtins::set::set_entries::<crate::vm::Vm> as *const (), 0),
|
||||
("values", oxide_builtins::set::set_values::<crate::vm::Vm> as *const (), 0),
|
||||
("keys", oxide_builtins::set::set_keys::<crate::vm::Vm> as *const (), 0),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
Reference in New Issue
Block a user