This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 924898f5fc GROOVY-11993: Support serializable method reference (#2514)
924898f5fc is described below
commit 924898f5fcd3c40d8debaeb93a8eaa5a9b3f02a0
Author: Daniel Sun <[email protected]>
AuthorDate: Wed May 6 06:16:20 2026 +0800
GROOVY-11993: Support serializable method reference (#2514)
---
.../asm/sc/AbstractFunctionalInterfaceWriter.java | 348 +++++--
.../StaticTypesFunctionalInterfaceMetadataKey.java | 52 +
.../classgen/asm/sc/StaticTypesLambdaAnalyzer.java | 25 +-
.../classgen/asm/sc/StaticTypesLambdaWriter.java | 123 +--
...StaticTypesMethodReferenceExpressionWriter.java | 438 +++++++--
.../groovy/groovy/transform/stc/LambdaTest.groovy | 407 +++++++-
.../transform/stc/MethodReferenceTest.groovy | 1023 ++++++++++++++++++++
7 files changed, 2159 insertions(+), 257 deletions(-)
diff --git
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/AbstractFunctionalInterfaceWriter.java
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/AbstractFunctionalInterfaceWriter.java
index ceb280c43b..7d437bf566 100644
---
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/AbstractFunctionalInterfaceWriter.java
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/AbstractFunctionalInterfaceWriter.java
@@ -22,91 +22,89 @@ import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.tools.GeneralUtils;
+import org.codehaus.groovy.classgen.asm.BytecodeHelper;
+import org.codehaus.groovy.classgen.asm.WriterController;
import org.codehaus.groovy.syntax.RuntimeParserException;
+import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys;
import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
-import java.util.List;
+import java.util.Arrays;
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.SERIALIZEDLAMBDA_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.getUnwrapper;
import static org.codehaus.groovy.ast.ClassHelper.getWrapper;
import static org.codehaus.groovy.ast.ClassHelper.isDynamicTyped;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.eqX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static
org.codehaus.groovy.ast.tools.GenericsUtils.hasUnresolvedGenerics;
import static
org.codehaus.groovy.classgen.asm.BytecodeHelper.getClassInternalName;
import static
org.codehaus.groovy.classgen.asm.BytecodeHelper.getMethodDescriptor;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.DESERIALIZE_LAMBDA_DISPATCHER;
+import static
org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
/**
- * Represents functional interface writer which contains some common methods
to complete generating bytecode
+ * Shared bytecode and deserialization support for statically-compiled
functional interface implementations,
+ * including both lambdas and method references.
+ *
* @since 3.0.0
*/
public interface AbstractFunctionalInterfaceWriter {
- default String createMethodDescriptor(final MethodNode method) {
- return getMethodDescriptor(method.getReturnType(),
method.getParameters());
- }
-
- default Handle createBootstrapMethod(final boolean isInterface, final
boolean serializable) {
- return new Handle(
- Opcodes.H_INVOKESTATIC,
- "java/lang/invoke/LambdaMetafactory",
- serializable ? "altMetafactory" : "metafactory",
- serializable ?
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"
- :
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
- false // GROOVY-8299, GROOVY-8989, GROOVY-11265
+ default void writeFunctionalInterfaceIndy(final MethodVisitor
methodVisitor,
+ final String samMethodName,
final String invokedTypeDescriptor,
+ final String
samMethodDescriptor, final int implMethodKind,
+ final ClassNode implClassNode,
final MethodNode implMethodNode,
+ final Parameter[]
implMethodParameters, final boolean serializable) {
+ methodVisitor.visitInvokeDynamicInsn(
+ samMethodName,
+ invokedTypeDescriptor,
+ createBootstrapMethod(serializable),
+ createBootstrapMethodArguments(samMethodDescriptor,
implMethodKind, implClassNode, implMethodNode, implMethodParameters,
serializable)
);
- }
-
- default Object[] createBootstrapMethodArguments(final String
abstractMethodDesc, final int insn, final ClassNode methodOwner, final
MethodNode methodNode, final Parameter[] parameters, final boolean
serializable) {
- ClassNode returnType = methodNode.getReturnType();
- switch (Type.getReturnType(abstractMethodDesc).getSort()) {
- case Type.BOOLEAN:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Boolean_TYPE; // GROOVY-10975
- break;
- case Type.BYTE:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Byte_TYPE;
- break;
- case Type.CHAR:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Character_TYPE;
- break;
- case Type.DOUBLE:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Double_TYPE;
- break;
- case Type.FLOAT:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Float_TYPE;
- break;
- case Type.INT:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Integer_TYPE;
- break;
- case Type.LONG:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Long_TYPE;
- break;
- case Type.SHORT:
- if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Short_TYPE;
- break;
- case Type.VOID:
- returnType = ClassHelper.VOID_TYPE; // GROOVY-10933
+ if (serializable) {
+ methodVisitor.visitTypeInsn(CHECKCAST,
BytecodeHelper.getClassInternalName(ClassHelper.SERIALIZABLE_TYPE));
}
+ }
- Object[] arguments = !serializable ? new Object[3] : new
Object[]{null, null, null, 5, 0};
-
- arguments[0] = Type.getMethodType(abstractMethodDesc);
-
- arguments[1] = new Handle(
- insn, // H_INVOKESTATIC or H_INVOKEVIRTUAL or
H_INVOKEINTERFACE (GROOVY-9853)
- getClassInternalName(methodOwner.getName()),
- methodNode.getName(),
- getMethodDescriptor(methodNode),
- methodOwner.isInterface());
+ default String createMethodDescriptor(final MethodNode method) {
+ return createMethodDescriptor(method.getReturnType(),
method.getParameters());
+ }
- arguments[2] = Type.getMethodType(getMethodDescriptor(returnType,
parameters));
+ default String createMethodDescriptor(final ClassNode returnType, final
Parameter[] parameters) {
+ return getMethodDescriptor(returnType, parameters);
+ }
- return arguments;
+ default String createFunctionalInterfaceFactoryDescriptor(final ClassNode
functionalType, final Parameter[] capturedParameters) {
+ return createMethodDescriptor(functionalType.redirect(),
capturedParameters);
}
- default ClassNode convertParameterType(final ClassNode parameterType,
final ClassNode inferredType) {
- return convertParameterType(parameterType, parameterType,
inferredType);
+ default Parameter createCapturedReceiverParameter(final ClassNode
receiverType, final String parameterName) {
+ Parameter parameter = new Parameter(receiverType, parameterName);
+ parameter.setClosureSharedVariable(false);
+ return parameter;
}
default ClassNode convertParameterType(final ClassNode targetType, final
ClassNode parameterType, final ClassNode inferredType) {
@@ -128,7 +126,7 @@ public interface AbstractFunctionalInterfaceWriter {
// (1) java.lang.invoke.LambdaConversionException: Type mismatch
for instantiated parameter 0: class java.lang.Integer is not a subtype of int
// (2) java.lang.BootstrapMethodError: bootstrap method
initialization exception
if (!(isDynamicTyped(parameterType) &&
isPrimitiveType(targetType)) // (1)
- && (parameterType.equals(getUnwrapper(parameterType)) ||
inferredType.equals(getWrapper(inferredType)))) { // (2)
+ && (parameterType.equals(getUnwrapper(parameterType)) ||
inferredType.equals(getWrapper(inferredType)))) { // (2)
// The non-primitive type and primitive type are not allowed
to mix since Java 9+
// java.lang.invoke.LambdaConversionException: Type mismatch
for instantiated parameter 0: int is not a subtype of class java.lang.Object
type = getWrapper(inferredType).getPlainNodeReference();
@@ -150,10 +148,230 @@ public interface AbstractFunctionalInterfaceWriter {
return type;
}
- default Parameter prependParameter(final List<Parameter> parameterList,
final String parameterName, final ClassNode parameterType) {
- Parameter parameter = new Parameter(parameterType, parameterName);
- parameter.setClosureSharedVariable(false);
- parameterList.add(0, parameter);
- return parameter;
+ default SerializedLambdaFingerprint
createSerializedLambdaFingerprint(final String samMethodDescriptor, final
ClassNode capturingClass,
+
final int implMethodKind, final ClassNode implClassNode,
+
final MethodNode implMethodNode, final Parameter[] implMethodParameters,
+
final ClassNode functionalType, final MethodNode abstractMethod,
+
final int capturedArgumentCount) {
+ return new SerializedLambdaFingerprint(
+ getClassInternalName(capturingClass),
+ implMethodKind,
+ getClassInternalName(implClassNode),
+ implMethodNode.getName(),
+ getMethodDescriptor(implMethodNode),
+ getClassInternalName(functionalType.redirect()),
+ abstractMethod.getName(),
+ samMethodDescriptor,
+ createInstantiatedMethodType(samMethodDescriptor, implMethodNode,
implMethodParameters).getDescriptor(),
+ capturedArgumentCount
+ );
+ }
+
+ default void addDeserializeDispatcherEntry(final WriterController
controller, final Parameter[] deserializeMethodParameters,
+ final
SerializedLambdaFingerprint serializedLambdaFingerprint,
+ final MethodNode helperMethod) {
+ BlockStatement dispatcherGuards =
getOrAddDeserializeDispatcherGuards(controller, deserializeMethodParameters);
+ MethodCallExpression helperCall =
callX(classX(controller.getClassNode()), helperMethod.getName(),
args(varX(deserializeMethodParameters[0])));
+ helperCall.setImplicitThis(false);
+ helperCall.setMethodTarget(helperMethod);
+
+ dispatcherGuards.addStatement(
+ // Keep this guard strict: deserialization must route to exactly
one synthetic helper
+ // whose serialized-lambda fingerprint fully matches the incoming
SerializedLambda.
+
ifS(boolX(matchesSerializedFunctionalInterface(varX(deserializeMethodParameters[0]),
serializedLambdaFingerprint)),
+ returnS(helperCall)
+ )
+ );
+ }
+
+ default Parameter[] createDeserializeMethodParameters() {
+ return new Parameter[] { new Parameter(SERIALIZEDLAMBDA_TYPE,
"serializedLambda") };
+ }
+
+ private Handle createBootstrapMethod(final boolean serializable) {
+ return new Handle(
+ Opcodes.H_INVOKESTATIC,
+ "java/lang/invoke/LambdaMetafactory",
+ serializable ? "altMetafactory" : "metafactory",
+ serializable ?
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"
+ :
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
+ false // GROOVY-8299, GROOVY-8989, GROOVY-11265
+ );
+ }
+
+ private Object[] createBootstrapMethodArguments(final String
samMethodDescriptor, final int implMethodKind,
+ final ClassNode
implClassNode, final MethodNode implMethodNode,
+ final Parameter[]
implMethodParameters, final boolean serializable) {
+ Object[] arguments = !serializable ? new Object[3] : new
Object[]{null, null, null, 5, 0};
+
+ arguments[0] = Type.getMethodType(samMethodDescriptor);
+
+ arguments[1] = new Handle(
+ implMethodKind, // H_INVOKESTATIC or H_INVOKEVIRTUAL or
H_INVOKEINTERFACE (GROOVY-9853)
+ getClassInternalName(implClassNode.getName()),
+ implMethodNode.getName(),
+ getMethodDescriptor(implMethodNode),
+ implClassNode.isInterface());
+
+ arguments[2] = createInstantiatedMethodType(samMethodDescriptor,
implMethodNode, implMethodParameters);
+
+ return arguments;
+ }
+
+ private Type createInstantiatedMethodType(final String
samMethodDescriptor, final MethodNode implMethodNode, final Parameter[]
implMethodParameters) {
+ ClassNode returnType = implMethodNode.getReturnType();
+ switch (Type.getReturnType(samMethodDescriptor).getSort()) {
+ case Type.BOOLEAN:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Boolean_TYPE; // GROOVY-10975
+ break;
+ case Type.BYTE:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Byte_TYPE;
+ break;
+ case Type.CHAR:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Character_TYPE;
+ break;
+ case Type.DOUBLE:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Double_TYPE;
+ break;
+ case Type.FLOAT:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Float_TYPE;
+ break;
+ case Type.INT:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Integer_TYPE;
+ break;
+ case Type.LONG:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Long_TYPE;
+ break;
+ case Type.SHORT:
+ if (returnType.isGenericsPlaceHolder()) returnType =
ClassHelper.Short_TYPE;
+ break;
+ case Type.VOID:
+ returnType = ClassHelper.VOID_TYPE; // GROOVY-10933
+ }
+
+ return Type.getMethodType(createMethodDescriptor(returnType,
implMethodParameters));
+ }
+
+ private BlockStatement getOrAddDeserializeDispatcherGuards(final
WriterController controller, final Parameter[] deserializeMethodParameters) {
+ ClassNode enclosingClass = controller.getClassNode();
+ BlockStatement dispatcherGuards =
enclosingClass.getNodeMetaData(DESERIALIZE_LAMBDA_DISPATCHER);
+ if (dispatcherGuards != null) {
+ return dispatcherGuards;
+ }
+
+ dispatcherGuards = new BlockStatement();
+ BlockStatement dispatcher = new BlockStatement();
+ dispatcher.addStatement(dispatcherGuards);
+ dispatcher.addStatement(createInvalidDeserializationStatement());
+
+ MethodNode deserializeLambda = enclosingClass.addSyntheticMethod(
+ "$deserializeLambda$",
+ Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
+ OBJECT_TYPE,
+ deserializeMethodParameters,
+ ClassNode.EMPTY_ARRAY,
+ dispatcher);
+
deserializeLambda.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE,
Boolean.TRUE);
+ enclosingClass.putNodeMetaData(DESERIALIZE_LAMBDA_DISPATCHER,
dispatcherGuards);
+ return dispatcherGuards;
+ }
+
+ private Statement createInvalidDeserializationStatement() {
+ final ClassNode cn = ClassHelper.make(IllegalArgumentException.class);
+ ConstructorCallExpression exception = ctorX(cn, constX("Invalid
serialized functional interface"));
+ exception.putNodeMetaData(DIRECT_METHOD_CALL_TARGET,
cn.getDeclaredConstructor(
+ params(param(ClassHelper.STRING_TYPE, "message"))
+ ));
+ return throwS(exception);
+ }
+
+ /**
+ * Builds the identity check used by {@code $deserializeLambda$} to select
the
+ * correct synthetic helper for a serialized lambda/method reference.
+ * <p>
+ * The generated expression is a conjunction over all stable
+ * {@link java.lang.invoke.SerializedLambda} identity fields we emit via
+ * {@link #createSerializedLambdaFingerprint(String, ClassNode, int,
ClassNode, MethodNode, Parameter[], ClassNode, MethodNode, int)}:
+ * capturing class, implementation kind/class/name/signature, functional
interface class/SAM method/signature,
+ * instantiated method type, and captured argument count.
+ * <p>
+ * Do not weaken this predicate (for example, by checking only method
name/class or
+ * by returning a constant). Doing so can misroute deserialization to the
wrong helper,
+ * while overly strict constant-false behavior breaks valid
deserialization.
+ *
+ * @param serializedForm the deserialized {@code SerializedLambda}
expression
+ * @param serializedLambdaFingerprint compile-time fingerprint of one
serialized lambda target
+ * @return expression that evaluates to {@code true} only for this exact
target
+ */
+ private Expression matchesSerializedFunctionalInterface(final Expression
serializedForm, final SerializedLambdaFingerprint serializedLambdaFingerprint) {
+ return allMatch(
+ matchesSerializedFormInt(serializedForm, "getCapturedArgCount",
serializedLambdaFingerprint.capturedArgCount()),
+ matchesSerializedFormString(serializedForm, "getCapturingClass",
serializedLambdaFingerprint.capturingClass()),
+ matchesSerializedFormInt(serializedForm, "getImplMethodKind",
serializedLambdaFingerprint.implMethodKind()),
+ matchesSerializedFormString(serializedForm, "getImplClass",
serializedLambdaFingerprint.implClass()),
+ matchesSerializedFormString(serializedForm, "getImplMethodName",
serializedLambdaFingerprint.implMethodName()),
+ matchesSerializedFormString(serializedForm,
"getImplMethodSignature", serializedLambdaFingerprint.implMethodSignature()),
+ matchesSerializedFormString(serializedForm,
"getFunctionalInterfaceClass",
serializedLambdaFingerprint.functionalInterfaceClass()),
+ matchesSerializedFormString(serializedForm,
"getFunctionalInterfaceMethodName",
serializedLambdaFingerprint.functionalInterfaceMethodName()),
+ matchesSerializedFormString(serializedForm,
"getFunctionalInterfaceMethodSignature",
serializedLambdaFingerprint.functionalInterfaceMethodSignature()),
+ matchesSerializedFormString(serializedForm,
"getInstantiatedMethodType",
serializedLambdaFingerprint.instantiatedMethodType())
+ );
+ }
+
+ /**
+ * Combines predicates with logical AND.
+ *
+ * @param expressions match predicates to combine
+ * @return conjunction of all predicates
+ * @throws IllegalArgumentException if no predicates are supplied
+ */
+ private Expression allMatch(final Expression... expressions) {
+ return Arrays.stream(expressions)
+ .reduce(GeneralUtils::andX)
+ .orElseThrow(() -> new IllegalArgumentException("expressions
must not be empty"));
+ }
+
+ private Expression matchesSerializedFormInt(final Expression
serializedForm, final String accessorName, final int expectedValue) {
+ return eqX(serializedLambdaAccessorCall(serializedForm, accessorName),
constX(expectedValue, true));
+ }
+
+ private Expression matchesSerializedFormString(final Expression
serializedForm, final String accessorName, final String expectedValue) {
+ return eqX(serializedLambdaAccessorCall(serializedForm, accessorName),
constX(expectedValue));
+ }
+
+ /**
+ * Creates a direct {@link java.lang.invoke.SerializedLambda} accessor
call for the generated
+ * deserialization dispatcher.
+ * <p>
+ * The dispatcher is emitted during bytecode generation rather than type
checking, so it must
+ * resolve accessor targets explicitly instead of relying on a later
static-type-checking pass.
+ * This keeps the generated bytecode on the direct invocation path and
prevents accidental
+ * fallback to Groovy's dynamic method dispatch for JDK accessors.
+ *
+ * @param serializedForm the deserialized {@code SerializedLambda}
expression
+ * @param accessorName the zero-argument accessor to invoke
+ * @return method call expression with a resolved direct-call target
+ * @throws IllegalArgumentException if the accessor does not exist on
{@code SerializedLambda}
+ */
+ private MethodCallExpression serializedLambdaAccessorCall(final Expression
serializedForm, final String accessorName) {
+ MethodNode accessor = SERIALIZEDLAMBDA_TYPE.getMethod(accessorName,
Parameter.EMPTY_ARRAY);
+ if (accessor == null) {
+ throw new IllegalArgumentException("Unknown SerializedLambda
accessor: " + accessorName);
+ }
+
+ MethodCallExpression accessorCall = callX(serializedForm,
accessorName);
+ accessorCall.setMethodTarget(accessor);
+ accessorCall.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, accessor);
+ return accessorCall;
+ }
+
+ /**
+ * Compile-time identity of one serialized functional-interface target
used by
+ * {@code $deserializeLambda$} dispatch.
+ */
+ record SerializedLambdaFingerprint(String capturingClass, int
implMethodKind, String implClass, String implMethodName,
+ String implMethodSignature, String
functionalInterfaceClass,
+ String functionalInterfaceMethodName,
String functionalInterfaceMethodSignature,
+ String instantiatedMethodType, int
capturedArgCount) {
}
}
diff --git
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesFunctionalInterfaceMetadataKey.java
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesFunctionalInterfaceMetadataKey.java
new file mode 100644
index 0000000000..8deddab27a
--- /dev/null
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesFunctionalInterfaceMetadataKey.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+/**
+ * Internal AST node metadata keys used by the statically-compiled lambda and
method-reference pipeline.
+ *
+ * @since 6.0.0
+ */
+enum StaticTypesFunctionalInterfaceMetadataKey {
+ /**
+ * Stores the shared {@code $deserializeLambda$} guard block on the
enclosing class node.
+ */
+ DESERIALIZE_LAMBDA_DISPATCHER,
+ /**
+ * Marks the synthetic constructor created for a generated lambda class.
+ */
+ LAMBDA_GENERATED_CONSTRUCTOR,
+ /**
+ * Stores the captured shared variables prepared for a lambda expression.
+ */
+ LAMBDA_SHARED_VARIABLES,
+ /**
+ * Marks the deserialize helper method that preloads a captured lambda
receiver.
+ */
+ LAMBDA_PRELOADED_RECEIVER,
+ /**
+ * Caches whether the analyzed lambda method touches enclosing-instance
state.
+ */
+ LAMBDA_ACCESSES_INSTANCE_MEMBERS,
+ /**
+ * Stores the synthetic deserialize helper name allocated for a
method-reference expression so
+ * repeated bytecode-generation visits can reuse the same helper slot.
+ */
+ METHOD_REFERENCE_DESERIALIZE_METHOD_NAME
+}
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
index 5509899d86..3561d25937 100644
---
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaAnalyzer.java
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaAnalyzer.java
@@ -40,6 +40,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.LAMBDA_ACCESSES_INSTANCE_MEMBERS;
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;
@@ -68,14 +69,14 @@ class StaticTypesLambdaAnalyzer {
}
boolean accessesInstanceMembers(final MethodNode lambdaMethod) {
- Boolean accessingInstanceMembers =
lambdaMethod.getNodeMetaData(MetaDataKey.ACCESSES_INSTANCE_MEMBERS);
+ Boolean accessingInstanceMembers =
lambdaMethod.getNodeMetaData(LAMBDA_ACCESSES_INSTANCE_MEMBERS);
if (accessingInstanceMembers != null) return accessingInstanceMembers;
InstanceMemberAccessFinder finder = new
InstanceMemberAccessFinder(getOrCreateResolver(lambdaMethod));
lambdaMethod.getCode().visit(finder);
accessingInstanceMembers = finder.isAccessingInstanceMembers();
- lambdaMethod.putNodeMetaData(MetaDataKey.ACCESSES_INSTANCE_MEMBERS,
accessingInstanceMembers);
+ lambdaMethod.putNodeMetaData(LAMBDA_ACCESSES_INSTANCE_MEMBERS,
accessingInstanceMembers);
return accessingInstanceMembers;
}
@@ -161,9 +162,7 @@ class StaticTypesLambdaAnalyzer {
PropertyExpression qualifiedReference = new
PropertyExpression(classX(owner), expression.getName());
qualifiedReference.setImplicitThis(false);
- qualifiedReference.copyNodeMetaData(expression);
- setSourcePosition(qualifiedReference, expression);
- return qualifiedReference;
+ return finishQualifiedReference(qualifiedReference, expression);
}
private Expression qualify(final AttributeExpression expression) {
@@ -177,9 +176,7 @@ class StaticTypesLambdaAnalyzer {
);
qualifiedReference.setImplicitThis(false);
qualifiedReference.setSpreadSafe(expression.isSpreadSafe());
- qualifiedReference.copyNodeMetaData(expression);
- setSourcePosition(qualifiedReference, expression);
- return qualifiedReference;
+ return finishQualifiedReference(qualifiedReference, expression);
}
private Expression qualify(final PropertyExpression expression) {
@@ -193,9 +190,7 @@ class StaticTypesLambdaAnalyzer {
);
qualifiedReference.setImplicitThis(false);
qualifiedReference.setSpreadSafe(expression.isSpreadSafe());
- qualifiedReference.copyNodeMetaData(expression);
- setSourcePosition(qualifiedReference, expression);
- return qualifiedReference;
+ return finishQualifiedReference(qualifiedReference, expression);
}
private Expression qualify(final MethodCallExpression expression) {
@@ -212,6 +207,10 @@ class StaticTypesLambdaAnalyzer {
qualifiedReference.setSpreadSafe(expression.isSpreadSafe());
qualifiedReference.setGenericsTypes(expression.getGenericsTypes());
qualifiedReference.setMethodTarget(expression.getMethodTarget());
+ return finishQualifiedReference(qualifiedReference, expression);
+ }
+
+ private <T extends Expression> T finishQualifiedReference(final T
qualifiedReference, final Expression expression) {
qualifiedReference.copyNodeMetaData(expression);
setSourcePosition(qualifiedReference, expression);
return qualifiedReference;
@@ -394,10 +393,6 @@ class StaticTypesLambdaAnalyzer {
}
}
- private enum MetaDataKey {
- ACCESSES_INSTANCE_MEMBERS
- }
-
private final SourceUnit sourceUnit;
private final Map<MethodNode, OuterStaticMemberResolver> resolverCache =
new HashMap<>();
}
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 b61dc6c930..3b393b41c9 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
@@ -25,11 +25,8 @@ 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.Expression;
import org.codehaus.groovy.ast.expr.LambdaExpression;
-import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.classgen.BytecodeInstruction;
import org.codehaus.groovy.classgen.BytecodeSequence;
@@ -50,14 +47,14 @@ import static
org.codehaus.groovy.ast.ClassHelper.CLOSURE_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.GENERATED_LAMBDA_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.SERIALIZABLE_TYPE;
-import static org.codehaus.groovy.ast.ClassHelper.SERIALIZEDLAMBDA_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE;
import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe;
import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.LAMBDA_GENERATED_CONSTRUCTOR;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.LAMBDA_PRELOADED_RECEIVER;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.LAMBDA_SHARED_VARIABLES;
import static
org.codehaus.groovy.transform.stc.StaticTypesMarker.CLOSURE_ARGUMENTS;
import static
org.codehaus.groovy.transform.stc.StaticTypesMarker.PARAMETER_TYPE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
@@ -99,7 +96,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
boolean serializable = makeSerializableIfNeeded(expression,
functionalType);
GeneratedLambda generatedLambda = getOrAddGeneratedLambda(expression,
abstractMethod);
- ensureDeserializeLambdaSupport(expression, generatedLambda,
serializable);
+ ensureDeserializeLambdaSupport(expression, functionalType,
abstractMethod, generatedLambda, serializable);
if (generatedLambda.isCapturing() &&
!isPreloadedLambdaReceiver(generatedLambda)) {
loadLambdaReceiver(generatedLambda);
}
@@ -107,10 +104,6 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
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;
@@ -125,28 +118,38 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
return expression.isSerializable();
}
- private void ensureDeserializeLambdaSupport(final LambdaExpression
expression, final GeneratedLambda generatedLambda, final boolean serializable) {
+ private void ensureDeserializeLambdaSupport(final LambdaExpression
expression, final ClassNode functionalType, final MethodNode abstractMethod,
final GeneratedLambda generatedLambda, final boolean serializable) {
if (!serializable ||
hasDeserializeLambdaMethod(generatedLambda.lambdaClass)) {
return;
}
- addDeserializeLambdaMethodForLambdaExpression(expression,
generatedLambda);
- addDeserializeLambdaMethod();
+ String samMethodDescriptor = createMethodDescriptor(abstractMethod);
+ MethodNode helperMethod =
addDeserializeLambdaMethodForLambdaExpression(expression, generatedLambda);
+ addDeserializeDispatcherEntry(controller,
createDeserializeMethodParameters(), createSerializedLambdaFingerprint(
+ samMethodDescriptor,
+ controller.getClassNode(),
+ generatedLambda.getImplMethodKind(),
+ generatedLambda.lambdaClass,
+ generatedLambda.lambdaMethod,
+ generatedLambda.lambdaMethod.getParameters(),
+ functionalType,
+ abstractMethod,
+ generatedLambda.isCapturing() ? 1 : 0
+ ), helperMethod);
}
private void writeLambdaFactoryInvocation(final ClassNode functionalType,
final MethodNode abstractMethod, final GeneratedLambda generatedLambda, final
boolean serializable) {
- MethodVisitor mv = controller.getMethodVisitor();
- mv.visitInvokeDynamicInsn(
+ writeFunctionalInterfaceIndy(
+ controller.getMethodVisitor(),
abstractMethod.getName(),
createLambdaFactoryMethodDescriptor(functionalType,
generatedLambda),
- createBootstrapMethod(controller.getClassNode().isInterface(),
serializable),
-
createBootstrapMethodArguments(createMethodDescriptor(abstractMethod),
- generatedLambda.getMethodHandleKind(),
- generatedLambda.lambdaClass, generatedLambda.lambdaMethod,
generatedLambda.lambdaMethod.getParameters(), serializable)
+ createMethodDescriptor(abstractMethod),
+ generatedLambda.getImplMethodKind(),
+ generatedLambda.lambdaClass,
+ generatedLambda.lambdaMethod,
+ generatedLambda.lambdaMethod.getParameters(),
+ serializable
);
- if (serializable) {
- mv.visitTypeInsn(CHECKCAST, "java/io/Serializable");
- }
if (generatedLambda.nonCapturing()) {
controller.getOperandStack().push(functionalType);
@@ -156,7 +159,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
}
private boolean hasDeserializeLambdaMethod(final ClassNode lambdaClass) {
- return
controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass),
createDeserializeLambdaMethodParams());
+ return
controller.getClassNode().hasMethod(createDeserializeLambdaMethodName(lambdaClass),
createDeserializeMethodParameters());
}
private static MethodNode getLambdaMethod(final ClassNode lambdaClass) {
@@ -169,7 +172,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
private static ConstructorNode getGeneratedConstructor(final ClassNode
lambdaClass) {
for (ConstructorNode constructorNode :
lambdaClass.getDeclaredConstructors()) {
- if
(Boolean.TRUE.equals(constructorNode.getNodeMetaData(MetaDataKey.GENERATED_CONSTRUCTOR)))
{
+ if
(Boolean.TRUE.equals(constructorNode.getNodeMetaData(LAMBDA_GENERATED_CONSTRUCTOR)))
{
return constructorNode;
}
}
@@ -179,7 +182,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
private boolean isPreloadedLambdaReceiver(final GeneratedLambda
generatedLambda) {
MethodNode enclosingMethod = controller.getMethodNode();
return enclosingMethod != null
- &&
enclosingMethod.getNodeMetaData(MetaDataKey.PRELOADED_LAMBDA_RECEIVER) ==
generatedLambda.lambdaClass;
+ && enclosingMethod.getNodeMetaData(LAMBDA_PRELOADED_RECEIVER) ==
generatedLambda.lambdaClass;
}
private void loadLambdaReceiver(final GeneratedLambda generatedLambda) {
@@ -202,7 +205,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
loadSharedVariables(generatedLambda.sharedVariables);
Parameter[] lambdaClassConstructorParameters =
generatedLambda.constructor.getParameters();
- mv.visitMethodInsn(INVOKESPECIAL, lambdaClassInternalName, "<init>",
BytecodeHelper.getMethodDescriptor(VOID_TYPE,
lambdaClassConstructorParameters), generatedLambda.lambdaClass.isInterface());
+ mv.visitMethodInsn(INVOKESPECIAL, lambdaClassInternalName, "<init>",
createMethodDescriptor(VOID_TYPE, lambdaClassConstructorParameters),
generatedLambda.lambdaClass.isInterface());
operandStack.replace(CLOSURE_TYPE,
lambdaClassConstructorParameters.length);
}
@@ -217,16 +220,10 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
}
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;
+ return createFunctionalInterfaceFactoryDescriptor(functionalInterface,
+ generatedLambda.nonCapturing()
+ ? Parameter.EMPTY_ARRAY
+ : new
Parameter[]{createCapturedReceiverParameter(generatedLambda.lambdaClass,
"__lambda_this")});
}
private GeneratedLambda getOrAddGeneratedLambda(final LambdaExpression
expression, final MethodNode abstractMethod) {
@@ -279,7 +276,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
addFieldsForLocalVariables(lambdaClass, localVariableParameters);
ConstructorNode constructorNode = addConstructor(expression,
localVariableParameters, lambdaClass,
createBlockStatementForConstructor(expression, outermostClass, enclosingClass));
- constructorNode.putNodeMetaData(MetaDataKey.GENERATED_CONSTRUCTOR,
Boolean.TRUE);
+ constructorNode.putNodeMetaData(LAMBDA_GENERATED_CONSTRUCTOR,
Boolean.TRUE);
syntheticLambdaMethodNode.getCode().visit(new
CorrectAccessedVariableVisitor(lambdaClass));
@@ -297,7 +294,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
Parameter[] localVariableParameters =
getLambdaSharedVariables(expression);
removeInitialValues(localVariableParameters);
- expression.putNodeMetaData(MetaDataKey.STORED_LAMBDA_SHARED_VARIABLES,
localVariableParameters);
+ expression.putNodeMetaData(LAMBDA_SHARED_VARIABLES,
localVariableParameters);
MethodNode doCallMethod = lambdaClass.addMethod(
DO_CALL,
@@ -327,36 +324,11 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
return lambdaParameters;
}
- private void addDeserializeLambdaMethod() {
- ClassNode enclosingClass = controller.getClassNode();
- Parameter[] parameters = createDeserializeLambdaMethodParams();
- if (enclosingClass.hasMethod("$deserializeLambda$", parameters)) {
- return;
- }
-
- 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)
- );
-
- enclosingClass.addSyntheticMethod(
- "$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 addDeserializeLambdaMethodForLambdaExpression(final
LambdaExpression expression, final GeneratedLambda generatedLambda) {
+ private MethodNode addDeserializeLambdaMethodForLambdaExpression(final
LambdaExpression expression, final GeneratedLambda generatedLambda) {
ClassNode enclosingClass = controller.getClassNode();
Statement code;
if (generatedLambda.nonCapturing()) {
@@ -385,15 +357,16 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
MethodNode deserializeLambdaMethod = enclosingClass.addSyntheticMethod(
createDeserializeLambdaMethodName(generatedLambda.lambdaClass),
- ACC_PUBLIC | ACC_STATIC,
+ ACC_PRIVATE | ACC_STATIC,
OBJECT_TYPE,
- createDeserializeLambdaMethodParams(),
+ createDeserializeMethodParameters(),
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);
+ deserializeLambdaMethod.putNodeMetaData(LAMBDA_PRELOADED_RECEIVER,
generatedLambda.lambdaClass);
}
+ return deserializeLambdaMethod;
}
private static String createDeserializeLambdaMethodName(final ClassNode
lambdaClass) {
@@ -401,19 +374,17 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
}
private static Parameter[] getStoredLambdaSharedVariables(final
LambdaExpression expression) {
- Parameter[] sharedVariables =
expression.getNodeMetaData(MetaDataKey.STORED_LAMBDA_SHARED_VARIABLES);
+ Parameter[] sharedVariables =
expression.getNodeMetaData(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
- }
-
+ /**
+ * Cached lambda generation result reused across the emitted indy call and
+ * any synthetic deserialization helpers for the same expression.
+ */
private record GeneratedLambda(ClassNode lambdaClass, MethodNode
lambdaMethod, ConstructorNode constructor,
Parameter[] sharedVariables, boolean
nonCapturing,
boolean accessingInstanceMembers) {
@@ -422,7 +393,7 @@ public class StaticTypesLambdaWriter extends LambdaWriter
implements AbstractFun
return !nonCapturing;
}
- private int getMethodHandleKind() {
+ private int getImplMethodKind() {
return nonCapturing ? H_INVOKESTATIC : H_INVOKEVIRTUAL;
}
}
diff --git
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java
index c526caa7b4..0ee559c9ad 100644
---
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java
+++
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java
@@ -32,7 +32,8 @@ import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.MethodReferenceExpression;
import org.codehaus.groovy.ast.tools.GeneralUtils;
import org.codehaus.groovy.classgen.AsmClassGenerator;
-import org.codehaus.groovy.classgen.asm.BytecodeHelper;
+import org.codehaus.groovy.classgen.BytecodeInstruction;
+import org.codehaus.groovy.classgen.BytecodeSequence;
import org.codehaus.groovy.classgen.asm.MethodReferenceExpressionWriter;
import org.codehaus.groovy.classgen.asm.WriterController;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
@@ -40,7 +41,7 @@ import org.codehaus.groovy.syntax.RuntimeParserException;
import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys;
import org.codehaus.groovy.transform.stc.ExtensionMethodNode;
import org.codehaus.groovy.transform.stc.StaticTypesMarker;
-import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.Arrays;
@@ -62,12 +63,21 @@ import static
org.codehaus.groovy.ast.tools.GenericsUtils.extractPlaceholders;
import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafe0;
import static org.codehaus.groovy.ast.tools.ParameterUtils.isVargs;
import static
org.codehaus.groovy.ast.tools.ParameterUtils.parametersCompatible;
+import static
org.codehaus.groovy.classgen.asm.BytecodeHelper.getClassInternalName;
+import static
org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.METHOD_REFERENCE_DESERIALIZE_METHOD_NAME;
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last;
import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.allParametersAndArgumentsMatch;
import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.filterMethodsByVisibility;
import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsForClassNode;
import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isAssignableTo;
import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.resolveClassNodeGenerics;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.ICONST_0;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
/**
* Generates bytecode for method reference expressions in statically-compiled
code.
@@ -82,132 +92,228 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
@Override
public void writeMethodReferenceExpression(final MethodReferenceExpression
methodReferenceExpression) {
- // functional interface target is required for native method reference
generation
- ClassNode functionalType =
methodReferenceExpression.getNodeMetaData(StaticTypesMarker.PARAMETER_TYPE);
- MethodNode abstractMethod = ClassHelper.findSAM(functionalType);
- if (abstractMethod == null || !functionalType.isInterface()) {
+ FunctionalInterfaceContext functionalInterface =
resolveFunctionalInterfaceContext(methodReferenceExpression);
+ if (functionalInterface == null) {
// generate the default bytecode -- most likely a method closure
super.writeMethodReferenceExpression(methodReferenceExpression);
return;
}
- ClassNode classNode = controller.getClassNode();
+ MethodReferenceTarget referenceTarget =
resolveMethodReferenceTarget(methodReferenceExpression);
+ ResolvedMethodReference resolvedMethodReference =
resolveMethodReference(methodReferenceExpression, functionalInterface,
referenceTarget);
+ validate(methodReferenceExpression, referenceTarget.type(),
resolvedMethodReference.methodName(),
+ resolvedMethodReference.implementationMethod(),
functionalInterface.parametersWithExactType(),
+
resolveClassNodeGenerics(extractPlaceholders(functionalInterface.functionalType()),
null, functionalInterface.abstractMethod().getReturnType()));
+
+ ResolvedMethodReference adaptedMethodReference =
adaptMethodReference(functionalInterface, resolvedMethodReference);
+ ResolvedMethodReference invocationReadyMethodReference =
prepareInvocationTarget(methodReferenceExpression, adaptedMethodReference);
+ MethodReferenceInvocation invocation =
createMethodReferenceInvocation(functionalInterface.functionalType(),
invocationReadyMethodReference);
+
+ if (functionalInterface.serializable()) {
+ ensureDeserializeLambdaSupport(methodReferenceExpression,
functionalInterface, invocationReadyMethodReference, invocation);
+ }
+
+ writeFunctionalInterfaceIndy(
+ controller.getMethodVisitor(),
+ functionalInterface.abstractMethod().getName(),
+ invocation.invokedTypeDescriptor(),
+ functionalInterface.samMethodDescriptor(),
+ invocation.implMethodKind(),
+
invocationReadyMethodReference.implementationMethod().getDeclaringClass(),
+ invocationReadyMethodReference.implementationMethod(),
+ functionalInterface.parametersWithExactType(),
+ functionalInterface.serializable()
+ );
+
+ updateOperandStack(functionalInterface.functionalType(),
invocation.capturing());
+ }
+
+ private FunctionalInterfaceContext resolveFunctionalInterfaceContext(final
MethodReferenceExpression methodReferenceExpression) {
+ // functional interface target is required for native method reference
generation
+ ClassNode functionalType =
methodReferenceExpression.getNodeMetaData(StaticTypesMarker.PARAMETER_TYPE);
+ if (functionalType == null || !functionalType.isInterface()) return
null;
+
+ MethodNode abstractMethod = ClassHelper.findSAM(functionalType);
+ if (abstractMethod == null) return null;
+
+ ClassNode[] inferredParameterTypes =
methodReferenceExpression.getNodeMetaData(StaticTypesMarker.CLOSURE_ARGUMENTS);
+ return new FunctionalInterfaceContext(
+ functionalType,
+ abstractMethod,
+ createParametersWithExactType(abstractMethod,
inferredParameterTypes),
+ createMethodDescriptor(abstractMethod),
+ functionalType.implementsInterface(ClassHelper.SERIALIZABLE_TYPE)
+ );
+ }
+
+ private MethodReferenceTarget resolveMethodReferenceTarget(final
MethodReferenceExpression methodReferenceExpression) {
Expression typeOrTargetRef = methodReferenceExpression.getExpression();
- boolean isClassExpression = (typeOrTargetRef instanceof
ClassExpression);
- boolean targetIsArgument = false; // implied argument for
expr::staticMethod?
- ClassNode typeOrTargetRefType = isClassExpression ?
typeOrTargetRef.getType()
- : controller.getTypeChooser().resolveType(typeOrTargetRef,
classNode);
-
- if (ClassHelper.isPrimitiveType(typeOrTargetRefType)) // GROOVY-11353
- typeOrTargetRefType = ClassHelper.getWrapper(typeOrTargetRefType);
-
- ClassNode[] methodReferenceParamTypes =
methodReferenceExpression.getNodeMetaData(StaticTypesMarker.CLOSURE_ARGUMENTS);
- Parameter[] parametersWithExactType =
createParametersWithExactType(abstractMethod, methodReferenceParamTypes);
- String methodRefName =
methodReferenceExpression.getMethodName().getText();
- boolean isConstructorReference = isConstructorReference(methodRefName);
-
- MethodNode methodRefMethod;
- if (isConstructorReference) {
- methodRefName =
controller.getContext().getNextConstructorReferenceSyntheticMethodName(controller.getMethodNode());
- methodRefMethod =
addSyntheticMethodForConstructorReference(methodRefName, typeOrTargetRefType,
parametersWithExactType);
- } else {
- // TODO: move the findMethodRefMethod and checking to
StaticTypeCheckingVisitor
- methodRefMethod = findMethodRefMethod(methodRefName,
parametersWithExactType, typeOrTargetRef, typeOrTargetRefType);
- if
(methodReferenceExpression.getNodeMetaData(StaticTypesMarker.PV_METHODS_ACCESS)
!= null) { // GROOVY-11301, GROOVY-11365: access bridge indicated
- Map<MethodNode,MethodNode> bridgeMethods =
typeOrTargetRefType.redirect().getNodeMetaData(StaticCompilationMetadataKeys.PRIVATE_BRIDGE_METHODS);
- if (bridgeMethods != null) methodRefMethod =
bridgeMethods.getOrDefault(methodRefMethod, methodRefMethod); // bridge may not
have been generated
+ boolean classExpression = typeOrTargetRef instanceof ClassExpression;
+ ClassNode targetType = classExpression
+ ? typeOrTargetRef.getType()
+ : controller.getTypeChooser().resolveType(typeOrTargetRef,
controller.getClassNode());
+
+ if (ClassHelper.isPrimitiveType(targetType)) { // GROOVY-11353
+ targetType = ClassHelper.getWrapper(targetType);
+ }
+
+ return new MethodReferenceTarget(typeOrTargetRef, targetType,
classExpression, false);
+ }
+
+ private ResolvedMethodReference resolveMethodReference(final
MethodReferenceExpression methodReferenceExpression,
+ final
FunctionalInterfaceContext functionalInterface,
+ final
MethodReferenceTarget referenceTarget) {
+ String methodName =
methodReferenceExpression.getMethodName().getText();
+ if (isConstructorReference(methodName)) {
+ String syntheticMethodName =
controller.getContext().getNextConstructorReferenceSyntheticMethodName(controller.getMethodNode());
+ MethodNode constructorReferenceMethod =
addSyntheticMethodForConstructorReference(
+ syntheticMethodName,
+ referenceTarget.type(),
+ functionalInterface.parametersWithExactType()
+ );
+ return new ResolvedMethodReference(referenceTarget,
syntheticMethodName, constructorReferenceMethod, true);
+ }
+
+ return new ResolvedMethodReference(
+ referenceTarget,
+ methodName,
+ findMethodReferenceImplementation(methodReferenceExpression,
methodName, functionalInterface.parametersWithExactType(), referenceTarget),
+ false
+ );
+ }
+
+ private MethodNode findMethodReferenceImplementation(final
MethodReferenceExpression methodReferenceExpression, final String methodName,
+ final Parameter[]
samParameters, final MethodReferenceTarget referenceTarget) {
+ // TODO: move the method lookup and validation to
StaticTypeCheckingVisitor
+ MethodNode methodRefMethod = findMethodRefMethod(methodName,
samParameters, referenceTarget.expression(), referenceTarget.type());
+ if
(methodReferenceExpression.getNodeMetaData(StaticTypesMarker.PV_METHODS_ACCESS)
!= null) { // GROOVY-11301, GROOVY-11365: access bridge indicated
+ Map<MethodNode, MethodNode> bridgeMethods =
referenceTarget.type().redirect().getNodeMetaData(StaticCompilationMetadataKeys.PRIVATE_BRIDGE_METHODS);
+ if (bridgeMethods != null) {
+ methodRefMethod = bridgeMethods.getOrDefault(methodRefMethod,
methodRefMethod); // bridge may not have been generated
}
- if (methodRefMethod == null && isClassExpression) {
- var classValue = varX("_class_", typeOrTargetRefType);
- var classClass = makeClassSafe0(ClassHelper.CLASS_Type, new
GenericsType(typeOrTargetRefType));
- methodRefMethod = findMethodRefMethod(methodRefName,
parametersWithExactType, classValue, classClass);
- if (methodRefMethod != null) methodRefMethod =
addSyntheticMethodForClassReference(methodRefMethod, typeOrTargetRefType);
+ }
+ if (methodRefMethod == null && referenceTarget.classExpression()) {
+ Expression classValue = varX("_class_", referenceTarget.type());
+ ClassNode classClass = makeClassSafe0(ClassHelper.CLASS_Type, new
GenericsType(referenceTarget.type()));
+ methodRefMethod = findMethodRefMethod(methodName, samParameters,
classValue, classClass);
+ if (methodRefMethod != null) {
+ methodRefMethod =
addSyntheticMethodForClassReference(methodRefMethod, referenceTarget.type());
}
}
+ return methodRefMethod;
+ }
- validate(methodReferenceExpression, typeOrTargetRefType,
methodRefName, methodRefMethod, parametersWithExactType,
-
resolveClassNodeGenerics(extractPlaceholders(functionalType), null,
abstractMethod.getReturnType()));
+ private ResolvedMethodReference adaptMethodReference(final
FunctionalInterfaceContext functionalInterface,
+ final
ResolvedMethodReference resolvedMethodReference) {
+ MethodReferenceTarget referenceTarget =
resolvedMethodReference.referenceTarget();
+ MethodNode methodRefMethod =
resolvedMethodReference.implementationMethod();
if (isBridgeMethod(methodRefMethod)) {
- targetIsArgument = true; // GROOVY-11301, GROOVY-11365
- if (isClassExpression) { // method expects an instance argument
+ referenceTarget = referenceTarget.markTargetAsArgument(); //
GROOVY-11301, GROOVY-11365
+ if (referenceTarget.classExpression()) { // method expects an
instance argument
methodRefMethod = addSyntheticMethodForDGSM(methodRefMethod);
}
- } else if (isExtensionMethod(methodRefMethod)) {
+ return resolvedMethodReference.with(referenceTarget,
methodRefMethod);
+ }
+
+ if (isExtensionMethod(methodRefMethod)) {
ExtensionMethodNode extensionMethodNode = (ExtensionMethodNode)
methodRefMethod;
- methodRefMethod = extensionMethodNode.getExtensionMethodNode();
- boolean isStatic = extensionMethodNode.isStaticExtension();
- if (isStatic) { // create adapter method to pass extra argument
+ methodRefMethod = extensionMethodNode.getExtensionMethodNode();
+ boolean staticExtension = extensionMethodNode.isStaticExtension();
+ if (staticExtension) { // create adapter method to pass extra
argument
methodRefMethod = addSyntheticMethodForDGSM(methodRefMethod);
}
- if (isStatic || isClassExpression) {
- // replace expression with declaring type
- typeOrTargetRefType = methodRefMethod.getDeclaringClass();
- typeOrTargetRef = makeClassTarget(typeOrTargetRefType,
typeOrTargetRef);
+ if (staticExtension || referenceTarget.classExpression()) {
+ referenceTarget =
referenceTarget.asClassTarget(methodRefMethod.getDeclaringClass());
} else { // GROOVY-10653
- targetIsArgument = true; // ex: "string"::size
- }
- } else if (isVargs(methodRefMethod.getParameters())) {
- int mParameters = abstractMethod.getParameters().length;
- int nParameters = methodRefMethod.getParameters().length;
- if (isTypeReferringInstanceMethod(typeOrTargetRef,
methodRefMethod)) nParameters += 1;
- if (mParameters > nParameters || mParameters == nParameters-1 ||
(mParameters == nParameters
- &&
!isAssignableTo(last(parametersWithExactType).getType(),
last(methodRefMethod.getParameters()).getType()))) {
- // GROOVY-9813: reference to variadic method which needs
adapter method to match runtime arguments to its parameters
- if (!isClassExpression && !methodRefMethod.isStatic() &&
!methodRefMethod.getDeclaringClass().equals(classNode)) {
- targetIsArgument = true; // GROOVY-10653: create static
adapter in source class with target as first parameter
- mParameters += 1;
- }
- methodRefMethod =
addSyntheticMethodForVariadicReference(methodRefMethod, mParameters,
isClassExpression || targetIsArgument);
- if (methodRefMethod.isStatic() && !targetIsArgument) {
- // replace expression with declaring type
- typeOrTargetRefType = methodRefMethod.getDeclaringClass();
- typeOrTargetRef = makeClassTarget(typeOrTargetRefType,
typeOrTargetRef);
- }
+ referenceTarget = referenceTarget.markTargetAsArgument(); //
ex: "string"::size
}
+ return resolvedMethodReference.with(referenceTarget,
methodRefMethod);
}
- if (!isClassExpression) {
- if (isConstructorReference) { // TODO: move this check to the
parser
- addFatalError("Constructor reference must be TypeName::new",
methodReferenceExpression);
- } else if (methodRefMethod.isStatic() && !targetIsArgument) {
- // "string"::valueOf refers to static method, so instance is
superfluous
- typeOrTargetRef = makeClassTarget(typeOrTargetRefType,
typeOrTargetRef);
- isClassExpression = true;
- } else {
- typeOrTargetRef.visit(controller.getAcg());
- controller.getOperandStack().box(); // GROOVY-11353
+ if (needsVariadicAdapter(functionalInterface, referenceTarget,
methodRefMethod)) {
+ int samParameterCount =
functionalInterface.abstractMethod().getParameters().length;
+ if (!referenceTarget.classExpression() &&
!methodRefMethod.isStatic() &&
!methodRefMethod.getDeclaringClass().equals(controller.getClassNode())) {
+ referenceTarget = referenceTarget.markTargetAsArgument(); //
GROOVY-10653: create static adapter in source class with target as first
parameter
+ samParameterCount += 1;
+ }
+ methodRefMethod =
addSyntheticMethodForVariadicReference(methodRefMethod, samParameterCount,
+ referenceTarget.classExpression() ||
referenceTarget.targetIsArgument());
+ if (methodRefMethod.isStatic() &&
!referenceTarget.targetIsArgument()) {
+ referenceTarget =
referenceTarget.asClassTarget(methodRefMethod.getDeclaringClass());
}
}
- int referenceKind;
- if (isConstructorReference || methodRefMethod.isStatic()) {
- referenceKind = Opcodes.H_INVOKESTATIC;
+ return resolvedMethodReference.with(referenceTarget, methodRefMethod);
+ }
+
+ private boolean needsVariadicAdapter(final FunctionalInterfaceContext
functionalInterface,
+ final MethodReferenceTarget
referenceTarget,
+ final MethodNode methodRefMethod) {
+ if (!isVargs(methodRefMethod.getParameters())) {
+ return false;
+ }
+
+ int samParameterCount =
functionalInterface.abstractMethod().getParameters().length;
+ int methodParameterCount = methodRefMethod.getParameters().length;
+ if (isTypeReferringInstanceMethod(referenceTarget.expression(),
methodRefMethod)) {
+ methodParameterCount += 1;
+ }
+
+ return samParameterCount > methodParameterCount
+ || samParameterCount == methodParameterCount - 1
+ || (samParameterCount == methodParameterCount
+ &&
!isAssignableTo(last(functionalInterface.parametersWithExactType()).getType(),
last(methodRefMethod.getParameters()).getType()));
+ }
+
+ private ResolvedMethodReference prepareInvocationTarget(final
MethodReferenceExpression methodReferenceExpression,
+ final
ResolvedMethodReference resolvedMethodReference) {
+ MethodReferenceTarget referenceTarget =
resolvedMethodReference.referenceTarget();
+ if (referenceTarget.classExpression()) {
+ return resolvedMethodReference;
+ }
+
+ if (resolvedMethodReference.constructorReference()) { // TODO: move
this check to the parser
+ addFatalError("Constructor reference must be TypeName::new",
methodReferenceExpression);
+ } else if (resolvedMethodReference.implementationMethod().isStatic()
&& !referenceTarget.targetIsArgument()) {
+ // "string"::valueOf refers to static method, so the bound
instance is superfluous.
+ return
resolvedMethodReference.withTarget(referenceTarget.asClassTarget(referenceTarget.type()));
+ } else {
+ referenceTarget.expression().visit(controller.getAcg());
+ controller.getOperandStack().box(); // GROOVY-11353
+ }
+
+ return resolvedMethodReference;
+ }
+
+ private MethodReferenceInvocation createMethodReferenceInvocation(final
ClassNode functionalType,
+ final
ResolvedMethodReference resolvedMethodReference) {
+ MethodNode methodRefMethod =
resolvedMethodReference.implementationMethod();
+ int implMethodKind;
+ if (resolvedMethodReference.constructorReference() ||
methodRefMethod.isStatic()) {
+ implMethodKind = Opcodes.H_INVOKESTATIC;
} else if (methodRefMethod.getDeclaringClass().isInterface()) {
- referenceKind = Opcodes.H_INVOKEINTERFACE; // GROOVY-9853
+ implMethodKind = Opcodes.H_INVOKEINTERFACE; // GROOVY-9853
} else {
- referenceKind = Opcodes.H_INVOKEVIRTUAL;
+ implMethodKind = Opcodes.H_INVOKEVIRTUAL;
}
- String methodName = abstractMethod.getName();
- String methodDesc =
BytecodeHelper.getMethodDescriptor(functionalType.redirect(),
- isClassExpression ? Parameter.EMPTY_ARRAY : new
Parameter[]{new Parameter(typeOrTargetRefType, "__METHODREF_EXPR_INSTANCE")});
-
- Handle bootstrapMethod =
createBootstrapMethod(classNode.isInterface(), false);
- Object[] bootstrapArgs = createBootstrapMethodArguments(
- createMethodDescriptor(abstractMethod),
- referenceKind,
- methodRefMethod.getDeclaringClass(),
- methodRefMethod,
- parametersWithExactType,
- false
+ MethodReferenceTarget referenceTarget =
resolvedMethodReference.referenceTarget();
+ Parameter[] capturedParameters = referenceTarget.classExpression()
+ ? Parameter.EMPTY_ARRAY
+ : new
Parameter[]{createCapturedReceiverParameter(referenceTarget.type(),
"__METHODREF_EXPR_INSTANCE")};
+ return new MethodReferenceInvocation(
+ implMethodKind,
+ createFunctionalInterfaceFactoryDescriptor(functionalType,
capturedParameters),
+ referenceTarget.isCapturing()
);
- controller.getMethodVisitor().visitInvokeDynamicInsn(methodName,
methodDesc, bootstrapMethod, bootstrapArgs);
+ }
- if (isClassExpression) {
- controller.getOperandStack().push(functionalType);
- } else {
+ private void updateOperandStack(final ClassNode functionalType, final
boolean capturing) {
+ if (capturing) {
controller.getOperandStack().replace(functionalType, 1);
+ } else {
+ controller.getOperandStack().push(functionalType);
}
}
@@ -255,7 +361,7 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
methodCall.setMethodTarget(mn);
methodCall.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, mn);
- String methodName = "class$" + classType.getNameWithoutPackage() + "$"
+ mn.getName() + "$" + System.nanoTime();
+ String methodName = createSyntheticMethodName("class", classType,
mn.getName());
ClassNode returnType = resolveClassNodeGenerics(Map.of(new
GenericsType.GenericsTypeName("T"), new GenericsType(classType)), null,
mn.getReturnType());
@@ -297,7 +403,7 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
methodCall.setMethodTarget(mn);
methodCall.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, mn);
- String methodName = "adapt$" +
mn.getDeclaringClass().getNameWithoutPackage() + "$" + mn.getName() + "$" +
System.nanoTime();
+ String methodName = createSyntheticMethodName("adapt",
mn.getDeclaringClass(), mn.getName());
MethodNode delegateMethod = addSyntheticMethod(methodName,
mn.getReturnType(), methodCall, parameters, mn.getExceptions());
if (!isStaticTarget && !mn.isStatic())
delegateMethod.setModifiers(delegateMethod.getModifiers() &
~Opcodes.ACC_STATIC);
@@ -346,7 +452,7 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
if (inferredParamType == null) continue;
Parameter parameter = parameters[i];
- ClassNode type = convertParameterType(parameter.getType(),
inferredParamType);
+ ClassNode type = convertParameterType(parameter.getType(),
parameter.getType(), inferredParamType);
parameter.setOriginType(type);
parameter.setType(type);
}
@@ -454,6 +560,90 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
throw new
MultipleCompilationErrorsException(controller.getSourceUnit().getErrorCollector());
}
+ private void ensureDeserializeLambdaSupport(final
MethodReferenceExpression methodReferenceExpression,
+ final
FunctionalInterfaceContext functionalInterface,
+ final ResolvedMethodReference
resolvedMethodReference,
+ final
MethodReferenceInvocation invocation) {
+ String helperName =
getOrCreateDeserializeLambdaMethodName(methodReferenceExpression);
+ Parameter[] parameters = createDeserializeMethodParameters();
+ if (controller.getClassNode().hasMethod(helperName, parameters)) {
+ return;
+ }
+
+ MethodReferenceTarget referenceTarget =
resolvedMethodReference.referenceTarget();
+ MethodNode methodRefMethod =
resolvedMethodReference.implementationMethod();
+ MethodNode helperMethod = addDeserializeLambdaMethodForMethodReference(
+ helperName,
+ functionalInterface.abstractMethod(),
+ methodRefMethod,
+ functionalInterface.parametersWithExactType(),
+ invocation.implMethodKind(),
+ invocation.capturing(),
+ referenceTarget.type(),
+ invocation.invokedTypeDescriptor(),
+ functionalInterface.samMethodDescriptor()
+ );
+ addDeserializeDispatcherEntry(controller, parameters,
+
createSerializedLambdaFingerprint(functionalInterface.samMethodDescriptor(),
controller.getClassNode(), invocation.implMethodKind(),
+ methodRefMethod.getDeclaringClass(), methodRefMethod,
+ functionalInterface.parametersWithExactType(),
functionalInterface.functionalType(),
+ functionalInterface.abstractMethod(), invocation.capturing() ?
1 : 0),
+ helperMethod);
+ }
+
+ private String getOrCreateDeserializeLambdaMethodName(final
MethodReferenceExpression methodReferenceExpression) {
+ String helperName =
methodReferenceExpression.getNodeMetaData(METHOD_REFERENCE_DESERIALIZE_METHOD_NAME);
+ if (helperName == null) {
+ helperName = createDeserializeLambdaMethodName();
+
methodReferenceExpression.putNodeMetaData(METHOD_REFERENCE_DESERIALIZE_METHOD_NAME,
helperName);
+ }
+ return helperName;
+ }
+
+ private MethodNode addDeserializeLambdaMethodForMethodReference(final
String methodName, final MethodNode abstractMethod,
+ final
MethodNode methodRefMethod, final Parameter[] parametersWithExactType,
+ final int
implMethodKind, final boolean capturing,
+ final
ClassNode capturedTargetType, final String invokedTypeDescriptor,
+ final
String samMethodDescriptor) {
+ return controller.getClassNode().addSyntheticMethod(
+ methodName,
+ ACC_PRIVATE | ACC_STATIC,
+ ClassHelper.OBJECT_TYPE,
+ createDeserializeMethodParameters(),
+ ClassNode.EMPTY_ARRAY,
+ new BytecodeSequence(new BytecodeInstruction() {
+ @Override
+ public void visit(final MethodVisitor mv) {
+ if (capturing) {
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitInsn(ICONST_0);
+ mv.visitMethodInsn(INVOKEVIRTUAL,
"java/lang/invoke/SerializedLambda", "getCapturedArg", "(I)Ljava/lang/Object;",
false);
+ mv.visitTypeInsn(CHECKCAST,
getClassInternalName(capturedTargetType));
+ }
+ writeFunctionalInterfaceIndy(
+ mv,
+ abstractMethod.getName(),
+ invokedTypeDescriptor,
+ samMethodDescriptor,
+ implMethodKind,
+ methodRefMethod.getDeclaringClass(),
+ methodRefMethod,
+ parametersWithExactType,
+ true
+ );
+ mv.visitInsn(ARETURN);
+ }
+ }));
+ }
+
+ private String createSyntheticMethodName(final String prefix, final
ClassNode owner, final String name) {
+ return prefix + "$" + owner.getNameWithoutPackage() + "$" + name + "$"
+ controller.getNextHelperMethodIndex();
+ }
+
+ private String createDeserializeLambdaMethodName() {
+ return "$deserializeLambda_methodref$" +
controller.getNextHelperMethodIndex() + "$";
+ }
+
//--------------------------------------------------------------------------
private static boolean isBridgeMethod(final MethodNode mn) {
@@ -487,4 +677,52 @@ public class StaticTypesMethodReferenceExpressionWriter
extends MethodReferenceE
private static Parameter[] removeFirstParameter(final Parameter[]
parameters) {
return Arrays.copyOfRange(parameters, 1, parameters.length);
}
+
+ /**
+ * Captures the functional-interface side of a method reference after type
+ * inference has fixed the SAM signature.
+ */
+ private record FunctionalInterfaceContext(ClassNode functionalType,
MethodNode abstractMethod,
+ Parameter[]
parametersWithExactType, String samMethodDescriptor,
+ boolean serializable) {
+ }
+
+ /**
+ * Models the source-side target of a method reference, including whether
+ * the target must still be captured at runtime.
+ */
+ private record MethodReferenceTarget(Expression expression, ClassNode
type, boolean classExpression, boolean targetIsArgument) {
+ private MethodReferenceTarget markTargetAsArgument() {
+ return new MethodReferenceTarget(expression, type,
classExpression, true);
+ }
+
+ private MethodReferenceTarget asClassTarget(final ClassNode
targetType) {
+ return new MethodReferenceTarget(makeClassTarget(targetType,
expression), targetType, true, targetIsArgument);
+ }
+
+ private boolean isCapturing() {
+ return !classExpression;
+ }
+ }
+
+ /**
+ * Selected implementation method together with the possibly rewritten
+ * method-reference target used to invoke it.
+ */
+ private record ResolvedMethodReference(MethodReferenceTarget
referenceTarget, String methodName,
+ MethodNode implementationMethod,
boolean constructorReference) {
+ private ResolvedMethodReference with(final MethodReferenceTarget
updatedTarget, final MethodNode updatedMethod) {
+ return new ResolvedMethodReference(updatedTarget, methodName,
updatedMethod, constructorReference);
+ }
+
+ private ResolvedMethodReference withTarget(final MethodReferenceTarget
updatedTarget) {
+ return new ResolvedMethodReference(updatedTarget, methodName,
implementationMethod, constructorReference);
+ }
+ }
+
+ /**
+ * Bytecode-level invocation details derived from the resolved method
reference.
+ */
+ private record MethodReferenceInvocation(int implMethodKind, String
invokedTypeDescriptor, boolean capturing) {
+ }
}
diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
index 37eab1575c..f3a784ef9a 100644
--- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
@@ -3423,6 +3423,411 @@ final class LambdaTest {
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'
+ private static final String SERIALIZED_LAMBDA_GET_CAPTURED_ARG =
'INVOKEVIRTUAL java/lang/invoke/SerializedLambda.getCapturedArg
(I)Ljava/lang/Object;'
+ }
+
+ @Nested
+ class NativeLambdaBytecodeTest extends AbstractBytecodeTestCase {
+ @Test
+ void testNonCapturingLambdaUsesCaptureFreeInvokeDynamic() {
+ def script = '''
+ @CompileStatic
+ class C {
+ static IntUnaryOperator create() {
+ (int i) -> i * 2
+ }
+ }
+ '''
+
+ def lambdaBytecode = compileStaticBytecode(classNamePattern:
'C\\$_create_lambda1', method: 'doCall', script)
+ assert lambdaBytecode.hasSequence(['public static doCall(I)I'])
+
+ def outerBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesCaptureFreeLambdaFactory(
+ outerBytecode,
+ 'C$_create_lambda1',
+ 'INVOKEDYNAMIC
applyAsInt()Ljava/util/function/IntUnaryOperator;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C$_create_lambda1.doCall(I)I'
+ )
+ }
+
+ @Test
+ void testCapturingLambdaUsesInvokeDynamicWithCapturedLambdaInstance() {
+ def script = '''
+ @CompileStatic
+ class C {
+ static IntUnaryOperator create() {
+ int offset = 1
+ IntUnaryOperator op = (int i) -> i + offset
+ 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)
+ assertUsesCapturingLambdaFactory(
+ outerBytecode,
+ 'NEW C$_create_lambda1',
+ 'INVOKEDYNAMIC
applyAsInt(LC$_create_lambda1;)Ljava/util/function/IntUnaryOperator;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C$_create_lambda1.doCall(I)I'
+ )
+ }
+
+ @Test
+ void
testSerializableNonCapturingLambdaUsesAltMetafactoryWithoutCapturedArgLookup() {
+ def script = '''
+ @CompileStatic
+ class C {
+ interface SerFunc<I,O> extends Serializable, Function<I,O>
{}
+
+ static SerFunc<Integer, String> create() {
+ (Integer i) -> 'v' + i
+ }
+ }
+ '''
+
+ def lambdaBytecode = compileStaticBytecode(classNamePattern:
'C\\$_create_lambda1', method: 'doCall', script)
+ assert lambdaBytecode.hasSequence(['public static
doCall(Ljava/lang/Integer;)Ljava/lang/Object;'])
+
+ def createBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesCaptureFreeLambdaFactory(
+ createBytecode,
+ 'C$_create_lambda1',
+ 'INVOKEDYNAMIC apply()LC$SerFunc;',
+ 'java/lang/invoke/LambdaMetafactory.altMetafactory',
+
'C$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;'
+ )
+ assert createBytecode.hasSequence(['CHECKCAST
java/io/Serializable'])
+
+ def deserializeHelperBytecode =
compileStaticBytecode(classNamePattern: 'C', method:
'$deserializeLambda_C$_create_lambda1$', script)
+ assert
!deserializeHelperBytecode.hasSequence([SERIALIZED_LAMBDA_GET_CAPTURED_ARG])
+ }
+
+ @Test
+ void
testSerializableCapturingLambdaReadsCapturedArgDuringDeserialization() {
+ def script = '''
+ @CompileStatic
+ class C {
+ interface SerFunc<I,O> extends Serializable, Function<I,O>
{}
+
+ static SerFunc<Integer, String> create() {
+ String prefix = 'v'
+ SerFunc<Integer, String> fn = (Integer i) -> prefix + i
+ fn
+ }
+ }
+ '''
+
+ 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 createBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesCapturingLambdaFactory(
+ createBytecode,
+ 'NEW C$_create_lambda1',
+ 'INVOKEDYNAMIC apply(LC$_create_lambda1;)LC$SerFunc;',
+ 'java/lang/invoke/LambdaMetafactory.altMetafactory',
+
'C$_create_lambda1.doCall(Ljava/lang/Integer;)Ljava/lang/Object;'
+ )
+ assert createBytecode.hasSequence(['CHECKCAST
java/io/Serializable'])
+
+ def deserializeHelperBytecode =
compileStaticBytecode(classNamePattern: 'C', method:
'$deserializeLambda_C$_create_lambda1$', script)
+ assert
deserializeHelperBytecode.hasSequence([SERIALIZED_LAMBDA_GET_CAPTURED_ARG])
+ }
+
+ @Test
+ void testLambdaInFieldInitializerUsesNativeFactoryFromConstructor() {
+ 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 initBytecode = compileStaticBytecode(classNamePattern: 'C',
method: '<init>', script)
+ assertUsesCaptureFreeLambdaFactory(
+ initBytecode,
+ 'C$_lambda1',
+ 'INVOKEDYNAMIC
applyAsInt()Ljava/util/function/IntUnaryOperator;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C$_lambda1.doCall(I)I'
+ )
+ }
+
+ @Test
+ void testInnerClassLambdaUsingOuterStaticMembersRemainsCaptureFree() {
+ 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)
+ assertUsesCaptureFreeLambdaFactory(
+ outerBytecode,
+ 'Outer$Inner$_create_lambda1',
+ 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'Outer$Inner$_create_lambda1.doCall()Ljava/lang/Object;'
+ )
+ }
+
+ private static void assertUsesCaptureFreeLambdaFactory(final bytecode,
final String lambdaClassName, final String invokedynamicInstruction, final
String bootstrapMethod, final String implementationTarget) {
+ assertUsesNativeLambdaFactory(bytecode, invokedynamicInstruction,
bootstrapMethod, implementationTarget)
+ assert !bytecode.hasSequence(["NEW $lambdaClassName"])
+ }
+
+ private static void assertUsesCapturingLambdaFactory(final bytecode,
final String allocationInstruction, final String invokedynamicInstruction,
final String bootstrapMethod, final String implementationTarget) {
+ assert bytecode.hasSequence([
+ allocationInstruction,
+ invokedynamicInstruction,
+ bootstrapMethod,
+ implementationTarget
+ ])
+ }
+
+ private static void assertUsesNativeLambdaFactory(final bytecode,
final String invokedynamicInstruction, final String bootstrapMethod, final
String implementationTarget) {
+ assert bytecode.hasSequence([
+ invokedynamicInstruction,
+ bootstrapMethod,
+ implementationTarget
+ ])
+ }
+
+ 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.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
(I)Ljava/lang/Object;'
+ }
+
+ @Test
+ void testLambdaWithDoubleReturnSAM() {
+ assertScript shell, '''
+ import java.util.function.DoubleUnaryOperator
+
+ DoubleUnaryOperator op = (double d) -> d * 2.0d
+ assert op.applyAsDouble(1.5d) == 3.0d
+ assert op.applyAsDouble(0.0d) == 0.0d
+ '''
+ }
+
+ @Test
+ void testLambdaWithLongReturnSAM() {
+ assertScript shell, '''
+ import java.util.function.LongUnaryOperator
+
+ LongUnaryOperator op = (long n) -> n * 2L
+ assert op.applyAsLong(3L) == 6L
+ assert op.applyAsLong(0L) == 0L
+ '''
+ }
+
+ @Test
+ void testLambdaWithByteReturnSAM() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToByteFunc<T> { byte apply(T t) }
+
+ ToByteFunc<Integer> f = (Integer n) -> n.byteValue()
+ assert f.apply(65) == (byte) 65
+ assert f.apply(300) == (byte) 44
+ '''
+ }
+
+ @Test
+ void testLambdaWithCharReturnSAM() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToCharFunc<T> { char apply(T t) }
+
+ ToCharFunc<String> f = (String s) -> s.charAt(0)
+ assert f.apply('hello') == 'h'
+ assert f.apply('world') == 'w'
+ '''
+ }
+
+ @Test
+ void testLambdaWithFloatReturnSAM() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToFloatFunc<T> { float apply(T t) }
+
+ ToFloatFunc<Integer> f = (Integer n) -> n.floatValue()
+ assert f.apply(3) == 3.0f
+ assert f.apply(0) == 0.0f
+ '''
+ }
+
+ @Test
+ void testLambdaWithShortReturnSAM() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToShortFunc<T> { short apply(T t) }
+
+ ToShortFunc<Integer> f = (Integer n) -> n.shortValue()
+ assert f.apply(42) == (short) 42
+ assert f.apply(300) == (short) 300
+ '''
+ }
+
+ @Test
+ void testLambdaWithDynamicParamAndPrimitiveSAM() {
+ assertScript shell, '''
+ import java.util.function.IntUnaryOperator
+
+ IntUnaryOperator op = i -> i + 1
+ assert op.applyAsInt(5) == 6
+ assert op.applyAsInt(0) == 1
+ '''
+ }
+
+ @Test
+ void testTwoLambdasSharingOuterVariableReuseReference() {
+ assertScript shell, '''
+ import java.util.function.IntSupplier
+
+ def sharedRefTest() {
+ int x = 42
+ IntSupplier s1 = () -> x
+ IntSupplier s2 = () -> x + 1
+ assert s1.getAsInt() == 42
+ assert s2.getAsInt() == 43
+ }
+
+ sharedRefTest()
+ '''
+ }
+
+ @Test
+ void testNonCapturingLambdaInStaticMethodUsesClassRef() {
+ assertScript shell, '''
+ class StaticLambdaHolder {
+ static Function<Integer, String> create() {
+ (Integer i) -> 'x' + i
+ }
+ }
+
+ def fn = StaticLambdaHolder.create()
+ assert fn.apply(1) == 'x1'
+ assert fn.apply(9) == 'x9'
+ '''
+ }
+
+ @Test
+ void testLambdaCapturingInstanceFieldLoadsThis() {
+ assertScript shell, '''
+ import java.util.function.IntSupplier
+
+ class Counter {
+ int count = 7
+ IntSupplier supplier() {
+ () -> count
+ }
+ }
+
+ def s = new Counter().supplier()
+ assert s.getAsInt() == 7
+ '''
+ }
+
+ @Test
+ void testLambdaWithWrapperParamForPrimitiveSAM() {
+ assertScript shell, '''
+ import java.util.function.IntUnaryOperator
+
+ IntUnaryOperator op = (Integer n) -> n + 10
+ assert op.applyAsInt(5) == 15
+ assert op.applyAsInt(-3) == 7
+ '''
+ }
+
+ @Test
+ void testLambdaToByteFunctionFromMapGetBytePlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToByteFunc<K> { byte apply(K k) }
+
+ Map<String, Byte> m = [a: (byte) 42, b: (byte) 99]
+ ToByteFunc<String> f = m::get
+ assert f.apply('a') == (byte) 42
+ assert f.apply('b') == (byte) 99
+ '''
+ }
+
+ @Test
+ void testLambdaToCharFunctionFromMapGetCharPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToCharFunc<K> { char apply(K k) }
+
+ Map<String, Character> m = [p: (char) 'P', q: (char) 'Q']
+ ToCharFunc<String> f = m::get
+ assert f.apply('p') == 'P'
+ assert f.apply('q') == 'Q'
+ '''
+ }
+
+ @Test
+ void testLambdaToFloatFunctionFromMapGetFloatPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToFloatFunc<K> { float apply(K k) }
+
+ Map<String, Float> m = [x: 1.5f, y: 2.5f]
+ ToFloatFunc<String> f = m::get
+ assert f.apply('x') == 1.5f
+ assert f.apply('y') == 2.5f
+ '''
+ }
+
+ @Test
+ void testLambdaToShortFunctionFromMapGetShortPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToShortFunc<K> { short apply(K k) }
+
+ Map<String, Short> m = [lo: (short) 1, hi: (short) 500]
+ ToShortFunc<String> f = m::get
+ assert f.apply('lo') == (short) 1
+ assert f.apply('hi') == (short) 500
+ '''
+ }
+
+ @Test
+ void testLambdaWithSupertypeExplicitParameter() {
+ assertScript shell, '''
+ List<String> results = []
+ Consumer<String> c = (Object s) -> results.add(s.toString())
+ c.accept('hello')
+ c.accept('world')
+ assert results == ['hello', 'world']
+ '''
}
}
diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy
b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy
index 3c6b5e1b90..a3150d2795 100644
--- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.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 org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
@@ -1158,6 +1160,144 @@ final class MethodReferenceTest {
'''
}
+ @Test // class::new
+ void testSerializableConstructorReference() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+
+ static class Box {
+ final String value
+
+ Box(String value) {
+ this.value = value.trim()
+ }
+ }
+
+ static SerFunc<String, Box> create() {
+ Box::new
+ }
+
+ 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<String, C.Box> factory =
C.deserialize(C.serialize(C.create()))
+ assert factory instanceof Serializable
+ assert factory.apply(' ok ').value == 'ok'
+ '''
+ }
+
+ @Test // arrayClass::new
+ void testSerializableArrayConstructorReference() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerIntFunc<T> extends Serializable, IntFunction<T> {}
+
+ static SerIntFunc<String[]> create() {
+ String[]::new
+ }
+
+ 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()
+ }
+ }
+ }
+
+ C.SerIntFunc<String[]> factory =
C.deserialize(C.serialize(C.create()))
+ String[] values = factory.apply(3)
+ assert values.length == 3
+ assert values.toList() == [null, null, null]
+ '''
+ }
+
+ @Test
+ void
testSerializableConstructorReferencesShareDeserializeDispatcherWithLambdas() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+ interface SerIntFunc<T> extends Serializable, IntFunction<T> {}
+
+ static class Box {
+ final String value
+
+ Box(String value) {
+ this.value = value
+ }
+ }
+
+ static SerFunc<String, Box> createConstructorReference() {
+ Box::new
+ }
+
+ static SerIntFunc<Box[]> createArrayConstructorReference() {
+ Box[]::new
+ }
+
+ static SerFunc<Integer, String> createLambda() {
+ (Integer i) -> 'L' + i
+ }
+
+ 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
+ assert C.declaredMethods.findAll {
it.name.startsWith('$deserializeLambda') && it.name != '$deserializeLambda$' }
+ .every { java.lang.reflect.Modifier.isPrivate(it.modifiers) &&
java.lang.reflect.Modifier.isStatic(it.modifiers) }
+
+ C.SerFunc<String, C.Box> ctor =
C.deserialize(C.serialize(C.createConstructorReference()))
+ C.SerIntFunc<C.Box[]> arrayCtor =
C.deserialize(C.serialize(C.createArrayConstructorReference()))
+ C.SerFunc<Integer, String> lambda =
C.deserialize(C.serialize(C.createLambda()))
+
+ assert ctor.apply('box').value == 'box'
+ assert arrayCtor.apply(2).length == 2
+ assert lambda.apply(4) == 'L4'
+ '''
+ }
+
@Test // class::staticMethod
void testFunctionCS() {
assertScript shell, '''
@@ -1662,6 +1802,283 @@ final class MethodReferenceTest {
'''
}
+ @Test
+ void testSerializableNonCapturingMethodReference() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+
+ static SerFunc<Integer, String> create() {
+ Integer::toString
+ }
+
+ 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> fn =
C.deserialize(C.serialize(C.create()))
+ assert fn instanceof Serializable
+ assert fn.apply(7) == '7'
+ '''
+ }
+
+ @Test
+ void testSerializableCapturingMethodReference() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerSupplier<T> extends Serializable, Supplier<T> {}
+
+ private final String text
+
+ C(String text) {
+ this.text = text
+ }
+
+ SerSupplier<String> create() {
+ text::trim
+ }
+
+ 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()
+ }
+ }
+ }
+
+ C.SerSupplier<String> supplier = C.deserialize(C.serialize(new C('
answer ').create()))
+ assert supplier instanceof Serializable
+ assert supplier.get() == 'answer'
+ '''
+ }
+
+ @Test
+ void
testSerializableMethodReferencesShareDeserializeDispatcherWithLambdas() {
+ assertScript shell, '''
+ import java.io.ByteArrayInputStream
+ import java.io.ByteArrayOutputStream
+ import java.io.Serializable
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+ interface SerSupplier<T> extends Serializable, Supplier<T> {}
+
+ private final String text
+
+ C(String text) {
+ this.text = text
+ }
+
+ static SerFunc<Integer, String> createMethodReference() {
+ Integer::toString
+ }
+
+ static SerFunc<Integer, String> createLambda() {
+ (Integer i) -> 'L' + i
+ }
+
+ SerSupplier<String> createBoundMethodReference() {
+ text::trim
+ }
+
+ 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
+ assert C.declaredMethods.findAll {
it.name.startsWith('$deserializeLambda') && it.name != '$deserializeLambda$' }
+ .every { java.lang.reflect.Modifier.isPrivate(it.modifiers) &&
java.lang.reflect.Modifier.isStatic(it.modifiers) }
+
+ C.SerFunc<Integer, String> methodRef =
C.deserialize(C.serialize(C.createMethodReference()))
+ C.SerFunc<Integer, String> lambda =
C.deserialize(C.serialize(C.createLambda()))
+ C.SerSupplier<String> bound = C.deserialize(C.serialize(new C(' x
').createBoundMethodReference()))
+
+ assert methodRef.apply(3) == '3'
+ assert lambda.apply(4) == 'L4'
+ assert bound.get() == 'x'
+ '''
+ }
+
+ @Test
+ void
testDeserializeDispatcherReturnsMatchingMethodReferenceAndLambdaBeforeFallback()
{
+ assertScript shell, '''
+ import java.io.Serializable
+ import java.lang.invoke.SerializedLambda
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+
+ static SerFunc<Integer, String> createMethodReference() {
+ Integer::toString
+ }
+
+ static SerFunc<Integer, String> createLambda() {
+ (Integer i) -> 'L' + i
+ }
+
+ @CompileDynamic
+ static SerializedLambda serialized(Serializable value) {
+ def writeReplace =
value.class.getDeclaredMethod('writeReplace')
+ writeReplace.accessible = true
+ (SerializedLambda) writeReplace.invoke(value)
+ }
+ }
+
+ def dispatcher = C.getDeclaredMethod('$deserializeLambda$',
SerializedLambda)
+ dispatcher.accessible = true
+
+ C.SerFunc<Integer, String> methodRef =
+ (C.SerFunc<Integer, String>) dispatcher.invoke(null,
C.serialized(C.createMethodReference()))
+ C.SerFunc<Integer, String> lambda =
+ (C.SerFunc<Integer, String>) dispatcher.invoke(null,
C.serialized(C.createLambda()))
+
+ assert methodRef.apply(3) == '3'
+ assert lambda.apply(3) == 'L3'
+ '''
+ }
+
+ @Test
+ void
testDeserializeDispatcherRejectsWrongCapturingClassEvenWhenOtherSerializedFieldsMatch()
{
+ assertScript shell, '''
+ import java.io.Serializable
+ import java.lang.invoke.SerializedLambda
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+
+ static SerFunc<Integer, String> createMethodReference() {
+ Integer::toString
+ }
+
+ static SerFunc<Integer, String> createLambda() {
+ (Integer i) -> 'L' + i
+ }
+
+ @CompileDynamic
+ static SerializedLambda serialized(Serializable value) {
+ def writeReplace =
value.class.getDeclaredMethod('writeReplace')
+ writeReplace.accessible = true
+ (SerializedLambda) writeReplace.invoke(value)
+ }
+
+ @CompileDynamic
+ static SerializedLambda withCapturingClass(SerializedLambda
serialized, Class capturingClass) {
+ new SerializedLambda(
+ capturingClass,
+ serialized.functionalInterfaceClass,
+ serialized.functionalInterfaceMethodName,
+ serialized.functionalInterfaceMethodSignature,
+ serialized.implMethodKind,
+ serialized.implClass,
+ serialized.implMethodName,
+ serialized.implMethodSignature,
+ serialized.instantiatedMethodType,
+ (0..<serialized.capturedArgCount).collect {
serialized.getCapturedArg(it) } as Object[]
+ )
+ }
+ }
+
+ def dispatcher = C.getDeclaredMethod('$deserializeLambda$',
SerializedLambda)
+ dispatcher.accessible = true
+
+ def assertInvalid = { SerializedLambda serialized ->
+ def err
+ try {
+ dispatcher.invoke(null, serialized)
+ assert false: 'dispatcher invocation should fail'
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ err = e
+ }
+ assert err.cause instanceof IllegalArgumentException
+ assert err.cause.message == 'Invalid serialized functional
interface'
+ }
+
+
assertInvalid(C.withCapturingClass(C.serialized(C.createMethodReference()),
String))
+ assertInvalid(C.withCapturingClass(C.serialized(C.createLambda()),
Integer))
+ '''
+ }
+
+ @Test
+ void
testDeserializeDispatcherReportsClearErrorForMismatchedSerializedForm() {
+ assertScript shell, '''
+ import java.lang.invoke.MethodHandleInfo
+ import java.lang.invoke.SerializedLambda
+
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I, O> {}
+
+ static SerFunc<Integer, String> create() {
+ Integer::toString
+ }
+ }
+
+ def dispatcher = C.getDeclaredMethod('$deserializeLambda$',
SerializedLambda)
+ dispatcher.accessible = true
+
+ def serialized = new SerializedLambda(
+ C,
+ 'java/util/function/Function',
+ 'apply',
+ '(Ljava/lang/Object;)Ljava/lang/Object;',
+ MethodHandleInfo.REF_invokeStatic,
+ 'java/lang/Integer',
+ 'toString',
+ '(I)Ljava/lang/String;',
+ '(Ljava/lang/Integer;)Ljava/lang/String;',
+ [] as Object[]
+ )
+
+ def err
+ try {
+ dispatcher.invoke(null, serialized)
+ assert false: 'dispatcher invocation should fail'
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ err = e
+ }
+ assert err.cause instanceof IllegalArgumentException
+ assert err.cause.message == 'Invalid serialized functional
interface'
+ '''
+ }
+
// GROOVY-11467
@Test
void testSuperInterfaceMethodReference() {
@@ -1757,4 +2174,610 @@ final class MethodReferenceTest {
new C().test()
'''
}
+
+ @Test
+ void testDoubleSupplierMethodReference() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ DoubleSupplier s = Math::random
+ assert s.getAsDouble() >= 0.0d && s.getAsDouble() < 1.0d
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testLongSupplierMethodReference() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ LongSupplier s = System::currentTimeMillis
+ assert s.getAsLong() > 0L
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testBiPredicateFromMapPutBooleanPlaceholder() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Map<String, Boolean> m = [k: true]
+ BiPredicate<String, Boolean> p = m::put
+ assert p.test('k', false) == true // old value was true
+ assert p.test('k', true) == false // old value was false
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToDoubleFunctionFromMapGetDoublePlaceholder() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Map<String, Double> m = [pi: 3.14d, e: 2.71d]
+ ToDoubleFunction<String> f = m::get
+ assert Math.abs(f.applyAsDouble('pi') - 3.14d) < 1e-9d
+ assert Math.abs(f.applyAsDouble('e') - 2.71d) < 1e-9d
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToLongFunctionFromMapGetLongPlaceholder() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Map<String, Long> m = [a: 100L, b: 200L]
+ ToLongFunction<String> f = m::get
+ assert f.applyAsLong('a') == 100L
+ assert f.applyAsLong('b') == 200L
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testByteReturnSAMMethodReference() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToByteFunc<T> { byte apply(T t) }
+
+ @CompileStatic
+ void test() {
+ ToByteFunc<Number> f = Number::byteValue
+ assert f.apply(65) == (byte) 65
+ assert f.apply(300) == (byte) 44 // 300 % 256 = 44
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testCharReturnSAMMethodReference() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToCharFunc { char apply(String s,
int i) }
+
+ @CompileStatic
+ void test() {
+ ToCharFunc f = String::charAt
+ assert f.apply('hello', 0) == 'h'
+ assert f.apply('hello', 4) == 'o'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testFloatReturnSAMMethodReference() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToFloatFunc<T> { float apply(T t) }
+
+ @CompileStatic
+ void test() {
+ ToFloatFunc<Number> f = Number::floatValue
+ assert f.apply(3.14) == 3.14f
+ assert f.apply(42) == 42.0f
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testShortReturnSAMMethodReference() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToShortFunc<T> { short apply(T t) }
+
+ @CompileStatic
+ void test() {
+ ToShortFunc<Number> f = Number::shortValue
+ assert f.apply(42) == (short) 42
+ assert f.apply(300) == (short) 300
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testVarargAdapterWhenLastSamParamNotAssignableToVarargArray() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ BiFunction<String, String, String> f = String::format
+ assert f.apply('%s', 'hello') == 'hello'
+ assert f.apply('[%s]', 'world') == '[world]'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testVarargZeroArgSAMMethodReference() {
+ assertScript shell, '''
+ import java.util.Arrays
+
+ @CompileStatic
+ void test() {
+ Supplier<List<Object>> s = Arrays::asList
+ assert s.get() == []
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testVarargMultiArgSAMMethodReference() {
+ assertScript shell, '''
+ @FunctionalInterface
+ interface StringFormatter { String format(String fmt, String a,
String b) }
+
+ @CompileStatic
+ void test() {
+ StringFormatter sf = String::format
+ assert sf.format('%s and %s', 'foo', 'bar') == 'foo and bar'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testBoundInstanceVarargExternalClassMethodReference() {
+ assertScript shell, '''
+ class StringJoiner {
+ String join(String... parts) { parts.join('-') }
+ }
+
+ @CompileStatic
+ void test() {
+ StringJoiner joiner = new StringJoiner()
+ // 0-arg SAM; join is vararg instance method on external class
+ Supplier<String> s = joiner::join
+ assert s.get() == ''
+ assert new StringJoiner().join('a', 'b', 'c') == 'a-b-c'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testVarargMethodReferenceInCurrentClass() {
+ assertScript shell, '''
+ @CompileStatic
+ class C {
+ String join(String... parts) { parts.join('-') }
+
+ Supplier<String> createRef() {
+ this::join
+ }
+ }
+
+ def result = new C().createRef().get()
+ assert result == ''
+ '''
+ }
+
+ @Test
+ void testInstanceBoundToStaticMethodReference() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Integer num = 42
+ ToIntFunction<String> f = num::parseInt
+ assert f.applyAsInt('123') == 123
+ assert f.applyAsInt('-7') == -7
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToByteFunctionFromMapGetBytePlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToByteFunc<K> { byte apply(K k) }
+
+ @CompileStatic
+ void test() {
+ Map<String, Byte> m = [a: (byte) 1, b: (byte) 127]
+ ToByteFunc<String> f = m::get
+ assert f.apply('a') == (byte) 1
+ assert f.apply('b') == (byte) 127
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToCharFunctionFromMapGetCharPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToCharFunc<K> { char apply(K k) }
+
+ @CompileStatic
+ void test() {
+ Map<String, Character> m = [x: (char) 'A', y: (char) 'Z']
+ ToCharFunc<String> f = m::get
+ assert f.apply('x') == 'A'
+ assert f.apply('y') == 'Z'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToFloatFunctionFromMapGetFloatPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToFloatFunc<K> { float apply(K k) }
+
+ @CompileStatic
+ void test() {
+ Map<String, Float> m = [pi: 3.14f, e: 2.71f]
+ ToFloatFunc<String> f = m::get
+ assert Math.abs(f.apply('pi') - 3.14f) < 1e-4f
+ assert Math.abs(f.apply('e') - 2.71f) < 1e-4f
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testToShortFunctionFromMapGetShortPlaceholder() {
+ assertScript shell, '''
+ @FunctionalInterface interface ToShortFunc<K> { short apply(K k) }
+
+ @CompileStatic
+ void test() {
+ Map<String, Short> m = [lo: (short) 0, hi: (short) 100]
+ ToShortFunc<String> f = m::get
+ assert f.apply('lo') == (short) 0
+ assert f.apply('hi') == (short) 100
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testTypeReferringInstanceVarargMethodReference() {
+ assertScript shell, '''
+ class Joiner {
+ String join(String... parts) { parts.join('-') }
+ }
+
+ @FunctionalInterface interface JoinerBiFunc { String apply(Joiner
j, String a, String b) }
+
+ @CompileStatic
+ void test() {
+ JoinerBiFunc f = Joiner::join
+ assert f.apply(new Joiner(), 'a', 'b') == 'a-b'
+ assert f.apply(new Joiner(), 'x', 'y') == 'x-y'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testVarargMethodRefNoAdapterWhenArrayParamCompatible() {
+ assertScript shell, '''
+ @FunctionalInterface interface StrFormatter { String fmt(String
format, Object[] args) }
+
+ @CompileStatic
+ void test() {
+ StrFormatter f = String::format
+ assert f.fmt('%s=%d', ['answer', 42] as Object[]) ==
'answer=42'
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testGenericTypeResolutionForMethodReference() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Function<List<String>, Integer> f = List::size
+ assert f.apply(['a', 'b', 'c']) == 3
+ assert f.apply([]) == 0
+ }
+
+ test()
+ '''
+ }
+
+ @Test
+ void testMethodRefReceiverTypeNarrowerThanSAMParam() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ ToIntFunction<String> f = String::length
+ assert f.applyAsInt('hello') == 5
+ assert f.applyAsInt('') == 0
+ }
+ test()
+ '''
+ }
+
+ @Test
+ void testInstanceBoundStaticVarargMethodRef() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ String fmt = "unused"
+ BiFunction<String, String, String> f = fmt::format
+ assert f.apply('Hello %s!', 'world') == 'Hello world!'
+ assert f.apply('Hi %s', 'Groovy') == 'Hi Groovy'
+ }
+ test()
+ '''
+ }
+
+ @Test
+ void testMethodRefAssignedToClosureType() {
+ assertScript shell, '''
+ @CompileStatic
+ void test() {
+ Closure<?> fn = String::length
+ assert fn.call('hello') == 5
+ assert fn.call('') == 0
+ }
+ test()
+ '''
+ }
+
+ @Nested
+ class NativeMethodReferenceBytecodeTest extends AbstractBytecodeTestCase {
+ @Test
+ void testClassReferringInstanceMethodUsesCaptureFreeInvokeDynamic() {
+ def bytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', '''
+ @CompileStatic
+ class C {
+ static ToIntFunction<String> create() {
+ String::length
+ }
+ }
+ ''')
+
+ assertUsesInvokeDynamicFactory(
+ bytecode,
+ 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/ToIntFunction;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'java/lang/String.length()I'
+ )
+ }
+
+ @Test
+ void testBoundInstanceMethodCapturesReceiverInFactoryDescriptor() {
+ def bytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', '''
+ @CompileStatic
+ class C {
+ static Supplier<String> create() {
+ String text = ' ok '
+ text::trim
+ }
+ }
+ ''')
+
+ assertUsesInvokeDynamicFactory(
+ bytecode,
+ 'INVOKEDYNAMIC
get(Ljava/lang/String;)Ljava/util/function/Supplier;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'java/lang/String.trim()Ljava/lang/String;'
+ )
+ assert !bytecode.hasSequence(['INVOKEDYNAMIC
get()Ljava/util/function/Supplier;'])
+ }
+
+ @Test
+ void testInstanceBoundToStaticMethodDropsSuperfluousReceiverCapture() {
+ def bytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', '''
+ @CompileStatic
+ class C {
+ static ToIntFunction<String> create() {
+ Integer number = 42
+ number::parseInt
+ }
+ }
+ ''')
+
+ assertUsesInvokeDynamicFactory(
+ bytecode,
+ 'INVOKEDYNAMIC applyAsInt()Ljava/util/function/ToIntFunction;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'java/lang/Integer.parseInt(Ljava/lang/String;)I'
+ )
+ assert !bytecode.hasSequence(['INVOKEDYNAMIC
applyAsInt(Ljava/lang/Integer;)Ljava/util/function/ToIntFunction;'])
+ }
+
+ @Test
+ void testClassReferringStaticMethodUsesDirectBootstrapTarget() {
+ def bytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', '''
+ @CompileStatic
+ class C {
+ static IntUnaryOperator create() {
+ Math::abs
+ }
+ }
+ ''')
+
+ assertUsesInvokeDynamicFactory(
+ bytecode,
+ 'INVOKEDYNAMIC
applyAsInt()Ljava/util/function/IntUnaryOperator;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'java/lang/Math.abs(I)I'
+ )
+ }
+
+ @Test
+ void
testConstructorReferenceUsesSyntheticFactoryMethodAsBootstrapTarget() {
+ def script = '''
+ @CompileStatic
+ class C {
+ static class Box {
+ final String value
+ Box(String value) {
+ this.value = value
+ }
+ }
+
+ static Function<String, Box> create() {
+ Box::new
+ }
+ }
+ '''
+
+ def helperBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'ctorRef$create$0', script)
+ assert helperBytecode.hasSequence([
+ 'private final static synthetic
ctorRef$create$0(Ljava/lang/String;)LC$Box;',
+ 'LDC LC$Box;.class',
+ 'INVOKEDYNAMIC
init(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Object;',
+ '"<init>"',
+ 'INVOKEDYNAMIC cast(Ljava/lang/Object;)LC$Box;',
+ 'ARETURN'
+ ])
+
+ def outerBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesInvokeDynamicFactory(
+ outerBytecode,
+ 'INVOKEDYNAMIC apply()Ljava/util/function/Function;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C.ctorRef$create$0(Ljava/lang/String;)LC$Box;'
+ )
+ }
+
+ @Test
+ void testArrayConstructorReferenceUsesSyntheticArrayFactoryMethod() {
+ def script = '''
+ @CompileStatic
+ class C {
+ static IntFunction<String[]> create() {
+ String[]::new
+ }
+ }
+ '''
+
+ def helperBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'ctorRef$create$0', script)
+ assert helperBytecode.hasSequence([
+ 'private final static synthetic
ctorRef$create$0(I)[Ljava/lang/String;',
+ 'ANEWARRAY java/lang/String',
+ 'ARETURN'
+ ])
+
+ def outerBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesInvokeDynamicFactory(
+ outerBytecode,
+ 'INVOKEDYNAMIC apply()Ljava/util/function/IntFunction;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C.ctorRef$create$0(I)[Ljava/lang/String;'
+ )
+ }
+
+ @Test
+ void
testSerializableMethodReferenceUsesAltMetafactoryAndDeserializeDispatcher() {
+ def script = '''
+ @CompileStatic
+ class C {
+ interface SerFunc<I, O> extends Serializable, Function<I,
O> {}
+
+ static SerFunc<String, String> create() {
+ String::trim
+ }
+ }
+ '''
+
+ def createBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesInvokeDynamicFactory(
+ createBytecode,
+ 'INVOKEDYNAMIC apply()LC$SerFunc;',
+ 'java/lang/invoke/LambdaMetafactory.altMetafactory',
+ 'java/lang/String.trim()Ljava/lang/String;'
+ )
+ assert createBytecode.hasSequence(['CHECKCAST
java/io/Serializable'])
+
+ def dispatcherBytecode = compileStaticBytecode(classNamePattern:
'C', method: '$deserializeLambda$', script)
+ assert dispatcherBytecode.hasSequence(['INVOKESTATIC
C.$deserializeLambda_methodref$'])
+ }
+
+ @Test
+ void testVarargMethodReferenceUsesSyntheticAdapterInsteadOfFallback() {
+ def script = '''
+ @CompileStatic
+ class C {
+ static Supplier<List> create() {
+ Arrays::asList
+ }
+ }
+ '''
+
+ def outerBytecode = compileStaticBytecode(classNamePattern: 'C',
method: 'create', script)
+ assertUsesInvokeDynamicFactory(
+ outerBytecode,
+ 'INVOKEDYNAMIC get()Ljava/util/function/Supplier;',
+ 'java/lang/invoke/LambdaMetafactory.metafactory',
+ 'C.adapt$Arrays$asList$'
+ )
+ }
+
+ private static void assertUsesInvokeDynamicFactory(final bytecode,
final String invokedynamicInstruction, final String bootstrapMethod, final
String implementationTarget) {
+ assert bytecode.hasSequence([
+ invokedynamicInstruction,
+ bootstrapMethod,
+ implementationTarget
+ ])
+ assertNoMethodPointerFallback(bytecode)
+ }
+
+ private static void assertNoMethodPointerFallback(final bytecode) {
+ assert !bytecode.hasSequence(['INVOKESTATIC
org/codehaus/groovy/runtime/ScriptBytecodeAdapter.getMethodPointer'])
+ }
+
+ 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.Arrays
+ import java.util.function.*
+ '''
+ }
}