From 77f25fa89caefff7065e2df47f95d7cc4e156a9f Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 3 Jul 2025 15:17:16 +0300 Subject: [PATCH] Add DisposableStack and AsyncDisposableStack --- Jint.Repl/Program.cs | 26 +++- Jint.Tests/Runtime/TypedArrayInteropTests.cs | 4 +- .../AsyncDisposableStackConstructor.cs | 34 +++++ .../AsyncDisposableStackPrototype.cs | 101 +++++++++++++ Jint/Native/Disposable/DisposableStack.cs | 95 ++++++++++++ .../Disposable/DisposableStackConstructor.cs | 34 +++++ .../Disposable/DisposableStackPrototype.cs | 102 +++++++++++++ Jint/Native/Disposable/DisposeCapability.cs | 141 ++++++++++++++++++ Jint/Native/Global/GlobalObject.Properties.cs | 8 +- Jint/Native/Iterator/IteratorPrototype.cs | 16 +- Jint/Native/Object/ObjectInstance.cs | 38 +++++ .../SuppressedErrorConstructor.cs | 61 ++++++++ .../SuppressedErrorPrototype.cs | 32 ++++ Jint/Native/Symbol/GlobalSymbolRegistry.cs | 28 ++-- Jint/Native/Symbol/SymbolConstructor.cs | 6 +- Jint/Runtime/Intrinsics.cs | 15 ++ 16 files changed, 715 insertions(+), 26 deletions(-) create mode 100644 Jint/Native/Disposable/AsyncDisposableStackConstructor.cs create mode 100644 Jint/Native/Disposable/AsyncDisposableStackPrototype.cs create mode 100644 Jint/Native/Disposable/DisposableStack.cs create mode 100644 Jint/Native/Disposable/DisposableStackConstructor.cs create mode 100644 Jint/Native/Disposable/DisposableStackPrototype.cs create mode 100644 Jint/Native/Disposable/DisposeCapability.cs create mode 100644 Jint/Native/SuppressedError/SuppressedErrorConstructor.cs create mode 100644 Jint/Native/SuppressedError/SuppressedErrorPrototype.cs diff --git a/Jint.Repl/Program.cs b/Jint.Repl/Program.cs index 36a13ac8e3..2038da152b 100644 --- a/Jint.Repl/Program.cs +++ b/Jint.Repl/Program.cs @@ -4,6 +4,8 @@ using Jint.Native.Json; using Jint.Runtime; +// ReSharper disable LocalizableElement + #pragma warning disable IL2026 #pragma warning disable IL2111 @@ -13,6 +15,7 @@ engine .SetValue("print", new Action(Console.WriteLine)) + .SetValue("console", new JsConsole()) .SetValue("load", new Func( path => engine.Evaluate(File.ReadAllText(path))) ); @@ -22,7 +25,7 @@ { if (!File.Exists(filename)) { - Console.WriteLine("Could not find file: {0}", filename); + Console.WriteLine($"Could not find file: {filename}"); } var script = File.ReadAllText(filename); @@ -33,10 +36,8 @@ var assembly = Assembly.GetExecutingAssembly(); var version = assembly.GetName().Version?.ToString(); -Console.WriteLine("Welcome to Jint ({0})", version); -Console.WriteLine("Type 'exit' to leave, " + - "'print()' to write on the console, " + - "'load()' to load scripts."); +Console.WriteLine($"Welcome to Jint ({version})"); +Console.WriteLine("Type 'exit' to leave, 'print()' to write on the console, 'load()' to load scripts."); Console.WriteLine(); var defaultColor = Console.ForegroundColor; @@ -86,3 +87,18 @@ Console.WriteLine(e.Message); } } + +file sealed class JsConsole +{ + public void Log(object value) + { + Console.WriteLine(value?.ToString() ?? "null"); + } + + public void Error(object value) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(value?.ToString() ?? "null"); + Console.ResetColor(); + } +} diff --git a/Jint.Tests/Runtime/TypedArrayInteropTests.cs b/Jint.Tests/Runtime/TypedArrayInteropTests.cs index e83a1ef7be..87b8402c1f 100644 --- a/Jint.Tests/Runtime/TypedArrayInteropTests.cs +++ b/Jint.Tests/Runtime/TypedArrayInteropTests.cs @@ -1,5 +1,3 @@ -using Jint.Native; - namespace Jint.Tests.Runtime; public class TypedArrayInteropTests @@ -136,7 +134,7 @@ public void CanInteropWithFloat16() Assert.True(fromEngine.IsFloat16Array()); Assert.Equal(source, fromEngine.AsFloat16Array()); - engine.SetValue("testFunc", new Func(v => v)); + engine.SetValue("testFunc", new Func(v => v)); Assert.Equal(source, engine.Evaluate("testFunc(testSubject)").AsFloat16Array()); } #endif diff --git a/Jint/Native/Disposable/AsyncDisposableStackConstructor.cs b/Jint/Native/Disposable/AsyncDisposableStackConstructor.cs new file mode 100644 index 0000000000..f9fdaf94d3 --- /dev/null +++ b/Jint/Native/Disposable/AsyncDisposableStackConstructor.cs @@ -0,0 +1,34 @@ +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Descriptors; + +namespace Jint.Native.Disposable; + +internal sealed class AsyncDisposableStackConstructor : Constructor +{ + private static readonly JsString _name = new("AsyncDisposableStack"); + + public AsyncDisposableStackConstructor(Engine engine, Realm realm) : base(engine, realm, _name) + { + PrototypeObject = new AsyncDisposableStackPrototype(engine, realm, this, engine.Intrinsics.Object.PrototypeObject); + _length = new PropertyDescriptor(0, PropertyFlag.Configurable); + _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden); + } + + internal AsyncDisposableStackPrototype PrototypeObject { get; } + + public override ObjectInstance Construct(JsCallArguments arguments, JsValue newTarget) + { + if (newTarget.IsUndefined()) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + var stack = OrdinaryCreateFromConstructor( + newTarget, + static intrinsics => intrinsics.AsyncDisposableStack.PrototypeObject, + static (Engine engine, Realm _, object? _) => new DisposableStack(engine, DisposeHint.Async)); + + return stack; + } +} diff --git a/Jint/Native/Disposable/AsyncDisposableStackPrototype.cs b/Jint/Native/Disposable/AsyncDisposableStackPrototype.cs new file mode 100644 index 0000000000..ddf350ecef --- /dev/null +++ b/Jint/Native/Disposable/AsyncDisposableStackPrototype.cs @@ -0,0 +1,101 @@ +using Jint.Native.Object; +using Jint.Native.Symbol; +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Interop; + +namespace Jint.Native.Disposable; + +internal sealed class AsyncDisposableStackPrototype : Prototype +{ + private readonly AsyncDisposableStackConstructor _constructor; + + internal AsyncDisposableStackPrototype( + Engine engine, + Realm realm, + AsyncDisposableStackConstructor constructor, + ObjectPrototype objectPrototype) : base(engine, realm) + { + _prototype = objectPrototype; + _constructor = constructor; + } + + protected override void Initialize() + { + var disposeFunction = new ClrFunction(Engine, "disposeAsync", Dispose, 0, PropertyFlag.Configurable); + + const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; + var properties = new PropertyDictionary(8, checkExistingKeys: false) + { + ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable), + ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), + ["adopt"] = new PropertyDescriptor(new ClrFunction(Engine, "adopt", Adopt, 2, PropertyFlag.Configurable), PropertyFlags), + ["defer"] = new PropertyDescriptor(new ClrFunction(Engine, "defer", Defer, 1, PropertyFlag.Configurable), PropertyFlags), + ["disposeAsync"] = new PropertyDescriptor(disposeFunction, PropertyFlags), + ["disposed"] = new GetSetPropertyDescriptor(get: new ClrFunction(Engine, "get disposed", Disposed, 0, PropertyFlag.Configurable), set: Undefined, PropertyFlags), + ["move"] = new PropertyDescriptor(new ClrFunction(Engine, "move", Move, 0, PropertyFlag.Configurable), PropertyFlags), + ["use"] = new PropertyDescriptor(new ClrFunction(Engine, "use", Use, 1, PropertyFlag.Configurable), PropertyFlags), + }; + SetProperties(properties); + + var symbols = new SymbolDictionary(2) + { + [GlobalSymbolRegistry.AsyncDispose] = new PropertyDescriptor(disposeFunction, PropertyFlags), + [GlobalSymbolRegistry.ToStringTag] = new PropertyDescriptor("AsyncDisposableStack", PropertyFlag.Configurable), + }; + SetSymbols(symbols); + } + + private JsValue Adopt(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Adopt(arguments.At(0), arguments.At(1)); + } + + private JsValue Defer(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + stack.Defer(arguments.At(0)); + return Undefined; + } + + private JsValue Dispose(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Dispose(); + } + + private JsValue Disposed(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.State == DisposableState.Disposed; + } + + private JsValue Move(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + var newDisposableStack = _engine.Intrinsics.Function.OrdinaryCreateFromConstructor( + _engine.Intrinsics.AsyncDisposableStack, + static intrinsics => intrinsics.AsyncDisposableStack.PrototypeObject, + static (Engine engine, Realm _, object? _) => new DisposableStack(engine, DisposeHint.Async)); + + return stack.Move(newDisposableStack); + } + + private JsValue Use(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Use(arguments.At(0)); + } + + private DisposableStack AssertDisposableStack(JsValue thisObject) + { + if (thisObject is not DisposableStack { _hint: DisposeHint.Async } stack) + { + ExceptionHelper.ThrowTypeError(_engine.Realm, "This is not a AsyncDisposableStack instance."); + return null!; + } + + return stack; + } +} diff --git a/Jint/Native/Disposable/DisposableStack.cs b/Jint/Native/Disposable/DisposableStack.cs new file mode 100644 index 0000000000..db7b4b2404 --- /dev/null +++ b/Jint/Native/Disposable/DisposableStack.cs @@ -0,0 +1,95 @@ +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Interop; + +namespace Jint.Native.Disposable; + +internal enum DisposableState +{ + Pending, + Disposed, +} + +internal sealed class DisposableStack : ObjectInstance +{ + internal readonly DisposeHint _hint; + private DisposeCapability _disposeCapability; + + public DisposableStack(Engine engine, DisposeHint hint) : base(engine) + { + _hint = hint; + State = DisposableState.Pending; + _disposeCapability = new DisposeCapability(engine); + } + + public DisposableState State { get; private set; } + + public JsValue Dispose() + { + if (State == DisposableState.Disposed) + { + return Undefined; + } + + State = DisposableState.Disposed; + var completion = _disposeCapability.DisposeResources(new Completion(CompletionType.Normal, Undefined, _engine.GetLastSyntaxElement())); + if (completion.Type == CompletionType.Throw) + { + ExceptionHelper.ThrowJavaScriptException(_engine, completion.Value, completion); + } + return completion.Value; + } + + public void Defer(JsValue onDispose) + { + AddDisposableResource(Undefined, _hint, onDispose.GetCallable(_engine.Realm)); + } + + public JsValue Use(JsValue value) + { + AddDisposableResource(value, _hint); + return value; + } + + public JsValue Adopt(JsValue value, JsValue onDispose) + { + AssertNotDisposed(); + + var callable = onDispose.GetCallable(_engine.Realm); + JsCallDelegate closure = (_, _) => + { + callable.Call(Undefined, value); + return Undefined; + }; + + var f = new ClrFunction(_engine, string.Empty, closure); + AddDisposableResource(Undefined, DisposeHint.Sync, f); + + return value; + } + + public JsValue Move(DisposableStack newDisposableStack) + { + AssertNotDisposed(); + + newDisposableStack.State = DisposableState.Pending; + newDisposableStack._disposeCapability = this._disposeCapability; + this._disposeCapability = new DisposeCapability(_engine); + State = DisposableState.Disposed; + return newDisposableStack; + } + + private void AddDisposableResource(JsValue v, DisposeHint hint, ICallable? method = null) + { + AssertNotDisposed(); + _disposeCapability.AddDisposableResource(v, hint, method); + } + + private void AssertNotDisposed() + { + if (State == DisposableState.Disposed) + { + ExceptionHelper.ThrowReferenceError(_engine.Realm, "Stack already disposed."); + } + } +} diff --git a/Jint/Native/Disposable/DisposableStackConstructor.cs b/Jint/Native/Disposable/DisposableStackConstructor.cs new file mode 100644 index 0000000000..4c65ba258e --- /dev/null +++ b/Jint/Native/Disposable/DisposableStackConstructor.cs @@ -0,0 +1,34 @@ +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Descriptors; + +namespace Jint.Native.Disposable; + +internal sealed class DisposableStackConstructor : Constructor +{ + private static readonly JsString _name = new("DisposableStack"); + + public DisposableStackConstructor(Engine engine, Realm realm) : base(engine, realm, _name) + { + PrototypeObject = new DisposableStackPrototype(engine, realm, this, engine.Intrinsics.Object.PrototypeObject); + _length = new PropertyDescriptor(0, PropertyFlag.Configurable); + _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden); + } + + internal DisposableStackPrototype PrototypeObject { get; } + + public override ObjectInstance Construct(JsCallArguments arguments, JsValue newTarget) + { + if (newTarget.IsUndefined()) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + var stack = OrdinaryCreateFromConstructor( + newTarget, + static intrinsics => intrinsics.DisposableStack.PrototypeObject, + static (Engine engine, Realm _, object? _) => new DisposableStack(engine, DisposeHint.Sync)); + + return stack; + } +} diff --git a/Jint/Native/Disposable/DisposableStackPrototype.cs b/Jint/Native/Disposable/DisposableStackPrototype.cs new file mode 100644 index 0000000000..d60f22e76f --- /dev/null +++ b/Jint/Native/Disposable/DisposableStackPrototype.cs @@ -0,0 +1,102 @@ +using Jint.Native.Object; +using Jint.Native.Symbol; +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Interop; + +namespace Jint.Native.Disposable; + +internal sealed class DisposableStackPrototype : Prototype +{ + private readonly DisposableStackConstructor _constructor; + + internal DisposableStackPrototype( + Engine engine, + Realm realm, + DisposableStackConstructor constructor, + ObjectPrototype objectPrototype) : base(engine, realm) + { + _prototype = objectPrototype; + _constructor = constructor; + } + + protected override void Initialize() + { + var disposeFunction = new ClrFunction(Engine, "dispose", Dispose, 0, PropertyFlag.Configurable); + + const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; + var properties = new PropertyDictionary(8, checkExistingKeys: false) + { + ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable), + ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), + ["adopt"] = new PropertyDescriptor(new ClrFunction(Engine, "adopt", Adopt, 2, PropertyFlag.Configurable), PropertyFlags), + ["defer"] = new PropertyDescriptor(new ClrFunction(Engine, "defer", Defer, 1, PropertyFlag.Configurable), PropertyFlags), + ["dispose"] = new PropertyDescriptor(disposeFunction, PropertyFlags), + ["disposed"] = new GetSetPropertyDescriptor(get: new ClrFunction(Engine, "get disposed", Disposed, 0, PropertyFlag.Configurable), set: Undefined, PropertyFlags), + ["move"] = new PropertyDescriptor(new ClrFunction(Engine, "move", Move, 0, PropertyFlag.Configurable), PropertyFlags), + ["use"] = new PropertyDescriptor(new ClrFunction(Engine, "use", Use, 1, PropertyFlag.Configurable), PropertyFlags), + }; + SetProperties(properties); + + var symbols = new SymbolDictionary(2) + { + [GlobalSymbolRegistry.Dispose] = new PropertyDescriptor(disposeFunction, PropertyFlags), + [GlobalSymbolRegistry.ToStringTag] = new PropertyDescriptor("DisposableStack", PropertyFlag.Configurable), + }; + SetSymbols(symbols); + } + + + private JsValue Adopt(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Adopt(arguments.At(0), arguments.At(1)); + } + + private JsValue Defer(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + stack.Defer(arguments.At(0)); + return Undefined; + } + + private JsValue Dispose(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Dispose(); + } + + private JsValue Disposed(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.State == DisposableState.Disposed; + } + + private JsValue Move(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + var newDisposableStack = _engine.Intrinsics.Function.OrdinaryCreateFromConstructor( + _engine.Intrinsics.DisposableStack, + static intrinsics => intrinsics.DisposableStack.PrototypeObject, + static (Engine engine, Realm _, object? _) => new DisposableStack(engine, DisposeHint.Sync)); + + return stack.Move(newDisposableStack); + } + + private JsValue Use(JsValue thisObject, JsCallArguments arguments) + { + var stack = AssertDisposableStack(thisObject); + return stack.Use(arguments.At(0)); + } + + private DisposableStack AssertDisposableStack(JsValue thisObject) + { + if (thisObject is not DisposableStack { _hint: DisposeHint.Sync } stack) + { + ExceptionHelper.ThrowTypeError(_engine.Realm, "This is not a DisposableStack instance."); + return null!; + } + + return stack; + } +} diff --git a/Jint/Native/Disposable/DisposeCapability.cs b/Jint/Native/Disposable/DisposeCapability.cs new file mode 100644 index 0000000000..b91bdbf512 --- /dev/null +++ b/Jint/Native/Disposable/DisposeCapability.cs @@ -0,0 +1,141 @@ +using Jint.Runtime; + +namespace Jint.Native.Disposable; + +internal enum DisposeHint +{ + Normal, + Sync, + Async, +} + +internal sealed class DisposeCapability +{ + private readonly Engine _engine; + private readonly List _disposableResourceStack = []; + + public DisposeCapability(Engine engine) + { + _engine = engine; + } + + public void AddDisposableResource(JsValue v, DisposeHint hint, ICallable? method = null) + { + DisposableResource resource; + if (method is null) + { + if (v.IsNullOrUndefined() && hint == DisposeHint.Sync) + { + return; + } + + resource = CreateDisposableResource(v, hint); + } + else + { + resource = CreateDisposableResource(JsValue.Undefined, hint, method); + } + + _disposableResourceStack.Add(resource); + } + + private DisposableResource CreateDisposableResource(JsValue v, DisposeHint hint, ICallable? method = null) + { + if (method is null) + { + if (v.IsNullOrUndefined()) + { + v = JsValue.Undefined; + method = null; + } + else + { + if (!v.IsObject()) + { + ExceptionHelper.ThrowTypeError(_engine.Realm, "Expected an object for disposable resource."); + return default; + } + method = v.AsObject().GetDisposeMethod(hint); + if (method is null) + { + ExceptionHelper.ThrowTypeError(_engine.Realm, "No dispose method found for the resource."); + return default; + } + } + } + + return new DisposableResource(v, hint, method); + } + + public Completion DisposeResources(Completion c) + { + var needsAwait = false; + var hasAwaited = false; + + for (var i = _disposableResourceStack.Count - 1; i >= 0; i--) + { + var (value, hint, method) = _disposableResourceStack[i]; + + if (hint == DisposeHint.Sync && needsAwait && !hasAwaited) + { + _engine.RunAvailableContinuations(); + needsAwait = false; + } + + if (method is not null) + { + var result = JsValue.Undefined; + JavaScriptException? exception = null; + try + { + result = method.Call(value); + if (hint == DisposeHint.Async) + { + hasAwaited = true; + try + { + result = result.UnwrapIfPromise(); + } + catch (JavaScriptException e) + { + exception = e; + } + } + } + catch (JavaScriptException e) + { + exception = e; + } + + if (exception is not null) + { + if (c.Type == CompletionType.Throw) + { + var error = _engine.Intrinsics.SuppressedError.Construct(_engine.Intrinsics.SuppressedError, "", exception.Error, c.Value); + c = new Completion(CompletionType.Throw, error, c._source); + } + else + { + c = new Completion(CompletionType.Throw, exception.Error, c._source); + } + } + } + else + { + // Assert: hint is "async-dispose" + needsAwait = true; + } + } + + if (needsAwait && !hasAwaited) + { + _engine.RunAvailableContinuations(); + } + + _disposableResourceStack.Clear(); + return c; + } + + private readonly record struct DisposableResource(JsValue ResourceValue, DisposeHint Hint, ICallable? DisposeMethod); +} + diff --git a/Jint/Native/Global/GlobalObject.Properties.cs b/Jint/Native/Global/GlobalObject.Properties.cs index 8a94dfb471..575aef1ba7 100644 --- a/Jint/Native/Global/GlobalObject.Properties.cs +++ b/Jint/Native/Global/GlobalObject.Properties.cs @@ -10,6 +10,7 @@ public partial class GlobalObject private static readonly Key propertyAggregateError = "AggregateError"; private static readonly Key propertyArray = "Array"; private static readonly Key propertyArrayBuffer = "ArrayBuffer"; + private static readonly Key propertyAsyncDisposableStack = "AsyncDisposableStack"; private static readonly Key propertyAtomics = "Atomics"; private static readonly Key propertyBigInt = "BigInt"; private static readonly Key propertyBigInt64Array = "BigInt64Array"; @@ -17,6 +18,7 @@ public partial class GlobalObject private static readonly Key propertyBoolean = "Boolean"; private static readonly Key propertyDataView = "DataView"; private static readonly Key propertyDate = "Date"; + private static readonly Key propertyDisposableStack = "DisposableStack"; private static readonly Key propertyError = "Error"; private static readonly Key propertyEvalError = "EvalError"; private static readonly Key propertyFinalizationRegistry = "FinalizationRegistry"; @@ -46,6 +48,7 @@ public partial class GlobalObject private static readonly Key propertyString = "String"; private static readonly Key propertySymbol = "Symbol"; private static readonly Key propertySyntaxError = "SyntaxError"; + private static readonly Key propertySuppressedError = "SuppressedError"; private static readonly Key propertyTypeError = "TypeError"; private static readonly Key propertyTypedArray = "TypedArray"; private static readonly Key propertyURIError = "URIError"; @@ -82,10 +85,11 @@ protected override void Initialize() const PropertyFlag LengthFlags = PropertyFlag.Configurable; const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; - var properties = new StringDictionarySlim(65); + var properties = new StringDictionarySlim(70); properties.AddDangerous(propertyAggregateError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.AggregateError, PropertyFlags)); properties.AddDangerous(propertyArray, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Array, PropertyFlags)); properties.AddDangerous(propertyArrayBuffer, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.ArrayBuffer, PropertyFlags)); + properties.AddDangerous(propertyAsyncDisposableStack, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.AsyncDisposableStack, PropertyFlags)); properties.AddDangerous(propertyAtomics, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Atomics, PropertyFlags)); properties.AddDangerous(propertyBigInt, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.BigInt, PropertyFlags)); properties.AddDangerous(propertyBigInt64Array, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.BigInt64Array, PropertyFlags)); @@ -93,6 +97,7 @@ protected override void Initialize() properties.AddDangerous(propertyBoolean, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Boolean, PropertyFlags)); properties.AddDangerous(propertyDataView, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.DataView, PropertyFlags)); properties.AddDangerous(propertyDate, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Date, PropertyFlags)); + properties.AddDangerous(propertyDisposableStack, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.DisposableStack, PropertyFlags)); properties.AddDangerous(propertyError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Error, PropertyFlags)); properties.AddDangerous(propertyEvalError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.EvalError, PropertyFlags)); properties.AddDangerous(propertyFinalizationRegistry, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.FinalizationRegistry, PropertyFlags)); @@ -122,6 +127,7 @@ protected override void Initialize() properties.AddDangerous(propertyString, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.String, PropertyFlags)); properties.AddDangerous(propertySymbol, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.Symbol, PropertyFlags)); properties.AddDangerous(propertySyntaxError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.SyntaxError, PropertyFlags)); + properties.AddDangerous(propertySuppressedError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.SuppressedError, PropertyFlags)); properties.AddDangerous(propertyTypeError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.TypeError, PropertyFlags)); properties.AddDangerous(propertyTypedArray, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.TypedArray, PropertyFlags)); properties.AddDangerous(propertyURIError, new LazyPropertyDescriptor(this, static global => global._realm.Intrinsics.UriError, PropertyFlags)); diff --git a/Jint/Native/Iterator/IteratorPrototype.cs b/Jint/Native/Iterator/IteratorPrototype.cs index 662e373edc..60b9d0b67c 100644 --- a/Jint/Native/Iterator/IteratorPrototype.cs +++ b/Jint/Native/Iterator/IteratorPrototype.cs @@ -20,9 +20,10 @@ internal IteratorPrototype( protected override void Initialize() { - var symbols = new SymbolDictionary(1) + var symbols = new SymbolDictionary(2) { - [GlobalSymbolRegistry.Iterator] = new(new ClrFunction(Engine, "[Symbol.iterator]", ToIterator, 0, PropertyFlag.Configurable), true, false, true), + [GlobalSymbolRegistry.Iterator] = new(new ClrFunction(Engine, "[Symbol.iterator]", ToIterator, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + [GlobalSymbolRegistry.Dispose] = new(new ClrFunction(Engine, "[Symbol.dispose]", Dispose, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), }; SetSymbols(symbols); } @@ -32,6 +33,17 @@ private static JsValue ToIterator(JsValue thisObject, JsCallArguments arguments) return thisObject; } + private static JsValue Dispose(JsValue thisObject, JsCallArguments arguments) + { + var method = thisObject.AsObject().GetMethod(CommonProperties.Return); + if (method is not null) + { + method.Call(thisObject, arguments); + } + + return Undefined; + } + internal JsValue Next(JsValue thisObject, JsCallArguments arguments) { var iterator = thisObject as IteratorInstance; diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index 18c25ae3c8..5846ed5043 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -5,8 +5,10 @@ using Jint.Native.Array; using Jint.Native.BigInt; using Jint.Native.Boolean; +using Jint.Native.Disposable; using Jint.Native.Json; using Jint.Native.Number; +using Jint.Native.Promise; using Jint.Native.String; using Jint.Native.Symbol; using Jint.Native.TypedArray; @@ -1396,6 +1398,42 @@ internal static JsObject OrdinaryObjectCreate(Engine engine, ObjectInstance? pro return callable; } + internal ICallable? GetDisposeMethod(DisposeHint hint) + { + if (hint == DisposeHint.Async) + { + var method = GetMethod(GlobalSymbolRegistry.AsyncDispose); + if (method is null) + { + method = GetMethod(GlobalSymbolRegistry.Dispose); + if (method is not null) + { + JsCallDelegate closure = (_, _) => + { + var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Intrinsics.Promise); + try + { + method.Call(this); + promiseCapability.Resolve.Call(Undefined, Undefined); + } + catch + { + promiseCapability.Reject.Call(Undefined, Undefined); + } + return promiseCapability.PromiseInstance; + }; + + return new ClrFunction(_engine, string.Empty, closure); + } + } + + return method; + } + + return GetMethod(GlobalSymbolRegistry.Dispose); + } + + internal void CopyDataProperties( ObjectInstance target, HashSet? excludedItems) diff --git a/Jint/Native/SuppressedError/SuppressedErrorConstructor.cs b/Jint/Native/SuppressedError/SuppressedErrorConstructor.cs new file mode 100644 index 0000000000..871dde6a5a --- /dev/null +++ b/Jint/Native/SuppressedError/SuppressedErrorConstructor.cs @@ -0,0 +1,61 @@ +using Jint.Native.Error; +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Descriptors; + +namespace Jint.Native.SuppressedError; + +internal sealed class SuppressedErrorConstructor : Constructor +{ + private static readonly JsString _name = new("SuppressedError"); + + internal SuppressedErrorConstructor( + Engine engine, + Realm realm, + ErrorConstructor errorConstructor) + : base(engine, realm, _name) + { + _prototype = errorConstructor; + PrototypeObject = new SuppressedErrorPrototype(engine, realm, this, errorConstructor.PrototypeObject); + _length = new PropertyDescriptor(JsNumber.PositiveThree, PropertyFlag.Configurable); + _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden); + } + + private SuppressedErrorPrototype PrototypeObject { get; } + + protected internal override JsValue Call(JsValue thisObject, JsCallArguments arguments) + { + return Construct(arguments, this); + } + + /// + /// https://tc39.es/ecma262/#sec-nativeerror + /// + public override ObjectInstance Construct(JsCallArguments arguments, JsValue newTarget) + { + var error = arguments.At(0); + var suppressed = arguments.At(1); + var message = arguments.At(2); + + return Construct(newTarget, message, error, suppressed); + } + + internal JsError Construct(JsValue newTarget, JsValue message, JsValue error, JsValue suppressed) + { + var o = OrdinaryCreateFromConstructor( + newTarget, + static intrinsics => intrinsics.SuppressedError.PrototypeObject, + static (Engine engine, Realm _, object? _) => new JsError(engine)); + + if (!message.IsUndefined()) + { + var msg = TypeConverter.ToString(message); + o.CreateNonEnumerableDataPropertyOrThrow(CommonProperties.Message, msg); + } + + o.DefinePropertyOrThrow("error", new PropertyDescriptor(error, configurable: true, enumerable: false, writable: true)); + o.DefinePropertyOrThrow("suppressed", new PropertyDescriptor(suppressed, configurable: true, enumerable: false, writable: true)); + + return o; + } +} diff --git a/Jint/Native/SuppressedError/SuppressedErrorPrototype.cs b/Jint/Native/SuppressedError/SuppressedErrorPrototype.cs new file mode 100644 index 0000000000..f47bc23c49 --- /dev/null +++ b/Jint/Native/SuppressedError/SuppressedErrorPrototype.cs @@ -0,0 +1,32 @@ +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Descriptors; + +namespace Jint.Native.SuppressedError; + +internal sealed class SuppressedErrorPrototype : Prototype +{ + private readonly SuppressedErrorConstructor _constructor; + + internal SuppressedErrorPrototype( + Engine engine, + Realm realm, + SuppressedErrorConstructor constructor, + ObjectInstance prototype) + : base(engine, realm) + { + _constructor = constructor; + _prototype = prototype; + } + + protected override void Initialize() + { + var properties = new PropertyDictionary(3, checkExistingKeys: false) + { + ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), + ["message"] = new PropertyDescriptor(JsString.Empty, PropertyFlag.Configurable | PropertyFlag.Writable), + ["name"] = new PropertyDescriptor("SuppressedError", PropertyFlag.Configurable | PropertyFlag.Writable), + }; + SetProperties(properties); + } +} diff --git a/Jint/Native/Symbol/GlobalSymbolRegistry.cs b/Jint/Native/Symbol/GlobalSymbolRegistry.cs index 6b197b9784..ac092a8375 100644 --- a/Jint/Native/Symbol/GlobalSymbolRegistry.cs +++ b/Jint/Native/Symbol/GlobalSymbolRegistry.cs @@ -4,19 +4,21 @@ namespace Jint.Native.Symbol; public sealed class GlobalSymbolRegistry { - public static readonly JsSymbol AsyncIterator = new JsSymbol("Symbol.asyncIterator"); - public static readonly JsSymbol HasInstance = new JsSymbol("Symbol.hasInstance"); - public static readonly JsSymbol IsConcatSpreadable = new JsSymbol("Symbol.isConcatSpreadable"); - public static readonly JsSymbol Iterator = new JsSymbol("Symbol.iterator"); - public static readonly JsSymbol Match = new JsSymbol("Symbol.match"); - public static readonly JsSymbol MatchAll = new JsSymbol("Symbol.matchAll"); - public static readonly JsSymbol Replace = new JsSymbol("Symbol.replace"); - public static readonly JsSymbol Search = new JsSymbol("Symbol.search"); - public static readonly JsSymbol Species = new JsSymbol("Symbol.species"); - public static readonly JsSymbol Split = new JsSymbol("Symbol.split"); - public static readonly JsSymbol ToPrimitive = new JsSymbol("Symbol.toPrimitive"); - public static readonly JsSymbol ToStringTag = new JsSymbol("Symbol.toStringTag"); - public static readonly JsSymbol Unscopables = new JsSymbol("Symbol.unscopables"); + public static readonly JsSymbol AsyncDispose = new("Symbol.asyncDispose"); + public static readonly JsSymbol AsyncIterator = new("Symbol.asyncIterator"); + public static readonly JsSymbol Dispose = new("Symbol.dispose"); + public static readonly JsSymbol HasInstance = new("Symbol.hasInstance"); + public static readonly JsSymbol IsConcatSpreadable = new("Symbol.isConcatSpreadable"); + public static readonly JsSymbol Iterator = new("Symbol.iterator"); + public static readonly JsSymbol Match = new("Symbol.match"); + public static readonly JsSymbol MatchAll = new("Symbol.matchAll"); + public static readonly JsSymbol Replace = new("Symbol.replace"); + public static readonly JsSymbol Search = new("Symbol.search"); + public static readonly JsSymbol Species = new("Symbol.species"); + public static readonly JsSymbol Split = new("Symbol.split"); + public static readonly JsSymbol ToPrimitive = new("Symbol.toPrimitive"); + public static readonly JsSymbol ToStringTag = new("Symbol.toStringTag"); + public static readonly JsSymbol Unscopables = new("Symbol.unscopables"); // engine-specific created by scripts private Dictionary? _customSymbolLookup; diff --git a/Jint/Native/Symbol/SymbolConstructor.cs b/Jint/Native/Symbol/SymbolConstructor.cs index 5713f18050..5226cfeb71 100644 --- a/Jint/Native/Symbol/SymbolConstructor.cs +++ b/Jint/Native/Symbol/SymbolConstructor.cs @@ -36,7 +36,7 @@ protected override void Initialize() const PropertyFlag lengthFlags = PropertyFlag.Configurable; const PropertyFlag propertyFlags = PropertyFlag.AllForbidden; - var properties = new PropertyDictionary(15, checkExistingKeys: false) + var properties = new PropertyDictionary(17, checkExistingKeys: false) { ["for"] = new PropertyDescriptor(new ClrFunction(Engine, "for", For, 1, lengthFlags), PropertyFlag.Writable | PropertyFlag.Configurable), ["keyFor"] = new PropertyDescriptor(new ClrFunction(Engine, "keyFor", KeyFor, 1, lengthFlags), PropertyFlag.Writable | PropertyFlag.Configurable), @@ -52,7 +52,9 @@ protected override void Initialize() ["toPrimitive"] = new PropertyDescriptor(GlobalSymbolRegistry.ToPrimitive, propertyFlags), ["toStringTag"] = new PropertyDescriptor(GlobalSymbolRegistry.ToStringTag, propertyFlags), ["unscopables"] = new PropertyDescriptor(GlobalSymbolRegistry.Unscopables, propertyFlags), - ["asyncIterator"] = new PropertyDescriptor(GlobalSymbolRegistry.AsyncIterator, propertyFlags) + ["asyncIterator"] = new PropertyDescriptor(GlobalSymbolRegistry.AsyncIterator, propertyFlags), + ["dispose"] = new PropertyDescriptor(GlobalSymbolRegistry.Dispose, propertyFlags), + ["asyncDispose"] = new PropertyDescriptor(GlobalSymbolRegistry.AsyncDispose, propertyFlags), }; SetProperties(properties); } diff --git a/Jint/Runtime/Intrinsics.cs b/Jint/Runtime/Intrinsics.cs index 6a3f20ddfb..e268cb28d9 100644 --- a/Jint/Runtime/Intrinsics.cs +++ b/Jint/Runtime/Intrinsics.cs @@ -8,6 +8,7 @@ using Jint.Native.Boolean; using Jint.Native.DataView; using Jint.Native.Date; +using Jint.Native.Disposable; using Jint.Native.Error; using Jint.Native.FinalizationRegistry; using Jint.Native.Function; @@ -26,6 +27,7 @@ using Jint.Native.ShadowRealm; using Jint.Native.SharedArrayBuffer; using Jint.Native.String; +using Jint.Native.SuppressedError; using Jint.Native.Symbol; using Jint.Native.TypedArray; using Jint.Native.WeakMap; @@ -50,6 +52,7 @@ public sealed partial class Intrinsics // lazy properties private ThrowTypeError? _throwTypeError; private AggregateErrorConstructor? _aggregateError; + private SuppressedErrorConstructor? _suppressedError; private ErrorConstructor? _error; private ErrorConstructor? _evalError; private ErrorConstructor? _rangeError; @@ -107,6 +110,9 @@ public sealed partial class Intrinsics private ShadowRealmConstructor? _shadowRealm; + private AsyncDisposableStackConstructor? _asyncDisposableStack; + private DisposableStackConstructor? _disposableStack; + internal Intrinsics(Engine engine, Realm realm) { _engine = engine; @@ -141,6 +147,9 @@ internal Intrinsics(Engine engine, Realm realm) internal AggregateErrorConstructor AggregateError => _aggregateError ??= new AggregateErrorConstructor(_engine, _realm, Error); + internal SuppressedErrorConstructor SuppressedError => + _suppressedError ??= new SuppressedErrorConstructor(_engine, _realm, Error); + internal ArrayIteratorPrototype ArrayIteratorPrototype => _arrayIteratorPrototype ??= new ArrayIteratorPrototype(_engine, _realm, this.IteratorPrototype); @@ -293,4 +302,10 @@ internal Intrinsics(Engine engine, Realm realm) internal ThrowTypeError ThrowTypeError => _throwTypeError ??= new ThrowTypeError(_engine, _engine.Realm) { _prototype = _engine.Realm.Intrinsics.Function.PrototypeObject }; + + internal AsyncDisposableStackConstructor AsyncDisposableStack => + _asyncDisposableStack ??= new AsyncDisposableStackConstructor(_engine, _engine.Realm) { _prototype = _engine.Realm.Intrinsics.Function.PrototypeObject }; + + internal DisposableStackConstructor DisposableStack => + _disposableStack ??= new DisposableStackConstructor(_engine, _engine.Realm) { _prototype = _engine.Realm.Intrinsics.Function.PrototypeObject }; }