diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-nested-yields-triggered-by-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-nested-yields-triggered-by-return.js new file mode 100644 index 00000000000..1aeedad5d44 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-nested-yields-triggered-by-return.js @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() is called while paused in nested try blocks, + yields in both inner and outer finally blocks should suspend the generator. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + When return triggers nested finally blocks, each yield in each finally + block causes the generator to suspend. The generator completes only + after all finally blocks have finished executing. +features: [generators] +---*/ + +function* nestedCleanup() { + try { + try { + yield "work"; + } finally { + yield "inner-cleanup"; + } + } finally { + yield "outer-cleanup"; + } +} + +var gen = nestedCleanup(); +var result; + +result = gen.next(); +assert.sameValue(result.value, "work", "r1.value"); +assert.sameValue(result.done, false, "r1.done"); + +result = gen.return("cancelled"); +assert.sameValue( + result.value, + "inner-cleanup", + "r2.value (inner finally yield)", +); +assert.sameValue(result.done, false, "r2.done (suspended at inner finally)"); + +result = gen.next(); +assert.sameValue( + result.value, + "outer-cleanup", + "r3.value (outer finally yield)", +); +assert.sameValue(result.done, false, "r3.done (suspended at outer finally)"); + +result = gen.next(); +assert.sameValue(result.value, "cancelled", "r4.value (return value)"); +assert.sameValue(result.done, true, "r4.done (completed)"); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-return-overrides-during-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-return-overrides-during-return.js new file mode 100644 index 00000000000..fca840548b8 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-return-overrides-during-return.js @@ -0,0 +1,34 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() triggers a finally block that contains its own + return statement, the finally's return value overrides the original. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + If a finally block contains a return statement, that return value + replaces the original return value from generator.return(). +features: [generators] +---*/ + +function* genWithOverride() { + try { + yield "work"; + } finally { + return "cleanup-override"; + } +} + +var gen = genWithOverride(); +gen.next(); + +var result = gen.return("cancelled"); + +assert.sameValue( + result.value, + "cleanup-override", + "Finally return overrides generator.return() value", +); +assert.sameValue(result.done, true, "Generator is done"); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-throw-after-yield-during-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-throw-after-yield-during-return.js new file mode 100644 index 00000000000..88edbe709c3 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-throw-after-yield-during-return.js @@ -0,0 +1,51 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() triggers a finally block that yields and then + throws, the yield suspends normally and the throw occurs on next resume. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + If a finally block yields and then throws, the generator suspends at + the yield. When resumed with next(), execution continues and the + exception is thrown. +features: [generators] +---*/ + +function* genWithSuspendingThrowingCleanup() { + try { + yield "work"; + } finally { + yield "cleanup"; + throw new Error("cleanup-failed-after-yield"); + } +} + +var gen = genWithSuspendingThrowingCleanup(); +var result; + +result = gen.next(); +assert.sameValue(result.value, "work", "r1.value"); + +result = gen.return("cancelled"); +assert.sameValue(result.value, "cleanup", "r2.value (yield in finally)"); +assert.sameValue(result.done, false, "r2.done (suspended at yield)"); + +var caught; +try { + gen.next(); +} catch (e) { + caught = e.message; +} + +assert.sameValue( + caught, + "cleanup-failed-after-yield", + "Exception after yield should propagate on resume", +); + +result = gen.next(); +assert.sameValue(result.value, undefined, "Generator completion value"); +assert.sameValue(result.done, true, "Generator should be completed"); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-throw-before-yield-during-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-throw-before-yield-during-return.js new file mode 100644 index 00000000000..93ee5b15dd4 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-throw-before-yield-during-return.js @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() triggers a finally block that throws before + yielding, the exception propagates to the caller. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + If a finally block throws an exception before any yield, that exception + replaces the return completion and propagates to the caller. +features: [generators] +---*/ + +function* genWithThrowingCleanup() { + try { + yield "work"; + } finally { + throw new Error("cleanup-failed"); + } +} + +var gen = genWithThrowingCleanup(); +gen.next(); + +var caught; +try { + gen.return("cancelled"); +} catch (e) { + caught = e.message; +} + +assert.sameValue( + caught, + "cleanup-failed", + "Exception from finally should propagate", +); + +var result = gen.next(); +assert.sameValue(result.value, undefined, "Generator completion value"); +assert.sameValue(result.done, true, "Generator should be completed"); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-yield-resume-value-during-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-yield-resume-value-during-return.js new file mode 100644 index 00000000000..baaa54a5fe7 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-yield-resume-value-during-return.js @@ -0,0 +1,49 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() triggers a finally block with yield, the yield + can receive a value from the subsequent next() call, just like normal yields. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + Yields in finally blocks during return behave like normal yields - + they can receive values passed to next(). +features: [generators] +---*/ + +var cleanupInput; + +function* cleanupNeedsAck() { + try { + yield "work"; + } finally { + cleanupInput = yield "cleanup-1"; + yield "cleanup-2"; + } +} + +var gen = cleanupNeedsAck(); +var result; + +result = gen.next(); +assert.sameValue(result.value, "work", "r1.value"); + +result = gen.return("cancelled"); +assert.sameValue(result.value, "cleanup-1", "r2.value"); +assert.sameValue(result.done, false, "r2.done"); + +result = gen.next("ack"); +assert.sameValue(result.value, "cleanup-2", "r3.value"); +assert.sameValue(result.done, false, "r3.done"); + +result = gen.next(); +assert.sameValue(result.value, "cancelled", "r4.value"); +assert.sameValue(result.done, true, "r4.done"); + +assert.sameValue( + cleanupInput, + "ack", + "yield in finally received value from next()", +); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-delegation-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-delegation-return.js new file mode 100644 index 00000000000..85256197ddf --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-delegation-return.js @@ -0,0 +1,74 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() is called on an outer generator delegating via + yield*, and the inner generator has a yield in its finally block, the + generator suspends at the inner finally yield. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + When return is called on a generator that is delegating via yield*, + the return is forwarded to the inner generator. If the inner generator + has a finally block with yield, it suspends there. +features: [generators] +---*/ + +var cleanupOrder = []; + +function* inner() { + try { + yield "inner-work"; + return "inner-done"; + } finally { + yield "inner-cleanup"; + cleanupOrder.push("inner"); + } +} + +function* outer() { + try { + var result = yield* inner(); + return result; + } finally { + yield "outer-cleanup"; + cleanupOrder.push("outer"); + } +} + +var gen = outer(); +var result; + +result = gen.next(); +assert.sameValue( + result.value, + "inner-work", + "First yield from inner generator", +); +assert.sameValue(result.done, false, "First result done"); + +result = gen.return("cancelled"); +assert.sameValue( + result.value, + "inner-cleanup", + "Should yield from inner finally", +); +assert.sameValue(result.done, false, "Should suspend at inner finally yield"); + +result = gen.next(); +assert.sameValue( + result.value, + "outer-cleanup", + "Should yield from outer finally", +); +assert.sameValue(result.done, false, "Should suspend at outer finally yield"); + +result = gen.next(); +assert.sameValue(result.value, "cancelled", "Should return original return value"); +assert.sameValue(result.done, true, "Should be done after all finally blocks"); +assert.sameValue( + cleanupOrder.join(","), + "inner,outer", + "Cleanup order should be inner then outer", +); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-during-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-during-return.js new file mode 100644 index 00000000000..d6f56599137 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-yield-star-during-return.js @@ -0,0 +1,50 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() triggers a finally block containing yield*, + the delegated generator's values are yielded before completion. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + yield* in a finally block during return delegates to the inner generator. + Each value from the inner generator is yielded before the outer generator + completes with the return value. +features: [generators] +---*/ + +function* delegatedCleanup() { + yield "cleanup-1"; + yield "cleanup-2"; +} + +function* withYieldStarCleanup() { + try { + yield "work"; + } finally { + yield* delegatedCleanup(); + } +} + +var gen = withYieldStarCleanup(); +var result; + +result = gen.next(); +assert.sameValue(result.value, "work", "r1.value"); + +result = gen.return("cancelled"); +assert.sameValue(result.value, "cleanup-1", "r2.value (first delegated yield)"); +assert.sameValue(result.done, false, "r2.done"); + +result = gen.next(); +assert.sameValue( + result.value, + "cleanup-2", + "r3.value (second delegated yield)", +); +assert.sameValue(result.done, false, "r3.done"); + +result = gen.next(); +assert.sameValue(result.value, "cancelled", "r4.value (return value)"); +assert.sameValue(result.done, true, "r4.done"); diff --git a/test/built-ins/GeneratorPrototype/return/try-finally-yield-triggered-by-return.js b/test/built-ins/GeneratorPrototype/return/try-finally-yield-triggered-by-return.js new file mode 100644 index 00000000000..93cdbe2eee7 --- /dev/null +++ b/test/built-ins/GeneratorPrototype/return/try-finally-yield-triggered-by-return.js @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Taras Mankovski. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-generator.prototype.return +description: > + When generator.return() is called while paused in try block, and the + finally block contains a yield, the generator suspends at that yield. +info: | + 27.5.3.2 Generator.prototype.return ( value ) + + 8. Return ? GeneratorResumeAbrupt(generator, Completion Record + { [[Type]]: return, [[Value]]: value, [[Target]]: empty }, generatorBrand). + + When the return completion triggers a finally block containing yield, + GeneratorYield suspends the generator with done: false. +features: [generators] +---*/ + +var inFinally = 0; +var afterYield = 0; + +function* g() { + try { + yield "in-try"; + } finally { + inFinally += 1; + yield "in-finally"; + afterYield += 1; + } +} + +var iter = g(); +var result; + +result = iter.next(); +assert.sameValue(result.value, "in-try", "First result value"); +assert.sameValue(result.done, false, "First result done"); +assert.sameValue(inFinally, 0, "finally not yet entered"); + +result = iter.return(42); +assert.sameValue( + result.value, + "in-finally", + "Second result value (yield in finally)", +); +assert.sameValue(result.done, false, "Second result done (suspended at yield)"); +assert.sameValue(inFinally, 1, "finally block entered"); +assert.sameValue(afterYield, 0, "code after yield not yet executed"); + +result = iter.next(); +assert.sameValue(result.value, 42, "Third result value (return value)"); +assert.sameValue(result.done, true, "Third result done (completed)"); +assert.sameValue(afterYield, 1, "code after yield executed");