Reference implementation: non-local flow control for closures (GROOVY-12126)#2677
Draft
paulk-asert wants to merge 5 commits into
Draft
Reference implementation: non-local flow control for closures (GROOVY-12126)#2677paulk-asert wants to merge 5 commits into
paulk-asert wants to merge 5 commits into
Conversation
Adds a returnAtStmtAlt alternative to the statement rule (RETURN AT identifier expression?), an optional target field on ReturnStatement, and the corresponding AstBuilder visitor. return @foo was previously unparseable, so the new alternative is unambiguous; no nls between the tokens keeps 'return' followed by an annotated declaration on the next line parsing as before. The target is recorded but not yet acted upon; the desugaring pass follows in a separate commit. Assisted-by: Claude Fable 5 (Anthropic)
Desugars return@name inside a closure into a call to NonLocalReturn.raise(token, value), a stack-trace-free signal carrying a per-activation token synthesized at entry to the target method; the target method's body is wrapped in a handler that returns the carried value when the token matches and rethrows otherwise. Runs as a new CANONICALIZATION operation (NonLocalControlFlowRewriter) before static type checking, so dynamic and @CompileStatic code compile identically. Methods without targeted returns are untouched; return@m at method level lowers to a plain return; return@script targets the script body. Targets that name no lexically enclosing method, cross a class boundary, or appear in constructors/initializers are compile errors. Variable scopes are repaired after rewriting since closures capture the token local. Assisted-by: Claude Fable 5 (Anthropic)
Adds LoopControl, a pair of preallocated, stack-trace-free, non-user-constructible signals (BREAK/CONTINUE), and the groovy.transform.SupportsLoopControl marker annotation. Cooperating DefaultGroovyMethods iterators catch the signals around each per-element closure invocation: each/eachWithIndex/times/upto/downto/ step stop on BREAK and proceed on CONTINUE; collect/findAll/inject stop excluding the current element's contribution on BREAK and skip it on CONTINUE (inject keeps the prior accumulator). The legacy Closure.DONE directive remains honored where it was before. Signals reaching a non-cooperating method surface with an explanatory message. Array (ArrayGroovyMethods), String and IO iterator families are left for follow-up. Assisted-by: Claude Fable 5 (Anthropic)
…trol signals Extends NonLocalControlFlowRewriter: a break/continue inside a closure passed directly as a method call argument - and not bound to a loop (or switch, for break) within the closure - is lowered to 'throw LoopControl.BREAK/CONTINUE', which cooperating iterator methods catch per element. The closure is tagged with node metadata so static type checking can later verify the callee is marked @SupportsLoopControl. Labeled break/continue in closures and break/continue in non-argument closures are compile errors. As a side effect, break inside a closure lexically nested in a loop - which previously slipped past LabelVerifier into broken codegen - now has well-defined semantics (binds the closure's iteration). Assisted-by: Claude Fable 5 (Anthropic)
When a closure tagged by the non-local control flow rewriter as using break/continue is passed to a statically resolved method, the static type checker now requires the target method (unwrapping extension method nodes for DGM) to carry @SupportsLoopControl, turning the dynamic runtime escape into a compile-time error under @TypeChecked/@CompileStatic. Assisted-by: Claude Fable 5 (Anthropic)
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2677 +/- ##
==================================================
+ Coverage 68.7019% 68.7355% +0.0336%
- Complexity 34123 34215 +92
==================================================
Files 1536 1539 +3
Lines 128912 129153 +241
Branches 23369 23451 +82
==================================================
+ Hits 88565 88774 +209
- Misses 32517 32521 +4
- Partials 7830 7858 +28
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: da4e8fb Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is NOT to be submitted as is - it is a reference implementation for on-going discussion.
The
returnstory is that bare returns remain as is, for a closure that is returning from the closurecallmethod. "at" returns (semi Kotlin familiar) return from the surrounding method, e.g.:Implementation is via stack-trace-free exceptions.
The
continue/breakstory summary is that we rework DGM methods like so:Then users can write:
Which is lowered to this:
We might decide to take some, all or none of these forms. It is also only a partial implementation, see notes below.
Deferred surface (deferred purely to limit the spike; zero new machinery needed) / known edges:
One improvement the rewrite brought for free — break in a closure lexically inside a loop, which previously slipped past LabelVerifier into broken codegen, now has defined semantics.
The genuinely open design questions — the real "more work needed" tier: