You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/04/11 14:28:57 UTC
(groovy) branch GROOVY-11905 updated (663722a867 -> a5d3dfb231)
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a change to branch GROOVY-11905
in repository https://gitbox.apache.org/repos/asf/groovy.git
discard 663722a867 GROOVY-11905: Optimize non-capturing lambdas
new a5d3dfb231 GROOVY-11905: Optimize non-capturing lambdas
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (663722a867)
\
N -- N -- N refs/heads/GROOVY-11905 (a5d3dfb231)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../classgen/asm/sc/StaticTypesLambdaAnalyzer.java | 182 ++++++--------
.../groovy/groovy/transform/stc/LambdaTest.groovy | 267 +++++++++------------
2 files changed, 177 insertions(+), 272 deletions(-)
(groovy) 01/01: GROOVY-11905: Optimize non-capturing lambdas
Posted by su...@apache.org.
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 a5d3dfb23137477917e7754513e7e31d12b150ae
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Apr 5 22:28:54 2026 +0900
GROOVY-11905: Optimize non-capturing lambdas
---
.../java/org/codehaus/groovy/ast/ClassNode.java | 8 +-
.../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 +-
5 files changed, 2123 insertions(+), 119 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index b91da80224..3bbd62afd8 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -1452,17 +1452,21 @@ faces: if (method == null && asBoolean(getInterfaces())) { // GROOVY-11323
return null;
}
+ private List<ClassNode> outerClasses;
public List<ClassNode> getOuterClasses() {
+ List<ClassNode> ocs = outerClasses;
+ if (ocs != null) return ocs;
+
ClassNode outer = getOuterClass();
if (outer == null) {
- return Collections.emptyList();
+ return outerClasses = Collections.emptyList();
}
List<ClassNode> result = new ArrayList<>(4);
do {
result.add(outer);
} while ((outer = outer.getOuterClass()) != null);
- return result;
+ return outerClasses = Collections.unmodifiableList(result);
}
/**
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() {
+ () -> Outer.this.@label.toUpperCase() + 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() {
+ () -> Outer.this.@label.toUpperCase() + 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'
])