From 2fcd163c796661a2d2eb21a42893600a8cec619f Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 14 Jan 2024 14:33:23 +0200 Subject: [PATCH] Implement Set Methods for JavaScript (#1741) * Implement Set.difference * implement Set.symmetricDifference * implement Set.isSubsetOf * Implement Set.isSupersetOf * Implement Set.intersection * Implement Set.isDisjointFrom * update README --- .../Test262Harness.settings.json | 7 +- Jint/Native/Set/JsSet.cs | 40 +- Jint/Native/Set/SetPrototype.cs | 370 ++++++++++++++++-- Jint/Runtime/OrderedSet.cs | 8 +- README.md | 1 + 5 files changed, 374 insertions(+), 52 deletions(-) diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 8a7382fdc7..9a84905da8 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -1,5 +1,5 @@ { - "SuiteGitSha": "2060494f280ba89d71a0f51d4ff171bafe476a05", + "SuiteGitSha": "a1ba783ca340e4bf3d80b5f5e11fa54f2ee5f1ef", //"SuiteDirectory": "//mnt/c/work/test262", "TargetPath": "./Generated", "Namespace": "Jint.Tests.Test262", @@ -84,6 +84,11 @@ "language/expressions/class/elements/*-generator-method-*.js", "built-ins/Set/prototype/union/allows-set-like-class.js", "built-ins/Set/prototype/union/allows-set-like-object.js", + "built-ins/Set/prototype/symmetricDifference/allows-set-like-class.js", + "built-ins/Set/prototype/symmetricDifference/allows-set-like-object.js", + "built-ins/Set/prototype/isSupersetOf/allows-set-like-class.js", + "built-ins/Set/prototype/isSupersetOf/allows-set-like-object.js", + "built-ins/Set/prototype/isSupersetOf/set-like-class-mutation.js", // generators not implemented "built-ins/Object/prototype/toString/proxy-function.js", diff --git a/Jint/Native/Set/JsSet.cs b/Jint/Native/Set/JsSet.cs index ea95fccecb..09b46d4ec4 100644 --- a/Jint/Native/Set/JsSet.cs +++ b/Jint/Native/Set/JsSet.cs @@ -16,6 +16,14 @@ internal sealed class JsSet : ObjectInstance public JsSet(Engine engine, OrderedSet set) : base(engine) { _set = set; + _prototype = _engine.Realm.Intrinsics.Set.PrototypeObject; + } + + public int Size => _set.Count; + + public JsValue? this[int index] + { + get { return index < _set._list.Count ? _set._list[index] : null; } } public override PropertyDescriptor GetOwnProperty(JsValue property) @@ -39,25 +47,15 @@ protected override bool TryGetProperty(JsValue property, [NotNullWhen(true)] out return base.TryGetProperty(property, out descriptor); } - internal void Add(JsValue value) - { - _set.Add(value); - } + internal void Add(JsValue value) => _set.Add(value); - internal void Clear() - { - _set.Clear(); - } + internal void Remove(JsValue value) => _set.Remove(value); - internal bool Has(JsValue key) - { - return _set.Contains(key); - } + internal void Clear() => _set.Clear(); - internal bool SetDelete(JsValue key) - { - return _set.Remove(key); - } + internal bool Has(JsValue key) => _set.Contains(key); + + internal bool SetDelete(JsValue key) => _set.Remove(key); internal void ForEach(ICallable callable, JsValue thisArg) { @@ -75,13 +73,7 @@ internal void ForEach(ICallable callable, JsValue thisArg) _engine._jsValueArrayPool.ReturnArray(args); } - internal ObjectInstance Entries() - { - return _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructEntryIterator(this); - } + internal ObjectInstance Entries() => _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructEntryIterator(this); - internal ObjectInstance Values() - { - return _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructValueIterator(this); - } + internal ObjectInstance Values() => _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructValueIterator(this); } diff --git a/Jint/Native/Set/SetPrototype.cs b/Jint/Native/Set/SetPrototype.cs index 3e0278bb3f..d70e03fd6a 100644 --- a/Jint/Native/Set/SetPrototype.cs +++ b/Jint/Native/Set/SetPrototype.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue - using Jint.Collections; using Jint.Native.Object; using Jint.Native.Symbol; @@ -30,30 +28,36 @@ protected override void Initialize() { var properties = new PropertyDictionary(12, checkExistingKeys: false) { - ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable), - ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), - ["add"] = new PropertyDescriptor(new ClrFunction(Engine, "add", Add, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["clear"] = new PropertyDescriptor(new ClrFunction(Engine, "clear", Clear, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["delete"] = new PropertyDescriptor(new ClrFunction(Engine, "delete", Delete, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["entries"] = new PropertyDescriptor(new ClrFunction(Engine, "entries", Entries, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["forEach"] = new PropertyDescriptor(new ClrFunction(Engine, "forEach", ForEach, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["has"] = new PropertyDescriptor(new ClrFunction(Engine, "has", Has, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["keys"] = new PropertyDescriptor(new ClrFunction(Engine, "keys", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), - ["values"] = new PropertyDescriptor(new ClrFunction(Engine, "values", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["length"] = new(0, PropertyFlag.Configurable), + ["constructor"] = new(_constructor, PropertyFlag.NonEnumerable), + ["add"] = new(new ClrFunction(Engine, "add", Add, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["clear"] = new(new ClrFunction(Engine, "clear", Clear, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["delete"] = new(new ClrFunction(Engine, "delete", Delete, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["difference"] = new(new ClrFunction(Engine, "difference", Difference, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["entries"] = new(new ClrFunction(Engine, "entries", Entries, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["forEach"] = new(new ClrFunction(Engine, "forEach", ForEach, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["has"] = new(new ClrFunction(Engine, "has", Has, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["intersection"] = new(new ClrFunction(Engine, "intersection", Intersection, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["isDisjointFrom"] = new(new ClrFunction(Engine, "isDisjointFrom", IsDisjointFrom, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["isSubsetOf"] = new(new ClrFunction(Engine, "isSubsetOf", IsSubsetOf, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["isSupersetOf"] = new(new ClrFunction(Engine, "isSupersetOf", IsSupersetOf, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["keys"] = new(new ClrFunction(Engine, "keys", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["values"] = new(new ClrFunction(Engine, "values", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), ["size"] = new GetSetPropertyDescriptor(get: new ClrFunction(Engine, "get size", Size, 0, PropertyFlag.Configurable), set: null, PropertyFlag.Configurable), - ["union"] = new PropertyDescriptor(new ClrFunction(Engine, "union", Union, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable) + ["symmetricDifference"] = new(new ClrFunction(Engine, "symmetricDifference", SymmetricDifference, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["union"] = new(new ClrFunction(Engine, "union", Union, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable) }; SetProperties(properties); var symbols = new SymbolDictionary(2) { - [GlobalSymbolRegistry.Iterator] = new PropertyDescriptor(new ClrFunction(Engine, "iterator", Values, 1, PropertyFlag.Configurable), true, false, true), - [GlobalSymbolRegistry.ToStringTag] = new PropertyDescriptor("Set", false, false, true) + [GlobalSymbolRegistry.Iterator] = new(new ClrFunction(Engine, "iterator", Values, 1, PropertyFlag.Configurable), true, false, true), + [GlobalSymbolRegistry.ToStringTag] = new("Set", false, false, true) }; SetSymbols(symbols); } - private JsValue Size(JsValue thisObject, JsValue[] arguments) + private JsNumber Size(JsValue thisObject, JsValue[] arguments) { AssertSetInstance(thisObject); return JsNumber.Create(0); @@ -78,7 +82,7 @@ private JsValue Clear(JsValue thisObject, JsValue[] arguments) return Undefined; } - private JsValue Delete(JsValue thisObject, JsValue[] arguments) + private JsBoolean Delete(JsValue thisObject, JsValue[] arguments) { var set = AssertSetInstance(thisObject); return set.SetDelete(arguments.At(0)) @@ -86,7 +90,325 @@ private JsValue Delete(JsValue thisObject, JsValue[] arguments) : JsBoolean.False; } - private JsValue Has(JsValue thisObject, JsValue[] arguments) + private JsSet Difference(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + var otherRec = GetSetRecord(other); + var resultSetData = new JsSet(_engine, new OrderedSet(set._set._set)); + + if (set.Size <= otherRec.Size) + { + + if (other is JsSet otherSet) + { + // fast path + var result = new HashSet(set._set._set, SameValueZeroComparer.Instance); + result.ExceptWith(otherSet._set._set); + return new JsSet(_engine, new OrderedSet(result)); + } + + var index = 0; + var args = new JsValue[1]; + while (index < set.Size) + { + var e = resultSetData[index]; + if (e is not null) + { + args[0] = e; + var inOther = TypeConverter.ToBoolean(otherRec.Has.Call(otherRec.Set, args)); + if (inOther) + { + resultSetData.Remove(e); + index--; + } + } + + index++; + } + + return resultSetData; + } + + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + while (true) + { + if (!keysIter.TryIteratorStep(out var next)) + { + break; + } + + var nextValue = next.Get(CommonProperties.Value); + if (nextValue == JsNumber.NegativeZero) + { + nextValue = JsNumber.PositiveZero; + } + + resultSetData.Remove(nextValue); + } + + return resultSetData; + } + + private JsBoolean IsDisjointFrom(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + var otherRec = GetSetRecord(other); + var resultSetData = new JsSet(_engine, new OrderedSet(set._set._set)); + + if (set.Size <= otherRec.Size) + { + if (other is JsSet otherSet) + { + // fast path + return set._set._set.Overlaps(otherSet._set._set) ? JsBoolean.False : JsBoolean.True; + } + + var index = 0; + var args = new JsValue[1]; + while (index < set.Size) + { + var e = resultSetData[index]; + index++; + if (e is not null) + { + args[0] = e; + var inOther = TypeConverter.ToBoolean(otherRec.Has.Call(otherRec.Set, args)); + if (inOther) + { + return JsBoolean.False; + } + } + } + + return JsBoolean.True; + } + + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + while (true) + { + if (!keysIter.TryIteratorStep(out var next)) + { + break; + } + + var nextValue = next.Get(CommonProperties.Value); + if (set.Has(nextValue)) + { + keysIter.Close(CompletionType.Normal); + return JsBoolean.False; + } + } + + return JsBoolean.True; + } + + + private JsSet Intersection(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + + var otherRec = GetSetRecord(other); + var resultSetData = new JsSet(_engine); + var thisSize = set.Size; + + if (thisSize <= otherRec.Size) + { + if (other is JsSet otherSet) + { + // fast path + var result = new HashSet(set._set._set, SameValueZeroComparer.Instance); + result.IntersectWith(otherSet._set._set); + return new JsSet(_engine, new OrderedSet(result)); + } + + var index = 0; + var args = new JsValue[1]; + while (index < thisSize) + { + var e = set[index]; + index++; + if (e is not null) + { + args[0] = e; + var inOther = TypeConverter.ToBoolean(otherRec.Has.Call(otherRec.Set, args)); + if (inOther) + { + var alreadyInResult = resultSetData.Has(e); + if (!alreadyInResult) + { + resultSetData.Add(e); + } + } + thisSize = set.Size; + } + } + + return resultSetData; + } + + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + while (true) + { + if (!keysIter.TryIteratorStep(out var next)) + { + break; + } + + var nextValue = next.Get(CommonProperties.Value); + if (nextValue == JsNumber.NegativeZero) + { + nextValue = JsNumber.PositiveZero; + } + + var alreadyInResult = resultSetData.Has(nextValue); + var inThis = set.Has(nextValue); + if (!alreadyInResult && inThis) + { + resultSetData.Add(nextValue); + } + } + + return resultSetData; + } + + private JsSet SymmetricDifference(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + + if (other is JsSet otherSet) + { + // fast path + var result = new HashSet(set._set._set, SameValueZeroComparer.Instance); + result.SymmetricExceptWith(otherSet._set._set); + return new JsSet(_engine, new OrderedSet(result)); + } + + var otherRec = GetSetRecord(other); + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + var resultSetData = new JsSet(_engine, new OrderedSet(set._set._set)); + while (true) + { + if (!keysIter.TryIteratorStep(out var next)) + { + break; + } + + var nextValue = next.Get(CommonProperties.Value); + if (nextValue == JsNumber.NegativeZero) + { + nextValue = JsNumber.PositiveZero; + } + + var inResult = resultSetData.Has(nextValue); + if (set.Has(nextValue)) + { + if (inResult) + { + resultSetData.Remove(nextValue); + } + } + else + { + if (!inResult) + { + resultSetData.Add(nextValue); + } + } + } + + return resultSetData; + } + + private JsBoolean IsSubsetOf(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + + if (other is JsSet otherSet) + { + // fast path + return set._set._set.IsSubsetOf(otherSet._set._set) ? JsBoolean.True : JsBoolean.False; + } + + var otherRec = GetSetRecord(other); + var resultSetData = new JsSet(_engine, new OrderedSet(set._set._set)); + var thisSize = set.Size; + + if (thisSize > otherRec.Size) + { + return JsBoolean.False; + } + + if (thisSize <= otherRec.Size) + { + var index = 0; + var args = new JsValue[1]; + while (index < thisSize) + { + var e = resultSetData[index]; + if (e is not null) + { + args[0] = e; + var inOther = TypeConverter.ToBoolean(otherRec.Has.Call(otherRec.Set, args)); + if (!inOther) + { + return JsBoolean.False; + } + } + + thisSize = set.Size; + index++; + } + } + + return JsBoolean.True; + } + + private JsBoolean IsSupersetOf(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + + if (other is JsSet otherSet) + { + // fast path + var result = new HashSet(set._set._set, SameValueZeroComparer.Instance); + return result.IsSupersetOf(otherSet._set._set) ? JsBoolean.True : JsBoolean.False; + } + + var thisSize = set.Size; + var otherRec = GetSetRecord(other); + + if (thisSize < otherRec.Size) + { + return JsBoolean.False; + } + + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + while (true) + { + if (!keysIter.TryIteratorStep(out var next)) + { + break; + } + + var nextValue = next.Get(CommonProperties.Value); + if (!set.Has(nextValue)) + { + keysIter.Close(CompletionType.Normal); + return JsBoolean.False; + } + } + + return JsBoolean.True; + } + + + private JsBoolean Has(JsValue thisObject, JsValue[] arguments) { var set = AssertSetInstance(thisObject); return set.Has(arguments.At(0)) @@ -94,7 +416,7 @@ private JsValue Has(JsValue thisObject, JsValue[] arguments) : JsBoolean.False; } - private JsValue Entries(JsValue thisObject, JsValue[] arguments) + private ObjectInstance Entries(JsValue thisObject, JsValue[] arguments) { var set = AssertSetInstance(thisObject); return set.Entries(); @@ -113,7 +435,7 @@ private JsValue ForEach(JsValue thisObject, JsValue[] arguments) return Undefined; } - private JsValue Union(JsValue thisObject, JsValue[] arguments) + private JsSet Union(JsValue thisObject, JsValue[] arguments) { var set = AssertSetInstance(thisObject); var other = arguments.At(0); @@ -130,12 +452,8 @@ private JsValue Union(JsValue thisObject, JsValue[] arguments) resultSetData.Add(nextValue); } - var result = new JsSet(_engine, resultSetData) - { - _prototype = _engine.Realm.Intrinsics.Set.PrototypeObject - }; + var result = new JsSet(_engine, resultSetData); return result; - } private readonly record struct SetRecord(JsValue Set, double Size, ICallable Has, ICallable Keys); @@ -147,7 +465,7 @@ private SetRecord GetSetRecord(JsValue obj) ExceptionHelper.ThrowTypeError(_realm); } - var rawSize = obj.Get("size"); + var rawSize = obj.Get(CommonProperties.Size); var numSize = TypeConverter.ToNumber(rawSize); if (double.IsNaN(numSize)) { diff --git a/Jint/Runtime/OrderedSet.cs b/Jint/Runtime/OrderedSet.cs index 9db1ca1220..da1e93287d 100644 --- a/Jint/Runtime/OrderedSet.cs +++ b/Jint/Runtime/OrderedSet.cs @@ -3,7 +3,13 @@ namespace Jint.Runtime; internal sealed class OrderedSet { internal List _list; - private HashSet _set; + internal HashSet _set; + + public OrderedSet(HashSet values) + { + _list = new List(values); + _set = new HashSet(values); + } public OrderedSet(IEqualityComparer comparer) { diff --git a/README.md b/README.md index 855d802ab0..cb1b2e9a95 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Following features are supported in version 3.x. - ✔ Import attributes - ✔ JSON modules - ✔ `Promise.withResolvers` +- ✔ Set methods (`intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) - ✔ Resizable and growable ArrayBuffers - ✔ ShadowRealm