Browse Source

Implement WeakMap and WeakSet (#912)

* Add WeakMap and WeakSet implementations.

* Enable Test262 tests for WeakMap/WeakSet, add missing toStringTag, tick feature in README
Joseph Da Silva 4 years ago
parent
commit
5a058bf9c1

+ 15 - 0
Jint.Tests.Test262/BuiltIns/WeakMapTests.cs

@@ -0,0 +1,15 @@
+using Xunit;
+
+namespace Jint.Tests.Test262.BuiltIns
+{
+    public class WeakMapTests : Test262Test
+    {
+        [Theory(DisplayName = "built-ins\\WeakMap")]
+        [MemberData(nameof(SourceFiles), "built-ins\\WeakMap", false)]
+        [MemberData(nameof(SourceFiles), "built-ins\\WeakMap", true, Skip = "Skipped")]
+        protected void RunTest(SourceFile sourceFile)
+        {
+            RunTestInternal(sourceFile);
+        }
+    }
+}

+ 15 - 0
Jint.Tests.Test262/BuiltIns/WeakSetTests.cs

@@ -0,0 +1,15 @@
+using Xunit;
+
+namespace Jint.Tests.Test262.BuiltIns
+{
+    public class WeakSetTests : Test262Test
+    {
+        [Theory(DisplayName = "built-ins\\WeakSet")]
+        [MemberData(nameof(SourceFiles), "built-ins\\WeakSet", false)]
+        [MemberData(nameof(SourceFiles), "built-ins\\WeakSet", true, Skip = "Skipped")]
+        protected void RunTest(SourceFile sourceFile)
+        {
+            RunTestInternal(sourceFile);
+        }
+    }
+}

+ 1 - 13
Jint.Tests.Test262/Test262Test.cs

@@ -316,19 +316,7 @@ namespace Jint.Tests.Test262
                     skip = true;
                     skip = true;
                     reason = "TypedArray not implemented";
                     reason = "TypedArray not implemented";
                 }
                 }
-
-                if (name.StartsWith("language/statements/class/subclass/builtin-objects/WeakMap"))
-                {
-                    skip = true;
-                    reason = "WeakMap not implemented";
-                }
-
-                if (name.StartsWith("language/statements/class/subclass/builtin-objects/WeakSet"))
-                {
-                    skip = true;
-                    reason = "WeakSet not implemented";
-                }
-
+                
                 if (name.StartsWith("language/statements/class/subclass/builtin-objects/ArrayBuffer/"))
                 if (name.StartsWith("language/statements/class/subclass/builtin-objects/ArrayBuffer/"))
                 {
                 {
                     skip = true;
                     skip = true;

+ 67 - 0
Jint.Tests/Runtime/WeakSetMapTests.cs

@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using Jint.Native;
+using Jint.Native.Object;
+using Jint.Native.WeakMap;
+using Jint.Native.WeakSet;
+using Jint.Runtime;
+using Xunit;
+
+namespace Jint.Tests.Runtime
+{
+    public class WeakSetMapTests
+    {
+
+        private static readonly Engine _engine = new Engine();
+
+        [Fact]
+        public void WeakMapShouldThrowWhenCalledWithoutNew()
+        {
+            var e = Assert.Throws<JavaScriptException>(() => _engine.Execute("{ const m = new WeakMap(); WeakMap.call(m,[]); }"));
+            Assert.Equal("Constructor WeakMap requires 'new'", e.Message);
+        }
+
+        [Fact]
+        public void WeakSetShouldThrowWhenCalledWithoutNew()
+        {
+            var e = Assert.Throws<JavaScriptException>(() => _engine.Execute("{ const s = new WeakSet(); WeakSet.call(s,[]); }"));
+            Assert.Equal("Constructor WeakSet requires 'new'", e.Message);
+        }
+
+        public static IEnumerable<object[]> PrimitiveKeys = new TheoryData<JsValue>
+        {
+            JsValue.Null,
+            JsValue.Undefined,
+            0,
+            100.04,
+            double.NaN,
+            "hello",
+            true,
+            new JsSymbol("hello")
+        };
+
+        [Theory]
+        [MemberData(nameof(PrimitiveKeys))]
+        public void WeakSetAddShouldThrowForPrimitiveKey(JsValue key) {
+            var weakSet = new WeakSetInstance(_engine);
+            
+            var e = Assert.Throws<JavaScriptException>(() => weakSet.WeakSetAdd(key));
+            Assert.StartsWith("WeakSet value must be an object, got ", e.Message);
+
+            Assert.False(weakSet.WeakSetHas(key));
+        }
+
+        [Theory]
+        [MemberData(nameof(PrimitiveKeys))]
+        public void WeakMapSetShouldThrowForPrimitiveKey(JsValue key) {
+            var weakMap = new WeakMapInstance(_engine);
+            
+            var e = Assert.Throws<JavaScriptException>(() => weakMap.WeakMapSet(key, new ObjectInstance(_engine)));
+            Assert.StartsWith("WeakMap key must be an object, got ", e.Message);
+
+            Assert.False(weakMap.WeakMapHas(key));
+        }
+
+    }
+
+}

+ 6 - 0
Jint/Engine.cs

@@ -24,6 +24,8 @@ using Jint.Native.RegExp;
 using Jint.Native.Set;
 using Jint.Native.Set;
 using Jint.Native.String;
 using Jint.Native.String;
 using Jint.Native.Symbol;
 using Jint.Native.Symbol;
+using Jint.Native.WeakMap;
+using Jint.Native.WeakSet;
 using Jint.Pooling;
 using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime;
 using Jint.Runtime.CallStack;
 using Jint.Runtime.CallStack;
@@ -161,6 +163,8 @@ namespace Jint
             Array = ArrayConstructor.CreateArrayConstructor(this);
             Array = ArrayConstructor.CreateArrayConstructor(this);
             Map = MapConstructor.CreateMapConstructor(this);
             Map = MapConstructor.CreateMapConstructor(this);
             Set = SetConstructor.CreateSetConstructor(this);
             Set = SetConstructor.CreateSetConstructor(this);
+            WeakMap = WeakMapConstructor.CreateWeakMapConstructor(this);
+            WeakSet = WeakSetConstructor.CreateWeakSetConstructor(this);
             Promise = PromiseConstructor.CreatePromiseConstructor(this);
             Promise = PromiseConstructor.CreatePromiseConstructor(this);
             Iterator = IteratorConstructor.CreateIteratorConstructor(this);
             Iterator = IteratorConstructor.CreateIteratorConstructor(this);
             String = StringConstructor.CreateStringConstructor(this);
             String = StringConstructor.CreateStringConstructor(this);
@@ -220,6 +224,8 @@ namespace Jint
         public ArrayConstructor Array { get; }
         public ArrayConstructor Array { get; }
         public MapConstructor Map { get; }
         public MapConstructor Map { get; }
         public SetConstructor Set { get; }
         public SetConstructor Set { get; }
+        public WeakMapConstructor WeakMap { get; }
+        public WeakSetConstructor WeakSet { get; }
         public PromiseConstructor Promise { get; }
         public PromiseConstructor Promise { get; }
         public IteratorConstructor Iterator { get; }
         public IteratorConstructor Iterator { get; }
         public StringConstructor String { get; }
         public StringConstructor String { get; }

+ 3 - 1
Jint/Native/Global/GlobalObject.cs

@@ -31,7 +31,7 @@ namespace Jint.Native.Global
         {
         {
             const PropertyFlag lengthFlags = PropertyFlag.Configurable;
             const PropertyFlag lengthFlags = PropertyFlag.Configurable;
             const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
             const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
-            var properties = new PropertyDictionary(40, checkExistingKeys: false)
+            var properties = new PropertyDictionary(42, checkExistingKeys: false)
             {
             {
                 ["Object"] = new PropertyDescriptor(Engine.Object, propertyFlags),
                 ["Object"] = new PropertyDescriptor(Engine.Object, propertyFlags),
                 ["Function"] = new PropertyDescriptor(Engine.Function, propertyFlags),
                 ["Function"] = new PropertyDescriptor(Engine.Function, propertyFlags),
@@ -39,6 +39,8 @@ namespace Jint.Native.Global
                 ["Array"] = new PropertyDescriptor(Engine.Array, propertyFlags),
                 ["Array"] = new PropertyDescriptor(Engine.Array, propertyFlags),
                 ["Map"] = new PropertyDescriptor(Engine.Map, propertyFlags),
                 ["Map"] = new PropertyDescriptor(Engine.Map, propertyFlags),
                 ["Set"] = new PropertyDescriptor(Engine.Set, propertyFlags),
                 ["Set"] = new PropertyDescriptor(Engine.Set, propertyFlags),
+                ["WeakMap"] = new PropertyDescriptor(Engine.WeakMap, propertyFlags),
+                ["WeakSet"] = new PropertyDescriptor(Engine.WeakSet, propertyFlags),
                 ["Promise"] = new PropertyDescriptor(Engine.Promise, propertyFlags),
                 ["Promise"] = new PropertyDescriptor(Engine.Promise, propertyFlags),
                 ["String"] = new PropertyDescriptor(Engine.String, propertyFlags),
                 ["String"] = new PropertyDescriptor(Engine.String, propertyFlags),
                 ["RegExp"] = new PropertyDescriptor(Engine.RegExp, propertyFlags),
                 ["RegExp"] = new PropertyDescriptor(Engine.RegExp, propertyFlags),

+ 63 - 0
Jint/Native/WeakMap/WeakMapConstructor.cs

@@ -0,0 +1,63 @@
+using Jint.Native.Function;
+using Jint.Native.Iterator;
+using Jint.Native.Object;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+
+namespace Jint.Native.WeakMap
+{
+    public sealed class WeakMapConstructor : FunctionInstance, IConstructor
+    {
+        private static readonly JsString _functionName = new JsString("WeakMap");
+
+        private WeakMapConstructor(Engine engine)
+            : base(engine, _functionName)
+        {
+        }
+
+        public WeakMapPrototype PrototypeObject { get; private set; }
+
+        public static WeakMapConstructor CreateWeakMapConstructor(Engine engine)
+        {
+            var obj = new WeakMapConstructor(engine)
+            {
+                _prototype = engine.Function.PrototypeObject
+            };
+
+            // The value of the [[Prototype]] internal property of the WeakMap constructor is the WeakMap prototype object
+            obj.PrototypeObject = WeakMapPrototype.CreatePrototypeObject(engine, obj);
+
+            obj._length = new PropertyDescriptor(0, PropertyFlag.Configurable);
+
+            // The initial value of WeakMap.prototype is the WeakMap prototype object
+            obj._prototypeDescriptor = new PropertyDescriptor(obj.PrototypeObject, PropertyFlag.AllForbidden);
+
+            return obj;
+        }
+
+        public override JsValue Call(JsValue thisObject, JsValue[] arguments)
+        {
+            ExceptionHelper.ThrowTypeError(_engine, "Constructor WeakMap requires 'new'");
+            return null;
+        }
+
+        public ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
+        {
+            if (newTarget.IsUndefined())
+            {
+                ExceptionHelper.ThrowTypeError(_engine);
+            }
+
+            var map = OrdinaryCreateFromConstructor(newTarget, PrototypeObject, static (engine, _) => new WeakMapInstance(engine));
+            if (arguments.Length > 0 && !arguments[0].IsNullOrUndefined())
+            {
+                var adder = map.Get("set");
+                var iterator = arguments.At(0).GetIterator(_engine);
+
+                IteratorProtocol.AddEntriesFromIterable(map, iterator, adder);
+            }
+
+            return map;
+        }
+    }
+}

+ 53 - 0
Jint/Native/WeakMap/WeakMapInstance.cs

@@ -0,0 +1,53 @@
+using System.Runtime.CompilerServices;
+
+using Jint.Native.Object;
+using Jint.Runtime;
+
+namespace Jint.Native.WeakMap
+{
+    public class WeakMapInstance : ObjectInstance
+    {
+        private readonly ConditionalWeakTable<JsValue, JsValue> _table;
+
+        public WeakMapInstance(Engine engine) : base(engine)
+        {
+            _table = new ConditionalWeakTable<JsValue, JsValue>();
+        }
+
+        internal bool WeakMapHas(JsValue key)
+        {
+            return _table.TryGetValue(key, out _);
+        }
+
+        internal bool WeakMapDelete(JsValue key)
+        {
+            return _table.Remove(key);
+        }
+
+        internal void WeakMapSet(JsValue key, JsValue value)
+        {
+            if (key.IsPrimitive())
+            {
+                ExceptionHelper.ThrowTypeError(_engine, "WeakMap key must be an object, got " + key.ToString());
+            }
+
+#if NETSTANDARD2_1
+            _table.AddOrUpdate(key, value);
+#else
+            _table.Remove(key);
+            _table.Add(key, value);
+#endif
+        }
+
+        internal JsValue WeakMapGet(JsValue key)
+        {
+            if (!_table.TryGetValue(key, out var value))
+            {
+                return Undefined;
+            }
+
+            return value;
+        }
+
+    }
+}

+ 88 - 0
Jint/Native/WeakMap/WeakMapPrototype.cs

@@ -0,0 +1,88 @@
+using Jint.Collections;
+using Jint.Native.Object;
+using Jint.Native.Symbol;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
+
+namespace Jint.Native.WeakMap
+{
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-weakmap-objects
+    /// </summary>
+    public sealed class WeakMapPrototype : ObjectInstance
+    {
+        private WeakMapConstructor _weakMapConstructor;
+
+        private WeakMapPrototype(Engine engine) : base(engine)
+        {
+        }
+
+        public static WeakMapPrototype CreatePrototypeObject(Engine engine, WeakMapConstructor weakMapConstructor)
+        {
+            var obj = new WeakMapPrototype(engine)
+            {
+                _prototype = engine.Object.PrototypeObject,
+                _weakMapConstructor = weakMapConstructor
+            };
+
+            return obj;
+        }
+
+        protected override void Initialize()
+        {
+            const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+            var properties = new PropertyDictionary(6, checkExistingKeys: false)
+            {
+                ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable),
+                ["constructor"] = new PropertyDescriptor(_weakMapConstructor, PropertyFlag.NonEnumerable),
+                ["delete"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "delete", Delete, 1, PropertyFlag.Configurable), propertyFlags),
+                ["get"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "get", Get, 1, PropertyFlag.Configurable), propertyFlags),
+                ["has"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "has", Has, 1, PropertyFlag.Configurable), propertyFlags),
+                ["set"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "set", Set, 2, PropertyFlag.Configurable), propertyFlags),
+            };
+            SetProperties(properties);
+
+            var symbols = new SymbolDictionary(1)
+            {
+                [GlobalSymbolRegistry.ToStringTag] = new PropertyDescriptor("WeakMap", false, false, true)
+            };
+            SetSymbols(symbols);
+        }
+
+        private JsValue Get(JsValue thisObj, JsValue[] arguments)
+        {
+            var map = AssertWeakMapInstance(thisObj);
+            return map.WeakMapGet(arguments.At(0));
+        }
+
+        private JsValue Delete(JsValue thisObj, JsValue[] arguments)
+        {
+            var map = AssertWeakMapInstance(thisObj);
+            return (arguments.Length > 0 && map.WeakMapDelete(arguments.At(0))) ? JsBoolean.True : JsBoolean.False;
+        }
+
+        private JsValue Set(JsValue thisObj, JsValue[] arguments)
+        {
+            var map = AssertWeakMapInstance(thisObj);
+            map.WeakMapSet(arguments.At(0), arguments.At(1));
+            return thisObj;
+        }
+
+        private JsValue Has(JsValue thisObj, JsValue[] arguments)
+        {
+            var map = AssertWeakMapInstance(thisObj);
+            return map.WeakMapHas(arguments.At(0)) ? JsBoolean.True : JsBoolean.False;
+        }
+        
+        private WeakMapInstance AssertWeakMapInstance(JsValue thisObj)
+        {
+            if (!(thisObj is WeakMapInstance map))
+            {
+                return ExceptionHelper.ThrowTypeError<WeakMapInstance>(_engine, "object must be a WeakMap");
+            }
+
+            return map;
+        }
+    }
+}

+ 86 - 0
Jint/Native/WeakSet/WeakSetConstructor.cs

@@ -0,0 +1,86 @@
+using Jint.Native.Function;
+using Jint.Native.Object;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+
+namespace Jint.Native.WeakSet
+{
+    public sealed class WeakSetConstructor : FunctionInstance, IConstructor
+    {
+        private static readonly JsString _functionName = new JsString("WeakSet");
+
+        private WeakSetConstructor(Engine engine)
+            : base(engine, _functionName)
+        {
+        }
+
+        public WeakSetPrototype PrototypeObject { get; private set; }
+
+        public static WeakSetConstructor CreateWeakSetConstructor(Engine engine)
+        {
+            var obj = new WeakSetConstructor(engine)
+            {
+                _prototype = engine.Function.PrototypeObject
+            };
+
+            // The value of the [[Prototype]] internal property of the WeakSet constructor is the Function prototype object
+            obj.PrototypeObject = WeakSetPrototype.CreatePrototypeObject(engine, obj);
+
+            obj._length = new PropertyDescriptor(0, PropertyFlag.Configurable);
+
+            // The initial value of WeakSet.prototype is the WeakSet prototype object
+            obj._prototypeDescriptor = new PropertyDescriptor(obj.PrototypeObject, PropertyFlag.AllForbidden);
+
+            return obj;
+        }
+
+        public override JsValue Call(JsValue thisObject, JsValue[] arguments)
+        {
+            ExceptionHelper.ThrowTypeError(_engine, "Constructor WeakSet requires 'new'");
+            return null;
+        }
+
+        public ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
+        {
+            if (newTarget.IsUndefined())
+            {
+                ExceptionHelper.ThrowTypeError(_engine);
+            }
+
+            var set = OrdinaryCreateFromConstructor(newTarget, PrototypeObject, static (engine, _) => new WeakSetInstance(engine));
+            if (arguments.Length > 0 && !arguments[0].IsNullOrUndefined())
+            {
+                var adderValue = set.Get("add");
+                if (!(adderValue is ICallable adder))
+                {
+                    return ExceptionHelper.ThrowTypeError<ObjectInstance>(_engine, "add must be callable");
+                }
+
+                var iterable = arguments.At(0).GetIterator(_engine);
+
+                try
+                {
+                    var args = new JsValue[1];
+                    do
+                    {
+                        if (!iterable.TryIteratorStep(out var next))
+                        {
+                            return set;
+                        }
+
+                        next.TryGetValue(CommonProperties.Value, out var nextValue);
+                        args[0] = nextValue;
+                        adder.Call(set, args);
+                    } while (true);
+                }
+                catch
+                {
+                    iterable.Close(CompletionType.Throw);
+                    throw;
+                }
+            }
+
+            return set;
+        }
+    }
+}

+ 45 - 0
Jint/Native/WeakSet/WeakSetInstance.cs

@@ -0,0 +1,45 @@
+using System.Runtime.CompilerServices;
+
+using Jint.Native.Object;
+using Jint.Runtime;
+
+namespace Jint.Native.WeakSet
+{
+    public class WeakSetInstance : ObjectInstance
+    {
+        private static readonly object _tableValue = new object();
+
+        private readonly ConditionalWeakTable<JsValue, object> _table;
+
+        public WeakSetInstance(Engine engine) : base(engine)
+        {
+            _table = new ConditionalWeakTable<JsValue, object>();
+        }
+
+        internal bool WeakSetHas(JsValue value)
+        {
+            return _table.TryGetValue(value, out _);
+        }
+
+        internal bool WeakSetDelete(JsValue value)
+        {
+            return _table.Remove(value);
+        }
+
+        internal void WeakSetAdd(JsValue value)
+        {
+            if (value.IsPrimitive())
+            {
+                ExceptionHelper.ThrowTypeError(_engine, "WeakSet value must be an object, got " + value.ToString());
+            }
+
+#if NETSTANDARD2_1
+            _table.AddOrUpdate(value, _tableValue);
+#else
+            _table.Remove(value);
+            _table.Add(value, _tableValue);
+#endif
+        }
+
+    }
+}

+ 81 - 0
Jint/Native/WeakSet/WeakSetPrototype.cs

@@ -0,0 +1,81 @@
+using Jint.Collections;
+using Jint.Native.Object;
+using Jint.Native.Symbol;
+using Jint.Runtime;
+using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
+
+namespace Jint.Native.WeakSet
+{
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-weakset-objects
+    /// </summary>
+    public sealed class WeakSetPrototype : ObjectInstance
+    {
+        private WeakSetConstructor _weakSetConstructor;
+
+        private WeakSetPrototype(Engine engine) : base(engine)
+        {
+        }
+
+        public static WeakSetPrototype CreatePrototypeObject(Engine engine, WeakSetConstructor weakSetConstructor)
+        {
+            var obj = new WeakSetPrototype(engine)
+            {
+                _prototype = engine.Object.PrototypeObject,
+                _weakSetConstructor = weakSetConstructor
+            };
+
+            return obj;
+        }
+
+        protected override void Initialize()
+        {
+            const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+            var properties = new PropertyDictionary(5, checkExistingKeys: false)
+            {
+                ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable),
+                ["constructor"] = new PropertyDescriptor(_weakSetConstructor, PropertyFlag.NonEnumerable),
+                ["delete"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "delete", Delete, 1, PropertyFlag.Configurable), propertyFlags),
+                ["add"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "add", Add, 1, PropertyFlag.Configurable), propertyFlags),
+                ["has"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "has", Has, 1, PropertyFlag.Configurable), propertyFlags),
+            };
+            SetProperties(properties);
+
+            var symbols = new SymbolDictionary(1)
+            {
+                [GlobalSymbolRegistry.ToStringTag] = new PropertyDescriptor("WeakSet", false, false, true)
+            };
+            SetSymbols(symbols);
+        }
+
+        private JsValue Add(JsValue thisObj, JsValue[] arguments)
+        {
+            var set = AssertWeakSetInstance(thisObj);
+            set.WeakSetAdd(arguments.At(0));
+            return thisObj;
+        }
+
+        private JsValue Delete(JsValue thisObj, JsValue[] arguments)
+        {
+            var set = AssertWeakSetInstance(thisObj);
+            return set.WeakSetDelete(arguments.At(0)) ? JsBoolean.True : JsBoolean.False;
+        }
+
+        private JsValue Has(JsValue thisObj, JsValue[] arguments)
+        {
+            var set = AssertWeakSetInstance(thisObj);
+            return set.WeakSetHas(arguments.At(0)) ? JsBoolean.True : JsBoolean.False;
+        }
+        
+        private WeakSetInstance AssertWeakSetInstance(JsValue thisObj)
+        {
+            if (!(thisObj is WeakSetInstance set))
+            {
+                return ExceptionHelper.ThrowTypeError<WeakSetInstance>(_engine, "object must be a WeakSet");
+            }
+
+            return set;
+        }
+    }
+}

+ 1 - 1
README.md

@@ -39,7 +39,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
 - ❌ Generators
 - ❌ Generators
 - ❌ Unicode
 - ❌ Unicode
 - ❌ Modules and module loaders
 - ❌ Modules and module loaders
--  Weakmap and Weakset
+-  Weakmap and Weakset
 - ✔ Promises (Experimental, API is unstable)
 - ✔ Promises (Experimental, API is unstable)
 - ❌ Tail calls
 - ❌ Tail calls