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' ])
