You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/07/25 09:22:16 UTC

(groovy) branch GROOVY-11792 updated (1197010e93 -> feaf480cee)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch GROOVY-11792
in repository https://gitbox.apache.org/repos/asf/groovy.git


    omit 1197010e93 GROOVY-11792: add bytecode tests for for-in Reference store patterns
    omit e3e7c9acbb GROOVY-11792: for-in loop variable captured by closure/AIC sees final value, not per-iteration value
     add 361a02e91d GROOVY-10687: complete NestMembers after class generation instead of reordering classgen
     add fc1a14a6f6 GROOVY-12122: Provide a Regex timeout facility
     add 188718fb7c GROOVY-12187: Add SqlInjectionChecker type checker to detect quoted GString interpolation in SQL queries
     add f024ac677b GROOVY-12151: closure packing (GEP-27 S1) with per-class dispatch tables and full MOP integration Under @PackedClosures (dynamic trust path) or -Dgroovy.target.closure.pack=true under @CompileStatic (where the type checker PROVES delegate-independence), an eligible closure literal's body is hoisted into a synthetic private method on the enclosing class and the literal is replaced by a shared PackedClosure adapter — removing the per-closure generated class (and the nest [...]
     add 8fd428ca08 GROOVY-12190: @TypeChecked switch on enum: unqualified case constants pass type checking but throw MissingPropertyException at runtime
     add ccafb79419 minor refactor: fix adoc indent level
     add c92f1c926a Fix warnings from "SonarCloud Code Analysis"
     add c513a3ade4 GROOVY-9192: Implement optional parser error recovery for Parrot parser (#2724)
     add 0ca665dad0 Harden parallel concurrency tests against common-pool contention
     new feaf480cee GROOVY-11792: for-in loop variable captured by closure/AIC sees final value, not per-iteration value

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (1197010e93)
            \
             N -- N -- N   refs/heads/GROOVY-11792 (feaf480cee)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .github/workflows/groovy-build-performance.yml     |    3 +-
 .github/workflows/groovy-jmh-classic.yml           |    3 +-
 .github/workflows/groovy-jmh.yml                   |    3 +-
 .../main/groovy/org.apache.groovy-tested.gradle    |   14 +-
 src/main/java/groovy/lang/Closure.java             |    5 +-
 src/main/java/groovy/lang/GroovyClassLoader.java   |   14 +-
 src/main/java/groovy/lang/MetaClassRegistry.java   |   11 +-
 src/main/java/groovy/transform/PackedClosures.java |  126 +++
 src/main/java/groovy/transform/Parallel.java       |    5 +
 src/main/java/groovy/transform/SafeRegex.java      |   82 ++
 src/main/java/groovy/util/regex/RegexGuard.java    |  425 ++++++++
 .../groovy/util/regex/RegexTimeoutException.java   |   26 +-
 .../apache/groovy/parser/antlr4/AstBuilder.java    |   50 +-
 .../internal/AbstractFriendlyErrorStrategy.java    |  121 +++
 .../antlr4/internal/DescriptiveErrorStrategy.java  |  163 ++-
 .../RecoveringDescriptiveErrorStrategy.java        |   49 +
 .../parser/antlr4/internal/package-info.java       |    4 +-
 .../org/apache/groovy/runtime/Comprehensions.java  |   16 +-
 .../groovy/runtime/async/AsyncExecutors.java       |   13 +-
 .../apache/groovy/runtime/async/DefaultActor.java  |   19 +-
 .../java/org/codehaus/groovy/ast/ClassNode.java    |   30 +-
 .../codehaus/groovy/ast/tools/GenericsUtils.java   |   15 +
 .../groovy/classgen/AsmClassGenerator.java         |   72 +-
 .../groovy/classgen/asm/ClosureWriter.java         | 1144 ++++++++++++++++++++
 ...terControllerFactory.java => PackStrategy.java} |   22 +-
 .../classgen/asm/sc/StaticTypesCallSiteWriter.java |   26 +-
 .../classgen/asm/sc/StaticTypesClosureWriter.java  |  153 +++
 .../classgen/asm/sc/StaticTypesLambdaWriter.java   |    5 +-
 .../codehaus/groovy/control/CompilationUnit.java   |   93 +-
 .../groovy/control/CompilerConfiguration.java      |   58 +-
 .../groovy/runtime/GeneratedDispatcher.java        |  185 ++++
 .../codehaus/groovy/runtime/IOGroovyMethods.java   |    3 +-
 .../org/codehaus/groovy/runtime/InvokerHelper.java |    7 +-
 .../org/codehaus/groovy/runtime/PackedClosure.java |  526 +++++++++
 .../runtime/ParallelCollectionExtensions.java      |    8 +
 .../groovy/runtime/ScriptBytecodeAdapter.java      |   27 +
 .../groovy/runtime/StringGroovyMethods.java        |   53 +-
 .../runtime/metaclass/PackedClosureMetaClass.java  |   93 ++
 .../transform/SafeRegexASTTransformation.java      |  128 +++
 .../transform/StaticTypesTransformation.java       |   47 +
 .../codehaus/groovy/vmplugin/v8/IndyInterface.java |   25 +
 src/spec/doc/core-metaprogramming.adoc             |   66 ++
 src/spec/doc/core-operators.adoc                   |   51 +
 src/spec/test/OperatorsTest.groovy                 |   31 +
 .../test/SaferScriptingASTTransformsTest.groovy    |   34 +
 src/test/groovy/bugs/Groovy12188.groovy            |  102 ++
 src/test/groovy/bugs/Groovy8444.groovy             |   87 ++
 .../gls/generics/GenericsBytecodeTest.groovy       |    4 +
 .../groovy/gls/generics/GenericsUsageTest.groovy   |    4 +
 .../concurrent/ParallelAnnotationTest.groovy       |   42 +-
 .../concurrent/ParallelCollectionTest.groovy       |   26 +
 .../lang/ClosureSerializationCycleTest.groovy      |    3 +
 .../groovy/lang/ReferenceSerializationTest.groovy  |    2 +
 .../groovy/transform/stc/LambdaHoistTest.groovy    |    2 +-
 .../groovy/groovy/transform/stc/LambdaTest.groovy  |    7 +-
 .../groovy/groovy/util/regex/RegexGuardTest.groovy |  349 ++++++
 .../apache/groovy/parser/antlr4/Groovy9192.groovy  |  413 +++++++
 .../ClosureAndInnerClassNodeStructureTest.groovy   |    6 +
 .../groovy/classgen/asm/NestHostTests.groovy       |    8 +
 .../groovy/classgen/asm/TypeAnnotationsTest.groovy |    5 +-
 .../sc/FieldsAndPropertiesStaticCompileTest.groovy |   10 +
 ...ticCompileClosureGeneratedAnnotationTest.groovy |    4 +
 .../groovy/classgen/asm/sc/bugs/Groovy7276.groovy  |    4 +
 .../asm/sc/bugs/ReproducibleBytecodeBugs.groovy    |    4 +
 .../groovy/control/CompilerConfigurationTest.java  |   20 +
 .../groovy/runtime/ClosureCallMopGuardTest.groovy  |   33 +-
 .../runtime/PackedClosureMetaClassTest.groovy      |  104 ++
 .../transform/ClosurePackCapabilityTest.groovy     |  370 +++++++
 .../transform/PackedClosureBoundariesTest.groovy   |  134 +++
 .../PackedClosureDebugMetadataTest.groovy          |  165 +++
 .../transform/PackedClosuresTransformTest.groovy   |  552 ++++++++++
 .../groovy/transform/SafeRegexTransformTest.groovy |  301 +++++
 .../parser/antlr4/internal/ErrorStrategyTest.java  |  472 ++++++++
 .../antlr4/internal/ThrowingTokenStream.java       |   22 +-
 .../main/java/org/codehaus/groovy/ant/Groovyc.java |   16 +-
 .../console/ui/AstNodeToScriptAdapterTest.groovy   |   18 +
 .../groovy/contracts/MethodVariantSupport.java     |    4 +
 .../src/spec/doc/_working-with-dateutil-types.adoc |    2 +-
 subprojects/groovy-typecheckers/build.gradle       |    1 +
 .../groovy/groovy/typecheckers/RegexChecker.groovy |   41 +-
 .../groovy/typecheckers/SqlInjectionChecker.groovy |  285 +++++
 .../src/spec/doc/typecheckers.adoc                 |   51 +
 .../src/spec/test/RegexCheckerTest.groovy          |   45 +
 .../groovy/typecheckers/RegexCheckerTest.groovy    |  129 +++
 .../typecheckers/SqlInjectionCheckerTest.groovy    |  185 ++++
 .../org/apache/groovy/bench/ClosurePackBench.java  |  171 +++
 86 files changed, 7922 insertions(+), 335 deletions(-)
 create mode 100644 src/main/java/groovy/transform/PackedClosures.java
 create mode 100644 src/main/java/groovy/transform/SafeRegex.java
 create mode 100644 src/main/java/groovy/util/regex/RegexGuard.java
 copy subprojects/groovy-http-builder/src/main/java/groovy/http/Timeout.java => src/main/java/groovy/util/regex/RegexTimeoutException.java (63%)
 create mode 100644 src/main/java/org/apache/groovy/parser/antlr4/internal/AbstractFriendlyErrorStrategy.java
 create mode 100644 src/main/java/org/apache/groovy/parser/antlr4/internal/RecoveringDescriptiveErrorStrategy.java
 copy src/main/java/org/codehaus/groovy/classgen/asm/{WriterControllerFactory.java => PackStrategy.java} (54%)
 create mode 100644 src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java
 create mode 100644 src/main/java/org/codehaus/groovy/runtime/PackedClosure.java
 create mode 100644 src/main/java/org/codehaus/groovy/runtime/metaclass/PackedClosureMetaClass.java
 create mode 100644 src/main/java/org/codehaus/groovy/transform/SafeRegexASTTransformation.java
 create mode 100644 src/test/groovy/bugs/Groovy12188.groovy
 create mode 100644 src/test/groovy/groovy/util/regex/RegexGuardTest.groovy
 create mode 100644 src/test/groovy/org/apache/groovy/parser/antlr4/Groovy9192.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/runtime/PackedClosureMetaClassTest.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/transform/ClosurePackCapabilityTest.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/transform/PackedClosureBoundariesTest.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/transform/PackedClosureDebugMetadataTest.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/transform/PackedClosuresTransformTest.groovy
 create mode 100644 src/test/groovy/org/codehaus/groovy/transform/SafeRegexTransformTest.groovy
 create mode 100644 src/test/java/org/apache/groovy/parser/antlr4/internal/ErrorStrategyTest.java
 create mode 100644 subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/SqlInjectionChecker.groovy
 create mode 100644 subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/SqlInjectionCheckerTest.groovy
 create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ClosurePackBench.java


(groovy) 01/01: GROOVY-11792: for-in loop variable captured by closure/AIC sees final value, not per-iteration value

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-11792
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit feaf480ceefe4fae65a6701043be944c975a0823
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Jul 25 18:19:05 2026 +0900

    GROOVY-11792: for-in loop variable captured by closure/AIC sees final value, not per-iteration value
---
 .../codehaus/groovy/classgen/asm/CompileStack.java |  33 +
 .../groovy/classgen/asm/DelegatingController.java  |   8 +
 .../groovy/classgen/asm/StatementWriter.java       |  64 +-
 .../groovy/classgen/asm/WriterController.java      |  20 +
 .../asm/sc/StaticTypesStatementWriter.java         |  10 +-
 .../groovy/control/CompilerConfiguration.java      |  71 +-
 src/spec/doc/core-semantics.adoc                   |  15 +
 src/test/groovy/bugs/Groovy11792.groovy            | 818 +++++++++++++++++++++
 .../groovy/bugs/Groovy11792BytecodeTest.groovy     | 117 +++
 .../groovy/control/CompilerConfigurationTest.java  |  66 ++
 10 files changed, 1203 insertions(+), 19 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
index b2982ccf32..796617f32f 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
@@ -776,6 +776,10 @@ public class CompileStack {
 
     /**
      * Wraps the current stack value in a {@code groovy.lang.Reference} for the given variable slot.
+     * <p>
+     * Expects a boxed/object value already on the JVM operand stack. Prefer
+     * {@link #storeVar(BytecodeVariable, boolean)} when the value still needs
+     * casting and boxing.
      *
      * @param reference the variable that will receive the created reference
      */
@@ -788,6 +792,35 @@ public class CompileStack {
         mv.visitVarInsn(ASTORE, reference.getIndex());
     }
 
+    /**
+     * Stores the top-of-stack value into {@code variable}.
+     * <p>
+     * When {@code variable} is a {@linkplain BytecodeVariable#isHolder() holder}
+     * and {@code freshReferenceIfHolder} is {@code true}, the value is cast and
+     * boxed and a <em>new</em> {@link groovy.lang.Reference} is written to the
+     * slot (for-in per-iteration recapture, GROOVY-11792). Callers that already
+     * hold a boxed object on the JVM stack and do not need casting may call
+     * {@link #createReference(BytecodeVariable)} directly.
+     * <p>
+     * Otherwise delegates to {@link OperandStack#storeVar(BytecodeVariable)},
+     * which for an existing holder performs an in-place {@code Reference#set}.
+     *
+     * @param variable the target local slot
+     * @param freshReferenceIfHolder if {@code true} and the variable is a holder,
+     *        allocate a new {@code Reference} rather than updating the existing one
+     */
+    void storeVar(final BytecodeVariable variable, final boolean freshReferenceIfHolder) {
+        OperandStack operandStack = controller.getOperandStack();
+        if (variable.isHolder() && freshReferenceIfHolder) {
+            operandStack.doGroovyCast(variable.getType());
+            operandStack.box();
+            operandStack.remove(1);
+            createReference(variable);
+        } else {
+            operandStack.storeVar(variable);
+        }
+    }
+
     /**
      * Pushes the JVM default initial value for the supplied type.
      *
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java b/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
index cab638eba5..24a0cc8cfe 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
@@ -398,6 +398,14 @@ public class DelegatingController extends WriterController {
         return delegationController.shouldOptimizeForInt();
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isForLoopCaptureEnabled() {
+        return delegationController.isForLoopCaptureEnabled();
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
index 0db3a26312..67ff420eb8 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
@@ -223,20 +223,9 @@ public class StatementWriter {
         mv.visitVarInsn(ALOAD, iterator);
         writeIteratorNext(mv);
         operandStack.push(ClassHelper.OBJECT_TYPE);
-        operandStack.storeVar(valueVariable);
+        storeForLoopVariable(valueVariable);
         if (indexVariable != null) {
-            if (!indexVariable.isHolder()) {
-                mv.visitIincInsn(indexVariable.getIndex(), 1);
-            } else { // GROOVY-11751: shared variable reference
-                mv.visitVarInsn(ALOAD, indexVariable.getIndex());
-                mv.visitMethodInsn(INVOKEVIRTUAL, "groovy/lang/Reference", "get", "()Ljava/lang/Object;", false);
-                mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
-                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
-                mv.visitInsn(ICONST_1);
-                mv.visitInsn(IADD);
-                operandStack.push(ClassHelper.int_TYPE);
-                operandStack.storeVar(indexVariable);
-            }
+            incrementForLoopIndexVariable(indexVariable);
         }
 
         // generate the loop body
@@ -247,6 +236,55 @@ public class StatementWriter {
         compileStack.removeVar(iterator);
     }
 
+    /**
+     * Stores the top-of-stack value into a for-in loop variable slot.
+     * <p>
+     * Applies the for-in per-iteration capture policy
+     * ({@link WriterController#isForLoopCaptureEnabled()}, GROOVY-11792): when
+     * enabled, a shared (holder) loop variable receives a fresh
+     * {@link groovy.lang.Reference} so deferred closures, lambdas, and
+     * anonymous inner classes observe this iteration's value. When disabled,
+     * the historical in-place {@code Reference#set} path is used. Non-holder
+     * variables always take the plain store path.
+     * <p>
+     * Bytecode emission is delegated to
+     * {@link CompileStack#storeVar(BytecodeVariable, boolean)}.
+     *
+     * @param variable the for-in value (or similarly stored) loop variable
+     */
+    protected final void storeForLoopVariable(final BytecodeVariable variable) {
+        controller.getCompileStack().storeVar(variable, controller.isForLoopCaptureEnabled());
+    }
+
+    /**
+     * Increments the for-in index variable by one.
+     * <p>
+     * Plain (non-shared) indexes use {@code IINC}. Shared indexes are loaded
+     * from their {@link groovy.lang.Reference}, unboxed, incremented, then
+     * stored via {@link #storeForLoopVariable(BytecodeVariable)} so the same
+     * per-iteration capture policy applies as for value variables
+     * (GROOVY-11751, GROOVY-11792).
+     *
+     * @param indexVariable the for-in index variable; must not be {@code null}
+     */
+    protected final void incrementForLoopIndexVariable(final BytecodeVariable indexVariable) {
+        MethodVisitor mv = controller.getMethodVisitor();
+        if (!indexVariable.isHolder()) {
+            mv.visitIincInsn(indexVariable.getIndex(), 1);
+            return;
+        }
+        // GROOVY-11751: shared Reference — load, unbox, add 1, then store with
+        // the for-in recapture policy (storeForLoopVariable / GROOVY-11792).
+        mv.visitVarInsn(ALOAD, indexVariable.getIndex());
+        mv.visitMethodInsn(INVOKEVIRTUAL, "groovy/lang/Reference", "get", "()Ljava/lang/Object;", false);
+        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
+        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
+        mv.visitInsn(ICONST_1);
+        mv.visitInsn(IADD);
+        controller.getOperandStack().push(ClassHelper.int_TYPE);
+        storeForLoopVariable(indexVariable);
+    }
+
     protected final BytecodeVariable defineLoopIndexVariable(final ForStatement statement) {
         CompileStack compileStack = controller.getCompileStack();
         return Optional.ofNullable(statement.getIndexVariable()).map(iv -> {
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
index 6178523f01..64475386a5 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
@@ -78,6 +78,14 @@ public class WriterController {
      * comparable or better optimization.
      */
     public boolean optimizeForInt = true;
+    /**
+     * Language-compatibility flag (GROOVY-11792): when {@code true} (the default),
+     * for-in loop variables shared with closures, lambdas, or anonymous inner
+     * classes get a fresh {@link groovy.lang.Reference} each iteration. Cached
+     * from {@link CompilerConfiguration#isForLoopCaptureEnabled()} at
+     * {@link #init}. Independent of optimization options (including {@code "all"}).
+     */
+    private boolean forLoopCapture = true;
     private StatementWriter statementWriter;
     private boolean fastPath;
     private TypeChooser typeChooser;
@@ -103,6 +111,7 @@ public class WriterController {
         CompilerConfiguration config = cn.getCompileUnit().getConfig();
         Map<String,Boolean> optOptions = config.getOptimizationOptions();
         boolean invokedynamic = true;
+        this.forLoopCapture = config.isForLoopCaptureEnabled();
         if (optOptions.isEmpty()) {
             // IGNORE
         } else if (Boolean.FALSE.equals(optOptions.get("all"))) {
@@ -235,6 +244,17 @@ public class WriterController {
         return compileStack;
     }
 
+    /**
+     * Whether for-in loop variables shared with closures, lambdas, or AICs
+     * should be re-captured each iteration (language-compat flag, GROOVY-11792).
+     *
+     * @return {@code true} when per-iteration capture is enabled
+     * @see CompilerConfiguration#isForLoopCaptureEnabled()
+     */
+    public boolean isForLoopCaptureEnabled() {
+        return forLoopCapture;
+    }
+
     /**
      * Returns the OperandStack managing the JVM operand stack, tracking stack
      * depth and type information for bytecode verification and optimization.
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
index 01e1dea81d..5fd808db6a 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
@@ -151,7 +151,7 @@ public class StaticTypesStatementWriter extends StatementWriter {
         // $idx += 1
         mv.visitIincInsn(loopIdx, 1);
         if (indexVariable != null) {
-            mv.visitIincInsn(indexVariable.getIndex(), 1);
+            incrementForLoopIndexVariable(indexVariable);
         }
 
         // loop body
@@ -166,7 +166,7 @@ public class StaticTypesStatementWriter extends StatementWriter {
         compileStack.removeVar(array);
     }
 
-    private static void loadFromArray(final MethodVisitor mv, final OperandStack os, final BytecodeVariable variable, final int array, final int index) {
+    private void loadFromArray(final MethodVisitor mv, final OperandStack os, final BytecodeVariable variable, final int array, final int index) {
         mv.visitVarInsn(ALOAD, array);
         mv.visitVarInsn(ILOAD, index);
         ClassNode varType = variable.getType();
@@ -190,7 +190,7 @@ public class StaticTypesStatementWriter extends StatementWriter {
             mv.visitInsn(AALOAD);
         }
         os.push(varType);
-        os.storeVar(variable);
+        storeForLoopVariable(variable);
     }
 
     private void writeEnumerationBasedForEachLoop(final ForStatement loop, final Expression collectionExpression, final ClassNode collectionType) {
@@ -220,9 +220,9 @@ public class StaticTypesStatementWriter extends StatementWriter {
         mv.visitVarInsn(ALOAD, enumeration);
         ENUMERATION_NEXT_METHOD.call(mv);
         operandStack.push(ClassHelper.OBJECT_TYPE);
-        operandStack.storeVar(valueVariable);
+        storeForLoopVariable(valueVariable);
         if (indexVariable != null) {
-            mv.visitIincInsn(indexVariable.getIndex(), 1);
+            incrementForLoopIndexVariable(indexVariable);
         }
 
         loop.getLoopBlock().visit(controller.getAcg());
diff --git a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
index 1f1d53884f..d3f82024d8 100644
--- a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
+++ b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
@@ -260,6 +260,11 @@ public class CompilerConfiguration {
             throw new UnsupportedOperationException();
         }
 
+        @Override
+        public void setForLoopCaptureEnabled(final boolean forLoopCapture) {
+            throw new UnsupportedOperationException();
+        }
+
         @Override
         public void setJointCompilationOptions(final Map<String, Object> options) {
             throw new UnsupportedOperationException();
@@ -459,6 +464,18 @@ public class CompilerConfiguration {
      */
     private Map<String, Boolean> optimizationOptions;
 
+    /**
+     * Language-compatibility flag (GROOVY-11792): when {@code true} (the default),
+     * for-in loop variables shared with closures, lambdas, or anonymous inner
+     * classes receive a fresh {@link groovy.lang.Reference} each iteration.
+     * Classic {@code for} / {@code while} loops are unaffected. Independent of
+     * {@link #getOptimizationOptions()} (including {@code "all"} → false).
+     *
+     * @see #isForLoopCaptureEnabled()
+     * @see #setForLoopCaptureEnabled(boolean)
+     */
+    private boolean forLoopCapture = true;
+
     private final List<CompilationCustomizer> compilationCustomizers = new LinkedList<>();
 
     /**
@@ -497,6 +514,13 @@ public class CompilerConfiguration {
      *   <tr><td><code>groovy.parser.error.recovery</code></td><td>{@link #getOptimizationOptions} / {@link #isErrorRecoveryEnabled}</td></tr>
      * </table>
      * </blockquote>
+     * Additional language-compatibility system properties (not optimization options):
+     * <blockquote>
+     * <table summary="Groovy Language Compatibility Properties">
+     *   <tr><th>Property Key</th><th>Related Property Getter</th></tr>
+     *   <tr><td><code>groovy.for.loop.capture</code></td><td>{@link #isForLoopCaptureEnabled}</td></tr>
+     * </table>
+     * </blockquote>
      */
     public CompilerConfiguration() {
         classpath = new LinkedList<>();
@@ -513,6 +537,7 @@ public class CompilerConfiguration {
         setTargetDirectorySafe(getSystemPropertySafe("groovy.target.directory"));
         setTargetBytecodeIfValid(getSystemPropertySafe("groovy.target.bytecode", DEFAULT_TARGET_BYTECODE));
         defaultScriptExtension = getSystemPropertySafe("groovy.default.scriptExtension", ".groovy");
+        forLoopCapture = getBooleanSafe("groovy.for.loop.capture", true);
 
         optimizationOptions = new HashMap<>(5);
         handleOptimizationOption(INVOKEDYNAMIC, getSystemPropertySafe("groovy.target.indy", "true"));
@@ -568,6 +593,7 @@ public class CompilerConfiguration {
         setDisabledGlobalASTTransformations(configuration.getDisabledGlobalASTTransformations());
         setScriptExtensions(new LinkedHashSet<>(configuration.getScriptExtensions()));
         setOptimizationOptions(new HashMap<>(configuration.getOptimizationOptions()));
+        setForLoopCaptureEnabled(configuration.isForLoopCaptureEnabled());
         setBytecodePostprocessor(configuration.getBytecodePostprocessor());
 
         Map<String, Object> jointCompilationOptions = configuration.getJointCompilationOptions();
@@ -623,6 +649,7 @@ public class CompilerConfiguration {
      *   <tr><td><code>groovy.recompile</code></td><td>{@link #getRecompileGroovySource}</td></tr>
      *   <tr><td><code>groovy.recompile.minimumInterval</code></td><td>{@link #getMinimumRecompilationInterval}</td></tr>
      *   <tr><td><code>groovy.disabled.global.ast.transformations</code></td><td>{@link #getDisabledGlobalASTTransformations}</td></tr>
+     *   <tr><td><code>groovy.for.loop.capture</code></td><td>{@link #isForLoopCaptureEnabled}</td></tr>
      * </table>
      * </blockquote>
      *
@@ -818,6 +845,9 @@ public class CompilerConfiguration {
         text = configuration.getProperty("groovy.parameters");
         if (text != null) setParameters("true".equalsIgnoreCase(text));
 
+        text = configuration.getProperty("groovy.for.loop.capture");
+        if (text != null) setForLoopCaptureEnabled("true".equalsIgnoreCase(text));
+
         text = configuration.getProperty("groovy.preview.features");
         if (text != null) setPreviewFeatures("true".equalsIgnoreCase(text));
 
@@ -1312,7 +1342,10 @@ public class CompilerConfiguration {
      * Sets the optimization options for this configuration.
      * No entry or a true for that entry means to enable that optimization,
      * a false means the optimization is disabled.
-     * Valid keys are "all" and "int".
+     * Valid keys include {@code "all"}, {@code "int"}, {@link #INVOKEDYNAMIC},
+     * {@link #PARALLEL_PARSE}, {@link #GROOVYDOC}, and {@link #RUNTIME_GROOVYDOC}.
+     * For-in loop capture (GROOVY-11792) is not an optimization option; use
+     * {@link #setForLoopCaptureEnabled(boolean)} instead.
      * @param options the options.
      * @throws IllegalArgumentException if the options are null
      */
@@ -1425,4 +1458,40 @@ public class CompilerConfiguration {
     public boolean isErrorRecoveryEnabled() {
         return Boolean.TRUE.equals(getOptimizationOptions().get(ERROR_RECOVERY));
     }
+
+    /**
+     * Checks if per-iteration capture of for-in (enhanced for-each) loop
+     * variables is enabled (GROOVY-11792). Classic {@code for} and
+     * {@code while} loops are not affected by this flag.
+     * <p>
+     * This is a language-compatibility switch (not a performance optimization)
+     * and is independent of {@link #getOptimizationOptions()}. Enabled by
+     * default. Disable via system property {@code groovy.for.loop.capture=false}
+     * or {@link #setForLoopCaptureEnabled(boolean) setForLoopCaptureEnabled(false)},
+     * which restores the historical behaviour where a single shared
+     * {@link groovy.lang.Reference} is updated across for-in iterations.
+     *
+     * @return {@code true} if each for-in iteration should allocate a fresh
+     *         reference for loop variables shared with closures, lambdas, or
+     *         anonymous inner classes
+     * @see #setForLoopCaptureEnabled(boolean)
+     */
+    public boolean isForLoopCaptureEnabled() {
+        return forLoopCapture;
+    }
+
+    /**
+     * Enables or disables per-iteration capture of for-in (enhanced for-each)
+     * loop variables shared with closures, lambdas, or anonymous inner classes
+     * (GROOVY-11792). Classic {@code for} and {@code while} loops are not
+     * affected. Default is {@code true}. Setting {@code false} restores the
+     * historical single shared {@link groovy.lang.Reference} across iterations.
+     *
+     * @param forLoopCapture {@code true} to allocate a fresh reference each
+     *        for-in iteration for shared loop variables
+     * @see #isForLoopCaptureEnabled()
+     */
+    public void setForLoopCaptureEnabled(final boolean forLoopCapture) {
+        this.forLoopCapture = forLoopCapture;
+    }
 }
diff --git a/src/spec/doc/core-semantics.adoc b/src/spec/doc/core-semantics.adoc
index caa17a6771..8d777962d5 100644
--- a/src/spec/doc/core-semantics.adoc
+++ b/src/spec/doc/core-semantics.adoc
@@ -272,6 +272,21 @@ include::../test/SemanticsTest.groovy[tags=groovy_for_loop_example,indent=0]
 [NOTE]
 Groovy also supports the Java colon variation with colons: `for (char c : text) {}`
 
+[NOTE]
+====
+*Closure, lambda, and AIC capture of for-in variables (GROOVY-11792).*
+When a for-in loop variable is shared with a deferred closure, lambda, or
+anonymous inner class, each iteration uses a fresh binding so the capture
+observes the value from the iteration that created it (similar in spirit to
+Java enhanced-for / lambda capture for deferred use). Assignments to the loop
+variable within the same iteration remain visible to captures created in that
+iteration. Classic `for` / `while` loops are unchanged. To restore the
+historical single shared binding (every capture sees the final loop value), set
+system property `groovy.for.loop.capture=false` or call
+`CompilerConfiguration.setForLoopCaptureEnabled(false)` (language-compatibility
+switch, independent of optimization options including `"all" -> false`).
+====
+
 ===== while loop
 
 Groovy supports the usual while {...} loops like Java:
diff --git a/src/test/groovy/bugs/Groovy11792.groovy b/src/test/groovy/bugs/Groovy11792.groovy
new file mode 100644
index 0000000000..136a94ba01
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy11792.groovy
@@ -0,0 +1,818 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package bugs
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * GROOVY-11792: for-in loop variables captured by closures, lambdas, or AICs
+ * (GROOVY-11818) should observe the value from the iteration in which the
+ * capture was created, not the final loop value.
+ * <p>
+ * Dynamic lambdas compile as closures ({@code LambdaWriter} → {@code ClosureWriter}).
+ * Under {@code @CompileStatic}, SAM-targeted lambdas use
+ * {@code StaticTypesLambdaWriter} (native functional-interface bytecode) but still
+ * share for-in variables via {@code groovy.lang.Reference}, so the same per-iteration
+ * recapture policy applies.
+ * <p>
+ * Classgen patterns (fresh {@code Reference} vs in-place {@code Reference#set}) are
+ * asserted in {@link Groovy11792BytecodeTest}.
+ */
+@CompileStatic
+final class Groovy11792 {
+
+    /**
+     * Evaluates {@code script} with historical for-in capture
+     * ({@link CompilerConfiguration#setForLoopCaptureEnabled(boolean) setForLoopCaptureEnabled(false)}).
+     */
+    private static void evaluateWithLegacyCapture(final String script) {
+        CompilerConfiguration config = new CompilerConfiguration()
+        config.setForLoopCaptureEnabled(false)
+        new GroovyShell(config).evaluate(script)
+    }
+
+    @Test
+    void testForInClosureCapture() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            def numbers = [1, 2, 3]
+            List<Supplier> suppliers = []
+            for (n in numbers) {
+                Supplier s = { n * n }
+                suppliers << s
+            }
+            assert suppliers.collect { it.get() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInClosureCaptureColonSyntax() {
+        assertScript '''
+            def numbers = [1, 2, 3]
+            def closures = []
+            for (Integer n : numbers) {
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInClosureCaptureCompileStatic() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Integer> numbers = [1, 2, 3]
+                List<Closure<Integer>> closures = []
+                for (Integer n in numbers) {
+                    closures << { n * n }
+                }
+                return closures.collect { it.call() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    // GROOVY-11818
+    @Test
+    void testForInAnonymousInnerClassCapture() {
+        assertScript '''
+            interface Face {
+                int get()
+            }
+            def numbers = [1, 2, 3]
+            List<Face> faces = []
+            for (n in numbers) {
+                faces << new Face() {
+                    int get() { n * n }
+                }
+            }
+            assert faces.collect { it.get() } == [1, 4, 9]
+        '''
+    }
+
+    // GROOVY-11818
+    @Test
+    void testForInAnonymousInnerClassCaptureCompileStatic() {
+        assertScript '''
+            interface Face {
+                int get()
+            }
+            @groovy.transform.CompileStatic
+            class Runner {
+                static List<Integer> run() {
+                    List<Integer> numbers = [1, 2, 3]
+                    List<Face> faces = []
+                    for (Integer n in numbers) {
+                        faces << new Face() {
+                            int get() { n * n }
+                        }
+                    }
+                    return faces.collect { it.get() }
+                }
+            }
+            assert Runner.run() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInIndexedCapture() {
+        assertScript '''
+            def items = ['a', 'b', 'c']
+            def valueClosures = []
+            def indexClosures = []
+            for (i, v in items) {
+                valueClosures << { v }
+                indexClosures << { i }
+            }
+            assert valueClosures.collect { it() } == ['a', 'b', 'c']
+            assert indexClosures.collect { it() } == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInIndexedCaptureCompileStatic() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<String> items = ['a', 'b', 'c']
+                List<Closure<String>> valueClosures = []
+                List<Closure<Integer>> indexClosures = []
+                for (int i, String v in items) {
+                    valueClosures << { v }
+                    indexClosures << { i }
+                }
+                return [valueClosures.collect { it.call() }, indexClosures.collect { it.call() }]
+            }
+            def (values, indexes) = execute()
+            assert values == ['a', 'b', 'c']
+            assert indexes == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInArrayCaptureCompileStatic() {
+        // Exercises StaticTypesStatementWriter#writeOptimizedForEachLoop
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                int[] numbers = [1, 2, 3] as int[]
+                List<Closure<Integer>> closures = []
+                for (int n in numbers) {
+                    closures << { n * n }
+                }
+                return closures.collect { it.call() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInArrayIndexedCaptureCompileStatic() {
+        // SC array path + shared index (incrementForLoopIndexVariable)
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                int[] numbers = [10, 20, 30] as int[]
+                List<Closure<Integer>> valueClosures = []
+                List<Closure<Integer>> indexClosures = []
+                for (int i, int n in numbers) {
+                    valueClosures << { n }
+                    indexClosures << { i }
+                }
+                return [valueClosures.collect { it.call() }, indexClosures.collect { it.call() }]
+            }
+            def (values, indexes) = execute()
+            assert values == [10, 20, 30]
+            assert indexes == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInEnumerationCaptureCompileStatic() {
+        // Exercises StaticTypesStatementWriter#writeEnumerationBasedForEachLoop
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                Enumeration<Integer> numbers = Collections.enumeration([1, 2, 3])
+                List<Closure<Integer>> closures = []
+                for (Integer n in numbers) {
+                    closures << { n * n }
+                }
+                return closures.collect { it.call() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInEnumerationIndexedCaptureCompileStatic() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                Enumeration<Integer> numbers = Collections.enumeration([10, 20, 30])
+                List<Closure<Integer>> valueClosures = []
+                List<Closure<Integer>> indexClosures = []
+                for (int i, Integer n in numbers) {
+                    valueClosures << { n }
+                    indexClosures << { i }
+                }
+                return [valueClosures.collect { it.call() }, indexClosures.collect { it.call() }]
+            }
+            def (values, indexes) = execute()
+            assert values == [10, 20, 30]
+            assert indexes == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInLambdaSamCapture() {
+        // Dynamic mode: lambda → ClosureWriter path with SAM assignment
+        assertScript '''
+            import java.util.function.Supplier
+
+            def numbers = [1, 2, 3]
+            List suppliers = []
+            for (n in numbers) {
+                Supplier s = () -> n * n
+                suppliers << s
+            }
+            assert suppliers.collect { it.get() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaAsClosureCapture() {
+        // Dynamic mode without SAM target: lambda is a Closure
+        assertScript '''
+            def numbers = [1, 2, 3]
+            def closures = []
+            for (n in numbers) {
+                closures << (() -> n * n)
+            }
+            assert closures.collect { it() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaSamCaptureCompileStatic() {
+        // SC lambda / SAM conversion capturing the for-in variable
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Integer> numbers = [1, 2, 3]
+                List<Supplier<Integer>> suppliers = []
+                for (Integer n in numbers) {
+                    Supplier<Integer> s = () -> n * n
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaIndexedCaptureCompileStatic() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<String> items = ['a', 'b', 'c']
+                List<Supplier<String>> values = []
+                List<Supplier<Integer>> indexes = []
+                for (int i, String v in items) {
+                    Supplier<String> vs = () -> v
+                    Supplier<Integer> is = () -> i
+                    values << vs
+                    indexes << is
+                }
+                return [values.collect { it.get() }, indexes.collect { it.get() }]
+            }
+            def (v, i) = execute()
+            assert v == ['a', 'b', 'c']
+            assert i == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInLambdaArrayCaptureCompileStatic() {
+        // StaticTypesStatementWriter#writeOptimizedForEachLoop + native lambda
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                int[] numbers = [1, 2, 3] as int[]
+                List<Supplier<Integer>> suppliers = []
+                for (int n in numbers) {
+                    Supplier<Integer> s = () -> n * n
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaEnumerationCaptureCompileStatic() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                Enumeration<Integer> numbers = Collections.enumeration([1, 2, 3])
+                List<Supplier<Integer>> suppliers = []
+                for (Integer n in numbers) {
+                    Supplier<Integer> s = () -> n * n
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaNestedCaptureCompileStatic() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Supplier<Integer>> suppliers = []
+                for (Integer i in [1, 2]) {
+                    for (Integer j in [10, 20]) {
+                        Supplier<Integer> s = () -> i + j
+                        suppliers << s
+                    }
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [11, 21, 12, 22]
+        '''
+    }
+
+    @Test
+    void testForInLambdaSameIterationMutationCompileStatic() {
+        // Mutable Reference within one iteration remains shared across captures
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Supplier<Integer>> suppliers = []
+                for (Integer n in [1, 2, 3]) {
+                    Supplier<Integer> s = () -> n
+                    n = n + 10
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [11, 12, 13]
+        '''
+    }
+
+    @Test
+    void testForInLambdaFunctionCaptureCompileStatic() {
+        assertScript '''
+            import java.util.function.Function
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Function<Integer, Integer>> fns = []
+                for (Integer n in [1, 2, 3]) {
+                    Function<Integer, Integer> f = (Integer x) -> n + x
+                    fns << f
+                }
+                return fns.collect { it.apply(10) }
+            }
+            assert execute() == [11, 12, 13]
+        '''
+    }
+
+    @Test
+    void testForInLambdaDirectListAddCompileStatic() {
+        // SAM conversion via typed List#add argument (no intermediate local)
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Supplier<Integer>> suppliers = new ArrayList<>()
+                for (Integer n in [1, 2, 3]) {
+                    suppliers.add(() -> n * n)
+                }
+                List out = []
+                for (Supplier<Integer> s : suppliers) {
+                    out << s.get()
+                }
+                return out
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInLambdaColonSyntaxCompileStatic() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Integer> numbers = [1, 2, 3]
+                List<Supplier<Integer>> suppliers = []
+                for (Integer n : numbers) {
+                    Supplier<Integer> s = () -> n * n
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutLambda() {
+        evaluateWithLegacyCapture '''
+            import java.util.function.Supplier
+
+            def suppliers = []
+            for (n in [1, 2, 3]) {
+                Supplier s = () -> n * n
+                suppliers << s
+            }
+            assert suppliers.collect { it.get() } == [9, 9, 9]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutLambdaCompileStatic() {
+        evaluateWithLegacyCapture '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Integer> numbers = [1, 2, 3]
+                List<Supplier<Integer>> suppliers = []
+                for (Integer n in numbers) {
+                    Supplier<Integer> s = () -> n * n
+                    suppliers << s
+                }
+                return suppliers.collect { it.get() }
+            }
+            assert execute() == [9, 9, 9]
+        '''
+    }
+
+    @Test
+    void testNestedForInCapture() {
+        assertScript '''
+            def closures = []
+            for (i in [1, 2]) {
+                for (j in [10, 20]) {
+                    closures << { i + j }
+                }
+            }
+            assert closures.collect { it() } == [11, 21, 12, 22]
+        '''
+    }
+
+    @Test
+    void testSameIterationMutationStillShared() {
+        // Closures created in the same iteration still share one Reference,
+        // so an assignment after capture remains visible within that iteration.
+        assertScript '''
+            def closures = []
+            for (n in [1, 2, 3]) {
+                def c = { n }
+                n = n + 10
+                closures << c
+            }
+            assert closures.collect { it() } == [11, 12, 13]
+        '''
+    }
+
+    @Test
+    void testNonSharedLoopVariableUnchanged() {
+        // Loop variable not captured by a deferred closure — plain assignment path.
+        assertScript '''
+            def sum = 0
+            for (n in [1, 2, 3]) {
+                sum += n
+            }
+            assert sum == 6
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOut() {
+        evaluateWithLegacyCapture '''
+            import java.util.function.Supplier
+
+            def numbers = [1, 2, 3]
+            List suppliers = []
+            for (n in numbers) {
+                Supplier s = { n * n }
+                suppliers << s
+            }
+            // Historical behaviour: every supplier sees the final loop value.
+            assert suppliers.collect { it.get() } == [9, 9, 9]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutIndexed() {
+        evaluateWithLegacyCapture '''
+            def items = ['a', 'b', 'c']
+            def valueClosures = []
+            def indexClosures = []
+            for (i, v in items) {
+                valueClosures << { v }
+                indexClosures << { i }
+            }
+            assert valueClosures.collect { it() } == ['c', 'c', 'c']
+            assert indexClosures.collect { it() } == [2, 2, 2]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutArrayCompileStatic() {
+        evaluateWithLegacyCapture '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                int[] numbers = [1, 2, 3] as int[]
+                List<Closure<Integer>> closures = []
+                for (int n in numbers) {
+                    closures << { n * n }
+                }
+                return closures.collect { it.call() }
+            }
+            assert execute() == [9, 9, 9]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutIndexedCompileStatic() {
+        // SC indexed for-in with shared index uses storeForLoopVariable after increment
+        evaluateWithLegacyCapture '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<String> items = ['a', 'b', 'c']
+                List<Closure<String>> valueClosures = []
+                List<Closure<Integer>> indexClosures = []
+                for (int i, String v in items) {
+                    valueClosures << { v }
+                    indexClosures << { i }
+                }
+                return [valueClosures.collect { it.call() }, indexClosures.collect { it.call() }]
+            }
+            def (values, indexes) = execute()
+            assert values == ['c', 'c', 'c']
+            assert indexes == [2, 2, 2]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutEnumerationCompileStatic() {
+        evaluateWithLegacyCapture '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                Enumeration<Integer> numbers = Collections.enumeration([1, 2, 3])
+                List<Closure<Integer>> closures = []
+                for (Integer n in numbers) {
+                    closures << { n * n }
+                }
+                return closures.collect { it.call() }
+            }
+            assert execute() == [9, 9, 9]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutArrayIndexedCompileStatic() {
+        // SC optimized array path + shared index with recapture disabled
+        evaluateWithLegacyCapture '''
+            @groovy.transform.CompileStatic
+            def execute() {
+                int[] numbers = [10, 20, 30] as int[]
+                List<Closure<Integer>> valueClosures = []
+                List<Closure<Integer>> indexClosures = []
+                for (int i, int n in numbers) {
+                    valueClosures << { n }
+                    indexClosures << { i }
+                }
+                return [valueClosures.collect { it.call() }, indexClosures.collect { it.call() }]
+            }
+            def (values, indexes) = execute()
+            assert values == [30, 30, 30]
+            assert indexes == [2, 2, 2]
+        '''
+    }
+
+    @Test
+    void testForInRangeCapture() {
+        assertScript '''
+            def closures = []
+            for (n in 1..3) {
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testForInEmptyCollection() {
+        assertScript '''
+            def closures = []
+            for (n in []) {
+                closures << { n }
+            }
+            assert closures.isEmpty()
+        '''
+    }
+
+    @Test
+    void testForInSingleElementCapture() {
+        assertScript '''
+            def closures = []
+            for (n in [7]) {
+                closures << { n * 2 }
+            }
+            assert closures.collect { it() } == [14]
+        '''
+    }
+
+    @Test
+    void testForInCaptureWithContinue() {
+        // continue must not break per-iteration capture for later iterations
+        assertScript '''
+            def closures = []
+            for (n in [1, 2, 3, 4]) {
+                if (n % 2 == 0) continue
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 9]
+        '''
+    }
+
+    @Test
+    void testForInCaptureWithBreak() {
+        assertScript '''
+            def closures = []
+            for (n in [1, 2, 3, 4]) {
+                if (n == 3) break
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 4]
+        '''
+    }
+
+    @Test
+    void testAllOptimizationsOffDoesNotDisableForLoopCapture() {
+        // "all" -> false is a performance switch; for-in recapture stays on
+        def config = new CompilerConfiguration()
+        config.optimizationOptions['all'] = Boolean.FALSE
+        assert config.isForLoopCaptureEnabled()
+        def shell = new GroovyShell(config)
+        shell.evaluate '''
+            def closures = []
+            for (n in [1, 2, 3]) {
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testOuterSharedVariableStillMutable() {
+        // Non-loop shared variables must continue to use a single Reference.
+        assertScript '''
+            def x = 1
+            def c = { x }
+            x = 2
+            assert c() == 2
+        '''
+    }
+
+    @Test
+    void testEachStillPerCallBinding() {
+        assertScript '''
+            def closures = []
+            [1, 2, 3].each { n ->
+                closures << { n * n }
+            }
+            assert closures.collect { it() } == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testClassicForLoopCaptureStillShared() {
+        // Classic C-style for is intentionally unchanged (single shared binding).
+        assertScript '''
+            def closures = []
+            for (int i = 0; i < 3; i++) {
+                closures << { i }
+            }
+            assert closures.collect { it() } == [3, 3, 3]
+        '''
+    }
+
+    @Test
+    void testWhileLoopCaptureStillShared() {
+        // while loops are intentionally unchanged (single shared binding).
+        assertScript '''
+            def closures = []
+            def i = 0
+            while (i < 3) {
+                closures << { i }
+                i++
+            }
+            assert closures.collect { it() } == [3, 3, 3]
+        '''
+    }
+
+    @Test
+    void testForInLambdaIndexedCapture() {
+        assertScript '''
+            import java.util.function.Supplier
+
+            def values = []
+            def indexes = []
+            for (i, v in ['a', 'b', 'c']) {
+                Supplier vs = () -> v
+                Supplier is = () -> i
+                values << vs
+                indexes << is
+            }
+            assert values.collect { it.get() } == ['a', 'b', 'c']
+            assert indexes.collect { it.get() } == [0, 1, 2]
+        '''
+    }
+
+    @Test
+    void testForInLambdaDualCaptureSameIterationMutation() {
+        // Two lambdas in one iteration share one Reference; mutation is visible to both.
+        assertScript '''
+            import java.util.function.Supplier
+
+            @groovy.transform.CompileStatic
+            def execute() {
+                List<Supplier<Integer>> a = []
+                List<Supplier<Integer>> b = []
+                for (Integer n in [1, 2, 3]) {
+                    Supplier<Integer> sa = () -> n
+                    n = n + 100
+                    Supplier<Integer> sb = () -> n
+                    a << sa
+                    b << sb
+                }
+                return [a.collect { it.get() }, b.collect { it.get() }]
+            }
+            def (ra, rb) = execute()
+            assert ra == [101, 102, 103]
+            assert rb == [101, 102, 103]
+        '''
+    }
+
+    @Test
+    void testLegacySharedCaptureOptOutAnonymousInnerClass() {
+        evaluateWithLegacyCapture '''
+            interface Face {
+                int get()
+            }
+            def faces = []
+            for (n in [1, 2, 3]) {
+                faces << new Face() {
+                    int get() { n * n }
+                }
+            }
+            assert faces.collect { it.get() } == [9, 9, 9]
+        '''
+    }
+}
diff --git a/src/test/groovy/bugs/Groovy11792BytecodeTest.groovy b/src/test/groovy/bugs/Groovy11792BytecodeTest.groovy
new file mode 100644
index 0000000000..7d023d2718
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy11792BytecodeTest.groovy
@@ -0,0 +1,117 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package bugs
+
+import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase
+import org.codehaus.groovy.classgen.asm.InstructionSequence
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.Phases
+import org.junit.jupiter.api.Test
+
+/**
+ * GROOVY-11792 bytecode documentation tests.
+ * <p>
+ * Behavioural coverage lives in {@link Groovy11792}. These tests pin the
+ * classgen contract for for-in holder stores after {@code Iterator#next}:
+ * <ul>
+ *   <li><b>new (default)</b> — allocate a fresh {@code groovy.lang.Reference}
+ *       each iteration ({@code NEW} + {@code <init>(Object)} + {@code ASTORE})</li>
+ *   <li><b>old (legacy opt-out)</b> — in-place {@code Reference#set} on the
+ *       shared holder ({@code ALOAD} + {@code CHECKCAST} + {@code set})</li>
+ * </ul>
+ * Both modes still create an initial holder {@code Reference} before the loop
+ * ({@code defineVariable}); the distinguishing site is the store that follows
+ * {@code Iterator.next} inside the loop.
+ */
+final class Groovy11792BytecodeTest extends AbstractBytecodeTestCase {
+
+    /**
+     * Minimal for-in + deferred closure capture (no body assignment to the
+     * loop variable, so the only {@code Reference#set} would be the legacy
+     * loop-head store).
+     */
+    private static final String FOR_IN_CAPTURE_SCRIPT = '''
+def suppliers = []
+for (n in [1, 2, 3]) {
+    suppliers << { n }
+}
+'''
+
+    /**
+     * New loop-head store: after {@code next()}, wrap the element in a fresh
+     * {@code Reference} and store it into the loop-variable slot.
+     */
+    private static final List<String> NEW_LOOP_STORE = [
+            'INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;',
+            'NEW groovy/lang/Reference',
+            'DUP_X1',
+            'SWAP',
+            'INVOKESPECIAL groovy/lang/Reference.<init> (Ljava/lang/Object;)V',
+    ]
+
+    /**
+     * Historical loop-head store: after {@code next()}, update the existing
+     * shared {@code Reference} in place.
+     */
+    private static final List<String> OLD_LOOP_STORE = [
+            'INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;',
+            'ALOAD',
+            'CHECKCAST groovy/lang/Reference',
+            'SWAP',
+            'INVOKEVIRTUAL groovy/lang/Reference.set (Ljava/lang/Object;)V',
+    ]
+
+    private InstructionSequence compileForInCapture(final boolean forLoopCaptureEnabled) {
+        CompilerConfiguration config = new CompilerConfiguration()
+        config.setForLoopCaptureEnabled(forLoopCaptureEnabled)
+        CompilationUnit cu = new CompilationUnit(config)
+        cu.addSource('script', FOR_IN_CAPTURE_SCRIPT)
+        cu.compile(Phases.CLASS_GENERATION)
+        def gc = cu.classes.find { it.name == cu.firstClassNode.name }
+        extractSequence(gc.bytes, [method: 'run'])
+    }
+
+    @Test
+    void testNewBytecodePatternWhenForLoopCaptureEnabled() {
+        // Default language-compat: per-iteration fresh Reference after next()
+        InstructionSequence bytecode = compileForInCapture(true)
+
+        assert bytecode.hasStrictSequence(NEW_LOOP_STORE) :
+                "expected fresh Reference after Iterator.next:\n${bytecode}"
+        // No in-place set of the shared holder in the loop head
+        assert !bytecode.hasSequence(OLD_LOOP_STORE) :
+                "legacy Reference#set loop store must not appear:\n${bytecode}"
+        assert !bytecode.hasSequence([
+                'INVOKEVIRTUAL groovy/lang/Reference.set (Ljava/lang/Object;)V',
+        ]) : "Reference#set must not appear when body does not assign:\n${bytecode}"
+    }
+
+    @Test
+    void testOldBytecodePatternWhenForLoopCaptureDisabled() {
+        // Historical opt-out: single Reference updated with set() after next()
+        InstructionSequence bytecode = compileForInCapture(false)
+
+        assert bytecode.hasStrictSequence(OLD_LOOP_STORE) :
+                "expected in-place Reference#set after Iterator.next:\n${bytecode}"
+        // Loop head must not allocate a new Reference after next()
+        assert !bytecode.hasStrictSequence(NEW_LOOP_STORE) :
+                "fresh Reference after Iterator.next must not appear:\n${bytecode}"
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java b/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java
index 92a34633e3..0da656fbfe 100644
--- a/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java
+++ b/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Properties;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -310,4 +311,69 @@ public final class CompilerConfigurationTest {
         String[] expect = {"17" , "17" , "17" , "17" , "17" , "17" , "17" , "17", "17", "17", "17", "17", "17" , "17", "17", "17", "17", "17", "17", "17", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "27"};
         assertArrayEquals(expect, Arrays.stream(inputs).map(v -> { config.setTargetBytecode(v); return config.getTargetBytecode(); }).toArray(String[]::new));
     }
+
+    @Test // GROOVY-11792
+    public void testForLoopCaptureEnabledByDefault() {
+        CompilerConfiguration config = new CompilerConfiguration();
+        assertTrue(config.isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    public void testForLoopCaptureCanBeDisabled() {
+        CompilerConfiguration config = new CompilerConfiguration();
+        config.setForLoopCaptureEnabled(false);
+        assertFalse(config.isForLoopCaptureEnabled());
+        config.setForLoopCaptureEnabled(true);
+        assertTrue(config.isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    public void testForLoopCaptureNotClearedByAllFalse() {
+        CompilerConfiguration config = new CompilerConfiguration();
+        config.setForLoopCaptureEnabled(false);
+        config.getOptimizationOptions().put("all", Boolean.FALSE);
+        // optimization "all" must not re-enable or clear the language-compat field
+        assertFalse(config.isForLoopCaptureEnabled());
+        config.setForLoopCaptureEnabled(true);
+        config.getOptimizationOptions().put("all", Boolean.FALSE);
+        assertTrue(config.isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    public void testForLoopCaptureCopyConstructor() {
+        CompilerConfiguration src = new CompilerConfiguration();
+        src.setForLoopCaptureEnabled(false);
+        CompilerConfiguration copy = new CompilerConfiguration(src);
+        assertFalse(copy.isForLoopCaptureEnabled());
+        src.setForLoopCaptureEnabled(true);
+        // copy is independent
+        assertFalse(copy.isForLoopCaptureEnabled());
+        assertTrue(new CompilerConfiguration(src).isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    public void testForLoopCaptureViaConfigureProperties() {
+        CompilerConfiguration config = new CompilerConfiguration();
+        Properties props = new Properties();
+        props.setProperty("groovy.for.loop.capture", "false");
+        config.configure(props);
+        assertFalse(config.isForLoopCaptureEnabled());
+        props.setProperty("groovy.for.loop.capture", "true");
+        config.configure(props);
+        assertTrue(config.isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    @ForkedJvm(systemProperties = {"groovy.for.loop.capture=false"})
+    public void testForLoopCaptureDisabledViaSystemProperty() {
+        CompilerConfiguration config = new CompilerConfiguration(System.getProperties());
+        assertFalse(config.isForLoopCaptureEnabled());
+    }
+
+    @Test // GROOVY-11792
+    public void testDefaultConfigurationForLoopCaptureIsImmutable() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                CompilerConfiguration.DEFAULT.setForLoopCaptureEnabled(false));
+        assertTrue(CompilerConfiguration.DEFAULT.isForLoopCaptureEnabled());
+    }
 }