diff --git a/js/public/ErrorReport.h b/js/public/ErrorReport.h index 9bf824dea26a..9e76e50365be 100644 --- a/js/public/ErrorReport.h +++ b/js/public/ErrorReport.h @@ -62,24 +62,37 @@ enum ErrorArgumentsType { * using the generalized error reporting mechanism. (One side effect of this * type is to not prepend 'Error:' to warning messages.) This value can go away * if we ever decide to use an entirely separate mechanism for warnings. + * + * The errors and warnings are arranged in alphabetically within their + * respective categories as defined in the comments below. */ enum JSExnType { + // Generic Errors JSEXN_ERR, JSEXN_FIRST = JSEXN_ERR, + // Internal Errors JSEXN_INTERNALERR, + // ECMAScript Errors JSEXN_AGGREGATEERR, JSEXN_EVALERR, JSEXN_RANGEERR, JSEXN_REFERENCEERR, +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + JSEXN_SUPPRESSEDERR, +#endif JSEXN_SYNTAXERR, JSEXN_TYPEERR, JSEXN_URIERR, + // Debugger Errors JSEXN_DEBUGGEEWOULDRUN, + // WASM Errors JSEXN_WASMCOMPILEERROR, JSEXN_WASMLINKERROR, JSEXN_WASMRUNTIMEERROR, JSEXN_ERROR_LIMIT, + // Warnings JSEXN_WARN = JSEXN_ERROR_LIMIT, + // Error Notes JSEXN_NOTE, JSEXN_LIMIT }; diff --git a/js/public/ProtoKey.h b/js/public/ProtoKey.h index a472532f6add..7ebd48a03991 100644 --- a/js/public/ProtoKey.h +++ b/js/public/ProtoKey.h @@ -86,6 +86,8 @@ REAL(EvalError, ERROR_CLASP(JSEXN_EVALERR)) \ REAL(RangeError, ERROR_CLASP(JSEXN_RANGEERR)) \ REAL(ReferenceError, ERROR_CLASP(JSEXN_REFERENCEERR)) \ + IF_EXPLICIT_RESOURCE_MANAGEMENT( \ + REAL(SuppressedError, ERROR_CLASP(JSEXN_SUPPRESSEDERR))) \ REAL(SyntaxError, ERROR_CLASP(JSEXN_SYNTAXERR)) \ REAL(TypeError, ERROR_CLASP(JSEXN_TYPEERR)) \ REAL(URIError, ERROR_CLASP(JSEXN_URIERR)) \ diff --git a/js/public/friend/ErrorNumbers.msg b/js/public/friend/ErrorNumbers.msg index 810ffbd55419..85685813c605 100644 --- a/js/public/friend/ErrorNumbers.msg +++ b/js/public/friend/ErrorNumbers.msg @@ -959,4 +959,9 @@ MSG_DEF(JSMSG_TEMPORAL_PARSER_MONTH_DAY_CALENDAR_NOT_ISO8601, 0, JSEXN_RANGEERR, MSG_DEF(JSMSG_TEMPORAL_PARSER_YEAR_MONTH_CALENDAR_NOT_ISO8601, 0, JSEXN_RANGEERR, "Year-Month formats only support the \"iso8601\" calendar") MSG_DEF(JSMSG_TEMPORAL_PARSER_INVALID_SUBMINUTE_TIMEZONE, 0, JSEXN_RANGEERR, "time zone offset must not contain seconds precision") +// Explicit Resource Management + +// TODO: Improve the messaging for suppressed errors (Bug 1906150) +MSG_DEF(JSMSG_ERROR_WAS_SUPPRESSED, 0, IF_EXPLICIT_RESOURCE_MANAGEMENT(JSEXN_SUPPRESSEDERR, JSEXN_INTERNALERR), "An error is suppressed because another error happened while disposing an object") + //clang-format on diff --git a/js/src/frontend/ForOfLoopControl.cpp b/js/src/frontend/ForOfLoopControl.cpp index 590daeb75e2f..e6d13b1ff692 100644 --- a/js/src/frontend/ForOfLoopControl.cpp +++ b/js/src/frontend/ForOfLoopControl.cpp @@ -38,7 +38,12 @@ bool ForOfLoopControl::emitBeginCodeNeedingIteratorClose(BytecodeEmitter* bce) { } bool ForOfLoopControl::emitEndCodeNeedingIteratorClose(BytecodeEmitter* bce) { - if (!tryCatch_->emitCatch(TryEmitter::ExceptionStack::Yes)) { + if (!tryCatch_->emitCatch(TryEmitter::ExceptionStack::Yes +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + , + TryEmitter::ForForOfIteratorClose::Yes +#endif + )) { // [stack] ITER ... EXCEPTION STACK return false; } @@ -48,15 +53,7 @@ bool ForOfLoopControl::emitEndCodeNeedingIteratorClose(BytecodeEmitter* bce) { // [stack] ITER ... EXCEPTION STACK ITER return false; } -#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT - // Explicit Resource Management Proposal - // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset - // Step 9.i.i.1 Set result to - // Completion(DisposeResources(iterationEnv.[[DisposeCapability]], result)). - if (!bce->innermostEmitterScope()->prepareForForOfIteratorCloseOnThrow()) { - return false; - } -#endif + if (!emitIteratorCloseInInnermostScopeWithTryNote(bce, CompletionKind::Throw)) { return false; // ITER ... EXCEPTION STACK diff --git a/js/src/frontend/TryEmitter.cpp b/js/src/frontend/TryEmitter.cpp index 20fb29db637c..2b79807b336a 100644 --- a/js/src/frontend/TryEmitter.cpp +++ b/js/src/frontend/TryEmitter.cpp @@ -9,9 +9,12 @@ #include "mozilla/Assertions.h" // MOZ_ASSERT #include "frontend/BytecodeEmitter.h" // BytecodeEmitter -#include "frontend/IfEmitter.h" // BytecodeEmitter -#include "frontend/SharedContext.h" // StatementKind -#include "vm/Opcodes.h" // JSOp +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT +# include "frontend/EmitterScope.h" // EmitterScope +#endif +#include "frontend/IfEmitter.h" // BytecodeEmitter +#include "frontend/SharedContext.h" // StatementKind +#include "vm/Opcodes.h" // JSOp using namespace js; using namespace js::frontend; @@ -94,7 +97,12 @@ bool TryEmitter::emitTryEnd() { return true; } -bool TryEmitter::emitCatch(ExceptionStack stack) { +bool TryEmitter::emitCatch(ExceptionStack stack +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + , + ForForOfIteratorClose forForOfIteratorClose +#endif +) { MOZ_ASSERT(state_ == State::Try); if (!emitTryEnd()) { return false; @@ -115,6 +123,18 @@ bool TryEmitter::emitCatch(ExceptionStack stack) { } } +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + // Explicit Resource Management Proposal + // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset + // Step 9.i.i.1 Set result to + // Completion(DisposeResources(iterationEnv.[[DisposeCapability]], result)). + if (forForOfIteratorClose == ForForOfIteratorClose::Yes) { + if (!bce_->innermostEmitterScope()->prepareForForOfIteratorCloseOnThrow()) { + return false; + } + } +#endif + if (stack == ExceptionStack::No) { if (!bce_->emit1(JSOp::Exception)) { return false; diff --git a/js/src/frontend/TryEmitter.h b/js/src/frontend/TryEmitter.h index b74157bb0e7c..6f0986caed29 100644 --- a/js/src/frontend/TryEmitter.h +++ b/js/src/frontend/TryEmitter.h @@ -216,7 +216,23 @@ class MOZ_STACK_CLASS TryEmitter { Yes, }; - [[nodiscard]] bool emitCatch(ExceptionStack stack = ExceptionStack::No); +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + enum class ForForOfIteratorClose : bool { + No, + /** + * Emit additional code for the ForOfIteratorClose operation. + */ + Yes, + }; +#endif + + [[nodiscard]] bool emitCatch( + ExceptionStack stack = ExceptionStack::No +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + , + ForForOfIteratorClose forForOfIteratorClose = ForForOfIteratorClose::No +#endif + ); // If `finallyPos` is specified, it's an offset of the finally block's // "{" character in the source code text, to improve line:column number in diff --git a/js/src/frontend/UsingEmitter.cpp b/js/src/frontend/UsingEmitter.cpp index bad5277384eb..cbad37adff5c 100644 --- a/js/src/frontend/UsingEmitter.cpp +++ b/js/src/frontend/UsingEmitter.cpp @@ -6,6 +6,7 @@ #include "frontend/BytecodeEmitter.h" #include "frontend/EmitterScope.h" +#include "vm/DisposeJumpKind.h" using namespace js; using namespace js::frontend; @@ -36,7 +37,8 @@ bool UsingEmitter::prepareForAssignment(Kind kind) { bool UsingEmitter::prepareForForOfLoopIteration() { MOZ_ASSERT(bce_->innermostEmitterScopeNoCheck()->hasDisposables()); - if (!bce_->emit1(JSOp::DisposeDisposables)) { + if (!bce_->emit2(JSOp::DisposeDisposables, + uint8_t(DisposeJumpKind::JumpOnError))) { return false; } return true; @@ -44,7 +46,8 @@ bool UsingEmitter::prepareForForOfLoopIteration() { bool UsingEmitter::prepareForForOfIteratorCloseOnThrow() { MOZ_ASSERT(bce_->innermostEmitterScopeNoCheck()->hasDisposables()); - if (!bce_->emit1(JSOp::DisposeDisposables)) { + if (!bce_->emit2(JSOp::DisposeDisposables, + uint8_t(DisposeJumpKind::NoJumpOnError))) { return false; } return true; @@ -52,7 +55,8 @@ bool UsingEmitter::prepareForForOfIteratorCloseOnThrow() { bool UsingEmitter::emitNonLocalJump(EmitterScope* present) { MOZ_ASSERT(present->hasDisposables()); - if (!bce_->emit1(JSOp::DisposeDisposables)) { + if (!bce_->emit2(JSOp::DisposeDisposables, + uint8_t(DisposeJumpKind::JumpOnError))) { return false; } return true; @@ -67,7 +71,8 @@ bool UsingEmitter::emitEnd() { return false; } - if (!bce_->emit1(JSOp::DisposeDisposables)) { + if (!bce_->emit2(JSOp::DisposeDisposables, + uint8_t(DisposeJumpKind::JumpOnError))) { return false; } diff --git a/js/src/jit-test/lib/asserts.js b/js/src/jit-test/lib/asserts.js index f7a3cfabd8bf..314344892047 100644 --- a/js/src/jit-test/lib/asserts.js +++ b/js/src/jit-test/lib/asserts.js @@ -94,3 +94,45 @@ if (typeof assertArrayEq === 'undefined') { } }; } + +if (typeof assertSuppressionChain === 'undefined' && typeof globalThis.SuppressedError !== 'undefined') { + + function errorChainVerificationHelper(err, suppressions, verifier) { + let i = 0; + while (err instanceof SuppressedError) { + assertEq(verifier(err.error, suppressions[i]), true); + err = err.suppressed; + i++; + } + assertEq(verifier(err, suppressions[i]), true); + assertEq(i, suppressions.length - 1); + } + + var assertSuppressionChain = function assertSuppressionChain(fn, suppressions) { + let caught = false; + try { + fn(); + } catch (err) { + caught = true; + errorChainVerificationHelper(err, suppressions, function(err, suppression) { + return err === suppression; + }); + } finally { + assertEq(caught, true); + } + } + + var assertSuppressionChainErrorMessages = function assertSuppressionChainErrorMessages(fn, suppressions) { + let caught = false; + try { + fn(); + } catch (err) { + caught = true; + errorChainVerificationHelper(err, suppressions, function(err, suppression) { + return err instanceof suppression.ctor && err.message === suppression.message; + }); + } finally { + assertEq(caught, true); + } + } +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-async-generators.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-async-generators.js new file mode 100644 index 000000000000..0862ce6ea01a --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-async-generators.js @@ -0,0 +1,65 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const values = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + async function* gen() { + using d = { + value: "d", + [Symbol.dispose]() { + values.push(this.value); + } + } + yield await Promise.resolve("a"); + yield await Promise.resolve("b"); + using c = { + value: "c", + [Symbol.dispose]() { + values.push(this.value); + throw errorsToThrow[0]; // This error will suppress the error thrown below. + } + } + throw errorsToThrow[1]; // This error will be suppressed during disposal. + } + + async function testDisposeWithThrowAndPendingException() { + let x = gen(); + values.push((await x.next()).value); + values.push((await x.next()).value); + await x.next(); + } + let e = null; + testDisposeWithThrowAndPendingException().catch((err) => { e = err; }); + drainJobQueue(); + assertSuppressionChain(() => { throw e; }, errorsToThrow); + assertArrayEq(values, ["a", "b", "c", "d"]); +} + +{ + const values = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + async function* gen() { + using c = { + value: "c", + [Symbol.dispose]() { + values.push(this.value); + throw errorsToThrow[0]; + } + } + yield await Promise.resolve("a"); + yield await Promise.resolve("b"); + return; + } + async function testDisposeWithThrowAndForcedThrowInAsyncGenerator() { + let x = gen(); + values.push((await x.next()).value); + await x.throw(errorsToThrow[1]); + } + let e = null; + testDisposeWithThrowAndForcedThrowInAsyncGenerator().catch((err) => { e = err; }); + drainJobQueue(); + assertSuppressionChain(() => { throw e; }, errorsToThrow); + assertArrayEq(values, ["a", "c"]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-diff-global.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-diff-global.js new file mode 100644 index 000000000000..571268df8620 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-diff-global.js @@ -0,0 +1,31 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const disposed = []; + const g1 = newGlobal({ newCompartment: true }); + const g2 = newGlobal({ newCompartment: true }); + function testDifferentGlobalErrors() { + const g1Error = g1.evaluate(`new Error("g1")`); + const g2Error = g2.evaluate(`new Error("g2")`); + using x = { + [Symbol.dispose]() { + disposed.push(1); + throw g1Error; + } + } + using y = { + [Symbol.dispose]() { + disposed.push(2); + throw g2Error; + } + } + throw new Error("g"); + } + assertSuppressionChainErrorMessages(testDifferentGlobalErrors, [ + {ctor: g1.evaluate('Error'), message: 'g1'}, + {ctor: g2.evaluate('Error'), message: 'g2'}, + {ctor: Error, message: 'g'}, + ]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-generators.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-generators.js new file mode 100644 index 000000000000..46156278b773 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-generators.js @@ -0,0 +1,59 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const values = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function* gen() { + using d = { + value: "d", + [Symbol.dispose]() { + values.push(this.value); + } + } + yield "a"; + yield "b"; + using c = { + value: "c", + [Symbol.dispose]() { + values.push(this.value); + throw errorsToThrow[0]; // This error will suppress the error thrown below. + } + } + throw errorsToThrow[1]; // This error will be suppressed during disposal. + } + assertSuppressionChain(() => { + let x = gen(); + values.push(x.next().value); + values.push(x.next().value); + x.next(); + }, errorsToThrow); + + assertArrayEq(values, ["a", "b", "c", "d"]); +} + +{ + const values = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function* gen() { + using c = { + value: "c", + [Symbol.dispose]() { + values.push(this.value); + throw errorsToThrow[0]; + } + } + yield "a"; + yield "b"; + return; + } + + assertSuppressionChain(() => { + let x = gen(); + values.push(x.next().value); + x.throw(errorsToThrow[1]); // This error will be suppressed during disposal. + }, errorsToThrow); + + assertArrayEq(values, ["a", "c"]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-loop.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-loop.js new file mode 100644 index 000000000000..0d80ad4deea6 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-loop.js @@ -0,0 +1,116 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function testDisposedWithThrowInOrdinaryLoop() { + const disposables = [ + { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + } + ]; + for (let i = 0; i < 5; i++) { + using x = disposables[i]; + throw errorsToThrow[1]; + } + } + assertSuppressionChain(testDisposedWithThrowInOrdinaryLoop, errorsToThrow); + assertArrayEq(disposed, [1]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function testDisposedWithThrowInLoopRequiringIteratorClose() { + const disposables = [ + { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + } + ] + for (const d of disposables) { + using x = d; + throw errorsToThrow[1]; + } + } + assertSuppressionChain(testDisposedWithThrowInLoopRequiringIteratorClose, errorsToThrow); + assertArrayEq(disposed, [1]); +} + +{ + const values = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function testDisposedWithThrowInLoopWithCustomIterable() { + const disposables = [ + { + val: "a", + [Symbol.dispose]() { + values.push(this.val); + } + }, + { + val: "b", + [Symbol.dispose]() { + values.push(this.val); + throw errorsToThrow[0]; + } + }, + ]; + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + if (i === disposables.length) { + return { done: true }; + } + return { value: disposables[i++], done: false }; + }, + return() { + values.push("return"); + return { done: true }; + } + } + } + } + for (using d of iterable) { + if (d.val === "b") { + throw errorsToThrow[1]; + } + } + } + assertSuppressionChain(testDisposedWithThrowInLoopWithCustomIterable, errorsToThrow); + assertArrayEq(values, ["a", "b", "return"]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2"), new Error("test3"), new Error("test4")]; + function testDisposeWithThrowInForOfLoop() { + const d1 = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + } + const d2 = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + } + + for (using d of [d1, d2]) { + throw errorsToThrow[2]; + } + } + assertSuppressionChain(testDisposeWithThrowInForOfLoop, [errorsToThrow[0], errorsToThrow[2]]); + assertArrayEq(disposed, [1]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-non-Error.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-non-Error.js new file mode 100644 index 000000000000..8c91a1f4e345 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-non-Error.js @@ -0,0 +1,62 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const disposed = []; + const throwObject = { message: 'Hello world' }; + function testNonErrorObjectThrowsDuringDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + throw 1; + } + } + using y = { + [Symbol.dispose]() { + disposed.push(2); + throw "test"; + } + } + using z = { + [Symbol.dispose]() { + throw null; + } + } + + throw throwObject; + } + + assertSuppressionChain(testNonErrorObjectThrowsDuringDispose, [ + 1, "test", null, throwObject + ]); + assertArrayEq(disposed, [2, 1]); +} + +{ + const disposed = []; + class SubError extends Error { + constructor(num) { + super(); + this.name = 'SubError'; + this.ident = num; + } + } + const errorsToThrow = [new SubError(1), new SubError(2)]; + function testCustomErrorObjectThrowsDuringDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + } + using y = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + } + } + assertSuppressionChain(testCustomErrorObjectThrowsDuringDispose, errorsToThrow); + assertArrayEq(disposed, [2, 1]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-runtime-errors.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-runtime-errors.js new file mode 100644 index 000000000000..3168056ff278 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-runtime-errors.js @@ -0,0 +1,140 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const disposed = []; + function testUndefinedAccessSuppressedErrorWithThrowInDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + undefined.x; + } + } + } + assertSuppressionChainErrorMessages(testUndefinedAccessSuppressedErrorWithThrowInDispose, [{ctor: TypeError, message: "can't access property \"x\" of undefined"}]); + assertArrayEq(disposed, [1]); +} + +{ + const disposed = []; + function testReferenceErrorSuppressedErrorWithThrowInDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + y.x; + } + } + } + assertSuppressionChainErrorMessages(testReferenceErrorSuppressedErrorWithThrowInDispose, [{ctor: ReferenceError, message: "y is not defined"}]); +} + +{ + const disposed = []; + function testMultipleRuntimeErrorsWithThrowsDuringDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + undefined.x; + } + } + using y = { + [Symbol.dispose]() { + disposed.push(2); + a.x; + } + } + using z = { + [Symbol.dispose]() { + this[Symbol.dispose](); + } + } + } + assertSuppressionChainErrorMessages(testMultipleRuntimeErrorsWithThrowsDuringDispose, [ + {ctor: TypeError, message: "can't access property \"x\" of undefined"}, + {ctor: ReferenceError, message: "a is not defined"}, + {ctor: InternalError, message: "too much recursion"}, + ]); +} + +{ + const disposed = []; + function testMultipleRuntimeErrorsWithThrowsDuringDisposeAndOutsideDispose() { + using x = { + [Symbol.dispose]() { + disposed.push(1); + undefined.x; + } + } + using y = { + [Symbol.dispose]() { + disposed.push(2); + a.x; + } + } + using z = { + [Symbol.dispose]() { + this[Symbol.dispose](); + } + } + + null.x; + } + assertSuppressionChainErrorMessages(testMultipleRuntimeErrorsWithThrowsDuringDisposeAndOutsideDispose, [ + {ctor: TypeError, message: "can't access property \"x\" of undefined"}, + {ctor: ReferenceError, message: "a is not defined"}, + {ctor: InternalError, message: "too much recursion"}, + {ctor: TypeError, message: "can't access property \"x\" of null"}, + ]); +} + +{ + const values = []; + function* gen() { + using d = { + value: "d", + [Symbol.dispose]() { + values.push(this.value); + } + } + yield "a"; + yield "b"; + using c = { + value: "c", + [Symbol.dispose]() { + values.push(this.value); + a.x; + } + } + null.x; + } + function testRuntimeErrorsWithGenerators() { + let x = gen(); + values.push(x.next().value); + values.push(x.next().value); + x.next(); + } + assertSuppressionChainErrorMessages(testRuntimeErrorsWithGenerators, [ + {ctor: ReferenceError, message: "a is not defined"}, + {ctor: TypeError, message: "can't access property \"x\" of null"} + ]); +} + +{ + const disposed = []; + const d = { + [Symbol.dispose]() { + disposed.push(1); + null.x; + } + } + function testRuntimeErrorWithLoops() { + for (using x of [d]) { + y.a; + } + } + assertSuppressionChainErrorMessages(testRuntimeErrorWithLoops, [ + {ctor: TypeError, message: "can't access property \"x\" of null"}, + {ctor: ReferenceError, message: "y is not defined"}, + ]); +} diff --git a/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-scopes.js b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-scopes.js new file mode 100644 index 000000000000..f6b03d6b6728 --- /dev/null +++ b/js/src/jit-test/tests/explicit-resource-management/suppressed-error-handling-scopes.js @@ -0,0 +1,243 @@ +// |jit-test| skip-if: !getBuildConfiguration("explicit-resource-management") + +load(libdir + "asserts.js"); + +{ + const disposed = []; + function testExceptionBeforeDispose() { + throw new Error("test"); + using d = { + [Symbol.dispose]() { + disposed.push(1); + } + }; + } + assertThrowsInstanceOf(testExceptionOutsideDispose, Error); + assertEq(disposed.length, 0); +} + +{ + const disposed = []; + function testExceptionOutsideDispose() { + using d = { + [Symbol.dispose]() { + disposed.push(1); + } + }; + + throw new Error("test"); + } + assertThrowsInstanceOf(testExceptionOutsideDispose, Error); + assertArrayEq(disposed, [1]); +} + +{ + const disposed = []; + function testExceptionInsideDispose() { + using d = { + [Symbol.dispose]() { + disposed.push(1); + throw new Error("test"); + } + }; + } + assertThrowsInstanceOf(testExceptionInsideDispose, Error); + assertArrayEq(disposed, [1]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2")]; + function testExceptionInsideAndOutsideDispose() { + using d = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + + throw errorsToThrow[1]; + } + assertSuppressionChain(testExceptionInsideAndOutsideDispose, errorsToThrow); + assertArrayEq(disposed, [1]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2"), new Error("test3")]; + function testMultipleDisposeWithException() { + using d1 = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + using d2 = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + }; + using d3 = { + [Symbol.dispose]() { + disposed.push(3); + throw errorsToThrow[2]; + } + } + } + assertSuppressionChain(testMultipleDisposeWithException, errorsToThrow); + assertArrayEq(disposed, [3, 2, 1]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2"), new Error("test3"), new Error("test4")]; + function testMultipleDisposeWithThrowsAndOutsideThrow() { + using d1 = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + using d2 = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + }; + using d3 = { + [Symbol.dispose]() { + disposed.push(3); + throw errorsToThrow[2]; + } + } + throw errorsToThrow[3]; + } + assertSuppressionChain(testMultipleDisposeWithThrowsAndOutsideThrow, errorsToThrow); + assertArrayEq(disposed, [3, 2, 1]); +} + +{ + const disposed = []; + const errorsToThrow = [new Error("test1"), new Error("test2"), new Error("test3")]; + function testDisposeWithThrowInAnInnerScope() { + using d1 = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + { + using d2 = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + }; + { + let a = 0, b = () => a; + throw errorsToThrow[2]; + } + } + } + assertSuppressionChain(testDisposeWithThrowInAnInnerScope, errorsToThrow); + assertArrayEq(disposed, [2, 1]); +} + +{ + let disposed = []; + const errorsToThrow = [new Error('test1'), new Error('test2'), new Error('test3')]; + function testDisposeWithThrowInSwitchCase(cs) { + switch (cs) { + case "only_1_dispose": + using x = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + } + break; + case "2_dispose": + using y = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + using z = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + }; + break; + case "dispose_with_outside": + using a = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + using b = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + }; + throw errorsToThrow[2]; + break; + case 'fallthrough': + using aa = { + [Symbol.dispose]() { + disposed.push(1); + throw errorsToThrow[0]; + } + }; + case 'fall': + using bb = { + [Symbol.dispose]() { + disposed.push(2); + throw errorsToThrow[1]; + } + } + throw errorsToThrow[2]; + } + } + assertThrowsInstanceOf(() => testDisposeWithThrowInSwitchCase('only_1_dispose'), Error); + assertArrayEq(disposed, [1]); + disposed = []; + assertSuppressionChain(() => testDisposeWithThrowInSwitchCase('2_dispose'), [errorsToThrow[0], errorsToThrow[1]]); + assertArrayEq(disposed, [2,1]); + disposed = []; + assertSuppressionChain(() => testDisposeWithThrowInSwitchCase('dispose_with_outside'), errorsToThrow); + assertArrayEq(disposed, [2,1]); + disposed = []; + assertSuppressionChain(() => testDisposeWithThrowInSwitchCase('fallthrough'), errorsToThrow); + assertArrayEq(disposed, [2,1]); +} + +{ + globalThis.disposedModule = []; + globalThis.errorsToThrowModule = [new Error('test1'), new Error('test2'), new Error('test3')]; + const m = parseModule(` + using x = { + [Symbol.dispose]() { + globalThis.disposedModule.push(1); + throw globalThis.errorsToThrowModule[0]; + } + } + using y = { + [Symbol.dispose]() { + globalThis.disposedModule.push(2); + throw globalThis.errorsToThrowModule[1]; + } + } + throw globalThis.errorsToThrowModule[2]; + `); + moduleLink(m); + let e = null; + moduleEvaluate(m).catch((err) => { e = err; }); + drainJobQueue(); + assertSuppressionChain(() => { throw e; }, globalThis.errorsToThrowModule); + assertArrayEq(globalThis.disposedModule, [2, 1]); +} diff --git a/js/src/jsexn.h b/js/src/jsexn.h index 5e681248c75d..7469f4e4f23c 100644 --- a/js/src/jsexn.h +++ b/js/src/jsexn.h @@ -77,6 +77,9 @@ static_assert( JSProto_Error + int(JSEXN_EVALERR) == JSProto_EvalError && JSProto_Error + int(JSEXN_RANGEERR) == JSProto_RangeError && JSProto_Error + int(JSEXN_REFERENCEERR) == JSProto_ReferenceError && +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + JSProto_Error + int(JSEXN_SUPPRESSEDERR) == JSProto_SuppressedError && +#endif JSProto_Error + int(JSEXN_SYNTAXERR) == JSProto_SyntaxError && JSProto_Error + int(JSEXN_TYPEERR) == JSProto_TypeError && JSProto_Error + int(JSEXN_URIERR) == JSProto_URIError && diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index b4eb801ce33d..b2fd434de366 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -175,6 +175,7 @@ MACRO_(enumerate, "enumerate") \ MACRO_(era, "era") \ MACRO_(eraYear, "eraYear") \ + IF_EXPLICIT_RESOURCE_MANAGEMENT(MACRO_(error, "error")) \ MACRO_(errors, "errors") \ MACRO_(ErrorToStringWithTrailingNewline, "ErrorToStringWithTrailingNewline") \ MACRO_(escape, "escape") \ @@ -542,6 +543,7 @@ MACRO_(StructType, "StructType") \ MACRO_(style, "style") \ MACRO_(super, "super") \ + IF_EXPLICIT_RESOURCE_MANAGEMENT(MACRO_(suppressed, "suppressed")) \ MACRO_(switch_, "switch") \ MACRO_(symmetricDifference, "symmetricDifference") \ MACRO_(target, "target") \ diff --git a/js/src/vm/DisposeJumpKind.h b/js/src/vm/DisposeJumpKind.h new file mode 100644 index 000000000000..562ab971fefb --- /dev/null +++ b/js/src/vm/DisposeJumpKind.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef vm_DisposeJumpKind_h +#define vm_DisposeJumpKind_h + +#include // uint8_t + +namespace js { + +enum class DisposeJumpKind : uint8_t { + /* + * Jump out of interpreter Loop to error handling code + * if there was an exception during the Dispose Operation. + */ + JumpOnError, + /* + * Do not jump out of the interpreter loop to error handling + * even if there are errors pending. + */ + NoJumpOnError, +}; + +} // namespace js + +#endif /* vm_DisposeJumpKind_h */ diff --git a/js/src/vm/ErrorObject.cpp b/js/src/vm/ErrorObject.cpp index 193733d67a60..e872e7b02831 100644 --- a/js/src/vm/ErrorObject.cpp +++ b/js/src/vm/ErrorObject.cpp @@ -59,12 +59,10 @@ using namespace js; -#define IMPLEMENT_ERROR_PROTO_CLASS(name) \ - { \ - #name ".prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_##name), \ - JS_NULL_CLASS_OPS, \ - &ErrorObject::classSpecs[JSProto_##name - JSProto_Error] \ - } +#define IMPLEMENT_ERROR_PROTO_CLASS(name) \ + {#name ".prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_##name), \ + JS_NULL_CLASS_OPS, \ + &ErrorObject::classSpecs[JSProto_##name - JSProto_Error]} const JSClass ErrorObject::protoClasses[JSEXN_ERROR_LIMIT] = { IMPLEMENT_ERROR_PROTO_CLASS(Error), @@ -74,6 +72,9 @@ const JSClass ErrorObject::protoClasses[JSEXN_ERROR_LIMIT] = { IMPLEMENT_ERROR_PROTO_CLASS(EvalError), IMPLEMENT_ERROR_PROTO_CLASS(RangeError), IMPLEMENT_ERROR_PROTO_CLASS(ReferenceError), +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + IMPLEMENT_ERROR_PROTO_CLASS(SuppressedError), +#endif IMPLEMENT_ERROR_PROTO_CLASS(SyntaxError), IMPLEMENT_ERROR_PROTO_CLASS(TypeError), IMPLEMENT_ERROR_PROTO_CLASS(URIError), @@ -109,6 +110,9 @@ IMPLEMENT_NATIVE_ERROR_PROPERTIES(AggregateError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(EvalError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(RangeError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(ReferenceError) +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT +IMPLEMENT_NATIVE_ERROR_PROPERTIES(SuppressedError) +#endif IMPLEMENT_NATIVE_ERROR_PROPERTIES(SyntaxError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(TypeError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(URIError) @@ -117,18 +121,25 @@ IMPLEMENT_NATIVE_ERROR_PROPERTIES(CompileError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(LinkError) IMPLEMENT_NATIVE_ERROR_PROPERTIES(RuntimeError) -#define IMPLEMENT_NATIVE_ERROR_SPEC(name) \ - { \ - ErrorObject::createConstructor, ErrorObject::createProto, nullptr, \ - nullptr, nullptr, name##_properties, nullptr, JSProto_Error \ - } +#define IMPLEMENT_NATIVE_ERROR_SPEC(name) \ + {ErrorObject::createConstructor, \ + ErrorObject::createProto, \ + nullptr, \ + nullptr, \ + nullptr, \ + name##_properties, \ + nullptr, \ + JSProto_Error} -#define IMPLEMENT_NONGLOBAL_ERROR_SPEC(name) \ - { \ - ErrorObject::createConstructor, ErrorObject::createProto, nullptr, \ - nullptr, nullptr, name##_properties, nullptr, \ - JSProto_Error | ClassSpec::DontDefineConstructor \ - } +#define IMPLEMENT_NONGLOBAL_ERROR_SPEC(name) \ + {ErrorObject::createConstructor, \ + ErrorObject::createProto, \ + nullptr, \ + nullptr, \ + nullptr, \ + name##_properties, \ + nullptr, \ + JSProto_Error | ClassSpec::DontDefineConstructor} const ClassSpec ErrorObject::classSpecs[JSEXN_ERROR_LIMIT] = { {ErrorObject::createConstructor, ErrorObject::createProto, nullptr, nullptr, @@ -139,6 +150,9 @@ const ClassSpec ErrorObject::classSpecs[JSEXN_ERROR_LIMIT] = { IMPLEMENT_NATIVE_ERROR_SPEC(EvalError), IMPLEMENT_NATIVE_ERROR_SPEC(RangeError), IMPLEMENT_NATIVE_ERROR_SPEC(ReferenceError), +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + IMPLEMENT_NATIVE_ERROR_SPEC(SuppressedError), +#endif IMPLEMENT_NATIVE_ERROR_SPEC(SyntaxError), IMPLEMENT_NATIVE_ERROR_SPEC(TypeError), IMPLEMENT_NATIVE_ERROR_SPEC(URIError), @@ -148,15 +162,13 @@ const ClassSpec ErrorObject::classSpecs[JSEXN_ERROR_LIMIT] = { IMPLEMENT_NONGLOBAL_ERROR_SPEC(LinkError), IMPLEMENT_NONGLOBAL_ERROR_SPEC(RuntimeError)}; -#define IMPLEMENT_ERROR_CLASS_CORE(name, reserved_slots) \ - { \ - #name, \ - JSCLASS_HAS_CACHED_PROTO(JSProto_##name) | \ - JSCLASS_HAS_RESERVED_SLOTS(reserved_slots) | \ - JSCLASS_BACKGROUND_FINALIZE, \ - &ErrorObjectClassOps, \ - &ErrorObject::classSpecs[JSProto_##name - JSProto_Error] \ - } +#define IMPLEMENT_ERROR_CLASS_CORE(name, reserved_slots) \ + {#name, \ + JSCLASS_HAS_CACHED_PROTO(JSProto_##name) | \ + JSCLASS_HAS_RESERVED_SLOTS(reserved_slots) | \ + JSCLASS_BACKGROUND_FINALIZE, \ + &ErrorObjectClassOps, \ + &ErrorObject::classSpecs[JSProto_##name - JSProto_Error]} #define IMPLEMENT_ERROR_CLASS(name) \ IMPLEMENT_ERROR_CLASS_CORE(name, ErrorObject::RESERVED_SLOTS) @@ -187,6 +199,9 @@ const JSClass ErrorObject::classes[JSEXN_ERROR_LIMIT] = { IMPLEMENT_ERROR_CLASS_MAYBE_WASM_TRAP(InternalError), IMPLEMENT_ERROR_CLASS(AggregateError), IMPLEMENT_ERROR_CLASS(EvalError), IMPLEMENT_ERROR_CLASS(RangeError), IMPLEMENT_ERROR_CLASS(ReferenceError), +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + IMPLEMENT_ERROR_CLASS(SuppressedError), +#endif IMPLEMENT_ERROR_CLASS(SyntaxError), IMPLEMENT_ERROR_CLASS(TypeError), IMPLEMENT_ERROR_CLASS(URIError), // These Error subclasses are not accessible via the global object: @@ -293,6 +308,11 @@ static bool Error(JSContext* cx, unsigned argc, Value* vp) { MOZ_ASSERT(exnType != JSEXN_AGGREGATEERR, "AggregateError has its own constructor function"); +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + MOZ_ASSERT(exnType != JSEXN_SUPPRESSEDERR, + "SuppressedError has its own constuctor function"); +#endif + JSProtoKey protoKey = JSCLASS_CACHED_PROTO_KEY(&ErrorObject::classes[exnType]); @@ -358,6 +378,60 @@ static bool AggregateError(JSContext* cx, unsigned argc, Value* vp) { return true; } +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT +// Explicit Resource Management Proposal +// SuppressedError ( error, suppressed, message ) +// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-suppressederror +static bool SuppressedError(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + mozilla::DebugOnly exnType = + JSExnType(args.callee().as().getExtendedSlot(0).toInt32()); + + MOZ_ASSERT(exnType == JSEXN_SUPPRESSEDERR); + + // Step 1. If NewTarget is undefined, let newTarget be the active function + // object; else let newTarget be NewTarget. + // Step 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, + // "%SuppressedError.prototype%", « [[ErrorData]] »). + JS::Rooted proto(cx); + + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_SuppressedError, + &proto)) { + return false; + } + + // Step 3. If message is not undefined, then + // Step 3.a. Let messageString be ? ToString(message). + // Step 3.b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", + // messageString). + JS::Rooted obj( + cx, CreateErrorObject(cx, args, 2, JSEXN_SUPPRESSEDERR, proto)); + + if (!obj) { + return false; + } + + // Step 4. Perform CreateNonEnumerableDataPropertyOrThrow(O, "error", error). + JS::Rooted errorVal(cx, args.get(0)); + if (!NativeDefineDataProperty(cx, obj, cx->names().error, errorVal, 0)) { + return false; + } + + // Step 5. Perform CreateNonEnumerableDataPropertyOrThrow(O, "suppressed", + // suppressed). + JS::Rooted suppressedVal(cx, args.get(1)); + if (!NativeDefineDataProperty(cx, obj, cx->names().suppressed, suppressedVal, + 0)) { + return false; + } + + // Step 6. Return O. + args.rval().setObject(*obj); + return true; +} +#endif + /* static */ JSObject* ErrorObject::createProto(JSContext* cx, JSProtoKey key) { JSExnType type = ExnTypeFromProtoKey(key); @@ -397,7 +471,14 @@ JSObject* ErrorObject::createConstructor(JSContext* cx, JSProtoKey key) { if (type == JSEXN_AGGREGATEERR) { native = AggregateError; nargs = 2; - } else { + } +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + else if (type == JSEXN_SUPPRESSEDERR) { + native = SuppressedError; + nargs = 3; + } +#endif + else { native = Error; nargs = 1; } diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 2a29ff60d1cd..0ce24cb8edd8 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -139,6 +139,9 @@ bool GlobalObject::skipDeselectedConstructor(JSContext* cx, JSProtoKey key) { case JSProto_Error: case JSProto_InternalError: case JSProto_AggregateError: +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT + case JSProto_SuppressedError: +#endif case JSProto_EvalError: case JSProto_RangeError: case JSProto_ReferenceError: diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 3b2c2dfc9201..7a2196967007 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -45,7 +45,11 @@ #include "vm/AsyncFunction.h" #include "vm/AsyncIteration.h" #include "vm/BigIntType.h" -#include "vm/BytecodeUtil.h" // JSDVG_SEARCH_STACK +#include "vm/BytecodeUtil.h" // JSDVG_SEARCH_STACK +#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT +# include "vm/DisposeJumpKind.h" +# include "vm/ErrorObject.h" +#endif #include "vm/EqualityOperations.h" // js::StrictlyEqual #include "vm/GeneratorObject.h" #include "vm/Iteration.h" @@ -1745,6 +1749,49 @@ bool js::CreateDisposableResource(JSContext* cx, JS::Handle obj, return true; } +// Explicit Resource Management Proposal +// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources +// Steps 3.e.iii.1.c-e. +ErrorObject* js::CreateSuppressedError(JSContext* cx, + JS::Handle error, + JS::Handle suppressed) { + // Step 3.e.iii.1.c. Let error be a newly created SuppressedError object. + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_ERROR_WAS_SUPPRESSED); + + JS::Rooted thrownSuppressed(cx); + + if (!cx->getPendingException(&thrownSuppressed)) { + return nullptr; + } + + cx->clearPendingException(); + + JS::Rooted errorObj( + cx, &thrownSuppressed.toObject().as()); + + // Step 3.e.iii.1.d. Perform + // CreateNonEnumerableDataPropertyOrThrow(error, "error", result). + if (!NativeDefineDataProperty(cx, errorObj, cx->names().error, error, 0)) { + return nullptr; + } + + // Step 3.e.iii.1.e. Perform + // CreateNonEnumerableDataPropertyOrThrow(error, "suppressed", + // suppressed). + if (!NativeDefineDataProperty(cx, errorObj, cx->names().suppressed, + suppressed, 0)) { + return nullptr; + } + + // TODO: Improve the capturing of stack and error messages (Bug 1906150) + + return errorObj; +} + +// Explicit Resource Management Proposal +// DisposeResources ( disposeCapability, completion ) +// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources bool js::DisposeDisposablesOnScopeLeave(JSContext* cx, JS::Handle env) { if (!env->is() && @@ -1765,13 +1812,8 @@ bool js::DisposeDisposablesOnScopeLeave(JSContext* cx, uint32_t index = disposables->length(); + // hadError and latestException correspond to the completion value. bool hadError = false; - - // Explicit Resource Management Proposal - // DisposeResources ( disposeCapability, completion ) - // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources - // Step 1. For each element resource of - // disposeCapability.[[DisposableResourceStack]], in reverse list order, do JS::Rooted latestException(cx); if (cx->isExceptionPending()) { @@ -1782,67 +1824,83 @@ bool js::DisposeDisposablesOnScopeLeave(JSContext* cx, cx->clearPendingException(); } + // Step 3. For each element resource of + // disposeCapability.[[DisposableResourceStack]], in reverse list order, do while (index) { --index; Value val = disposables->get(index); MOZ_ASSERT(val.isObject()); - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted record( - cx, &obj->as()); + JS::Rooted resource( + cx, &val.toObject().as()); - // DisposeResources ( disposeCapability, completion ) - // Step 1.a. Let result be - // Completion(Dispose(resource.[[ResourceValue]], resource.[[Hint]], - // resource.[[DisposeMethod]])). - // - // Dispose ( V, hint, method ) - // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-dispose - // Step 1. If method is undefined, let result be undefined. - if (record->getObject().isUndefined()) { + // Step 3.a. Let value be resource.[[ResourceValue]]. + JS::Rooted value(cx, resource->getObject()); + + // Step 3.b. Let hint be resource.[[Hint]]. + // TODO: Implementation of async-dispose, implicitly sync-dispose for now + // (Bug 1906534). + // Step 3.c. Let method be resource.[[DisposeMethod]]. + JS::Rooted method(cx, resource->getMethod()); + + // Step 3.e. If method is not undefined, then + if (method.isUndefined()) { continue; } - JS::Rooted disposeProp(cx, record->getMethod()); - JS::Rooted recordedObj(cx, &record->getObject().toObject()); - - // Dispose ( V, hint, method ) - // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-dispose - // Step 2. Else, let result be ? Call(method, V). + // Step 3.e.i. Let result be Completion(Call(method, value)). JS::Rooted rval(cx); - if (!Call(cx, disposeProp, recordedObj, &rval)) { - // Step 1.b. If result is a throw completion, then - // TODO: Suppressed Error Object and subsequent steps in the spec need - // to be implemented (Bug 1899870). For now, we just keep track of the - // latest exception and continue with the disposal. - hadError = true; - if (cx->isExceptionPending()) { - if (!cx->getPendingException(&latestException)) { + if (!Call(cx, method, value, &rval)) { + // Step 3.e.iii. If result is a throw completion, then + if (hadError) { + // Step 3.e.iii.1.a. Set result to result.[[Value]]. + JS::Rooted result(cx); + if (!cx->getPendingException(&result)) { return false; } cx->clearPendingException(); + + // Step 3.e.iii.1.b. Let suppressed be completion.[[Value]]. + JS::Rooted suppressed(cx, latestException); + + // Steps 3.e.iii.1.c-e. + ErrorObject* errorObj = CreateSuppressedError(cx, result, suppressed); + if (!errorObj) { + return false; + } + // Step 3.e.iii.1.f. Set completion to ThrowCompletion(error). + latestException.set(ObjectValue(*errorObj)); + } else { + // Step 3.e.iii.2. Else, + // Step 3.e.iii.2.a. Set completion to result. + hadError = true; + if (cx->isExceptionPending()) { + if (!cx->getPendingException(&latestException)) { + return false; + } + cx->clearPendingException(); + } } } } - // DisposeResources ( disposeCapability, completion ) - // Step 3. Set disposeCapability.[[DisposableResourceStack]] to - // a new empty List. + // Step 6. Set disposeCapability.[[DisposableResourceStack]] to a new empty + // List. if (env->is()) { env->as().clearDisposables(); } else { env->as().clearDisposables(); } - // 4. Return ? completion. + // Step 7. Return ? completion. if (hadError) { - cx->clearPendingException(); cx->setPendingException(latestException, ShouldCaptureStack::Maybe); return false; } } + // Step 7. Return ? completion. return true; } #endif @@ -1856,7 +1914,8 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, */ #define INTERPRETER_LOOP() #define CASE(OP) label_##OP: -#define DEFAULT() label_default: +#define DEFAULT() \ + label_default: #define DISPATCH_TO(OP) goto* addresses[(OP)] #define LABEL(X) (&&label_##X) @@ -2225,9 +2284,21 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, CASE(DisposeDisposables) { ReservedRooted env(&rootObject0, REGS.fp()->environmentChain()); - - if (!DisposeDisposablesOnScopeLeave(cx, env)) { - goto error; + DisposeJumpKind jumpKind = DisposeJumpKind(GET_UINT8(REGS.pc)); + bool ok = DisposeDisposablesOnScopeLeave(cx, env); + if (jumpKind == DisposeJumpKind::JumpOnError) { + if (!ok) { + goto error; + } + } else { + MOZ_ASSERT(jumpKind == DisposeJumpKind::NoJumpOnError); + // The NoJumpOnError mode for this bytecode is used + // in the special case of For-of iterator close when there + // is an exception during the loop. Hence, if we reach this + // point in the execution we must have an exception + // pending and the bytecode following this must handle the + // exception. + MOZ_ASSERT(!ok, "NoJumpOnError used without a pending exception"); } } END_CASE(DisposeDisposables) diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 12598b92cc82..b58a3f24baf8 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -17,6 +17,7 @@ #include "vm/CheckIsObjectKind.h" // CheckIsObjectKind #ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT # include "vm/DisposableRecord.h" +# include "vm/ErrorObject.h" # include "vm/UsingHint.h" #endif #include "vm/Stack.h" @@ -648,6 +649,9 @@ bool DisposeDisposablesOnScopeLeave(JSContext* cx, JS::Handle env); bool GetDisposeMethod(JSContext* cx, JS::Handle obj, UsingHint hint, JS::MutableHandle disposeMethod); +ErrorObject* CreateSuppressedError(JSContext* cx, JS::Handle error, + JS::Handle suppressed); + bool CreateDisposableResource(JSContext* cx, JS::Handle objVal, UsingHint hint, JS::MutableHandle result); diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index 93dcd7c7aa9d..a1e2386fe0e2 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -3422,10 +3422,10 @@ * * Category: Variables and scopes * Type: Entering and leaving environments - * Operands: + * Operands: DisposeJumpKind jumpKind * Stack: => */ \ - IF_EXPLICIT_RESOURCE_MANAGEMENT(MACRO(DisposeDisposables, dispose_disposables, NULL, 1, 0, 0, JOF_BYTE)) \ + IF_EXPLICIT_RESOURCE_MANAGEMENT(MACRO(DisposeDisposables, dispose_disposables, NULL, 2, 0, 0, JOF_UINT8)) \ /* * Push the current VariableEnvironment (the environment on the environment * chain designated to receive new variables).