From 26984fff36f49f8117fc2846da83aa905d831a19 Mon Sep 17 00:00:00 2001 From: Geoffrey Parrier Date: Wed, 4 Nov 2026 14:39:07 +0100 Subject: [PATCH 1/4] feat(builtins): add GetSetRecord --- .../set_objects/abstract_operations.rs | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs diff --git a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs new file mode 100644 index 000000000..d8df7639a --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs @@ -0,0 +1,127 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ## [24.2 Set Objects](https://tc39.es/ecma262/#sec-set-objects) + +use crate::{ + ecmascript::{ + Agent, BUILTIN_STRING_MEMORY, + ExceptionType::{self}, + Function, IntegerOrInfinity, JsResult, Object, Value, get, is_callable, + to_integer_or_infinity_number, to_number, + }, + engine::{Bindable, GcScope, Scopable, bindable_handle}, +}; + +/// ### [24.2.1.1 Set Records](https://tc39.es/ecma262/#sec-set-records) +/// +/// A Set Record is a Record used to encapsulate the interface of a Set or +/// similar object. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SetRecord<'a> { + pub(crate) set_object: Object<'a>, + pub(crate) size: IntegerOrInfinity, + pub(crate) has: Function<'a>, + #[expect(dead_code, reason = "used by upcoming Set methods")] + pub(crate) keys: Function<'a>, +} + +bindable_handle!(SetRecord); + +/// ### [24.2.1.2 GetSetRecord ( obj )](https://tc39.es/ecma262/#sec-getsetrecord) +/// +/// The abstract operation GetSetRecord takes argument obj (an ECMAScript +/// language value) and returns either a normal completion containing a Set +/// Record or a throw completion. +pub(crate) fn get_set_record<'a>( + agent: &mut Agent, + obj: Value, + mut gc: GcScope<'a, '_>, +) -> JsResult<'a, SetRecord<'a>> { + let obj = obj.bind(gc.nogc()); + + // 1. If obj is not an Object, throw a TypeError exception. + let Ok(obj) = Object::try_from(obj) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Value is not an object", + gc.into_nogc(), + )); + }; + // 2. Let rawSize be ? Get(obj, "size"). + let scoped_obj = obj.scope(agent, gc.nogc()); + let raw_size = get( + agent, + obj.unbind(), + BUILTIN_STRING_MEMORY.size.into(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 3. Let numberSize be ? ToNumber(rawSize). + let number_size = to_number(agent, raw_size.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 4. NOTE: If rawSize is undefined, then numberSize will be NaN. + // 5. If numberSize is NaN, throw a TypeError exception. + if number_size.is_nan(agent) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Set size is NaN", + gc.into_nogc(), + )); + }; + // 6. Let intSize be ! ToIntegerOrInfinity(numberSize). + let int_size = to_integer_or_infinity_number(agent, number_size); + // 7. If intSize < 0, throw a RangeError exception. + if int_size < 0 { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "Size is less than 0", + gc.into_nogc(), + )); + } + // 8. Let has be ? Get(obj, "has"). + let has_fn = get( + agent, + scoped_obj.get(agent), + BUILTIN_STRING_MEMORY.has.into(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 9. If IsCallable(has) is false, throw a TypeError exception. + let Some(has_fn) = is_callable(has_fn, gc.nogc()) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Has is not callable", + gc.into_nogc(), + )); + }; + let scoped_has_fn = has_fn.scope(agent, gc.nogc()); + // 10. Let keys be ? Get(obj, "keys"). + let keys_fn = get( + agent, + scoped_obj.get(agent), + BUILTIN_STRING_MEMORY.keys.into(), + gc.reborrow(), + ) + .unbind()?; + let gc = gc.into_nogc(); + // 11. If IsCallable(keys) is false, throw a TypeError exception. + let Some(keys_fn) = is_callable(keys_fn, gc) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Keys is not callable", + gc, + )); + }; + // 12. Return a new Set Record { [[SetObject]]: obj, [[Size]]: intSize, [[Has]]: has, [[Keys]]: keys }. + Ok(SetRecord { + set_object: scoped_obj.get(agent).bind(gc), + size: int_size, + has: scoped_has_fn.get(agent).bind(gc), + keys: keys_fn, + }) +} From b3bcf7bb50c1eaa99c564616920130b8b187f5dc Mon Sep 17 00:00:00 2001 From: Geoffrey Parrier Date: Wed, 4 Nov 2026 14:39:48 +0100 Subject: [PATCH 2/4] feat(builtins): add SetPrototypeIsSubsetOf --- nova_vm/src/builtin_strings | 1 + .../builtins/keyed_collections/set_objects.rs | 2 + .../set_objects/set_prototype.rs | 73 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/nova_vm/src/builtin_strings b/nova_vm/src/builtin_strings index d3e7277e5..6fce7bf2e 100644 --- a/nova_vm/src/builtin_strings +++ b/nova_vm/src/builtin_strings @@ -266,6 +266,7 @@ isNaN isPrototypeOf isSafeInteger isSealed +isSubsetOf #[cfg(feature = "array-buffer")]isView isWellFormed #[cfg(feature = "annex-b-string")]italics diff --git a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects.rs b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects.rs index d95648c04..ccdccfdd2 100644 --- a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects.rs +++ b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +mod abstract_operations; +pub(crate) use abstract_operations::*; mod set_constructor; pub(crate) use set_constructor::*; mod set_iterator_objects; diff --git a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/set_prototype.rs b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/set_prototype.rs index be4ae0de4..b9cdcae7b 100644 --- a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/set_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/set_prototype.rs @@ -11,7 +11,7 @@ use crate::{ Agent, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour, Builtin, BuiltinGetter, BuiltinIntrinsic, CollectionIteratorKind, ExceptionType, JsResult, Number, PropertyKey, Realm, Set, SetIterator, String, Value, builders::OrdinaryObjectBuilder, call_function, - canonicalize_keyed_collection_key, is_callable, same_value, + canonicalize_keyed_collection_key, get_set_record, is_callable, same_value, to_boolean, }, engine::{Bindable, GcScope, NoGcScope, Scopable}, heap::{ @@ -58,6 +58,12 @@ impl Builtin for SetPrototypeHas { const LENGTH: u8 = 1; const BEHAVIOUR: Behaviour = Behaviour::Regular(SetPrototype::has); } +struct SetPrototypeIsSubsetOf; +impl Builtin for SetPrototypeIsSubsetOf { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.isSubsetOf; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(SetPrototype::is_subset_of); +} struct SetPrototypeGetSize; impl Builtin for SetPrototypeGetSize { const NAME: String<'static> = BUILTIN_STRING_MEMORY.get_size; @@ -384,6 +390,68 @@ impl SetPrototype { Ok(found.into()) } + /// ### [24.2.4.11 Set.prototype.isSubsetOf ( other )](https://tc39.es/ecma262/#sec-set.prototype.issubsetof) + fn is_subset_of<'gc>( + agent: &mut Agent, + this_value: Value, + arguments: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let other = arguments.get(0).bind(gc.nogc()); + // 1. Let set be the this value. + let set = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(set, [[SetData]]). + let set = require_set_data_internal_slot(agent, set, gc.nogc()) + .unbind()? + .bind(gc.nogc()); + let scoped_set = set.scope(agent, gc.nogc()); + // 3. Let otherRecord be ? GetSetRecord(other). + let other_record = get_set_record(agent, other.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 4. If SetDataSize(set.[[SetData]]) > otherRecord.[[Size]], return false. + let set_size = scoped_set.get(agent).get(agent).set_data.borrow().len() as i64; + if set_size > other_record.size.into_i64() { + return Ok(false.into()); + } + // 5. Let thisSize be the number of elements in set.[[SetData]]. + let mut this_size = scoped_set.get(agent).get(agent).values.len(); + // 6. Let index be 0. + let mut index = 0; + let scoped_has = other_record.has.scope(agent, gc.nogc()); + let scoped_set_object = other_record.set_object.scope(agent, gc.nogc()); + // 7. Repeat, while index < thisSize, + while index < this_size { + // a. Let entry be set.[[SetData]][index]. + let entry = scoped_set.get(agent).get(agent).values[index]; + // b. Set index to index + 1. + index += 1; + // c. If entry is not empty, then + if let Some(entry) = entry { + // i. Let inOther be ToBoolean(? Call(otherRecord.[[Has]], otherRecord.[[SetObject]], « entry »)). + let has_result = call_function( + agent, + scoped_has.get(agent), + scoped_set_object.get(agent).into(), + Some(ArgumentsList::from_mut_slice(&mut [entry.unbind()])), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + let in_other = to_boolean(agent, has_result); + // ii. If inOther is false, return false. + if !in_other { + return Ok(false.into()); + } + // iii. NOTE: The number of elements in set.[[SetData]] may have increased during execution of otherRecord.[[Has]]. + // iv. Set thisSize to the number of elements in set.[[SetData]]. + this_size = scoped_set.get(agent).get(agent).values.len(); + } + } + // 8. Return true. + Ok(true.into()) + } + /// ### [24.2.4.14 get Set.prototype.size](https://tc39.es/ecma262/#sec-get-set.prototype.size) /// /// Set.prototype.size is an accessor property whose set accessor function @@ -431,9 +499,10 @@ impl SetPrototype { let set_prototype_values = intrinsics.set_prototype_values(); OrdinaryObjectBuilder::new_intrinsic_object(agent, realm, this) - .with_property_capacity(12) + .with_property_capacity(13) .with_prototype(object_prototype) .with_builtin_function_property::() + .with_builtin_function_property::() .with_builtin_function_property::() .with_constructor_property(set_constructor) .with_builtin_function_property::() From 01d42dc000126a1e534814d154d17bd294ea2185 Mon Sep 17 00:00:00 2001 From: Geoffrey Parrier Date: Wed, 4 Nov 2026 14:40:41 +0100 Subject: [PATCH 3/4] chore: update expectations --- tests/expectations.json | 23 ----------------------- tests/metrics.json | 4 ++-- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index d104ae2b5..238978896 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -1932,24 +1932,6 @@ "built-ins/Set/prototype/isDisjointFrom/set-like-iter-return.js": "FAIL", "built-ins/Set/prototype/isDisjointFrom/size-is-a-number.js": "FAIL", "built-ins/Set/prototype/isDisjointFrom/subclass-receiver-methods.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/allows-set-like-class.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/allows-set-like-object.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/builtins.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/compares-Map.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/compares-empty-sets.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/compares-itself.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/compares-same-sets.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/compares-sets.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/isSubsetOf.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/length.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/name.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/not-a-constructor.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/require-internal-slot.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/set-like-array.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/set-like-class-mutation.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/set-like-class-order.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/size-is-a-number.js": "FAIL", - "built-ins/Set/prototype/isSubsetOf/subclass-receiver-methods.js": "FAIL", "built-ins/Set/prototype/isSupersetOf/allows-set-like-class.js": "FAIL", "built-ins/Set/prototype/isSupersetOf/allows-set-like-object.js": "FAIL", "built-ins/Set/prototype/isSupersetOf/builtins.js": "FAIL", @@ -7421,10 +7403,6 @@ "staging/explicit-resource-management/exception-handling.js": "CRASH", "staging/explicit-resource-management/mixed-call-dispose-methods.js": "CRASH", "staging/explicit-resource-management/using-with-null-or-undefined.js": "CRASH", - "staging/set-is-subset-of-empty-index.js": "FAIL", - "staging/set-is-subset-on-set-like.js": "FAIL", - "staging/set-is-subset-table-receiver-cleared.js": "FAIL", - "staging/set-is-subset-table-transition.js": "FAIL", "staging/set-methods/set-intersect-other-is-set-like.js": "FAIL", "staging/set-methods/set-intersection-other-is-map.js": "FAIL", "staging/set-methods/set-intersection-other-is-set.js": "FAIL", @@ -7651,7 +7629,6 @@ "staging/sm/Set/difference.js": "FAIL", "staging/sm/Set/intersection.js": "FAIL", "staging/sm/Set/is-disjoint-from.js": "FAIL", - "staging/sm/Set/is-subset-of.js": "FAIL", "staging/sm/Set/is-superset-of.js": "FAIL", "staging/sm/Set/symmetric-difference.js": "FAIL", "staging/sm/Set/union.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index 64570d3a7..725ebaaeb 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,8 +1,8 @@ { "results": { "crash": 162, - "fail": 7572, - "pass": 40692, + "fail": 7549, + "pass": 40715, "skip": 5127, "timeout": 22, "unresolved": 41 From d39fb317b6877cf692f25b6c18b8d37fc4dbe9b2 Mon Sep 17 00:00:00 2001 From: Geoffrey Parrier Date: Thu, 5 Nov 2026 09:55:06 +0100 Subject: [PATCH 4/4] chore: remove comment about Set Object --- .../keyed_collections/set_objects/abstract_operations.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs index d8df7639a..96ec558ec 100644 --- a/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs +++ b/nova_vm/src/ecmascript/builtins/keyed_collections/set_objects/abstract_operations.rs @@ -2,8 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! ## [24.2 Set Objects](https://tc39.es/ecma262/#sec-set-objects) - use crate::{ ecmascript::{ Agent, BUILTIN_STRING_MEMORY,