fix: generator.return() properly handles yield in finally blocks#115
fix: generator.return() properly handles yield in finally blocks#115taras wants to merge 12 commits intografana:mainfrom
Conversation
…rn() Update the Sobek fork from commit 2ff728d (first commit only) to 013550b (all 6 commits), which includes critical fixes for: - sp underflow guard preventing panic when generator.return() is called from timer/WebSocket callback contexts - callerSp capture before finally blocks run - return-in-finally completion handling - Refactored cleanup paths for consistency This resolves the panic that occurred in nested generator cleanup scenarios (demo 04-cleanup.js). Sobek PR: grafana/sobek#115
Document how @effectionx/k6 solves 20+ open k6 issues related to structured concurrency gaps: context loss, resource leaks, silent failures, unpredictable shutdown, and race conditions. References: - Sobek PR #115: grafana/sobek#115 - Effectionx PR #156: thefrontside/effectionx#156 Session-ID: ses_39d99b9c2ffeSKxHPZAk9P7E1O
|
I've opened an upstream PR that increases the test262 and there is a test there that fails without the changes here. I would prefer to get this merged in goja and then merge but if that takes longer than Monday I will make a new PR from https://github.com/grafana/sobek/compare/bumpTest262. You can potentially use that as your base for now to fix any future conflicts and remove the exceptions in the tc39_test.go file that are stopping the tests. I definitely prefer to update it than to copy-paste test back from it:) |
|
I'm not in a major hurry. I'll check with folks at work if they want to start using the Let me know if there is anything I can do to help. |
|
You can now rebase on the main branch |
Per ECMAScript specification, when generator.return(value) is called: 1. If the generator has finally blocks, they must execute 2. If a finally block contains a yield, the generator should suspend 3. Resuming the generator should continue executing the finally block 4. Only after all finally blocks complete should the generator be done Previously, Sobek would skip any yield statements in finally blocks during return(), immediately marking the generator as completed. This fix: - Adds 'returning' and 'retVal' fields to generatorObject to track return-in-progress state - Adds completeReturnYield() to properly handle yield suspension during finally block execution - Adds resumeReturn() to manage the return sequence across multiple next() calls when finally blocks yield - Modifies next() to detect returning state and continue the return sequence rather than normal execution This enables structured concurrency patterns where cleanup code needs to perform async operations (which require yielding). Fixes grafana#114
When resumeReturn() runs finally blocks, those blocks can call functions that modify vm.sb (e.g., native function calls). By the time the generator completes, vm.sb might be corrupted (e.g., 0), causing vm.sp = vm.sb - 1 to compute -1, which then causes a panic in the next resume() call. The fix saves the caller's sp immediately after enterNext() (when vm.sb is still correct), and uses that saved value in the completion cleanup instead of recomputing from the potentially-corrupted vm.sb. This fixes the sp underflow panic that occurred when generator.return() was called from K6 timer/WebSocket callback contexts.
Add tests for error propagation before/after cleanup yield, yield* cleanup sequencing, and return-before-start behavior. Also add a skipped regression test documenting a known issue where return inside finally during generator.return() still panics. Session-ID: ses_3a12cce1fffeIBbRZ0hw5WYuCd
Avoid corrupting return-unwind state when finally blocks complete without yielding. - Capture non-yield completion values during resumeReturn() so explicit return inside finally can override the original return argument. - Stop unwinding try frames at the generator resume baseline (tryStackLen) so the sentinel try frame is not consumed during finally processing. - Only remove the synthetic pc=-2 callStack frame when it is actually present, preventing cleanup from popping the wrong context. This resolves the panic path exercised by return inside finally and allows the regression test to run without skip. Session-ID: ses_3a12cce1fffeIBbRZ0hw5WYuCd
Tighten generator.return() unwind internals by centralizing repeated behavior: - extract return-value capture helper - extract sentinel frame pop helper - pass callerSp through completeReturnYield to avoid relying on live sb This removes duplicated conditional logic and makes stack/frame cleanup consistent across yield and non-yield return-unwind paths while preserving existing semantics. Session-ID: ses_3a11e2d8cffeb9KvPIsJVAm0uk
Prevent nil-pointer panics when callback/unwind paths leave vm.prg unset by treating nil program as halted in run paths. Also handle typed nil *Exception in exceptionFromValue and add regression tests for nil program and typed nil exception handling. Session-ID: ses_unknown
Track sentinel stack depth on generator enterNext() and trim call stack from sentinel frame during return-unwind completion. This avoids leaking callback frames above sentinel and hardens generator context restoration across async callback boundaries. Add regression tests for sentinel frame removal and fallback scan behavior. Session-ID: ses_unknown
Add regression coverage for Promise-job execution continuity after generator.return() yields in finally and a caught throw occurs in the same job. Session-ID: ses_unknown
…inally Add 8 test262-format JavaScript tests that verify generator.return() behavior when finally blocks contain yield statements. These tests fill a coverage gap in the upstream tc39/test262 suite. Tests added: - try-finally-yield-triggered-by-return.js - try-finally-yield-star-delegation-return.js - try-finally-nested-yields-triggered-by-return.js - try-finally-yield-resume-value-during-return.js - try-finally-throw-before-yield-during-return.js - try-finally-throw-after-yield-during-return.js - try-finally-yield-star-during-return.js - try-finally-return-overrides-during-return.js Also adds TestGeneratorPrototypeReturn to tc39_test.go to run these local test262-format tests, and a symlink to the test262 harness. Session-ID: ses_7dVdpXJLFJbq8JXWMYV5qz7F
Remove yield_return_test.go which duplicated test coverage already provided by test262-format JavaScript tests. Add the missing return-before-generator-started.js test to cover the case where .return() is called before the generator starts. This reduces duplication while maintaining full test coverage: - 9 test262-format JS tests now cover all generator.return() scenarios - yield_return_callback_test.go retained for Go-specific callback tests Session-ID: ses_o4m0bzLmndGj1v
This test duplicates the official test262 test at: test262/test/built-ins/GeneratorPrototype/return/from-state-suspended-start.js Both tests verify that calling .return() before .next() completes the generator immediately without entering the body. Since Sobek runs the full test262 suite, the custom test is redundant. Session-ID: ses_o4m0bzLmndGj1v
c02bd72 to
71d9772
Compare
|
Rebased |
|
I think the tests you've added under testdata are direct copies from the tc39 suite, so you can now remove them and related changes. We do run the test262 test suite. You need to run Also of note is that we try to keep in sync(;)) with goja from which this is a fork. This means that either we or you (if you desire will have to make a PR for there as well. Sometimes we prefer to make you do the original change there, but I am no certain that will be faster in this case |
|
Sorry I didn't make this clearer earlier but the tests under test262 coverage
I ran the new tests against the following environments to verify that they are in fact supported by the platforms.
I wasn't sure if you'd prefer for me to contribute them to test262 first. I never interacted with TC39, so I wasn't sure if I should go down that route. |
|
I think if those are new tests - they should be added to the test262 suite then. I kind of do not want to try to be certain they are actually following the specificaiton or if just happens to be what all of thsoe engines do - unlikely as that is |
|
That makes sense. Ok, I'll add them upstream. Wish me luck :) |
These tests cover the scenario where generator.return() is called while the generator is paused in a try block, and the finally block contains a yield statement. The existing tests don't cover this path: - try-finally-within-try.js: finally has no yield - try-finally-within-finally.js: generator already paused at yield in finally New tests: - try-finally-yield-triggered-by-return.js: basic yield in finally during return - try-finally-nested-yields-triggered-by-return.js: nested try-finally with yields - try-finally-yield-resume-value-during-return.js: resume value to yield in finally - try-finally-throw-before-yield-during-return.js: throw before yield in finally - try-finally-throw-after-yield-during-return.js: throw after yield in finally - try-finally-yield-star-during-return.js: yield* in finally during return - try-finally-yield-star-delegation-return.js: yield* delegation with finally - try-finally-return-overrides-during-return.js: return in finally overrides value All tests validated against V8 (Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari). This coverage gap was discovered while fixing a bug in Sobek (grafana/sobek#115), a Go-based JavaScript runtime.
Motivation
I'm trying to spike using structured concurrency in JavaScript with Effection to resolve the concurrency problems k6 users are experiencing.
To make the spike work, I need async clean up to work in finally block of
function*. I might be able to make it work without it, but considering that this is ECMAScript spec, I figured it would be best solved by adding asynchronous clean up to Sobek.Summary
This PR fixes a bug where
generator.return()skipsyieldstatements insidefinallyblocks, violating ECMAScript specification.Fixes #114
The Problem
When
generator.return(value)is called on a generator with ayieldinside afinallyblock, Sobek incorrectly:{value: returnArg, done: true}Expected behavior (per ECMAScript spec and V8/Node.js):
Actual behavior (before this fix):
The Solution
1. Yield handling during return sequence
New state fields on
generatorObject:returning bool- tracks when we're in a return sequenceretVal Value- stores the return value for use after all finally blocks completeNew methods:
completeReturnYield()- handles yield suspension during finally block execution, mirroring the logic instep()for normal yieldsresumeReturn()- manages the return sequence across multiplenext()calls when finally blocks yield, properly executing all finally blocks before completingModified
next()method detectsreturningstate and delegates toresumeReturn()instead of normal execution.2. sp underflow fix for callback contexts
When
generator.return()is called from K6's timer/WebSocket callback contexts,vm.sbcan be corrupted to 0 by finally block code (native function calls temporarily modify sb for their own stack frames). This resulted invm.sp = -1, causing a panic.Fix: Save the caller's sp immediately after
enterNext()(whenvm.sbis still valid), and use that saved value in the completion cleanup:3. Return-in-finally handling
When
returnis used inside a finally block duringgenerator.return():g.gen.tryStackLen), so the sentinel try frame is not consumed by the looppc==-2callStack frame when it is actually present4. Refactoring
Helper functions to reduce duplicated logic:
captureReturnValue(retVal)- unified return-value precedence handlingpopSentinelCallFrame(vm)- unified sentinel frame cleanupcallerSpintocompleteReturnYield()so both yield and non-yield return paths use the same stable caller SP restorationTest Cases
test262-Format Tests (Primary)
Added 8 test262-format JavaScript test files in
testdata/GeneratorPrototype/return/:try-finally-yield-triggered-by-return.jstry-finally-yield-star-delegation-return.jstry-finally-nested-yields-triggered-by-return.jstry-finally-yield-resume-value-during-return.jstry-finally-throw-before-yield-during-return.jstry-finally-throw-after-yield-during-return.jstry-finally-yield-star-during-return.jstry-finally-return-overrides-during-return.jsThese tests follow the tc39/test262 format and are candidates for upstream contribution.
Go Tests (Go-specific integration)
yield_return_callback_test.go:
These test Go-specific callback integration (
SetAsyncContextTracker) that cannot be tested via JavaScript alone.test262 Coverage Analysis
I examined test262 (commit
cb4a6c8) and found this is a coverage gap in test262 itself - no existing test covers the scenario where.return()triggers a finally block containing ayield.Existing test262 tests for
GeneratorPrototype/return/(22 tests):from-state-suspended-start.js,from-state-completed.js,from-state-executing.jslength.js,name.js,property-descriptor.js,not-a-constructor.jsthis-val-not-object.js,this-val-not-generator.jsthisvalues.return()with try-catch (no finally)try-finally-before-try.js,try-finally-following-finally.jstry,.return()runsfinallyfinally.return()called - different scenarioGap: No test covers this path:
Our tests fill this gap:
All 8 of our custom tests cover scenarios where
.return()triggers execution of finally blocks that contain yields, which is the buggy path this PR fixes.Additional Bug Fixed: Return-in-finally panic
While implementing the main fix, I discovered an additional bug: when a
returnstatement appears inside afinallyblock duringgenerator.return(), Sobek panicked.Root cause:
Fixed by:
returnin finally can override the original return argumentg.gen.tryStackLenpc==-2callStack frame when presentCross-Engine Validation
The test262-format tests were manually validated against all major JavaScript engines:
go testjscCLIValidation Methodology
go test -run "TestGeneratorPrototypeReturn" -v/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jscAll engines produce identical results, confirming the implementation matches ECMAScript specification.
Verification
K6 integration validation:
@effectionx/k6- K6 extension for Effection - includes tests and API for using structured concurrency in K6. It requires changes in this PR.