[
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 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)
was:
Expect 14(1+4+9), but get 27(9+9+9)
{code:java}
import java.util.function.Supplier
def numbers = [1, 2, 3]
List suppliers = []
for (n in numbers) {
println "v: ${n}"
Supplier s = {
def r = n * n
println "$n: $r"
return r
}
suppliers << s
}
println "Result: ${suppliers.sum({e -> e.get()})}"
{code}
yields:
{code:java}
v: 1
v: 2
v: 3
3: 9
3: 9
3: 9
Result: 27
{code}
> 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 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)
--
This message was sent by Atlassian Jira
(v8.20.10#820010)