[ 
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)

Reply via email to