[ 
https://issues.apache.org/jira/browse/GROOVY-11792?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Daniel Sun updated GROOVY-11792:
--------------------------------
    Description: 
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)


  was:
h2. Problem

When a *for-in* (enhanced for-each) loop variable is shared with a deferred 
closure or anonymous inner class (AIC), all captures observe the *final* loop 
value after the loop finishes, instead of the value from the iteration that 
created the capture.

Classic {{for}} / {{while}} loops are a separate construct and are not the 
subject of this report.

This is the same class of surprise as Java’s historical “effectively final loop 
variable” / deferred-lambda story, but Groovy’s shared variables use a single 
{{groovy.lang.Reference}} updated in place across for-in iterations, so 
deferred use always sees the last assignment.
h2. Expected behaviour

Each deferred capture should observe the for-in value (and index, when present) 
from the {*}iteration in which it was created{*}.
h2. Actual behaviour

Every deferred capture sees the *last* iteration’s value.
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}
Colon syntax ({{{}for (Integer n : numbers){}}}), {{{}@CompileStatic{}}}, 
indexed for-in ({{{}for (i, v in …){}}}), array/enumeration SC paths, and AIC 
capture (see GROOVY-11818) show the same pattern.
h2. Workaround

Introduce a fresh local per iteration so the closure captures a non-shared (or 
newly shared) binding:
{code:groovy}
for (n in numbers) {
    def local = n
    suppliers << { local * local }
}
{code}
h2. Root cause (classgen)

For-in loop heads store into a *single* shared {{Reference}} for closure-shared 
loop variables ({{{}OperandStack.storeVar{}}} → {{{}Reference#set{}}}). 
Closures/AICs created in the body capture that one holder; later iterations 
overwrite it.

Relevant area: {{org.codehaus.groovy.classgen.asm}} ({{{}StatementWriter{}}}, 
{{{}StaticTypesStatementWriter{}}}, {{{}CompileStack{}}}, 
{{{}WriterController{}}}).
h2. Resolution approach
 * When per-iteration capture is enabled (default), each for-in iteration that 
stores a *holder* loop variable allocates a *fresh* {{groovy.lang.Reference}} 
(value and shared index).
 * Within the *same* iteration, assignment to the loop variable remains visible 
to captures created there (still one {{Reference}} per iteration).
 * Non-shared loop variables and non-loop shared variables are unchanged.
 * Dynamic and static compilation for-in paths (iterator, SC array, SC 
enumeration) share the same store/increment helpers.

h3. Language-compatibility opt-out

This changes observable capture semantics (default-on). Historical “single 
shared {{Reference}} / final value” behaviour can be restored with either:
 * system property: {{groovy.for.loop.capture=false}}
 * {{{}CompilerConfiguration{}}}: put {{Boolean.FALSE}} for key 
{{CompilerConfiguration.FOR_LOOP_CAPTURE}} ({{{}"forLoopCapture"{}}}) in the 
optimization-options map

Notes:
 * This is a *language-compatibility* switch, not a performance optimization.
 * Setting optimization option {{"all"}} to {{false}} does *not* disable for-in 
recapture.

h2. Related issues
 * GROOVY-11818 — for-in variable captured by anonymous inner class (same root 
cause)
 * GROOVY-11751 — indexed for-in with shared index (holder index 
store/increment)


> 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
>            Priority: Major
>
> 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