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 a5a53a8d8acbcc2a1cb9ae9a0141ffc2e582e285 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 | 37 +++- .../groovy/groovy/transform/stc/LambdaTest.groovy | 228 +++++++++++++++++++++ .../groovy/classgen/asm/TypeAnnotationsTest.groovy | 2 +- 3 files changed, 257 insertions(+), 10 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..7843c8504e 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,45 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun ClassNode lambdaClass = getOrAddLambdaClass(expression, abstractMethod); MethodNode lambdaMethod = lambdaClass.getMethods(DO_CALL).get(0); - boolean canDeserialize = controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass), createDeserializeLambdaMethodParams()); - if (!canDeserialize) { - if (expression.isSerializable()) { - addDeserializeLambdaMethodForEachLambdaExpression(expression, lambdaClass); - addDeserializeLambdaMethod(); + 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 && !expression.isSerializable(); + + if (isNonCapturing) { + lambdaMethod.setModifiers(lambdaMethod.getModifiers() | ACC_STATIC); + } else { + boolean canDeserialize = controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass), createDeserializeLambdaMethodParams()); + if (!canDeserialize) { + if (expression.isSerializable()) { + addDeserializeLambdaMethodForEachLambdaExpression(expression, lambdaClass); + addDeserializeLambdaMethod(); + } + newGroovyLambdaWrapperAndLoad(lambdaClass, expression, accessingInstanceMembers); } - newGroovyLambdaWrapperAndLoad(lambdaClass, expression, isAccessingInstanceMembersOfEnclosingClass(lambdaMethod)); } 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() { diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy index f69eb1a332..22c5249899 100644 --- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy +++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy @@ -1888,4 +1888,232 @@ 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() + ''' + + // serializable lambda: still works (not optimized) + 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 + ''' + } } 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' ])
