Skip to content

Commit 7cad34b

Browse files
author
Taras Mankovski
committed
Add tests for Generator.prototype.return() with yield in finally
Tighten spec assertions for completion behavior after throws in finally and for return value propagation through yield* delegation during return.
1 parent e0cf381 commit 7cad34b

8 files changed

Lines changed: 407 additions & 0 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() is called while paused in nested try blocks,
7+
yields in both inner and outer finally blocks should suspend the generator.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
When return triggers nested finally blocks, each yield in each finally
12+
block causes the generator to suspend. The generator completes only
13+
after all finally blocks have finished executing.
14+
features: [generators]
15+
---*/
16+
17+
function* nestedCleanup() {
18+
try {
19+
try {
20+
yield "work";
21+
} finally {
22+
yield "inner-cleanup";
23+
}
24+
} finally {
25+
yield "outer-cleanup";
26+
}
27+
}
28+
29+
var gen = nestedCleanup();
30+
var result;
31+
32+
result = gen.next();
33+
assert.sameValue(result.value, "work", "r1.value");
34+
assert.sameValue(result.done, false, "r1.done");
35+
36+
result = gen.return("cancelled");
37+
assert.sameValue(
38+
result.value,
39+
"inner-cleanup",
40+
"r2.value (inner finally yield)",
41+
);
42+
assert.sameValue(result.done, false, "r2.done (suspended at inner finally)");
43+
44+
result = gen.next();
45+
assert.sameValue(
46+
result.value,
47+
"outer-cleanup",
48+
"r3.value (outer finally yield)",
49+
);
50+
assert.sameValue(result.done, false, "r3.done (suspended at outer finally)");
51+
52+
result = gen.next();
53+
assert.sameValue(result.value, "cancelled", "r4.value (return value)");
54+
assert.sameValue(result.done, true, "r4.done (completed)");
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() triggers a finally block that contains its own
7+
return statement, the finally's return value overrides the original.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
If a finally block contains a return statement, that return value
12+
replaces the original return value from generator.return().
13+
features: [generators]
14+
---*/
15+
16+
function* genWithOverride() {
17+
try {
18+
yield "work";
19+
} finally {
20+
return "cleanup-override";
21+
}
22+
}
23+
24+
var gen = genWithOverride();
25+
gen.next();
26+
27+
var result = gen.return("cancelled");
28+
29+
assert.sameValue(
30+
result.value,
31+
"cleanup-override",
32+
"Finally return overrides generator.return() value",
33+
);
34+
assert.sameValue(result.done, true, "Generator is done");
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() triggers a finally block that yields and then
7+
throws, the yield suspends normally and the throw occurs on next resume.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
If a finally block yields and then throws, the generator suspends at
12+
the yield. When resumed with next(), execution continues and the
13+
exception is thrown.
14+
features: [generators]
15+
---*/
16+
17+
function* genWithSuspendingThrowingCleanup() {
18+
try {
19+
yield "work";
20+
} finally {
21+
yield "cleanup";
22+
throw new Error("cleanup-failed-after-yield");
23+
}
24+
}
25+
26+
var gen = genWithSuspendingThrowingCleanup();
27+
var result;
28+
29+
result = gen.next();
30+
assert.sameValue(result.value, "work", "r1.value");
31+
32+
result = gen.return("cancelled");
33+
assert.sameValue(result.value, "cleanup", "r2.value (yield in finally)");
34+
assert.sameValue(result.done, false, "r2.done (suspended at yield)");
35+
36+
var caught;
37+
try {
38+
gen.next();
39+
} catch (e) {
40+
caught = e.message;
41+
}
42+
43+
assert.sameValue(
44+
caught,
45+
"cleanup-failed-after-yield",
46+
"Exception after yield should propagate on resume",
47+
);
48+
49+
result = gen.next();
50+
assert.sameValue(result.value, undefined, "Generator completion value");
51+
assert.sameValue(result.done, true, "Generator should be completed");
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() triggers a finally block that throws before
7+
yielding, the exception propagates to the caller.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
If a finally block throws an exception before any yield, that exception
12+
replaces the return completion and propagates to the caller.
13+
features: [generators]
14+
---*/
15+
16+
function* genWithThrowingCleanup() {
17+
try {
18+
yield "work";
19+
} finally {
20+
throw new Error("cleanup-failed");
21+
}
22+
}
23+
24+
var gen = genWithThrowingCleanup();
25+
gen.next();
26+
27+
var caught;
28+
try {
29+
gen.return("cancelled");
30+
} catch (e) {
31+
caught = e.message;
32+
}
33+
34+
assert.sameValue(
35+
caught,
36+
"cleanup-failed",
37+
"Exception from finally should propagate",
38+
);
39+
40+
var result = gen.next();
41+
assert.sameValue(result.value, undefined, "Generator completion value");
42+
assert.sameValue(result.done, true, "Generator should be completed");
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() triggers a finally block with yield, the yield
7+
can receive a value from the subsequent next() call, just like normal yields.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
Yields in finally blocks during return behave like normal yields -
12+
they can receive values passed to next().
13+
features: [generators]
14+
---*/
15+
16+
var cleanupInput;
17+
18+
function* cleanupNeedsAck() {
19+
try {
20+
yield "work";
21+
} finally {
22+
cleanupInput = yield "cleanup-1";
23+
yield "cleanup-2";
24+
}
25+
}
26+
27+
var gen = cleanupNeedsAck();
28+
var result;
29+
30+
result = gen.next();
31+
assert.sameValue(result.value, "work", "r1.value");
32+
33+
result = gen.return("cancelled");
34+
assert.sameValue(result.value, "cleanup-1", "r2.value");
35+
assert.sameValue(result.done, false, "r2.done");
36+
37+
result = gen.next("ack");
38+
assert.sameValue(result.value, "cleanup-2", "r3.value");
39+
assert.sameValue(result.done, false, "r3.done");
40+
41+
result = gen.next();
42+
assert.sameValue(result.value, "cancelled", "r4.value");
43+
assert.sameValue(result.done, true, "r4.done");
44+
45+
assert.sameValue(
46+
cleanupInput,
47+
"ack",
48+
"yield in finally received value from next()",
49+
);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() is called on an outer generator delegating via
7+
yield*, and the inner generator has a yield in its finally block, the
8+
generator suspends at the inner finally yield.
9+
info: |
10+
27.5.3.2 Generator.prototype.return ( value )
11+
12+
When return is called on a generator that is delegating via yield*,
13+
the return is forwarded to the inner generator. If the inner generator
14+
has a finally block with yield, it suspends there.
15+
features: [generators]
16+
---*/
17+
18+
var cleanupOrder = [];
19+
20+
function* inner() {
21+
try {
22+
yield "inner-work";
23+
return "inner-done";
24+
} finally {
25+
yield "inner-cleanup";
26+
cleanupOrder.push("inner");
27+
}
28+
}
29+
30+
function* outer() {
31+
try {
32+
var result = yield* inner();
33+
return result;
34+
} finally {
35+
yield "outer-cleanup";
36+
cleanupOrder.push("outer");
37+
}
38+
}
39+
40+
var gen = outer();
41+
var result;
42+
43+
result = gen.next();
44+
assert.sameValue(
45+
result.value,
46+
"inner-work",
47+
"First yield from inner generator",
48+
);
49+
assert.sameValue(result.done, false, "First result done");
50+
51+
result = gen.return("cancelled");
52+
assert.sameValue(
53+
result.value,
54+
"inner-cleanup",
55+
"Should yield from inner finally",
56+
);
57+
assert.sameValue(result.done, false, "Should suspend at inner finally yield");
58+
59+
result = gen.next();
60+
assert.sameValue(
61+
result.value,
62+
"outer-cleanup",
63+
"Should yield from outer finally",
64+
);
65+
assert.sameValue(result.done, false, "Should suspend at outer finally yield");
66+
67+
result = gen.next();
68+
assert.sameValue(result.value, "cancelled", "Should return original return value");
69+
assert.sameValue(result.done, true, "Should be done after all finally blocks");
70+
assert.sameValue(
71+
cleanupOrder.join(","),
72+
"inner,outer",
73+
"Cleanup order should be inner then outer",
74+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (C) 2026 Taras Mankovski. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
/*---
4+
esid: sec-generator.prototype.return
5+
description: >
6+
When generator.return() triggers a finally block containing yield*,
7+
the delegated generator's values are yielded before completion.
8+
info: |
9+
27.5.3.2 Generator.prototype.return ( value )
10+
11+
yield* in a finally block during return delegates to the inner generator.
12+
Each value from the inner generator is yielded before the outer generator
13+
completes with the return value.
14+
features: [generators]
15+
---*/
16+
17+
function* delegatedCleanup() {
18+
yield "cleanup-1";
19+
yield "cleanup-2";
20+
}
21+
22+
function* withYieldStarCleanup() {
23+
try {
24+
yield "work";
25+
} finally {
26+
yield* delegatedCleanup();
27+
}
28+
}
29+
30+
var gen = withYieldStarCleanup();
31+
var result;
32+
33+
result = gen.next();
34+
assert.sameValue(result.value, "work", "r1.value");
35+
36+
result = gen.return("cancelled");
37+
assert.sameValue(result.value, "cleanup-1", "r2.value (first delegated yield)");
38+
assert.sameValue(result.done, false, "r2.done");
39+
40+
result = gen.next();
41+
assert.sameValue(
42+
result.value,
43+
"cleanup-2",
44+
"r3.value (second delegated yield)",
45+
);
46+
assert.sameValue(result.done, false, "r3.done");
47+
48+
result = gen.next();
49+
assert.sameValue(result.value, "cancelled", "r4.value (return value)");
50+
assert.sameValue(result.done, true, "r4.done");

0 commit comments

Comments
 (0)