From 34985477c61050398d79743e81cc78f4e9074990 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 22 Mar 2026 14:58:54 +0100 Subject: [PATCH] Implement JS built-in Map, Set, WeakMap, WeakSet collections Add Map, Set, WeakMap, and WeakSet constructors with prototype methods to the JavaScript engine: - Map: constructor (from iterable), set/get/has/delete/clear, size property, forEach, keys/values/entries iterators. SameValueZero key equality, insertion order preserved. - Set: constructor (from iterable), add/has/delete/clear, size property, forEach, keys/values/entries iterators. SameValueZero value equality, insertion order preserved. - WeakMap: constructor, set/get/has/delete. Keys must be objects. - WeakSet: constructor, add/has/delete. Values must be objects. Internal storage uses indexed entry objects on the GC heap with deleted-slot markers for stable insertion-order iteration. 30 new integration tests covering all methods and edge cases (NaN keys, object keys, chaining, uniqueness, constructor from arrays, insertion order preservation). Also removes Map/Set from test262 unsupported features list. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/js/src/builtins.rs | 845 +++++++++++++++++++++++++++++++++++++ crates/js/src/vm.rs | 295 +++++++++++++ crates/js/tests/test262.rs | 2 - 3 files changed, 1140 insertions(+), 2 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index dc1c407..268d700 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -243,6 +243,9 @@ pub fn init_builtins(vm: &mut Vm) { // Create and register RegExp constructor. init_regexp_builtins(vm); + // Create and register Map, Set, WeakMap, WeakSet constructors. + init_map_set_builtins(vm); + // Create and register JSON object (static methods only). init_json_object(vm); @@ -3814,6 +3817,848 @@ fn regexp_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result> = const { std::cell::Cell::new(None) }; + static SET_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static WEAKMAP_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static WEAKSET_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; +} + +fn init_map_set_builtins(vm: &mut Vm) { + // ── Map ── + let mut map_proto_data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + map_proto_data.prototype = Some(proto); + } + let map_proto = vm.gc.alloc(HeapObject::Object(map_proto_data)); + init_map_prototype(&mut vm.gc, map_proto); + MAP_PROTO.with(|cell| cell.set(Some(map_proto))); + + let map_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "Map".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: map_constructor, + }), + prototype_obj: Some(map_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + vm.set_global("Map", Value::Function(map_ctor)); + + // ── Set ── + let mut set_proto_data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + set_proto_data.prototype = Some(proto); + } + let set_proto = vm.gc.alloc(HeapObject::Object(set_proto_data)); + init_set_prototype(&mut vm.gc, set_proto); + SET_PROTO.with(|cell| cell.set(Some(set_proto))); + + let set_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "Set".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: set_constructor, + }), + prototype_obj: Some(set_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + vm.set_global("Set", Value::Function(set_ctor)); + + // ── WeakMap ── + let mut wm_proto_data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + wm_proto_data.prototype = Some(proto); + } + let wm_proto = vm.gc.alloc(HeapObject::Object(wm_proto_data)); + init_weakmap_prototype(&mut vm.gc, wm_proto); + WEAKMAP_PROTO.with(|cell| cell.set(Some(wm_proto))); + + let wm_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "WeakMap".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: weakmap_constructor, + }), + prototype_obj: Some(wm_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + vm.set_global("WeakMap", Value::Function(wm_ctor)); + + // ── WeakSet ── + let mut ws_proto_data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + ws_proto_data.prototype = Some(proto); + } + let ws_proto = vm.gc.alloc(HeapObject::Object(ws_proto_data)); + init_weakset_prototype(&mut vm.gc, ws_proto); + WEAKSET_PROTO.with(|cell| cell.set(Some(ws_proto))); + + let ws_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "WeakSet".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: weakset_constructor, + }), + prototype_obj: Some(ws_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + vm.set_global("WeakSet", Value::Function(ws_ctor)); +} + +// ── Map / Set internal helpers ─────────────────────────────── + +/// Internal storage key for Map/Set entries. +/// We store entries as a hidden object with indexed key/value pairs: +/// __entries__: GcRef to an object with "0_k", "0_v", "1_k", "1_v", ... +/// __entry_count__: total slots allocated (some may be deleted) +/// __live_count__: number of non-deleted entries +/// Deleted entries have their key set to a special "__deleted__" marker. +const ENTRIES_KEY: &str = "__entries__"; +const ENTRY_COUNT_KEY: &str = "__entry_count__"; +const LIVE_COUNT_KEY: &str = "__live_count__"; +const DELETED_MARKER: &str = "__deleted__"; + +/// Create a new empty Map/Set internal storage object. +fn make_collection_obj(gc: &mut Gc, proto: Option) -> GcRef { + let entries_obj = gc.alloc(HeapObject::Object(ObjectData::new())); + let mut data = ObjectData::new(); + if let Some(p) = proto { + data.prototype = Some(p); + } + data.properties.insert( + ENTRIES_KEY.to_string(), + Property::builtin(Value::Object(entries_obj)), + ); + data.properties.insert( + ENTRY_COUNT_KEY.to_string(), + Property::builtin(Value::Number(0.0)), + ); + data.properties.insert( + LIVE_COUNT_KEY.to_string(), + Property::builtin(Value::Number(0.0)), + ); + // size is a read-only, non-enumerable property. + data.properties + .insert("size".to_string(), Property::builtin(Value::Number(0.0))); + gc.alloc(HeapObject::Object(data)) +} + +/// Get the entries object GcRef from a Map/Set object. +fn collection_entries(gc: &Gc, obj: &Value) -> Option { + let gc_ref = obj.gc_ref()?; + let heap = gc.get(gc_ref)?; + if let HeapObject::Object(data) = heap { + if let Some(prop) = data.properties.get(ENTRIES_KEY) { + return prop.value.gc_ref(); + } + } + None +} + +/// Get the entry count from a Map/Set object. +fn collection_entry_count(gc: &Gc, obj: &Value) -> usize { + let gc_ref = match obj.gc_ref() { + Some(r) => r, + None => return 0, + }; + match gc.get(gc_ref) { + Some(HeapObject::Object(data)) => data + .properties + .get(ENTRY_COUNT_KEY) + .map(|p| p.value.to_number() as usize) + .unwrap_or(0), + _ => 0, + } +} + +/// Get the live count from a Map/Set object. +fn collection_live_count(gc: &Gc, obj: &Value) -> usize { + let gc_ref = match obj.gc_ref() { + Some(r) => r, + None => return 0, + }; + match gc.get(gc_ref) { + Some(HeapObject::Object(data)) => data + .properties + .get(LIVE_COUNT_KEY) + .map(|p| p.value.to_number() as usize) + .unwrap_or(0), + _ => 0, + } +} + +/// Set the entry count on a Map/Set object and update the `size` property. +fn set_collection_count(gc: &mut Gc, obj: &Value, entry_count: usize, live: usize) { + let gc_ref = match obj.gc_ref() { + Some(r) => r, + None => return, + }; + if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) { + data.properties.insert( + ENTRY_COUNT_KEY.to_string(), + Property::builtin(Value::Number(entry_count as f64)), + ); + data.properties.insert( + LIVE_COUNT_KEY.to_string(), + Property::builtin(Value::Number(live as f64)), + ); + data.properties.insert( + "size".to_string(), + Property::builtin(Value::Number(live as f64)), + ); + } +} + +/// Get the key at index `i` in the entries object (returns None if deleted or missing). +fn entry_key_at(gc: &Gc, entries: GcRef, i: usize) -> Option { + match gc.get(entries) { + Some(HeapObject::Object(data)) => { + let k = format!("{i}_k"); + let prop = data.properties.get(&k)?; + if let Value::String(s) = &prop.value { + if s == DELETED_MARKER { + return None; + } + } + Some(prop.value.clone()) + } + _ => None, + } +} + +/// Get the value at index `i` in the entries object. +fn entry_value_at(gc: &Gc, entries: GcRef, i: usize) -> Value { + match gc.get(entries) { + Some(HeapObject::Object(data)) => { + let v = format!("{i}_v"); + data.properties + .get(&v) + .map(|p| p.value.clone()) + .unwrap_or(Value::Undefined) + } + _ => Value::Undefined, + } +} + +/// Set an entry at index `i`. +fn set_entry_at(gc: &mut Gc, entries: GcRef, i: usize, key: Value, value: Value) { + if let Some(HeapObject::Object(data)) = gc.get_mut(entries) { + data.properties + .insert(format!("{i}_k"), Property::builtin(key)); + data.properties + .insert(format!("{i}_v"), Property::builtin(value)); + } +} + +/// Mark entry at index `i` as deleted. +fn delete_entry_at(gc: &mut Gc, entries: GcRef, i: usize) { + if let Some(HeapObject::Object(data)) = gc.get_mut(entries) { + data.properties.insert( + format!("{i}_k"), + Property::builtin(Value::String(DELETED_MARKER.to_string())), + ); + data.properties.remove(&format!("{i}_v")); + } +} + +/// Find the index of a key in the entries, using SameValueZero. +fn find_key_index(gc: &Gc, entries: GcRef, count: usize, key: &Value) -> Option { + for i in 0..count { + if let Some(existing) = entry_key_at(gc, entries, i) { + if same_value_zero(&existing, key) { + return Some(i); + } + } + } + None +} + +// ── Map constructor & prototype ────────────────────────────── + +fn map_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let proto = MAP_PROTO.with(|cell| cell.get()); + let obj_ref = make_collection_obj(ctx.gc, proto); + let obj = Value::Object(obj_ref); + + // If an iterable argument is provided, add entries from it. + // We support arrays of [key, value] pairs. + if let Some(arg) = args.first() { + if !matches!(arg, Value::Undefined | Value::Null) { + if let Some(arr_ref) = arg.gc_ref() { + let len = array_length(ctx.gc, arr_ref); + for i in 0..len { + let pair = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string()); + if let Some(pair_ref) = pair.gc_ref() { + let k = get_property(ctx.gc, &Value::Object(pair_ref), "0"); + let v = get_property(ctx.gc, &Value::Object(pair_ref), "1"); + map_set_internal(ctx.gc, &obj, k, v); + } + } + } + } + } + + Ok(obj) +} + +fn map_set_internal(gc: &mut Gc, map: &Value, key: Value, value: Value) { + let entries = match collection_entries(gc, map) { + Some(e) => e, + None => return, + }; + let count = collection_entry_count(gc, map); + let live = collection_live_count(gc, map); + + // Check if key already exists. + if let Some(idx) = find_key_index(gc, entries, count, &key) { + set_entry_at(gc, entries, idx, key, value); + return; + } + + // Add new entry. + set_entry_at(gc, entries, count, key, value); + set_collection_count(gc, map, count + 1, live + 1); +} + +fn init_map_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("set", map_proto_set), + ("get", map_proto_get), + ("has", map_proto_has), + ("delete", map_proto_delete), + ("clear", map_proto_clear), + ("forEach", map_proto_for_each), + ("keys", map_proto_keys), + ("values", map_proto_values), + ("entries", map_proto_entries), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn map_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + let value = args.get(1).cloned().unwrap_or(Value::Undefined); + // Normalize -0 to +0 for key. + let key = normalize_zero(key); + map_set_internal(ctx.gc, &ctx.this, key, value); + Ok(ctx.this.clone()) +} + +fn map_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + let key = normalize_zero(key); + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Undefined), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { + return Ok(entry_value_at(ctx.gc, entries, idx)); + } + Ok(Value::Undefined) +} + +fn map_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + let key = normalize_zero(key); + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + Ok(Value::Boolean( + find_key_index(ctx.gc, entries, count, &key).is_some(), + )) +} + +fn map_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + let key = normalize_zero(key); + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let live = collection_live_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { + delete_entry_at(ctx.gc, entries, idx); + set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); + return Ok(Value::Boolean(true)); + } + Ok(Value::Boolean(false)) +} + +fn map_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result { + clear_collection(ctx.gc, &ctx.this); + Ok(Value::Undefined) +} + +/// Clear all entries from a Map/Set collection. +fn clear_collection(gc: &mut Gc, obj: &Value) { + let new_entries = gc.alloc(HeapObject::Object(ObjectData::new())); + if let Some(gc_ref) = obj.gc_ref() { + if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) { + data.properties.insert( + ENTRIES_KEY.to_string(), + Property::builtin(Value::Object(new_entries)), + ); + data.properties.insert( + ENTRY_COUNT_KEY.to_string(), + Property::builtin(Value::Number(0.0)), + ); + data.properties.insert( + LIVE_COUNT_KEY.to_string(), + Property::builtin(Value::Number(0.0)), + ); + data.properties + .insert("size".to_string(), Property::builtin(Value::Number(0.0))); + } + } +} + +fn map_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result { + let callback = match args.first() { + Some(Value::Function(r)) => *r, + _ => { + return Err(RuntimeError::type_error( + "Map.prototype.forEach requires a function argument", + )) + } + }; + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Undefined), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + // Collect entries first to avoid borrow issues. + let mut pairs = Vec::new(); + for i in 0..count { + if let Some(k) = entry_key_at(ctx.gc, entries, i) { + let v = entry_value_at(ctx.gc, entries, i); + pairs.push((k, v)); + } + } + let this_val = ctx.this.clone(); + for (k, v) in pairs { + call_native_callback(ctx.gc, callback, &[v, k, this_val.clone()])?; + } + Ok(Value::Undefined) +} + +fn map_proto_keys(args: &[Value], ctx: &mut NativeContext) -> Result { + map_proto_iter(args, ctx, IterKind::Keys) +} + +fn map_proto_values(args: &[Value], ctx: &mut NativeContext) -> Result { + map_proto_iter(args, ctx, IterKind::Values) +} + +fn map_proto_entries(args: &[Value], ctx: &mut NativeContext) -> Result { + map_proto_iter(args, ctx, IterKind::Entries) +} + +enum IterKind { + Keys, + Values, + Entries, +} + +fn map_proto_iter( + _args: &[Value], + ctx: &mut NativeContext, + kind: IterKind, +) -> Result { + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(make_value_array(ctx.gc, &[])), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let mut items = Vec::new(); + for i in 0..count { + if let Some(k) = entry_key_at(ctx.gc, entries, i) { + match kind { + IterKind::Keys => items.push(k), + IterKind::Values => { + let v = entry_value_at(ctx.gc, entries, i); + items.push(v); + } + IterKind::Entries => { + let v = entry_value_at(ctx.gc, entries, i); + let pair = make_value_array(ctx.gc, &[k, v]); + items.push(pair); + } + } + } + } + Ok(make_value_array(ctx.gc, &items)) +} + +/// Normalize -0 to +0 for Map/Set key equality. +fn normalize_zero(val: Value) -> Value { + if let Value::Number(n) = &val { + if *n == 0.0 { + return Value::Number(0.0); + } + } + val +} + +/// Helper to get a property from an object value by key (used for reading iterable pairs). +fn get_property(gc: &Gc, obj: &Value, key: &str) -> Value { + let gc_ref = match obj.gc_ref() { + Some(r) => r, + None => return Value::Undefined, + }; + match gc.get(gc_ref) { + Some(HeapObject::Object(data)) => data + .properties + .get(key) + .map(|p| p.value.clone()) + .unwrap_or(Value::Undefined), + _ => Value::Undefined, + } +} + +/// Call a native callback function (for forEach). +fn call_native_callback( + gc: &mut Gc, + func_ref: GcRef, + args: &[Value], +) -> Result { + match gc.get(func_ref) { + Some(HeapObject::Function(fdata)) => match &fdata.kind { + FunctionKind::Native(native) => { + let cb = native.callback; + let mut ctx = NativeContext { + gc, + this: Value::Undefined, + }; + cb(args, &mut ctx) + } + FunctionKind::Bytecode(_) => Err(RuntimeError::type_error( + "bytecode callbacks in forEach not yet supported", + )), + }, + _ => Err(RuntimeError::type_error("not a function")), + } +} + +// ── Set constructor & prototype ────────────────────────────── + +fn set_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let proto = SET_PROTO.with(|cell| cell.get()); + let obj_ref = make_collection_obj(ctx.gc, proto); + let obj = Value::Object(obj_ref); + + // If an iterable argument is provided, add values from it. + if let Some(arg) = args.first() { + if !matches!(arg, Value::Undefined | Value::Null) { + if let Some(arr_ref) = arg.gc_ref() { + let len = array_length(ctx.gc, arr_ref); + for i in 0..len { + let v = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string()); + set_add_internal(ctx.gc, &obj, v); + } + } + } + } + + Ok(obj) +} + +fn set_add_internal(gc: &mut Gc, set: &Value, value: Value) { + let entries = match collection_entries(gc, set) { + Some(e) => e, + None => return, + }; + let count = collection_entry_count(gc, set); + let live = collection_live_count(gc, set); + + // Check if value already exists. + if find_key_index(gc, entries, count, &value).is_some() { + return; + } + + // Add new entry (value stored as key, value slot unused). + set_entry_at(gc, entries, count, value, Value::Undefined); + set_collection_count(gc, set, count + 1, live + 1); +} + +fn init_set_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("add", set_proto_add), + ("has", set_proto_has), + ("delete", set_proto_delete), + ("clear", set_proto_clear), + ("forEach", set_proto_for_each), + ("keys", set_proto_keys), + ("values", set_proto_values), + ("entries", set_proto_entries), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn set_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + let value = normalize_zero(value); + set_add_internal(ctx.gc, &ctx.this, value); + Ok(ctx.this.clone()) +} + +fn set_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + let value = normalize_zero(value); + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + Ok(Value::Boolean( + find_key_index(ctx.gc, entries, count, &value).is_some(), + )) +} + +fn set_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + let value = normalize_zero(value); + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let live = collection_live_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) { + delete_entry_at(ctx.gc, entries, idx); + set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); + return Ok(Value::Boolean(true)); + } + Ok(Value::Boolean(false)) +} + +fn set_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result { + clear_collection(ctx.gc, &ctx.this); + Ok(Value::Undefined) +} + +fn set_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result { + let callback = match args.first() { + Some(Value::Function(r)) => *r, + _ => { + return Err(RuntimeError::type_error( + "Set.prototype.forEach requires a function argument", + )) + } + }; + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Undefined), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let mut values = Vec::new(); + for i in 0..count { + if let Some(k) = entry_key_at(ctx.gc, entries, i) { + values.push(k); + } + } + let this_val = ctx.this.clone(); + for v in values { + call_native_callback(ctx.gc, callback, &[v.clone(), v, this_val.clone()])?; + } + Ok(Value::Undefined) +} + +fn set_proto_keys(_args: &[Value], ctx: &mut NativeContext) -> Result { + set_proto_values(_args, ctx) +} + +fn set_proto_values(_args: &[Value], ctx: &mut NativeContext) -> Result { + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(make_value_array(ctx.gc, &[])), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let mut items = Vec::new(); + for i in 0..count { + if let Some(k) = entry_key_at(ctx.gc, entries, i) { + items.push(k); + } + } + Ok(make_value_array(ctx.gc, &items)) +} + +fn set_proto_entries(_args: &[Value], ctx: &mut NativeContext) -> Result { + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(make_value_array(ctx.gc, &[])), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let mut items = Vec::new(); + for i in 0..count { + if let Some(k) = entry_key_at(ctx.gc, entries, i) { + let pair = make_value_array(ctx.gc, &[k.clone(), k]); + items.push(pair); + } + } + Ok(make_value_array(ctx.gc, &items)) +} + +// ── WeakMap constructor & prototype ────────────────────────── + +fn weakmap_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result { + let proto = WEAKMAP_PROTO.with(|cell| cell.get()); + let obj_ref = make_collection_obj(ctx.gc, proto); + Ok(Value::Object(obj_ref)) +} + +fn is_object_value(val: &Value) -> bool { + matches!(val, Value::Object(_) | Value::Function(_)) +} + +fn init_weakmap_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("set", weakmap_proto_set), + ("get", weakmap_proto_get), + ("has", weakmap_proto_has), + ("delete", weakmap_proto_delete), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn weakmap_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&key) { + return Err(RuntimeError::type_error("WeakMap key must be an object")); + } + let value = args.get(1).cloned().unwrap_or(Value::Undefined); + map_set_internal(ctx.gc, &ctx.this, key, value); + Ok(ctx.this.clone()) +} + +fn weakmap_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&key) { + return Ok(Value::Undefined); + } + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Undefined), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { + return Ok(entry_value_at(ctx.gc, entries, idx)); + } + Ok(Value::Undefined) +} + +fn weakmap_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&key) { + return Ok(Value::Boolean(false)); + } + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + Ok(Value::Boolean( + find_key_index(ctx.gc, entries, count, &key).is_some(), + )) +} + +fn weakmap_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&key) { + return Ok(Value::Boolean(false)); + } + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let live = collection_live_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { + delete_entry_at(ctx.gc, entries, idx); + set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); + return Ok(Value::Boolean(true)); + } + Ok(Value::Boolean(false)) +} + +// ── WeakSet constructor & prototype ────────────────────────── + +fn weakset_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result { + let proto = WEAKSET_PROTO.with(|cell| cell.get()); + let obj_ref = make_collection_obj(ctx.gc, proto); + Ok(Value::Object(obj_ref)) +} + +fn init_weakset_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("add", weakset_proto_add), + ("has", weakset_proto_has), + ("delete", weakset_proto_delete), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn weakset_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&value) { + return Err(RuntimeError::type_error("WeakSet value must be an object")); + } + set_add_internal(ctx.gc, &ctx.this, value); + Ok(ctx.this.clone()) +} + +fn weakset_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&value) { + return Ok(Value::Boolean(false)); + } + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + Ok(Value::Boolean( + find_key_index(ctx.gc, entries, count, &value).is_some(), + )) +} + +fn weakset_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + if !is_object_value(&value) { + return Ok(Value::Boolean(false)); + } + let entries = match collection_entries(ctx.gc, &ctx.this) { + Some(e) => e, + None => return Ok(Value::Boolean(false)), + }; + let count = collection_entry_count(ctx.gc, &ctx.this); + let live = collection_live_count(ctx.gc, &ctx.this); + if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) { + delete_entry_at(ctx.gc, entries, idx); + set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); + return Ok(Value::Boolean(true)); + } + Ok(Value::Boolean(false)) +} + // ── JSON object ────────────────────────────────────────────── fn init_json_object(vm: &mut Vm) { diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index a5484d4..ffbd6c4 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -4521,4 +4521,299 @@ mod tests { v => panic!("expected 'xbx', got {v:?}"), } } + + // ── Map tests ───────────────────────────────────────────── + + #[test] + fn test_map_basic() { + match eval("var m = new Map(); m.set('a', 1); m.get('a')").unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_map_size() { + match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.size").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_map_has() { + match eval("var m = new Map(); m.set('x', 10); m.has('x')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("var m = new Map(); m.has('x')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_map_delete() { + match eval("var m = new Map(); m.set('a', 1); m['delete']('a'); m.has('a')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + match eval("var m = new Map(); m.set('a', 1); m['delete']('a'); m.size").unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_map_clear() { + match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.clear(); m.size").unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_map_overwrite() { + match eval("var m = new Map(); m.set('a', 1); m.set('a', 2); m.get('a')").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + // Size should still be 1 after overwriting. + match eval("var m = new Map(); m.set('a', 1); m.set('a', 2); m.size").unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_map_get_missing() { + match eval("var m = new Map(); m.get('missing')").unwrap() { + Value::Undefined => {} + v => panic!("expected undefined, got {v:?}"), + } + } + + #[test] + fn test_map_chaining() { + // set() returns the Map for chaining. + match eval("var m = new Map(); m.set('a', 1).set('b', 2); m.size").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_map_nan_key() { + // NaN === NaN for Map keys (SameValueZero). + match eval("var m = new Map(); m.set(NaN, 'nan'); m.get(NaN)").unwrap() { + Value::String(s) => assert_eq!(s, "nan"), + v => panic!("expected 'nan', got {v:?}"), + } + } + + #[test] + fn test_map_object_key() { + match eval("var m = new Map(); var o = {}; m.set(o, 'val'); m.get(o)").unwrap() { + Value::String(s) => assert_eq!(s, "val"), + v => panic!("expected 'val', got {v:?}"), + } + } + + #[test] + fn test_map_constructor_with_pairs() { + match eval("var m = new Map([['a', 1], ['b', 2]]); m.get('b')").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_map_keys_values_entries() { + match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.keys().length").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.values()[1]").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + match eval("var m = new Map(); m.set('a', 1); m.entries()[0][0]").unwrap() { + Value::String(s) => assert_eq!(s, "a"), + v => panic!("expected 'a', got {v:?}"), + } + } + + #[test] + fn test_map_insertion_order() { + match eval("var m = new Map(); m.set('c', 3); m.set('a', 1); m.set('b', 2); m.keys()[0]") + .unwrap() + { + Value::String(s) => assert_eq!(s, "c"), + v => panic!("expected 'c', got {v:?}"), + } + } + + // ── Set tests ───────────────────────────────────────────── + + #[test] + fn test_set_basic() { + match eval("var s = new Set(); s.add(1); s.add(2); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_set_has() { + match eval("var s = new Set(); s.add(42); s.has(42)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("var s = new Set(); s.has(42)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_set_delete() { + match eval("var s = new Set(); s.add(1); s['delete'](1); s.has(1)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + match eval("var s = new Set(); s.add(1); s['delete'](1); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_set_clear() { + match eval("var s = new Set(); s.add(1); s.add(2); s.clear(); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_set_uniqueness() { + match eval("var s = new Set(); s.add(1); s.add(1); s.add(1); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_set_chaining() { + match eval("var s = new Set(); s.add(1).add(2).add(3); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_set_nan() { + match eval("var s = new Set(); s.add(NaN); s.add(NaN); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + match eval("var s = new Set(); s.add(NaN); s.has(NaN)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_set_constructor_from_array() { + match eval("var s = new Set([1, 2, 3, 2, 1]); s.size").unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_set_values() { + match eval("var s = new Set(); s.add('a'); s.add('b'); s.values().length").unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_set_entries() { + // Set.entries() returns [value, value] pairs. + match eval("var s = new Set(); s.add('x'); s.entries()[0][0]").unwrap() { + Value::String(s) => assert_eq!(s, "x"), + v => panic!("expected 'x', got {v:?}"), + } + match eval("var s = new Set(); s.add('x'); s.entries()[0][1]").unwrap() { + Value::String(s) => assert_eq!(s, "x"), + v => panic!("expected 'x', got {v:?}"), + } + } + + #[test] + fn test_set_insertion_order() { + match eval("var s = new Set(); s.add('c'); s.add('a'); s.add('b'); s.values()[0]").unwrap() + { + Value::String(s) => assert_eq!(s, "c"), + v => panic!("expected 'c', got {v:?}"), + } + } + + // ── WeakMap tests ───────────────────────────────────────── + + #[test] + fn test_weakmap_basic() { + match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 'val'); wm.get(o)").unwrap() { + Value::String(s) => assert_eq!(s, "val"), + v => panic!("expected 'val', got {v:?}"), + } + } + + #[test] + fn test_weakmap_has_delete() { + match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 1); wm.has(o)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 1); wm['delete'](o); wm.has(o)") + .unwrap() + { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_weakmap_rejects_primitive_key() { + assert!(eval("var wm = new WeakMap(); wm.set('str', 1)").is_err()); + assert!(eval("var wm = new WeakMap(); wm.set(42, 1)").is_err()); + } + + // ── WeakSet tests ───────────────────────────────────────── + + #[test] + fn test_weakset_basic() { + match eval("var ws = new WeakSet(); var o = {}; ws.add(o); ws.has(o)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_weakset_delete() { + match eval("var ws = new WeakSet(); var o = {}; ws.add(o); ws['delete'](o); ws.has(o)") + .unwrap() + { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_weakset_rejects_primitive() { + assert!(eval("var ws = new WeakSet(); ws.add('str')").is_err()); + assert!(eval("var ws = new WeakSet(); ws.add(42)").is_err()); + } } diff --git a/crates/js/tests/test262.rs b/crates/js/tests/test262.rs index 537fd30..750fd1b 100644 --- a/crates/js/tests/test262.rs +++ b/crates/js/tests/test262.rs @@ -114,8 +114,6 @@ const UNSUPPORTED_FEATURES: &[&str] = &[ "Int8Array", "Int16Array", "Int32Array", - "Map", - "Set", "SharedArrayBuffer", "TypedArray", "Uint8Array", -- 2.51.2