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 078cdefcc289d8a2b0ccf71212641af2c18cb3d1 Author: Daniel Sun <[email protected]> AuthorDate: Sun Apr 5 22:28:54 2026 +0900 GROOVY-11905: Optimize non-capturing lambdas --- .../classgen/asm/sc/StaticTypesLambdaAnalyzer.java | 391 +++++ .../classgen/asm/sc/StaticTypesLambdaWriter.java | 304 ++-- .../groovy/groovy/transform/stc/LambdaTest.groovy | 1537 ++++++++++++++++++++ .../groovy/classgen/asm/TypeAnnotationsTest.groovy | 2 +- 4 files changed, 2117 insertions(+), 117 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaAnalyzer.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaAnalyzer.java new file mode 100644 index 0000000000..1bb61357b9 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaAnalyzer.java @@ -0,0 +1,391 @@ +/* + * 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 org.codehaus.groovy.classgen.asm.sc; + +import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.Variable; +import org.codehaus.groovy.ast.expr.AttributeExpression; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.groovy.util.BeanUtils.capitalize; +import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; +import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET; + +/** + * Analysis helper for lambda expressions that determines whether a lambda + * captures its enclosing instance or only references static members. + * <p> + * When a lambda is non-capturing (no shared variables and no instance member + * access), the lambda writer can emit it as a static method with + * {@code LambdaMetafactory}, avoiding per-call allocation. + * <p> + * This analyzer also qualifies outer-class static member references so that + * the generated {@code doCall} method can invoke them without an enclosing + * instance receiver. + */ +class StaticTypesLambdaAnalyzer { + + StaticTypesLambdaAnalyzer(final SourceUnit sourceUnit) { + this.sourceUnit = sourceUnit; + } + + boolean isNonCapturing(final MethodNode lambdaMethod, final Parameter[] sharedVariables) { + return (sharedVariables == null || sharedVariables.length == 0) + && !accessesInstanceMembers(lambdaMethod); + } + + boolean accessesInstanceMembers(final MethodNode lambdaMethod) { + Boolean accessingInstanceMembers = lambdaMethod.getNodeMetaData(MetaDataKey.ACCESSES_INSTANCE_MEMBERS); + if (accessingInstanceMembers != null) return accessingInstanceMembers; + + InstanceMemberAccessFinder finder = new InstanceMemberAccessFinder(resolveOuterStaticMemberOwner(lambdaMethod)); + lambdaMethod.getCode().visit(finder); + + accessingInstanceMembers = finder.isAccessingInstanceMembers(); + lambdaMethod.putNodeMetaData(MetaDataKey.ACCESSES_INSTANCE_MEMBERS, accessingInstanceMembers); + return accessingInstanceMembers; + } + + void qualifyOuterStaticMemberReferences(final MethodNode lambdaMethod) { + lambdaMethod.getCode().visit(new OuterStaticMemberQualifier(sourceUnit, resolveOuterStaticMemberOwner(lambdaMethod))); + } + + private static OuterStaticMemberResolver resolveOuterStaticMemberOwner(final MethodNode lambdaMethod) { + return new OuterStaticMemberResolver(lambdaMethod.getDeclaringClass().getOuterClasses()); + } + + private static boolean isThisReceiver(final Expression expression) { + return expression instanceof VariableExpression receiver && receiver.isThisExpression(); + } + + private static boolean isEnclosingInstanceReceiver(final Expression expression) { + return isThisReceiver(expression) || isQualifiedEnclosingInstanceReference(expression); + } + + private static boolean isQualifiedEnclosingInstanceReference(final Expression expression) { + if (!(expression instanceof PropertyExpression propertyExpression)) return false; + if (!(propertyExpression.getObjectExpression() instanceof ClassExpression)) return false; + + String property = propertyExpression.getPropertyAsString(); + return "this".equals(property) || "super".equals(property); + } + + /** + * Transforms unqualified outer-class static member references in a lambda + * body into class-qualified references (e.g., {@code label} becomes + * {@code Outer.label}), enabling the static {@code doCall} method to + * invoke them without an enclosing instance. + */ + private static final class OuterStaticMemberQualifier extends ClassCodeExpressionTransformer { + + private final SourceUnit sourceUnit; + private final OuterStaticMemberResolver resolver; + + private OuterStaticMemberQualifier(final SourceUnit sourceUnit, final OuterStaticMemberResolver resolver) { + this.sourceUnit = sourceUnit; + this.resolver = resolver; + } + + @Override + protected SourceUnit getSourceUnit() { + return sourceUnit; + } + + @Override + public Expression transform(final Expression expression) { + if (expression instanceof VariableExpression variableExpression) { + Expression qualifiedReference = qualify(variableExpression); + if (qualifiedReference != null) return qualifiedReference; + } + if (expression instanceof AttributeExpression attributeExpression) { + Expression qualifiedReference = qualify(attributeExpression); + if (qualifiedReference != null) return qualifiedReference; + } + if (expression instanceof PropertyExpression propertyExpression) { + Expression qualifiedReference = qualify(propertyExpression); + if (qualifiedReference != null) return qualifiedReference; + } + if (expression instanceof MethodCallExpression methodCallExpression) { + Expression qualifiedReference = qualify(methodCallExpression); + if (qualifiedReference != null) return qualifiedReference; + } + return super.transform(expression); + } + + private Expression qualify(final VariableExpression expression) { + ClassNode owner = resolver.findOwner(expression); + if (owner == null) return null; + + PropertyExpression qualifiedReference = new PropertyExpression(classX(owner), expression.getName()); + qualifiedReference.setImplicitThis(false); + qualifiedReference.copyNodeMetaData(expression); + setSourcePosition(qualifiedReference, expression); + return qualifiedReference; + } + + private Expression qualify(final AttributeExpression expression) { + ClassNode owner = resolver.findOwner(expression); + if (owner == null) return null; + + AttributeExpression qualifiedReference = new AttributeExpression( + classX(owner), + transform(expression.getProperty()), + expression.isSafe() + ); + qualifiedReference.setImplicitThis(false); + qualifiedReference.setSpreadSafe(expression.isSpreadSafe()); + qualifiedReference.copyNodeMetaData(expression); + setSourcePosition(qualifiedReference, expression); + return qualifiedReference; + } + + private Expression qualify(final PropertyExpression expression) { + ClassNode owner = resolver.findOwner(expression); + if (owner == null) return null; + + PropertyExpression qualifiedReference = new PropertyExpression( + classX(owner), + transform(expression.getProperty()), + expression.isSafe() + ); + qualifiedReference.setImplicitThis(false); + qualifiedReference.setSpreadSafe(expression.isSpreadSafe()); + qualifiedReference.copyNodeMetaData(expression); + setSourcePosition(qualifiedReference, expression); + return qualifiedReference; + } + + private Expression qualify(final MethodCallExpression expression) { + ClassNode owner = resolver.findOwner(expression); + if (owner == null) return null; + + MethodCallExpression qualifiedReference = new MethodCallExpression( + classX(owner), + transform(expression.getMethod()), + transform(expression.getArguments()) + ); + qualifiedReference.setImplicitThis(false); + qualifiedReference.setSafe(expression.isSafe()); + qualifiedReference.setSpreadSafe(expression.isSpreadSafe()); + qualifiedReference.setGenericsTypes(expression.getGenericsTypes()); + qualifiedReference.setMethodTarget(expression.getMethodTarget()); + qualifiedReference.copyNodeMetaData(expression); + setSourcePosition(qualifiedReference, expression); + return qualifiedReference; + } + } + + /** + * Resolves outer-class static member references for a lambda body by + * walking the enclosing class hierarchy (including supertypes and + * interfaces) to determine ownership of static fields, properties, and + * methods. + */ + private static final class OuterStaticMemberResolver { + private final Map<String, ClassNode> referenceOwnerIndex; + + private OuterStaticMemberResolver(final List<ClassNode> outerClasses) { + this.referenceOwnerIndex = new LinkedHashMap<>(); + for (ClassNode outerClass : outerClasses) { + collectReferenceOwners(outerClass, referenceOwnerIndex); + } + } + + private ClassNode findOwner(final VariableExpression expression) { + ClassNode owner = expression.getNodeMetaData(StaticCompilationMetadataKeys.PROPERTY_OWNER); + if (!isReferenceOwner(owner)) return null; + + return isStaticReference(expression) ? owner : null; + } + + private ClassNode findOwner(final MethodCallExpression expression) { + if (!expression.isImplicitThis() && !isQualifiedEnclosingInstanceReference(expression.getObjectExpression())) { + return null; + } + + MethodNode directMethodCallTarget = expression.getMethodTarget(); + if (directMethodCallTarget == null || !directMethodCallTarget.isStatic()) return null; + + ClassNode owner = directMethodCallTarget.getDeclaringClass(); + if (!isReferenceOwner(owner)) return null; + + return isEnclosingInstanceReceiver(expression.getObjectExpression()) ? owner : null; + } + + private ClassNode findOwner(final PropertyExpression expression) { + if (!expression.isImplicitThis() && !isQualifiedEnclosingInstanceReference(expression.getObjectExpression())) { + return null; + } + + MethodNode directMethodCallTarget = expression.getNodeMetaData(DIRECT_METHOD_CALL_TARGET); + if (directMethodCallTarget != null && directMethodCallTarget.isStatic() && isReferenceOwner(directMethodCallTarget.getDeclaringClass())) { + return directMethodCallTarget.getDeclaringClass(); + } + + String propertyName = expression.getPropertyAsString(); + if (propertyName == null) return null; + + for (ClassNode referenceOwner : referenceOwnerIndex.values()) { + if (isStaticMemberNamed(propertyName, referenceOwner)) { + return referenceOwner; + } + } + + return null; + } + + private boolean isReferenceOwner(final ClassNode owner) { + return owner != null && referenceOwnerIndex.containsKey(owner.redirect().getName()); + } + + private static void collectReferenceOwners(final ClassNode owner, final Map<String, ClassNode> referenceOwnerIndex) { + if (owner == null) return; + + ClassNode redirectedOwner = owner.redirect(); + if (referenceOwnerIndex.putIfAbsent(redirectedOwner.getName(), redirectedOwner) != null) return; + + collectReferenceOwners(redirectedOwner.getSuperClass(), referenceOwnerIndex); + for (ClassNode interfaceNode : redirectedOwner.getInterfaces()) { + collectReferenceOwners(interfaceNode, referenceOwnerIndex); + } + } + + private static boolean isStaticReference(final VariableExpression expression) { + Variable accessedVariable = expression.getAccessedVariable(); + + if (accessedVariable instanceof Parameter) return false; + + return (accessedVariable instanceof FieldNode || accessedVariable instanceof PropertyNode) + && accessedVariable.isStatic(); + } + + private static boolean isStaticMemberNamed(final String propertyName, final ClassNode owner) { + FieldNode field = owner.getField(propertyName); + if (field != null && field.isStatic()) return true; + + PropertyNode property = owner.getProperty(propertyName); + if (property != null && property.isStatic()) return true; + + String capitalizedPropertyName = capitalize(propertyName); + MethodNode getter = owner.getGetterMethod("is" + capitalizedPropertyName); + if (getter == null) getter = owner.getGetterMethod("get" + capitalizedPropertyName); + + return getter != null && getter.isStatic(); + } + } + + /** + * Visits a lambda body to detect any reference to enclosing-instance + * members. Short-circuits on the first instance member access found. + */ + private static final class InstanceMemberAccessFinder extends CodeVisitorSupport { + + private final OuterStaticMemberResolver resolver; + private boolean accessingInstanceMembers; + + private InstanceMemberAccessFinder(final OuterStaticMemberResolver resolver) { + this.resolver = resolver; + } + + @Override + public void visitVariableExpression(final VariableExpression expression) { + if (accessingInstanceMembers) return; + if (expression.isThisExpression() || expression.isSuperExpression() || "thisObject".equals(expression.getName())) { + accessingInstanceMembers = true; + return; + } + + ClassNode owner = expression.getNodeMetaData(StaticCompilationMetadataKeys.PROPERTY_OWNER); + if (owner != null && resolver.isReferenceOwner(owner) && resolver.findOwner(expression) == null) { + accessingInstanceMembers = true; + return; + } + + super.visitVariableExpression(expression); + } + + @Override + public void visitPropertyExpression(final PropertyExpression expression) { + if (accessingInstanceMembers) return; + if (resolver.findOwner(expression) != null) { + expression.getProperty().visit(this); + return; + } + if (isQualifiedEnclosingInstanceReference(expression)) { + accessingInstanceMembers = true; + return; + } + + super.visitPropertyExpression(expression); + } + + @Override + public void visitAttributeExpression(final AttributeExpression expression) { + if (accessingInstanceMembers) return; + if (resolver.findOwner(expression) != null) { + expression.getProperty().visit(this); + return; + } + if (isQualifiedEnclosingInstanceReference(expression.getObjectExpression())) { + accessingInstanceMembers = true; + return; + } + + super.visitAttributeExpression(expression); + } + + @Override + public void visitMethodCallExpression(final MethodCallExpression expression) { + if (accessingInstanceMembers) return; + if (resolver.findOwner(expression) != null) { + expression.getMethod().visit(this); + expression.getArguments().visit(this); + return; + } + + super.visitMethodCallExpression(expression); + } + + private boolean isAccessingInstanceMembers() { + return accessingInstanceMembers; + } + } + + private enum MetaDataKey { + ACCESSES_INSTANCE_MEMBERS + } + + private final SourceUnit sourceUnit; +} 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..b61dc6c930 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 @@ -21,17 +21,14 @@ package org.codehaus.groovy.classgen.asm.sc; import org.codehaus.groovy.GroovyBugError; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.CodeVisitorSupport; import org.codehaus.groovy.ast.ConstructorNode; import org.codehaus.groovy.ast.InnerClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.builder.AstStringCompiler; import org.codehaus.groovy.ast.expr.ClosureExpression; -import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.LambdaExpression; -import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.classgen.BytecodeInstruction; @@ -46,10 +43,8 @@ import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys; import org.objectweb.asm.MethodVisitor; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; import static org.codehaus.groovy.ast.ClassHelper.CLOSURE_TYPE; import static org.codehaus.groovy.ast.ClassHelper.GENERATED_LAMBDA_TYPE; @@ -72,6 +67,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; @@ -83,99 +79,119 @@ import static org.objectweb.asm.Opcodes.NEW; */ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFunctionalInterfaceWriter { - private static final String IS_GENERATED_CONSTRUCTOR = "__IS_GENERATED_CONSTRUCTOR"; - private static final String LAMBDA_SHARED_VARIABLES = "__LAMBDA_SHARED_VARIABLES"; - private static final String DO_CALL = "doCall"; - - private final Map<Expression, ClassNode> lambdaClassNodes = new HashMap<>(); - private final StaticTypesClosureWriter staticTypesClosureWriter; - public StaticTypesLambdaWriter(final WriterController controller) { super(controller); this.staticTypesClosureWriter = new StaticTypesClosureWriter(controller); + this.lambdaAnalyzer = new StaticTypesLambdaAnalyzer(controller.getSourceUnit()); } @Override public void writeLambda(final LambdaExpression expression) { // functional interface target is required for native lambda generation - ClassNode functionalType = expression.getNodeMetaData(PARAMETER_TYPE); - MethodNode abstractMethod = ClassHelper.findSAM(functionalType); - if (abstractMethod == null || !functionalType.isInterface()) { + ClassNode functionalType = expression.getNodeMetaData(PARAMETER_TYPE); + MethodNode abstractMethod = resolveFunctionalInterfaceMethod(functionalType); + if (abstractMethod == null) { // generate bytecode for closure super.writeLambda(expression); return; } + boolean serializable = makeSerializableIfNeeded(expression, functionalType); + GeneratedLambda generatedLambda = getOrAddGeneratedLambda(expression, abstractMethod); + + ensureDeserializeLambdaSupport(expression, generatedLambda, serializable); + if (generatedLambda.isCapturing() && !isPreloadedLambdaReceiver(generatedLambda)) { + loadLambdaReceiver(generatedLambda); + } + + writeLambdaFactoryInvocation(functionalType.redirect(), abstractMethod, generatedLambda, serializable); + } + + private static Parameter[] createDeserializeLambdaMethodParams() { + return new Parameter[]{new Parameter(SERIALIZEDLAMBDA_TYPE, "serializedLambda")}; + } + + private static MethodNode resolveFunctionalInterfaceMethod(final ClassNode functionalType) { + if (functionalType == null || !functionalType.isInterface()) { + return null; + } + return ClassHelper.findSAM(functionalType); + } + + private boolean makeSerializableIfNeeded(final LambdaExpression expression, final ClassNode functionalType) { if (!expression.isSerializable() && functionalType.implementsInterface(SERIALIZABLE_TYPE)) { expression.setSerializable(true); } + return expression.isSerializable(); + } - 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(); - } - newGroovyLambdaWrapperAndLoad(lambdaClass, expression, isAccessingInstanceMembersOfEnclosingClass(lambdaMethod)); + private void ensureDeserializeLambdaSupport(final LambdaExpression expression, final GeneratedLambda generatedLambda, final boolean serializable) { + if (!serializable || hasDeserializeLambdaMethod(generatedLambda.lambdaClass)) { + return; } + addDeserializeLambdaMethodForLambdaExpression(expression, generatedLambda); + addDeserializeLambdaMethod(); + } + + private void writeLambdaFactoryInvocation(final ClassNode functionalType, final MethodNode abstractMethod, final GeneratedLambda generatedLambda, final boolean serializable) { MethodVisitor mv = controller.getMethodVisitor(); mv.visitInvokeDynamicInsn( - abstractMethod.getName(), - createAbstractMethodDesc(functionalType.redirect(), lambdaClass), - createBootstrapMethod(controller.getClassNode().isInterface(), expression.isSerializable()), - createBootstrapMethodArguments(createMethodDescriptor(abstractMethod), H_INVOKEVIRTUAL, lambdaClass, lambdaMethod, lambdaMethod.getParameters(), expression.isSerializable()) + abstractMethod.getName(), + createLambdaFactoryMethodDescriptor(functionalType, generatedLambda), + createBootstrapMethod(controller.getClassNode().isInterface(), serializable), + createBootstrapMethodArguments(createMethodDescriptor(abstractMethod), + generatedLambda.getMethodHandleKind(), + generatedLambda.lambdaClass, generatedLambda.lambdaMethod, generatedLambda.lambdaMethod.getParameters(), serializable) ); - if (expression.isSerializable()) { + if (serializable) { mv.visitTypeInsn(CHECKCAST, "java/io/Serializable"); } - controller.getOperandStack().replace(functionalType.redirect(), 1); + if (generatedLambda.nonCapturing()) { + controller.getOperandStack().push(functionalType); + } else { + controller.getOperandStack().replace(functionalType, 1); + } } - private static Parameter[] createDeserializeLambdaMethodParams() { - return new Parameter[]{new Parameter(SERIALIZEDLAMBDA_TYPE, "serializedLambda")}; + private boolean hasDeserializeLambdaMethod(final ClassNode lambdaClass) { + return controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass), createDeserializeLambdaMethodParams()); } - private static boolean isAccessingInstanceMembersOfEnclosingClass(final MethodNode lambdaMethod) { - boolean[] result = new boolean[1]; + private static MethodNode getLambdaMethod(final ClassNode lambdaClass) { + List<MethodNode> lambdaMethods = lambdaClass.getMethods(DO_CALL); + if (lambdaMethods.isEmpty()) { + throw new GroovyBugError("Failed to find the synthetic lambda method in " + lambdaClass.getName()); + } + return lambdaMethods.get(0); + } - lambdaMethod.getCode().visit(new CodeVisitorSupport() { - @Override - public void visitConstantExpression(final ConstantExpression expression) { - if ("this".equals(expression.getValue())) { // as in Type.this.name - result[0] = true; - } - } - @Override - public void visitVariableExpression(final VariableExpression expression) { - if ("this".equals(expression.getName()) || "thisObject".equals(expression.getName())) { - result[0] = true; - } else { - var owner = expression.getNodeMetaData(StaticCompilationMetadataKeys.PROPERTY_OWNER); - if (owner != null && lambdaMethod.getDeclaringClass().getOuterClasses().contains(owner)) { - result[0] = true; - } - } + private static ConstructorNode getGeneratedConstructor(final ClassNode lambdaClass) { + for (ConstructorNode constructorNode : lambdaClass.getDeclaredConstructors()) { + if (Boolean.TRUE.equals(constructorNode.getNodeMetaData(MetaDataKey.GENERATED_CONSTRUCTOR))) { + return constructorNode; } - }); + } + throw new GroovyBugError("Failed to find the generated constructor in " + lambdaClass.getName()); + } - return result[0]; + private boolean isPreloadedLambdaReceiver(final GeneratedLambda generatedLambda) { + MethodNode enclosingMethod = controller.getMethodNode(); + return enclosingMethod != null + && enclosingMethod.getNodeMetaData(MetaDataKey.PRELOADED_LAMBDA_RECEIVER) == generatedLambda.lambdaClass; } - private void newGroovyLambdaWrapperAndLoad(final ClassNode lambdaClass, final LambdaExpression expression, final boolean accessingInstanceMembers) { + private void loadLambdaReceiver(final GeneratedLambda generatedLambda) { CompileStack compileStack = controller.getCompileStack(); OperandStack operandStack = controller.getOperandStack(); MethodVisitor mv = controller.getMethodVisitor(); - String lambdaClassInternalName = BytecodeHelper.getClassInternalName(lambdaClass); + String lambdaClassInternalName = BytecodeHelper.getClassInternalName(generatedLambda.lambdaClass); mv.visitTypeInsn(NEW, lambdaClassInternalName); mv.visitInsn(DUP); - if (controller.isStaticMethod() || compileStack.isInSpecialConstructorCall() || !accessingInstanceMembers) { + if (controller.isStaticMethod() || compileStack.isInSpecialConstructorCall() || !generatedLambda.accessingInstanceMembers) { classX(controller.getThisType()).visit(controller.getAcg()); } else { loadThis(); @@ -183,23 +199,15 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun operandStack.dup(); - loadSharedVariables(expression); - - Optional<ConstructorNode> generatedConstructor = lambdaClass.getDeclaredConstructors().stream() - .filter(ctor -> Boolean.TRUE.equals(ctor.getNodeMetaData(IS_GENERATED_CONSTRUCTOR))).findFirst(); - if (generatedConstructor.isEmpty()) { - throw new GroovyBugError("Failed to find the generated constructor"); - } + loadSharedVariables(generatedLambda.sharedVariables); - Parameter[] lambdaClassConstructorParameters = generatedConstructor.get().getParameters(); - mv.visitMethodInsn(INVOKESPECIAL, lambdaClassInternalName, "<init>", BytecodeHelper.getMethodDescriptor(VOID_TYPE, lambdaClassConstructorParameters), lambdaClass.isInterface()); + Parameter[] lambdaClassConstructorParameters = generatedLambda.constructor.getParameters(); + mv.visitMethodInsn(INVOKESPECIAL, lambdaClassInternalName, "<init>", BytecodeHelper.getMethodDescriptor(VOID_TYPE, lambdaClassConstructorParameters), generatedLambda.lambdaClass.isInterface()); operandStack.replace(CLOSURE_TYPE, lambdaClassConstructorParameters.length); } - private void loadSharedVariables(final LambdaExpression expression) { - Parameter[] lambdaSharedVariableParameters = expression.getNodeMetaData(LAMBDA_SHARED_VARIABLES); - + private void loadSharedVariables(final Parameter[] lambdaSharedVariableParameters) { for (Parameter parameter : lambdaSharedVariableParameters) { loadReference(parameter.getName(), controller); if (parameter.getNodeMetaData(UseExistingReference.class) == null) { @@ -208,20 +216,35 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun } } - private String createAbstractMethodDesc(final ClassNode functionalInterface, final ClassNode lambdaClass) { - List<Parameter> lambdaSharedVariables = new LinkedList<>(); - prependParameter(lambdaSharedVariables, "__lambda_this", lambdaClass); - return BytecodeHelper.getMethodDescriptor(functionalInterface, lambdaSharedVariables.toArray(Parameter.EMPTY_ARRAY)); + private String createLambdaFactoryMethodDescriptor(final ClassNode functionalInterface, final GeneratedLambda generatedLambda) { + if (generatedLambda.nonCapturing()) { + return BytecodeHelper.getMethodDescriptor(functionalInterface, Parameter.EMPTY_ARRAY); + } + return BytecodeHelper.getMethodDescriptor(functionalInterface, new Parameter[]{createLambdaReceiverParameter(generatedLambda.lambdaClass)}); + } + + private static Parameter createLambdaReceiverParameter(final ClassNode lambdaClass) { + Parameter parameter = new Parameter(lambdaClass, "__lambda_this"); + parameter.setClosureSharedVariable(false); + return parameter; } - private ClassNode getOrAddLambdaClass(final LambdaExpression expression, final MethodNode abstractMethod) { - return lambdaClassNodes.computeIfAbsent(expression, expr -> { - ClassNode lambdaClass = createLambdaClass((LambdaExpression) expr, ACC_FINAL | ACC_PUBLIC | ACC_STATIC, abstractMethod); + private GeneratedLambda getOrAddGeneratedLambda(final LambdaExpression expression, final MethodNode abstractMethod) { + return generatedLambdas.computeIfAbsent(expression, expr -> { + ClassNode lambdaClass = createLambdaClass(expr, ACC_FINAL | ACC_PUBLIC | ACC_STATIC, abstractMethod); controller.getAcg().addInnerClass(lambdaClass); lambdaClass.addInterface(GENERATED_LAMBDA_TYPE); lambdaClass.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, Boolean.TRUE); lambdaClass.putNodeMetaData(WriterControllerFactory.class, (WriterControllerFactory) x -> controller); - return lambdaClass; + MethodNode lambdaMethod = getLambdaMethod(lambdaClass); + return new GeneratedLambda( + lambdaClass, + lambdaMethod, + getGeneratedConstructor(lambdaClass), + getStoredLambdaSharedVariables(expr), + !requiresLambdaInstance(lambdaMethod), + lambdaAnalyzer.accessesInstanceMembers(lambdaMethod) + ); }); } @@ -243,7 +266,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun lambdaClass.setScriptBody(true); } if (controller.isStaticMethod() - || enclosingClass.isStaticClass()) { + || enclosingClass.isStaticClass()) { lambdaClass.setStaticClass(true); } if (expression.isSerializable()) { @@ -252,11 +275,11 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun MethodNode syntheticLambdaMethodNode = addSyntheticLambdaMethodNode(expression, lambdaClass, abstractMethod); - Parameter[] localVariableParameters = expression.getNodeMetaData(LAMBDA_SHARED_VARIABLES); + Parameter[] localVariableParameters = getStoredLambdaSharedVariables(expression); addFieldsForLocalVariables(lambdaClass, localVariableParameters); ConstructorNode constructorNode = addConstructor(expression, localVariableParameters, lambdaClass, createBlockStatementForConstructor(expression, outermostClass, enclosingClass)); - constructorNode.putNodeMetaData(IS_GENERATED_CONSTRUCTOR, Boolean.TRUE); + constructorNode.putNodeMetaData(MetaDataKey.GENERATED_CONSTRUCTOR, Boolean.TRUE); syntheticLambdaMethodNode.getCode().visit(new CorrectAccessedVariableVisitor(lambdaClass)); @@ -274,17 +297,21 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun Parameter[] localVariableParameters = getLambdaSharedVariables(expression); removeInitialValues(localVariableParameters); - expression.putNodeMetaData(LAMBDA_SHARED_VARIABLES, localVariableParameters); + expression.putNodeMetaData(MetaDataKey.STORED_LAMBDA_SHARED_VARIABLES, localVariableParameters); MethodNode doCallMethod = lambdaClass.addMethod( - DO_CALL, - ACC_PUBLIC, - abstractMethod.getReturnType(), - parametersWithExactType.clone(), - ClassNode.EMPTY_ARRAY, - expression.getCode() + DO_CALL, + ACC_PUBLIC, + abstractMethod.getReturnType(), + parametersWithExactType.clone(), + ClassNode.EMPTY_ARRAY, + expression.getCode() ); doCallMethod.setSourcePosition(expression); + if (lambdaAnalyzer.isNonCapturing(doCallMethod, localVariableParameters)) { + lambdaAnalyzer.qualifyOuterStaticMemberReferences(doCallMethod); + doCallMethod.setModifiers(doCallMethod.getModifiers() | ACC_STATIC); + } return doCallMethod; } @@ -308,55 +335,100 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun } Statement code = block( - declS(localVarX("enclosingClass", OBJECT_TYPE), classX(enclosingClass)), - ((BlockStatement) new AstStringCompiler().compile( - "return enclosingClass" + - ".getDeclaredMethod(\"\\$deserializeLambda_${serializedLambda.getImplClass().replace('/', '$')}\\$\", serializedLambda.getClass())" + - ".invoke(null, serializedLambda)" - ).get(0)).getStatements().get(0) + declS(localVarX("enclosingClass", OBJECT_TYPE), classX(enclosingClass)), + ((BlockStatement) new AstStringCompiler().compile( + "return enclosingClass" + + ".getDeclaredMethod(\"\\$deserializeLambda_${serializedLambda.getImplClass().replace('/', '$')}\\$\", serializedLambda.getClass())" + + ".invoke(null, serializedLambda)" + ).get(0)).getStatements().get(0) ); enclosingClass.addSyntheticMethod( - "$deserializeLambda$", - ACC_PRIVATE | ACC_STATIC, - OBJECT_TYPE, - parameters, - ClassNode.EMPTY_ARRAY, - code); + "$deserializeLambda$", + ACC_PRIVATE | ACC_STATIC, + OBJECT_TYPE, + parameters, + ClassNode.EMPTY_ARRAY, + code); + } + + private static boolean requiresLambdaInstance(final MethodNode lambdaMethod) { + return 0 == (lambdaMethod.getModifiers() & ACC_STATIC); } - private void addDeserializeLambdaMethodForEachLambdaExpression(final LambdaExpression expression, final ClassNode lambdaClass) { + private void addDeserializeLambdaMethodForLambdaExpression(final LambdaExpression expression, final GeneratedLambda generatedLambda) { ClassNode enclosingClass = controller.getClassNode(); - Statement code = block( + Statement code; + if (generatedLambda.nonCapturing()) { + 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)); + INVOKEVIRTUAL, + "java/lang/invoke/SerializedLambda", + "getCapturedArg", + "(I)Ljava/lang/Object;", + false); + mv.visitTypeInsn(CHECKCAST, BytecodeHelper.getClassInternalName(generatedLambda.lambdaClass)); OperandStack operandStack = controller.getOperandStack(); - operandStack.push(lambdaClass); + operandStack.push(generatedLambda.lambdaClass); } }), returnS(expression) - ); + ); + } - enclosingClass.addSyntheticMethod( - createDeserializeLambdaMethodName(lambdaClass), - ACC_PUBLIC | ACC_STATIC, - OBJECT_TYPE, - createDeserializeLambdaMethodParams(), - ClassNode.EMPTY_ARRAY, - code); + MethodNode deserializeLambdaMethod = enclosingClass.addSyntheticMethod( + createDeserializeLambdaMethodName(generatedLambda.lambdaClass), + ACC_PUBLIC | ACC_STATIC, + OBJECT_TYPE, + createDeserializeLambdaMethodParams(), + ClassNode.EMPTY_ARRAY, + code); + if (generatedLambda.isCapturing()) { + // The deserialize helper preloads the captured receiver before it reuses the original lambda expression. + deserializeLambdaMethod.putNodeMetaData(MetaDataKey.PRELOADED_LAMBDA_RECEIVER, generatedLambda.lambdaClass); + } } private static String createDeserializeLambdaMethodName(final ClassNode lambdaClass) { return "$deserializeLambda_" + lambdaClass.getName().replace('.', '$') + "$"; } + + private static Parameter[] getStoredLambdaSharedVariables(final LambdaExpression expression) { + Parameter[] sharedVariables = expression.getNodeMetaData(MetaDataKey.STORED_LAMBDA_SHARED_VARIABLES); + if (sharedVariables == null) { + throw new GroovyBugError("Failed to find shared variables for lambda expression"); + } + return sharedVariables; + } + + private enum MetaDataKey { + GENERATED_CONSTRUCTOR, + STORED_LAMBDA_SHARED_VARIABLES, + PRELOADED_LAMBDA_RECEIVER + } + + private record GeneratedLambda(ClassNode lambdaClass, MethodNode lambdaMethod, ConstructorNode constructor, + Parameter[] sharedVariables, boolean nonCapturing, + boolean accessingInstanceMembers) { + + private boolean isCapturing() { + return !nonCapturing; + } + + private int getMethodHandleKind() { + return nonCapturing ? H_INVOKESTATIC : H_INVOKEVIRTUAL; + } + } + + private static final String DO_CALL = "doCall"; + private final Map<LambdaExpression, GeneratedLambda> generatedLambdas = new HashMap<>(); + private final StaticTypesClosureWriter staticTypesClosureWriter; + private final StaticTypesLambdaAnalyzer lambdaAnalyzer; } diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy index f69eb1a332..4c434e6a29 100644 --- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy +++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy @@ -18,6 +18,8 @@ */ package groovy.transform.stc +import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import static groovy.test.GroovyAssert.assertScript @@ -1888,4 +1890,1539 @@ final class LambdaTest { assert this.class.classLoader.loadClass('Foo$_bar_lambda1').modifiers == 25 // public(1) + static(8) + final(16) ''' } + + // GROOVY-11905 + @Nested + class NonCapturingLambdaOptimizationTest extends AbstractBytecodeTestCase { + @Test + void testNonCapturingLambdaWithFunctionInStaticMethod() { + assertScript shell, ''' + class C { + static void test() { + assert [2, 3, 4] == [1, 2, 3].stream().map(e -> e + 1).toList() + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithFunctionInInstanceMethodWithoutThisAccess() { + assertScript shell, ''' + class C { + void test() { + assert [2, 3, 4] == [1, 2, 3].stream().map(e -> e + 1).toList() + } + } + new C().test() + ''' + } + + @Test + void testNonCapturingLambdaWithPredicate() { + assertScript shell, ''' + class C { + static void test() { + assert [2, 4] == [1, 2, 3, 4].stream().filter(e -> e % 2 == 0).toList() + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithSupplier() { + assertScript shell, ''' + class C { + static void test() { + Supplier<String> s = () -> 'constant' + assert s.get() == 'constant' + assert 'hello' == Optional.<String>empty().orElseGet(() -> 'hello') + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithBiFunction() { + assertScript shell, ''' + class C { + static void test() { + BiFunction<Integer, Integer, Integer> f = (a, b) -> a + b + assert f.apply(3, 4) == 7 + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithComparator() { + assertScript shell, ''' + class C { + static void test() { + assert [3, 2, 1] == [1, 2, 3].stream().sorted((a, b) -> b.compareTo(a)).toList() + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithPrimitiveParameterType() { + assertScript shell, ''' + class C { + static void test() { + IntUnaryOperator op = (int i) -> i * 2 + assert op.applyAsInt(5) == 10 + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaWithCustomFunctionalInterface() { + 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() + ''' + } + + @Test + void testNonCapturingLambdaCallingStaticMethodOnly() { + 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() + ''' + } + + @Test + void testMultipleNonCapturingLambdasInSameMethod() { + 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() + ''' + } + + @Test + void testNonCapturingLambdaInStaticInitializerBlock() { + 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] + ''' + + def script = ''' + @CompileStatic + class C { + static IntUnaryOperator op + static { + op = (int i) -> i + 1 + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$__clinit__lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(I)I']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: '<clinit>', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntUnaryOperator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$__clinit__lambda1.doCall(I)I' + ]) + assert !outerBytecode.hasSequence(['NEW C$__clinit__lambda1']) + } + + @Test + void testNonCapturingLambdaInFieldInitializer() { + assertScript shell, ''' + class C { + IntUnaryOperator op = (int i) -> i + 1 + void test() { assert op.applyAsInt(5) == 6 } + } + new C().test() + ''' + + def script = ''' + @CompileStatic + class C { + IntUnaryOperator op = (int i) -> i + 1 + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(I)I']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: '<init>', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntUnaryOperator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_lambda1.doCall(I)I' + ]) + assert !outerBytecode.hasSequence(['NEW C$_lambda1']) + } + + @Test + void testNonCapturingLambdaInInterfaceDefaultMethod() { + 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] + ''' + + def script = ''' + @CompileStatic + interface Processor { + default IntUnaryOperator process() { + (int i) -> i + 1 + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Processor\\$_process_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(I)I']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Processor', method: 'process', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntUnaryOperator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Processor$_process_lambda1.doCall(I)I' + ]) + assert !outerBytecode.hasSequence(['NEW Processor$_process_lambda1']) + } + + @Test + void testNonCapturingLambdaSingletonIdentity() { + 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() + ''' + } + + @Test + void testCapturingLambdaCreatesDistinctInstances() { + 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() + ''' + } + + @Test + void testCapturingLocalVariable() { + assertScript shell, ''' + class C { + static void test() { + String x = '#' + assert ['#1', '#2'] == [1, 2].stream().map(e -> x + e).toList() + } + } + C.test() + ''' + } + + @Test + void testAccessingThis() { + 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() + ''' + } + + @Test + void testCallingInstanceMethod() { + 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() + ''' + } + + @Test + void testCallingSuperMethod() { + assertScript shell, ''' + class Base { + String greet(int i) { "Hello $i" } + } + class C extends Base { + void test() { + assert ['Hello 1', 'Hello 2'] == [1, 2].stream().map(e -> super.greet(e)).toList() + } + } + new C().test() + ''' + } + + @Test + void testNonCapturingLambdaWithThisStringLiteralRemainsSingleton() { + assertScript shell, ''' + class C { + static void test() { + def identities = new HashSet() + for (int i = 0; i < 5; i++) { + Supplier<String> supplier = () -> 'this' + identities.add(System.identityHashCode(supplier)) + assert supplier.get() == 'this' + } + assert identities.size() == 1 : 'non-capturing lambda with string literal this should still be a singleton' + } + } + C.test() + ''' + + def script = ''' + @CompileStatic + class C { + static Supplier<String> create() { + () -> 'this' + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasStrictSequence([ + 'public static doCall()Ljava/lang/Object;', + 'L0' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testNonCapturingSerializableLambdaCanBeSerialized() { + 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 + ''' + } + + @Test + void testNonCapturingSerializableLambdaRoundTrips() { + 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> {} + } + ''' + } + + @Test + void testNonCapturingSerializableLambdaSingletonIdentity() { + 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() + ''' + } + + @Test + void testCapturingSerializableLambdaStillRoundTrips() { + 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> {} + } + ''' + } + + @Test + void testCapturingLambdaWithRunnable() { + assertScript shell, ''' + import java.util.concurrent.atomic.AtomicBoolean + class C { + static void test() { + AtomicBoolean ran = new AtomicBoolean(false) + Runnable r = () -> ran.set(true) + r.run() + assert ran.get() + } + } + C.test() + ''' + } + + @Test + void testCapturingLambdaWithConsumer() { + assertScript shell, ''' + class C { + static void test() { + def result = [] + Consumer<Integer> c = (Integer x) -> result.add(x * 2) + c.accept(3) + c.accept(5) + assert result == [6, 10] + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaAccessingStaticField() { + assertScript shell, ''' + class C { + static final int OFFSET = 100 + static void test() { + assert [101, 102, 103] == [1, 2, 3].stream().map(e -> e + OFFSET).toList() + } + } + C.test() + ''' + + def script = ''' + @CompileStatic + class C { + static final int OFFSET = 100 + static IntUnaryOperator create() { + (int i) -> i + OFFSET + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasStrictSequence([ + 'public static doCall(I)I', + 'L0' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntUnaryOperator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall(I)I' + ]) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testQualifiedOuterThisRemainsCapturing() { + assertScript shell, ''' + class Outer { + String name = 'outer' + class Inner { + void test() { + Function<Integer, String> f = (Integer x) -> Outer.this.name + x + assert f.apply(1) == 'outer1' + } + } + void test() { new Inner().test() } + } + new Outer().test() + ''' + } + + @Test + void testNestedNonCapturingLambdas() { + assertScript shell, ''' + class C { + static void test() { + Function<List<Integer>, List<Integer>> f = (List<Integer> list) -> + list.stream().map(e -> e * 2).toList() + assert f.apply([1, 2, 3]) == [2, 4, 6] + } + } + C.test() + ''' + } + + @Test + void testNonCapturingLambdaInStaticMethodUsesStaticDoCall() { + def bytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', ''' + @CompileStatic + class C { + static IntUnaryOperator create() { + (int i) -> i * 2 + } + } + ''') + assert bytecode.hasStrictSequence([ + 'public static doCall(I)I', + 'L0' + ]) + } + + @Test + void testNonCapturingLambdaInInstanceMethodWithoutThisAccessUsesCaptureFreeInvokeDynamic() { + def bytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', ''' + @CompileStatic + class C { + IntUnaryOperator create() { + (int i) -> i + 1 + } + } + ''') + assert bytecode.hasSequence([ + 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntUnaryOperator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall(I)I' + ]) + assert !bytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testCapturingLambdaRetainsInstanceDoCallAndCapturedReceiver() { + def script = ''' + @CompileStatic + class C { + static IntUnaryOperator create() { + int captured = 1 + IntUnaryOperator op = (int i) -> i + captured + op + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public doCall(I)I']) + assert !lambdaBytecode.hasSequence(['public static doCall(I)I']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'NEW C$_create_lambda1', + 'INVOKEDYNAMIC applyAsInt(LC$_create_lambda1;)Ljava/util/function/IntUnaryOperator;', + 'C$_create_lambda1.doCall(I)I' + ]) + } + + @Test + void testSuperMethodCallRetainsInstanceDoCallAndCapturedReceiver() { + def script = ''' + @CompileStatic + class Base { + String greet(int i) { "Hello $i" } + } + @CompileStatic + class C extends Base { + Function<Integer, String> create() { + (Integer i) -> super.greet(i) + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + assert !lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'NEW C$_create_lambda1', + 'INVOKEDYNAMIC apply(LC$_create_lambda1;)Ljava/util/function/Function;', + 'C$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;' + ]) + } + + @Test + void testNonCapturingSerializableLambdaDeserializeHelperSkipsCapturedArgLookup() { + def bytecode = compileStaticBytecode(classNamePattern: 'C', method: '$deserializeLambda_C$_create_lambda1$', ''' + @CompileStatic + class C { + static SerFunc<Integer, String> create() { + (Integer i) -> 'a' + i + } + interface SerFunc<I,O> extends Serializable, Function<I,O> {} + } + ''') + assert !bytecode.hasSequence([SERIALIZED_LAMBDA_GET_CAPTURED_ARG]) + } + + @Test + void testCapturingSerializableLambdaDeserializeHelperReadsCapturedArg() { + def bytecode = compileStaticBytecode(classNamePattern: 'C', method: '$deserializeLambda_C$_create_lambda1$', ''' + @CompileStatic + class C { + static SerFunc<Integer, String> create() { + String prefix = 'a' + SerFunc<Integer, String> f = (Integer i) -> prefix + i + f + } + interface SerFunc<I,O> extends Serializable, Function<I,O> {} + } + ''') + assert bytecode.hasSequence([SERIALIZED_LAMBDA_GET_CAPTURED_ARG]) + } + + @Test + void testNonCapturingLambdaCallingQualifiedStaticMethodOnlyUsesCaptureFreeInvokeDynamic() { + def script = ''' + @CompileStatic + class C { + static String prefix() { 'Hi ' } + static Function<Integer, String> create() { + (Integer i) -> C.prefix() + i + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC apply()Ljava/util/function/Function;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testNonCapturingComparatorLambdaUsesCaptureFreeInvokeDynamic() { + def script = ''' + @CompileStatic + class C { + static java.util.Comparator<Integer> create() { + (Integer left, Integer right) -> right.compareTo(left) + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/Integer;Ljava/lang/Integer;)I']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC compare()Ljava/util/Comparator;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall(Ljava/lang/Integer;Ljava/lang/Integer;)I' + ]) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testNonCapturingLambdaWithCustomFunctionalInterfaceUsesCaptureFreeInvokeDynamic() { + def script = ''' + interface Transformer<I, O> { + O transform(I input) + } + @CompileStatic + class C { + static Transformer<String, Integer> create() { + (String s) -> s.length() + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/String;)Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC transform()LTransformer;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'C$_create_lambda1.doCall(Ljava/lang/String;)Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testNonCapturingSerializableLambdaUsesCaptureFreeAltMetafactory() { + def script = ''' + @CompileStatic + class C { + static SerFunc<Integer, String> create() { + (Integer i) -> 'a' + i + } + interface SerFunc<I,O> extends Serializable, Function<I,O> {} + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'C\\$_create_lambda1', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC apply()LC$SerFunc;', + 'java/lang/invoke/LambdaMetafactory.altMetafactory', + 'C$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;' + ]) + assert outerBytecode.hasSequence(['CHECKCAST java/io/Serializable']) + assert !outerBytecode.hasSequence(['NEW C$_create_lambda1']) + } + + @Test + void testNonCapturingLambdaWithExceptionInBody() { + assertScript shell, ''' + class C { + static void test() { + Function<String, Integer> f = (String s) -> { + if (s == null) throw new IllegalArgumentException('null input') + return s.length() + } + assert f.apply('hello') == 5 + try { + f.apply(null) + assert false : 'should have thrown' + } catch (IllegalArgumentException e) { + assert e.message == 'null input' + } + } + } + C.test() + ''' + } + + @Test + void testAccessingThisObjectRemainsCapturing() { + assertScript shell, ''' + class C { + String name = 'test' + void test() { + Function<Integer, String> f = (Integer x) -> thisObject.name + x + assert f.apply(1) == 'test1' + } + } + new C().test() + ''' + + def bytecode = compileStaticBytecode(classNamePattern: 'C\\$_test_lambda1', method: 'doCall', ''' + @CompileStatic + class C { + String name = 'test' + Function<Integer, String> test() { + (Integer x) -> thisObject.name + x + } + } + ''') + assert bytecode.hasSequence(['public doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + assert !bytecode.hasSequence(['public static doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + } + + @Test + void testInnerClassLambdaUsingOuterStaticMembersQualifiesStaticCalls() { + assertScript shell, ''' + class Outer { + static String label = 'outer' + static boolean isReady() { true } + static String suffix() { '!' } + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix() : 'never' + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.get() == 'OUTER!' + assert right.get() == 'OUTER!' + ''' + + def script = ''' + @CompileStatic + class Outer { + static String label = 'outer' + static boolean isReady() { true } + static String suffix() { '!' } + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix() : 'never' + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Outer.isReady ()Z', + 'INVOKESTATIC Outer.getLabel ()Ljava/lang/String;', + 'INVOKESTATIC Outer.suffix ()Ljava/lang/String;' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testSerializableLambdasInSameClassShareSingleDeserializeDispatcher() { + assertScript shell, COMMON_IMPORTS + ''' + import java.io.ByteArrayInputStream + import java.io.ByteArrayOutputStream + + @CompileStatic + class C { + interface SerFunc<I, O> extends Serializable, Function<I, O> {} + + static SerFunc<Integer, String> createLeft() { + (Integer i) -> 'a' + i + } + + static SerFunc<Integer, String> createRight() { + (Integer i) -> 'b' + (i * 2) + } + + static byte[] serialize(Serializable value) { + def out = new ByteArrayOutputStream() + out.withObjectOutputStream { it.writeObject(value) } + out.toByteArray() + } + + static <T> T deserialize(byte[] bytes) { + new ByteArrayInputStream(bytes).withObjectInputStream(C.classLoader) { + (T) it.readObject() + } + } + } + + assert C.declaredMethods.count { it.name == '$deserializeLambda$' } == 1 + C.SerFunc<Integer, String> left = C.deserialize(C.serialize(C.createLeft())) + C.SerFunc<Integer, String> right = C.deserialize(C.serialize(C.createRight())) + assert left.apply(1) == 'a1' + assert right.apply(2) == 'b4' + ''' + } + + @Test + void testInnerClassLambdaUsingOuterStaticBeanPropertiesStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + static boolean isReady() { true } + static String getLabel() { 'outer' } + static String getSuffix() { '!' } + + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix : 'never' + } + } + } + + def supplier = new Outer().new Inner().create() + assert supplier.get() == 'OUTER!' + ''' + + def script = ''' + @CompileStatic + class Outer { + static boolean isReady() { true } + static String getLabel() { 'outer' } + static String getSuffix() { '!' } + + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix : 'never' + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Outer.isReady ()Z', + 'INVOKESTATIC Outer.getLabel ()Ljava/lang/String;', + 'INVOKESTATIC Outer.getSuffix ()Ljava/lang/String;' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaUsingInterfaceStaticMembersStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + interface Labels { + String LABEL = 'outer' + static boolean isReady() { true } + static String getSuffix() { '!' } + } + + class Outer implements Labels { + class Inner { + Supplier<String> create() { + () -> ready ? LABEL.toUpperCase() + suffix : 'never' + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'interface static members should keep the lambda capture-free' + assert left.get() == 'OUTER!' + assert right.get() == 'OUTER!' + ''' + + def script = ''' + @CompileStatic + interface Labels { + String LABEL = 'outer' + static boolean isReady() { true } + static String getSuffix() { '!' } + } + + @CompileStatic + class Outer implements Labels { + class Inner { + Supplier<String> create() { + () -> ready ? LABEL.toUpperCase() + suffix : 'never' + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Labels.isReady ()Z', + 'GETSTATIC Labels.LABEL :', + 'INVOKESTATIC Labels.getSuffix ()Ljava/lang/String;' + ]) + assert !lambdaBytecode.hasSequence(['CHECKCAST groovy/lang/GroovyObject']) + assert !lambdaBytecode.hasSequence(['INVOKEINTERFACE groovy/lang/GroovyObject.getProperty']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaUsingInheritedStaticMembersStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + class Base { + static boolean isReady() { true } + static String getLabel() { 'outer' } + static String getSuffix() { '!' } + } + + class Outer extends Base { + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix : 'never' + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'inherited static members should keep the lambda capture-free' + assert left.get() == 'OUTER!' + assert right.get() == 'OUTER!' + ''' + + def script = ''' + @CompileStatic + class Base { + static boolean isReady() { true } + static String getLabel() { 'outer' } + static String getSuffix() { '!' } + } + + @CompileStatic + class Outer extends Base { + class Inner { + Supplier<String> create() { + () -> ready ? label.toUpperCase() + suffix : 'never' + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Base.isReady ()Z', + 'INVOKESTATIC Base.getLabel ()Ljava/lang/String;', + 'INVOKESTATIC Base.getSuffix ()Ljava/lang/String;' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaUsingQualifiedOuterThisStaticMembersStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + static String label = 'outer' + static String suffix() { '!' } + + class Inner { + Supplier<String> create() { + () -> [email protected]() + Outer.this.suffix() + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'qualified outer-this static access should stay capture-free' + assert left.get() == 'OUTER!' + assert right.get() == 'OUTER!' + ''' + + def script = ''' + @CompileStatic + class Outer { + static String label = 'outer' + static String suffix() { '!' } + + class Inner { + Supplier<String> create() { + () -> [email protected]() + Outer.this.suffix() + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'GETSTATIC Outer.label :', + 'INVOKESTATIC Outer.suffix ()Ljava/lang/String;' + ]) + assert !lambdaBytecode.hasSequence(['CHECKCAST groovy/lang/GroovyObject']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testStaticNestedClassLambdaUsingOwnStaticMethodStaysNonCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + static class Nested { + static String helper() { 'nested' } + + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + + def left = new Outer.Nested().create() + def right = new Outer.Nested().create() + assert left.is(right) : 'static nested-class helpers should not be mistaken for outer-instance capture' + assert left.get() == 'NESTED' + assert right.get() == 'NESTED' + ''' + + def script = ''' + @CompileStatic + class Outer { + static class Nested { + static String helper() { 'nested' } + + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Nested\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Outer$Nested.helper ()Ljava/lang/String;', + 'INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;' + ]) + + def nestedBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Nested', method: 'create', script) + assert nestedBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Nested$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !nestedBytecode.hasSequence(['NEW Outer$Nested$_create_lambda1']) + } + + @Test + void testInnerClassLambdaUsingStaticallyImportedHelperMethodStaysNonCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + import static Helper.helper + + class Helper { + static String helper() { 'helper' } + } + + class Outer { + class Inner { + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'statically imported helper methods should not be treated as captured outer state' + assert left.get() == 'HELPER' + assert right.get() == 'HELPER' + ''' + + def script = ''' + import static Helper.helper + + @CompileStatic + class Helper { + static String helper() { 'helper' } + } + + @CompileStatic + class Outer { + class Inner { + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Helper.helper ()Ljava/lang/String;', + 'INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaUsingStaticallyImportedInterfaceMethodStaysNonCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + import static HelperApi.helper + + interface HelperApi { + static String helper() { 'iface' } + } + + class Outer implements HelperApi { + class Inner { + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'statically imported interface methods should stay receiver-free inside inner lambdas' + assert left.get() == 'IFACE' + assert right.get() == 'IFACE' + ''' + + def script = ''' + import static HelperApi.helper + + @CompileStatic + interface HelperApi { + static String helper() { 'iface' } + } + + @CompileStatic + class Outer implements HelperApi { + class Inner { + Supplier<String> create() { + () -> helper().toUpperCase() + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC HelperApi.helper ()Ljava/lang/String;', + 'INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaMutatingImplicitStaticPropertyStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + private static String backing = 'outer' + static String getLabel() { backing } + static void setLabel(String value) { backing = value } + + class Inner { + Supplier<String> create() { + () -> { + label += '!' + label + } + } + } + } + + Outer.label = 'outer' + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'static property mutation should not force receiver capture' + assert left.get() == 'outer!' + Outer.label = 'outer' + assert right.get() == 'outer!' + ''' + + def script = ''' + @CompileStatic + class Outer { + private static String backing = 'outer' + static String getLabel() { backing } + static void setLabel(String value) { backing = value } + + class Inner { + Supplier<String> create() { + () -> { + label += '!' + label + } + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Outer.getLabel ()Ljava/lang/String;', + 'INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.setProperty' + ]) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaMutatingQualifiedOuterThisStaticPropertyStaysCaptureFree() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + private static String backing = 'outer' + static String getLabel() { backing } + static void setLabel(String value) { backing = value } + + class Inner { + Supplier<String> create() { + () -> { + Outer.this.label += '!' + Outer.this.label + } + } + } + } + + Outer.label = 'outer' + def left = new Outer().new Inner().create() + def right = new Outer().new Inner().create() + assert left.is(right) : 'qualified outer-this static property mutation should stay capture-free' + assert left.get() == 'outer!' + Outer.label = 'outer' + assert right.get() == 'outer!' + ''' + + def script = ''' + @CompileStatic + class Outer { + private static String backing = 'outer' + static String getLabel() { backing } + static void setLabel(String value) { backing = value } + + class Inner { + Supplier<String> create() { + () -> { + Outer.this.label += '!' + Outer.this.label + } + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + assert lambdaBytecode.hasSequence([ + 'INVOKESTATIC Outer.getLabel ()Ljava/lang/String;', + 'INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.setProperty' + ]) + assert !lambdaBytecode.hasSequence(['CHECKCAST groovy/lang/GroovyObject']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;', + 'java/lang/invoke/LambdaMetafactory.metafactory', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + assert !outerBytecode.hasSequence(['NEW Outer$Inner$_create_lambda1']) + } + + @Test + void testInnerClassLambdaReadingQualifiedOuterThisInstancePropertyRemainsCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + String name = 'outer' + + class Inner { + Supplier<String> create() { + () -> Outer.this.name + ':' + Outer.this.name + } + } + } + + def inner = new Outer().new Inner() + def left = inner.create() + def right = inner.create() + assert !left.is(right) : 'qualified outer-instance property access must keep the lambda capturing' + assert left.get() == 'outer:outer' + assert right.get() == 'outer:outer' + ''' + + def script = ''' + @CompileStatic + class Outer { + String name = 'outer' + + class Inner { + Supplier<String> create() { + () -> Outer.this.name + ':' + Outer.this.name + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public doCall()Ljava/lang/Object;']) + assert !lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'NEW Outer$Inner$_create_lambda1', + 'INVOKEDYNAMIC get(LOuter$Inner$_create_lambda1;)Ljava/util/function/Supplier;', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + } + + @Test + void testInnerClassReadingQualifiedOuterThisInstanceFieldRemainsCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + String name = 'outer' + + class Inner { + Supplier<String> create() { + () -> Outer.this.@name + ':' + Outer.this.@name + } + } + } + + def inner = new Outer().new Inner() + def left = inner.create() + def right = inner.create() + assert !left.is(right) : 'qualified outer-instance field access must keep the lambda capturing' + assert left.get() == 'outer:outer' + assert right.get() == 'outer:outer' + ''' + + def script = ''' + @CompileStatic + class Outer { + String name = 'outer' + + class Inner { + Supplier<String> create() { + () -> Outer.this.@name + ':' + Outer.this.@name + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public doCall()Ljava/lang/Object;']) + assert !lambdaBytecode.hasSequence(['public static doCall()Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'NEW Outer$Inner$_create_lambda1', + 'INVOKEDYNAMIC get(LOuter$Inner$_create_lambda1;)Ljava/util/function/Supplier;', + 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;' + ]) + } + + @Test + void testInnerClassLambdaUsingImplicitOuterInstancePropertyRemainsCapturing() { + assertScript shell, COMMON_IMPORTS + ''' + class Outer { + String name = 'outer' + + class Inner { + Function<Integer, String> create() { + (Integer i) -> name + i + } + } + } + + def fn = new Outer().new Inner().create() + assert fn.apply(1) == 'outer1' + ''' + + def script = ''' + @CompileStatic + class Outer { + String name = 'outer' + + class Inner { + Function<Integer, String> create() { + (Integer i) -> name + i + } + } + } + ''' + def lambdaBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner\\$_create_lambda\\d+', method: 'doCall', script) + assert lambdaBytecode.hasSequence(['public doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + assert !lambdaBytecode.hasSequence(['public static doCall(Ljava/lang/Integer;)Ljava/lang/Object;']) + + def outerBytecode = compileStaticBytecode(classNamePattern: 'Outer\\$Inner', method: 'create', script) + assert outerBytecode.hasSequence([ + 'NEW Outer$Inner$_create_lambda1', + 'INVOKEDYNAMIC apply(LOuter$Inner$_create_lambda1;)Ljava/util/function/Function;', + 'Outer$Inner$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;' + ]) + } + + private compileStaticBytecode(final Map options = [:], final String script) { + compile(options, COMMON_IMPORTS + script) + } + + private static final String COMMON_IMPORTS = '''\ + import groovy.transform.CompileStatic + import java.io.Serializable + import java.util.function.Consumer + import java.util.function.Function + import java.util.function.IntUnaryOperator + import java.util.function.Supplier + '''.stripIndent() + private static final String SERIALIZED_LAMBDA_GET_CAPTURED_ARG = 'INVOKEVIRTUAL java/lang/invoke/SerializedLambda.getCapturedArg' + } } 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' ])
