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

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

commit 738ec2b5e9cde09b4207667d0ee693fd1f2d6ec1
Author: Daniel Sun <[email protected]>
AuthorDate: Sun Apr 5 22:28:54 2026 +0900

    GROOVY-11905: Optimize non-capturing lambdas
---
 .../classgen/asm/sc/StaticTypesLambdaWriter.java   |  78 ++++--
 .../groovy/groovy/transform/stc/LambdaTest.groovy  | 289 +++++++++++++++++++++
 .../groovy/classgen/asm/TypeAnnotationsTest.groovy |   2 +-
 3 files changed, 342 insertions(+), 27 deletions(-)

diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
 
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
index b804702754..4b73c4a4c3 100644
--- 
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
+++ 
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
@@ -72,6 +72,7 @@ import static org.objectweb.asm.Opcodes.ACC_STATIC;
 import static org.objectweb.asm.Opcodes.ALOAD;
 import static org.objectweb.asm.Opcodes.CHECKCAST;
 import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
 import static org.objectweb.asm.Opcodes.H_INVOKEVIRTUAL;
 import static org.objectweb.asm.Opcodes.ICONST_0;
 import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
@@ -113,27 +114,47 @@ public class StaticTypesLambdaWriter extends LambdaWriter 
implements AbstractFun
         ClassNode lambdaClass = getOrAddLambdaClass(expression, 
abstractMethod);
         MethodNode lambdaMethod = lambdaClass.getMethods(DO_CALL).get(0);
 
+        Parameter[] lambdaSharedVariables = 
expression.getNodeMetaData(LAMBDA_SHARED_VARIABLES);
+        boolean accessingInstanceMembers = 
isAccessingInstanceMembersOfEnclosingClass(lambdaMethod);
+        // For non-capturing lambdas: make doCall static and use a 
capture-free invokedynamic,
+        // so LambdaMetafactory creates a singleton instance — just like Java 
non-capturing lambdas.
+        boolean isNonCapturing = lambdaSharedVariables.length == 0 && 
!accessingInstanceMembers;
         boolean canDeserialize = 
controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass),
 createDeserializeLambdaMethodParams());
-        if (!canDeserialize) {
+
+        if (isNonCapturing) {
+            lambdaMethod.setModifiers(lambdaMethod.getModifiers() | 
ACC_STATIC);
+            if (expression.isSerializable() && !canDeserialize) {
+                addDeserializeLambdaMethodForEachLambdaExpression(expression, 
lambdaClass, true);
+                addDeserializeLambdaMethod();
+            }
+        } else if (!canDeserialize) {
             if (expression.isSerializable()) {
-                addDeserializeLambdaMethodForEachLambdaExpression(expression, 
lambdaClass);
+                addDeserializeLambdaMethodForEachLambdaExpression(expression, 
lambdaClass, false);
                 addDeserializeLambdaMethod();
             }
-            newGroovyLambdaWrapperAndLoad(lambdaClass, expression, 
isAccessingInstanceMembersOfEnclosingClass(lambdaMethod));
+            newGroovyLambdaWrapperAndLoad(lambdaClass, expression, 
accessingInstanceMembers);
         }
 
         MethodVisitor mv = controller.getMethodVisitor();
         mv.visitInvokeDynamicInsn(
                 abstractMethod.getName(),
-                createAbstractMethodDesc(functionalType.redirect(), 
lambdaClass),
+                isNonCapturing
+                        ? 
BytecodeHelper.getMethodDescriptor(functionalType.redirect(), 
Parameter.EMPTY_ARRAY)
+                        : createAbstractMethodDesc(functionalType.redirect(), 
lambdaClass),
                 createBootstrapMethod(controller.getClassNode().isInterface(), 
expression.isSerializable()),
-                
createBootstrapMethodArguments(createMethodDescriptor(abstractMethod), 
H_INVOKEVIRTUAL, lambdaClass, lambdaMethod, lambdaMethod.getParameters(), 
expression.isSerializable())
+                
createBootstrapMethodArguments(createMethodDescriptor(abstractMethod),
+                        isNonCapturing ? H_INVOKESTATIC : H_INVOKEVIRTUAL,
+                        lambdaClass, lambdaMethod, 
lambdaMethod.getParameters(), expression.isSerializable())
         );
         if (expression.isSerializable()) {
             mv.visitTypeInsn(CHECKCAST, "java/io/Serializable");
         }
 
-        controller.getOperandStack().replace(functionalType.redirect(), 1);
+        if (isNonCapturing) {
+            controller.getOperandStack().push(functionalType.redirect());
+        } else {
+            controller.getOperandStack().replace(functionalType.redirect(), 1);
+        }
     }
 
     private static Parameter[] createDeserializeLambdaMethodParams() {
@@ -325,27 +346,32 @@ public class StaticTypesLambdaWriter extends LambdaWriter 
implements AbstractFun
                 code);
     }
 
-    private void addDeserializeLambdaMethodForEachLambdaExpression(final 
LambdaExpression expression, final ClassNode lambdaClass) {
+    private void addDeserializeLambdaMethodForEachLambdaExpression(final 
LambdaExpression expression, final ClassNode lambdaClass, final boolean 
isNonCapturing) {
         ClassNode enclosingClass = controller.getClassNode();
-        Statement code = block(
-                new BytecodeSequence(new BytecodeInstruction() {
-                    @Override
-                    public void visit(final MethodVisitor mv) {
-                        mv.visitVarInsn(ALOAD, 0);
-                        mv.visitInsn(ICONST_0);
-                        mv.visitMethodInsn(
-                                INVOKEVIRTUAL,
-                                "java/lang/invoke/SerializedLambda",
-                                "getCapturedArg",
-                                "(I)Ljava/lang/Object;",
-                                false);
-                        mv.visitTypeInsn(CHECKCAST, 
BytecodeHelper.getClassInternalName(lambdaClass));
-                        OperandStack operandStack = 
controller.getOperandStack();
-                        operandStack.push(lambdaClass);
-                    }
-                }),
-                returnS(expression)
-        );
+        Statement code;
+        if (isNonCapturing) {
+            code = block(returnS(expression));
+        } else {
+            code = block(
+                    new BytecodeSequence(new BytecodeInstruction() {
+                        @Override
+                        public void visit(final MethodVisitor mv) {
+                            mv.visitVarInsn(ALOAD, 0);
+                            mv.visitInsn(ICONST_0);
+                            mv.visitMethodInsn(
+                                    INVOKEVIRTUAL,
+                                    "java/lang/invoke/SerializedLambda",
+                                    "getCapturedArg",
+                                    "(I)Ljava/lang/Object;",
+                                    false);
+                            mv.visitTypeInsn(CHECKCAST, 
BytecodeHelper.getClassInternalName(lambdaClass));
+                            OperandStack operandStack = 
controller.getOperandStack();
+                            operandStack.push(lambdaClass);
+                        }
+                    }),
+                    returnS(expression)
+            );
+        }
 
         enclosingClass.addSyntheticMethod(
                 createDeserializeLambdaMethodName(lambdaClass),
diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy 
b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
index f69eb1a332..677930869c 100644
--- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
@@ -1888,4 +1888,293 @@ final class LambdaTest {
             assert 
this.class.classLoader.loadClass('Foo$_bar_lambda1').modifiers == 25 // 
public(1) + static(8) + final(16)
         '''
     }
+
+    // GROOVY-11905
+    @Test
+    void testNonCapturingLambdaOptimization() {
+        // non-capturing lambda with Function in static method
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    assert [2, 3, 4] == [1, 2, 3].stream().map(e -> e + 
1).toList()
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with Function in instance method (no this 
access)
+        assertScript shell, '''
+            class C {
+                void test() {
+                    assert [2, 3, 4] == [1, 2, 3].stream().map(e -> e + 
1).toList()
+                }
+            }
+            new C().test()
+        '''
+
+        // non-capturing lambda with Predicate
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    assert [2, 4] == [1, 2, 3, 4].stream().filter(e -> e % 2 
== 0).toList()
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with Supplier (zero parameters)
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    Supplier<String> s = () -> 'constant'
+                    assert s.get() == 'constant'
+                    assert 'hello' == Optional.<String>empty().orElseGet(() -> 
'hello')
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with BiFunction (multiple parameters)
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    BiFunction<Integer, Integer, Integer> f = (a, b) -> a + b
+                    assert f.apply(3, 4) == 7
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with Comparator
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    assert [3, 2, 1] == [1, 2, 3].stream().sorted((a, b) -> 
b.compareTo(a)).toList()
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with IntUnaryOperator (primitive parameter 
type)
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    java.util.function.IntUnaryOperator op = (int i) -> i * 2
+                    assert op.applyAsInt(5) == 10
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda with custom functional interface
+        assertScript shell, '''
+            interface Transformer<I, O> {
+                O transform(I input)
+            }
+            class C {
+                static void test() {
+                    Transformer<String, Integer> t = (String s) -> s.length()
+                    assert t.transform('hello') == 5
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda calling static method only (no this access)
+        assertScript shell, '''
+            class C {
+                static String prefix() { 'Hi ' }
+                static void test() {
+                    assert ['Hi 1', 'Hi 2'] == [1, 2].stream().map(e -> 
C.prefix() + e).toList()
+                }
+            }
+            C.test()
+        '''
+
+        // multiple non-capturing lambdas in the same method
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    Function<Integer, Integer> f = (Integer x) -> x + 1
+                    Function<Integer, String>  g = (Integer x) -> 'v' + x
+                    Predicate<Integer>         p = (Integer x) -> x > 2
+                    assert f.apply(1) == 2
+                    assert g.apply(1) == 'v1'
+                    assert p.test(3) && !p.test(1)
+                }
+            }
+            C.test()
+        '''
+
+        // non-capturing lambda in static initializer block
+        assertScript shell, '''
+            class C {
+                static List<Integer> result
+                static { result = [1, 2, 3].stream().map(e -> e * 2).toList() }
+            }
+            assert C.result == [2, 4, 6]
+        '''
+
+        // non-capturing lambda in field initializer
+        assertScript shell, '''
+            class C {
+                java.util.function.IntUnaryOperator op = (int i) -> i + 1
+                void test() { assert op.applyAsInt(5) == 6 }
+            }
+            new C().test()
+        '''
+
+        // non-capturing lambda in interface default method
+        assertScript shell, '''
+            interface Processor {
+                default List<Integer> process(List<Integer> input) {
+                    input.stream().map(e -> e + 1).toList()
+                }
+            }
+            class C implements Processor {}
+            assert new C().process([1, 2, 3]) == [2, 3, 4]
+        '''
+
+        // singleton identity: non-capturing lambda in a loop returns same 
instance
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    def identities = new HashSet()
+                    for (int i = 0; i < 5; i++) {
+                        Function<Integer, Integer> f = (Integer x) -> x + 1
+                        identities.add(System.identityHashCode(f))
+                    }
+                    assert identities.size() == 1 : 'non-capturing lambda 
should be a singleton'
+                }
+            }
+            C.test()
+        '''
+
+        // non-singleton: capturing lambda creates different instances per 
iteration
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    def identities = new HashSet()
+                    for (int i = 0; i < 3; i++) {
+                        int captured = i
+                        Function<Integer, Integer> f = (Integer x) -> x + 
captured
+                        identities.add(System.identityHashCode(f))
+                        assert f.apply(10) == 10 + i
+                    }
+                    assert identities.size() == 3 : 'capturing lambda should 
create different instances'
+                }
+            }
+            C.test()
+        '''
+
+        // capturing local variable: still works (not optimized)
+        assertScript shell, '''
+            class C {
+                static void test() {
+                    String x = '#'
+                    assert ['#1', '#2'] == [1, 2].stream().map(e -> x + 
e).toList()
+                }
+            }
+            C.test()
+        '''
+
+        // accessing this: still works (not optimized)
+        assertScript shell, '''
+            class C {
+                String prefix = 'Hi '
+                void test() {
+                    assert ['Hi 1', 'Hi 2'] == [1, 2].stream().map(e -> 
this.prefix + e).toList()
+                }
+            }
+            new C().test()
+        '''
+
+        // calling instance method: still works (not optimized)
+        assertScript shell, '''
+            class C {
+                String greet(int i) { "Hello $i" }
+                void test() {
+                    assert ['Hello 1', 'Hello 2'] == [1, 2].stream().map(e -> 
greet(e)).toList()
+                }
+            }
+            new C().test()
+        '''
+
+        // non-capturing serializable lambda: serialization
+        assertScript shell, '''
+            import java.io.*
+            interface SerFunc<I,O> extends Serializable, Function<I,O> {}
+            byte[] test() {
+                try (def out = new ByteArrayOutputStream()) {
+                    out.withObjectOutputStream {
+                        SerFunc<Integer, String> f = ((Integer i) -> 'a' + i)
+                        it.writeObject(f)
+                    }
+                    out.toByteArray()
+                }
+            }
+            assert test().length > 0
+        '''
+
+        // non-capturing serializable lambda: full serialize/deserialize 
roundtrip
+        assertScript shell, '''
+            package tests.lambda
+            class C {
+                static byte[] test() {
+                    def out = new ByteArrayOutputStream()
+                    out.withObjectOutputStream { it ->
+                        SerFunc<Integer, String> f = (Integer i) -> 'a' + i
+                        it.writeObject(f)
+                    }
+                    out.toByteArray()
+                }
+                static main(args) {
+                    new 
ByteArrayInputStream(C.test()).withObjectInputStream(C.classLoader) {
+                        SerFunc<Integer, String> f = (SerFunc<Integer, 
String>) it.readObject()
+                        assert f.apply(1) == 'a1'
+                    }
+                }
+                interface SerFunc<I,O> extends Serializable, Function<I,O> {}
+            }
+        '''
+
+        // non-capturing serializable lambda: singleton identity
+        assertScript shell, '''
+            interface SerFunc<I,O> extends Serializable, Function<I,O> {}
+            class C {
+                static void test() {
+                    def identities = new HashSet()
+                    for (int i = 0; i < 5; i++) {
+                        SerFunc<Integer, Integer> f = (Integer x) -> x + 1
+                        identities.add(System.identityHashCode(f))
+                    }
+                    assert identities.size() == 1 : 'non-capturing 
serializable lambda should be a singleton'
+                }
+            }
+            C.test()
+        '''
+
+        // capturing serializable lambda: roundtrip still works (not optimized)
+        assertScript shell, '''
+            package tests.lambda
+            class C {
+                byte[] test() {
+                    def out = new ByteArrayOutputStream()
+                    out.withObjectOutputStream {
+                        String s = 'a'
+                        SerFunc<Integer, String> f = (Integer i) -> s + i
+                        it.writeObject(f)
+                    }
+                    out.toByteArray()
+                }
+                static main(args) {
+                    new 
ByteArrayInputStream(C.newInstance().test()).withObjectInputStream(C.classLoader)
 {
+                        SerFunc<Integer, String> f = (SerFunc<Integer, 
String>) it.readObject()
+                        assert f.apply(1) == 'a1'
+                    }
+                }
+                interface SerFunc<I,O> extends Serializable, Function<I,O> {}
+            }
+        '''
+    }
 }
diff --git 
a/src/test/groovy/org/codehaus/groovy/classgen/asm/TypeAnnotationsTest.groovy 
b/src/test/groovy/org/codehaus/groovy/classgen/asm/TypeAnnotationsTest.groovy
index c5270ddec5..cdcf626338 100644
--- 
a/src/test/groovy/org/codehaus/groovy/classgen/asm/TypeAnnotationsTest.groovy
+++ 
b/src/test/groovy/org/codehaus/groovy/classgen/asm/TypeAnnotationsTest.groovy
@@ -299,7 +299,7 @@ final class TypeAnnotationsTest extends 
AbstractBytecodeTestCase {
             }
         ''')
         assert bytecode.hasStrictSequence([
-                'public doCall(I)I',
+                'public static doCall(I)I',
                 '@LTypeAnno1;() : METHOD_FORMAL_PARAMETER 0, null',
                 'L0'
         ])

Reply via email to