Paul King created GROOVY-12126:
----------------------------------

             Summary: Non-local control flow from closures (return / break / 
continue)
                 Key: GROOVY-12126
                 URL: https://issues.apache.org/jira/browse/GROOVY-12126
             Project: Groovy
          Issue Type: Improvement
            Reporter: Paul King
             Fix For: 7.x


h2. break / continue via a loop-control protocol   (proposed, pending team 
review)

h3. Motivation
Groovy already ships a half-built version of this: Closure defines DONE=1, 
SKIP=2 and a
mutable {{directive}} field. In practice:
* DONE ("break") is honored by only THREE DGM methods 
(collect(Iterable,Collection,Closure),
  collectNested, times), via a POST-call check.
* SKIP ("continue") is defined but honored NOWHERE — a dead constant.
* The directive is mutable state on the shared, reusable Closure instance, with 
no ergonomic
  way to set it from inside the closure body.
Rather than add a wholly new mechanism, revive and repair this into a real 
loop-control
protocol that carries break/continue sugar.

h3. Two key facts
# {{continue}} ~= "return from the closure body". For value-IGNORING iterators 
(each,
  eachWithIndex, times, upto, step), continue needs NO protocol at all — it 
desugars to a
  plain closure-local {{return}} and the iterator proceeds. Universal, zero 
cooperation.
# {{break}} inherently requires the loop to cooperate — no closure can stop an 
arbitrary
  caller's iteration without the caller checking something. This is unavoidable 
(also true
  of @Inline, which cooperates by inlining).

h3. Design: a stateless return-sentinel (replaces the fragile directive field)
{code:groovy}
enum LoopControl { BREAK, CONTINUE }
{code}
A body may return {{LoopControl.BREAK}} / {{LoopControl.CONTINUE}} or a normal 
value.
Iteration methods interpret it. This is stateless (no shared field -> 
thread/re-entrancy
safe) and, per spike, ~0 ns/element overhead.

Per-method semantics:
|| Iterator kind || BREAK || CONTINUE ||
| value-ignoring (each, times, upto, step) | stop | proceed (== plain return) |
| value-consuming (collect, findAll, inject) | stop, do NOT contribute current 
element | skip contributing current element |

Note the collect fix: because the sentinel IS the return value, BREAK naturally 
excludes the
current element — unlike the legacy post-hoc {{directive}} check, which adds 
the value first
and so wrongly includes it (spike: legacy -> [10,20,30,999]; protocol -> 
[10,20,30]).

h3. Keyword sugar and how it is gated
Desugaring inside a loop body:
* {{continue}} -> {{return LoopControl.CONTINUE}}  (or plain {{return}} for 
value-ignoring iterators)
* {{break}}    -> {{return LoopControl.BREAK}}
SAFETY GATE: break/continue keyword sugar is enabled ONLY when the target 
method is statically
known to honor the protocol (annotated {{@SupportsLoopControl}}); otherwise it 
is a compile
error. Without the gate, {{break}} on a non-cooperating method would silently 
degrade to
"return from closure" (i.e. behave like continue) — a silent correctness bug. 
This trades
@Inline's inlinability requirement for a protocol-conformance requirement, but 
remains
DYNAMIC-DISPATCH capable and needs no inlining machinery.

h3. SPI / rollout
* Add {{@SupportsLoopControl}} and roll the sentinel checks across DGM 
iterators.
* Expose the annotation + a tiny helper so third-party iterators (the issue's
  {{sql.forEachRow}}, GPars, custom builders) can opt in.
* Give the previously-dead SKIP a real meaning ("skip contributing") for 
value-consuming
  iterators, or explicitly scope break/continue to value-ignoring iterators in 
v1.

h3. Evidence (spike (b))
* Correctness: break/each stops early; break/collect excludes the breaking 
element;
  continue/collect skips it.
* Legacy contrast: the stateful directive {{collect}} wrongly includes the 
broken element.
* Overhead: below measurement noise (~0 ns/element; two identity comparisons) 
vs ~15 ns per
  break for the token-exception path.

h3. Relationship to the other two mechanisms (this is NOT an either/or)
* break/continue  -> loop-control protocol (this section): cheap, dynamic, 
cooperative.
* return-from-method -> return@m token exception (main GEP section): universal, 
~15 ns.
* @Inline (GROOVY-8301 / GROOVY-6880) -> optional optimization that LOWERS 
either of the
  above to zero-cost real jumps where the callee is statically inlinable and 
non-escaping.
  It is the fast path, not the only path.

h3. Compatibility
* Legacy {{Closure.DONE}} / {{Closure.SKIP}} / {{directive}}: keep honoring on 
the current 3
  methods for backward compatibility; deprecate in favor of the return-sentinel 
protocol.
* No change to bare {{return}} semantics. New sentinel returns are additive; 
existing closures
  that never return a LoopControl are unaffected (identity check).

h3. Open questions (pending team review)
* Sentinel spelling: enum {{LoopControl.BREAK/CONTINUE}} vs reusing/rehoming 
{{Closure.DONE/SKIP}}.
* Should value-ignoring {{continue}} desugar to bare {{return}} (zero protocol) 
or always to
  the sentinel for uniformity?
* Whether a labeled form ({{break@method}} / {{continue@method}}) is wanted for 
nested loops,
  mirroring {{return@method}}.
* Escape/async: a body returning a sentinel to a NON-protocol caller is 
silently ignored;
  the static gate prevents the keyword form, but a hand-written {{return 
LoopControl.BREAK}}
  could still be misused — document, and consider a best-effort lint.

h2. Updated Scope / Non-goals  (supersedes the earlier Scope section)
* In scope: value-returning non-local return ({{return@m}}) AND break/continue 
via a
  loop-control protocol.
* Out of scope: changing bare {{return}} semantics; making {{break}} work 
through an
  arbitrary NON-cooperating method (impossible); full closure inlining (that is 
the separate
  @Inline / GROOVY-6880 track, referenced as the zero-cost optimization).




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to