[
https://issues.apache.org/jira/browse/GROOVY-11792?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Daniel Sun resolved GROOVY-11792.
---------------------------------
Fix Version/s: 6.0.0-beta-1
Assignee: Daniel Sun
Resolution: Fixed
> for-in loop variable captured by closure/AIC sees final value, not
> per-iteration value
> --------------------------------------------------------------------------------------
>
> Key: GROOVY-11792
> URL: https://issues.apache.org/jira/browse/GROOVY-11792
> Project: Groovy
> Issue Type: Bug
> Reporter: Daniel Sun
> Assignee: Daniel Sun
> Priority: Major
> Fix For: 6.0.0-beta-1
>
>
> h2. Problem
> When a *for-in* (enhanced for-each) loop variable is shared with a deferred
> closure, lambda, or anonymous inner class (AIC), every capture observes the
> *final* loop value after the loop finishes, not the value from the iteration
> that created the capture.
> Classic {{for}} / {{while}} are out of scope for this issue.
> Groovy implements shared locals with a single {{groovy.lang.Reference}}
> updated in place across for-in iterations, so deferred use always sees the
> last store.
> h2. Expected
> Each deferred capture observes the for-in *value* (and *index*, when present)
> from the iteration in which it was created.
> h2. Actual
> Every deferred capture sees the last iteration's value (and index).
> h2. Reproducer
> {code:groovy}
> import java.util.function.Supplier
> def numbers = [1, 2, 3]
> List suppliers = []
> for (n in numbers) {
> Supplier s = { n * n }
> suppliers << s
> }
> // Expected: [1, 4, 9]
> // Actual: [9, 9, 9]
> assert suppliers.collect { it.get() } == [1, 4, 9]
> {code}
> Same pattern for:
> * colon syntax: {{for (Integer n : numbers)}}
> * {{@CompileStatic}} (iterator, array, enumeration paths)
> * indexed for-in: {{for (i, v in items)}}
> * lambdas (dynamic and SC SAM conversion)
> * AIC capture (also tracked as GROOVY-11818)
> h2. Workaround
> Bind a fresh local each iteration so the deferred body does not share the
> loop holder:
> {code:groovy}
> for (n in numbers) {
> def local = n
> suppliers << { local * local }
> }
> {code}
> h2. Root cause
> For-in loop heads store shared (holder) loop variables via
> {{OperandStack.storeVar}} → in-place {{Reference#set}}. Closures / lambdas /
> AICs capture that one holder; later iterations overwrite it.
> Area: {{org.codehaus.groovy.classgen.asm}}
> * {{StatementWriter}} / {{StaticTypesStatementWriter}}
> * {{CompileStack}}
> * {{WriterController}} / {{DelegatingController}}
> h2. Fix
> Default (language-compat, *not* an optimization):
> * each for-in iteration allocates a *fresh* {{groovy.lang.Reference}} for
> holder value/index variables
> * same-iteration assignment still updates that iteration's {{Reference}}
> * non-holder loop vars and non-loop shared vars unchanged
> * dynamic + SC for-in paths share {{storeForLoopVariable}} /
> {{incrementForLoopIndexVariable}}
> Opt out of the new semantics (restore historical final-value sharing):
> * system property: {{groovy.for.loop.capture=false}}
> * API: {{CompilerConfiguration.setForLoopCaptureEnabled(false)}}
> Notes:
> * independent of optimization options (including {{"all"}} → {{false}})
> * classic {{for}} / {{while}} capture behaviour is intentionally unchanged
> h2. Related
> * GROOVY-11818 — for-in variable captured by anonymous inner class (same root
> cause)
> * GROOVY-11751 — indexed for-in with shared index (holder index load/store)
--
This message was sent by Atlassian Jira
(v8.20.10#820010)