Browse Source

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
Marko Lahma 1 year ago
parent
commit
d23b32cb21

+ 6 - 1
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",

+ 16 - 24
Jint/Native/Set/JsSet.cs

@@ -16,6 +16,14 @@ internal sealed class JsSet : ObjectInstance
     public JsSet(Engine engine, OrderedSet<JsValue> 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 @@ internal sealed class JsSet : ObjectInstance
         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 sealed class JsSet : ObjectInstance
         _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);
 }

+ 344 - 26
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 @@ internal sealed class SetPrototype : Prototype
     {
         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 @@ internal sealed class SetPrototype : Prototype
         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 @@ internal sealed class SetPrototype : Prototype
             : 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<JsValue>(set._set._set));
+
+        if (set.Size <= otherRec.Size)
+        {
+
+            if (other is JsSet otherSet)
+            {
+                // fast path
+                var result = new HashSet<JsValue>(set._set._set, SameValueZeroComparer.Instance);
+                result.ExceptWith(otherSet._set._set);
+                return new JsSet(_engine, new OrderedSet<JsValue>(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<JsValue>(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<JsValue>(set._set._set, SameValueZeroComparer.Instance);
+                result.IntersectWith(otherSet._set._set);
+                return new JsSet(_engine, new OrderedSet<JsValue>(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<JsValue>(set._set._set, SameValueZeroComparer.Instance);
+            result.SymmetricExceptWith(otherSet._set._set);
+            return new JsSet(_engine, new OrderedSet<JsValue>(result));
+        }
+
+        var otherRec = GetSetRecord(other);
+        var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys);
+        var resultSetData = new JsSet(_engine, new OrderedSet<JsValue>(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<JsValue>(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<JsValue>(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 @@ internal sealed class SetPrototype : Prototype
             : 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 @@ internal sealed class SetPrototype : Prototype
         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 @@ internal sealed class SetPrototype : Prototype
             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 @@ internal sealed class SetPrototype : Prototype
             ExceptionHelper.ThrowTypeError(_realm);
         }
 
-        var rawSize = obj.Get("size");
+        var rawSize = obj.Get(CommonProperties.Size);
         var numSize = TypeConverter.ToNumber(rawSize);
         if (double.IsNaN(numSize))
         {

+ 7 - 1
Jint/Runtime/OrderedSet.cs

@@ -3,7 +3,13 @@ namespace Jint.Runtime;
 internal sealed class OrderedSet<T>
 {
     internal List<T> _list;
-    private HashSet<T> _set;
+    internal HashSet<T> _set;
+
+    public OrderedSet(HashSet<T> values)
+    {
+        _list = new List<T>(values);
+        _set = new HashSet<T>(values);
+    }
 
     public OrderedSet(IEqualityComparer<T> comparer)
     {

+ 1 - 0
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