This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch GROOVY-11993 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 751293791c94cc093a0b0159b3d7b73c8cc6d0d5 Author: Daniel Sun <[email protected]> AuthorDate: Tue May 5 02:33:46 2026 +0900 GROOVY-11993: Support serializable method reference --- .../asm/sc/AbstractFunctionalInterfaceWriter.java | 306 +++++++++++---- .../StaticTypesFunctionalInterfaceMetadataKey.java | 51 +++ .../classgen/asm/sc/StaticTypesLambdaAnalyzer.java | 9 +- .../classgen/asm/sc/StaticTypesLambdaWriter.java | 97 ++--- ...StaticTypesMethodReferenceExpressionWriter.java | 116 +++++- .../transform/stc/MethodReferenceTest.groovy | 411 +++++++++++++++++++++ 6 files changed, 839 insertions(+), 151 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..28f1e88969 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,45 +22,186 @@ 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.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.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.DESERIALIZE_LAMBDA_DISPATCHER; +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.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 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) + ); + if (serializable) { + methodVisitor.visitTypeInsn(CHECKCAST, BytecodeHelper.getClassInternalName(ClassHelper.SERIALIZABLE_TYPE)); + } + } + default String createMethodDescriptor(final MethodNode method) { return getMethodDescriptor(method.getReturnType(), method.getParameters()); } - default Handle createBootstrapMethod(final boolean isInterface, final boolean serializable) { + default ClassNode convertParameterType(final ClassNode targetType, final ClassNode parameterType, final ClassNode inferredType) { + if (!getWrapper(inferredType).isDerivedFrom(getWrapper(parameterType))) { + throw new RuntimeParserException("The inferred type[" + inferredType.redirect() + "] is not compatible with the parameter type[" + parameterType.redirect() + "]", parameterType); + } + + ClassNode type; + if (isPrimitiveType(parameterType)) { + if (!isPrimitiveType(inferredType)) { + // 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: class java.lang.Integer is not a subtype of int + type = getUnwrapper(inferredType).getPlainNodeReference(false); + } else { + type = inferredType.getPlainNodeReference(false); + } + } else if (isPrimitiveType(inferredType)) { + // GROOVY-9790: bootstrap method initialization exception raised when lambda parameter type is wrong + // (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) + // 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(); + } else { + type = inferredType.getPlainNodeReference(false); + } + } else { + type = inferredType; + // GROOVY-11304: no placeholders + if (hasUnresolvedGenerics(type)) type = type.redirect(); + // GROOVY-11479: mutable for node metadata or type annotations + if (type.toString(false).equals(parameterType.toString(false))) { + type = parameterType; + } else { + // TODO: deep copy if type args set + type = type.getPlainNodeReference(); + } + } + return type; + } + + 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) + ) + ); + } + + 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 + 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 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()) { + 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; @@ -89,71 +230,102 @@ public interface AbstractFunctionalInterfaceWriter { returnType = ClassHelper.VOID_TYPE; // GROOVY-10933 } - Object[] arguments = !serializable ? new Object[3] : new Object[]{null, null, null, 5, 0}; + return Type.getMethodType(getMethodDescriptor(returnType, implMethodParameters)); + } - arguments[0] = Type.getMethodType(abstractMethodDesc); + 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; + } - arguments[1] = new Handle( - insn, // H_INVOKESTATIC or H_INVOKEVIRTUAL or H_INVOKEINTERFACE (GROOVY-9853) - getClassInternalName(methodOwner.getName()), - methodNode.getName(), - getMethodDescriptor(methodNode), - methodOwner.isInterface()); + dispatcherGuards = new BlockStatement(); + BlockStatement dispatcher = new BlockStatement(); + dispatcher.addStatement(dispatcherGuards); + dispatcher.addStatement(createInvalidDeserializationStatement()); - arguments[2] = Type.getMethodType(getMethodDescriptor(returnType, parameters)); + enclosingClass.addSyntheticMethod( + "$deserializeLambda$", + Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, + OBJECT_TYPE, + deserializeMethodParameters, + ClassNode.EMPTY_ARRAY, + dispatcher); + enclosingClass.putNodeMetaData(DESERIALIZE_LAMBDA_DISPATCHER, dispatcherGuards); + return dispatcherGuards; + } - return arguments; + 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); } - default ClassNode convertParameterType(final ClassNode parameterType, final ClassNode inferredType) { - return convertParameterType(parameterType, parameterType, inferredType); + /** + * 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()) + ); } - default ClassNode convertParameterType(final ClassNode targetType, final ClassNode parameterType, final ClassNode inferredType) { - if (!getWrapper(inferredType).isDerivedFrom(getWrapper(parameterType))) { - throw new RuntimeParserException("The inferred type[" + inferredType.redirect() + "] is not compatible with the parameter type[" + parameterType.redirect() + "]", parameterType); - } + /** + * Combines predicates with logical + * + * @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")); + } - ClassNode type; - if (isPrimitiveType(parameterType)) { - if (!isPrimitiveType(inferredType)) { - // 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: class java.lang.Integer is not a subtype of int - type = getUnwrapper(inferredType).getPlainNodeReference(false); - } else { - type = inferredType.getPlainNodeReference(false); - } - } else if (isPrimitiveType(inferredType)) { - // GROOVY-9790: bootstrap method initialization exception raised when lambda parameter type is wrong - // (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) - // 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(); - } else { - type = inferredType.getPlainNodeReference(false); - } - } else { - type = inferredType; - // GROOVY-11304: no placeholders - if (hasUnresolvedGenerics(type)) type = type.redirect(); - // GROOVY-11479: mutable for node metadata or type annotations - if (type.toString(false).equals(parameterType.toString(false))) { - type = parameterType; - } else { - // TODO: deep copy if type args set - type = type.getPlainNodeReference(); - } - } - return type; + private Expression matchesSerializedFormInt(final Expression serializedForm, final String accessorName, final int expectedValue) { + return eqX(callX(serializedForm, accessorName), constX(expectedValue, true)); + } + + private Expression matchesSerializedFormString(final Expression serializedForm, final String accessorName, final String expectedValue) { + return eqX(callX(serializedForm, accessorName), constX(expectedValue)); + } + + default Parameter[] createDeserializeMethodParameters() { + return new Parameter[] { new Parameter(SERIALIZEDLAMBDA_TYPE, "serializedLambda") }; } - 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; + 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..7b2dc26e24 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesFunctionalInterfaceMetadataKey.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + 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..eca48e99e7 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; } @@ -394,10 +395,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..5c8d754327 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; @@ -46,22 +43,21 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +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.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.transform.stc.StaticTypesMarker.CLOSURE_ARGUMENTS; import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PARAMETER_TYPE; import static org.objectweb.asm.Opcodes.ACC_FINAL; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; import static org.objectweb.asm.Opcodes.ALOAD; @@ -107,10 +103,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; @@ -130,23 +122,32 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun return; } - addDeserializeLambdaMethodForLambdaExpression(expression, generatedLambda); - addDeserializeLambdaMethod(); + MethodNode helperMethod = addDeserializeLambdaMethodForLambdaExpression(expression, generatedLambda); + addDeserializeDispatcherEntry(controller, createDeserializeMethodParameters(), createSerializedLambdaFingerprint( + createMethodDescriptor(ClassHelper.findSAM(expression.getNodeMetaData(PARAMETER_TYPE))), + controller.getClassNode(), + generatedLambda.getImplMethodKind(), + generatedLambda.lambdaClass, + generatedLambda.lambdaMethod, + generatedLambda.lambdaMethod.getParameters(), + expression.getNodeMetaData(PARAMETER_TYPE), + ClassHelper.findSAM(expression.getNodeMetaData(PARAMETER_TYPE)), + 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 +157,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 +170,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 +180,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) { @@ -279,7 +280,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 +298,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 +328,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()) { @@ -387,13 +363,14 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun createDeserializeLambdaMethodName(generatedLambda.lambdaClass), ACC_PUBLIC | 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 +378,13 @@ 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 - } - 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..4ee73639d8 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 @@ -31,6 +31,8 @@ import org.codehaus.groovy.ast.expr.Expression; 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.BytecodeInstruction; +import org.codehaus.groovy.classgen.BytecodeSequence; import org.codehaus.groovy.classgen.AsmClassGenerator; import org.codehaus.groovy.classgen.asm.BytecodeHelper; import org.codehaus.groovy.classgen.asm.MethodReferenceExpressionWriter; @@ -40,7 +42,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; @@ -51,6 +53,7 @@ import java.util.Set; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.joining; +import static org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.METHOD_REFERENCE_DESERIALIZE_METHOD_NAME; 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.ctorX; @@ -60,6 +63,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; import static org.codehaus.groovy.ast.tools.GenericsUtils.extractPlaceholders; import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafe0; +import static org.codehaus.groovy.classgen.asm.BytecodeHelper.getClassInternalName; import static org.codehaus.groovy.ast.tools.ParameterUtils.isVargs; import static org.codehaus.groovy.ast.tools.ParameterUtils.parametersCompatible; import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last; @@ -68,6 +72,13 @@ import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.filter 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. @@ -180,29 +191,35 @@ public class StaticTypesMethodReferenceExpressionWriter extends MethodReferenceE } } - int referenceKind; + int implMethodKind; if (isConstructorReference || methodRefMethod.isStatic()) { - referenceKind = Opcodes.H_INVOKESTATIC; + 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(), + String samMethodName = abstractMethod.getName(); + String invokedTypeDescriptor = BytecodeHelper.getMethodDescriptor(functionalType.redirect(), isClassExpression ? Parameter.EMPTY_ARRAY : new Parameter[]{new Parameter(typeOrTargetRefType, "__METHODREF_EXPR_INSTANCE")}); + boolean serializable = functionalType.implementsInterface(ClassHelper.SERIALIZABLE_TYPE); + if (serializable) { + ensureDeserializeLambdaSupport(methodReferenceExpression, functionalType, abstractMethod, methodRefMethod, parametersWithExactType, + implMethodKind, !isClassExpression, typeOrTargetRefType, invokedTypeDescriptor); + } - Handle bootstrapMethod = createBootstrapMethod(classNode.isInterface(), false); - Object[] bootstrapArgs = createBootstrapMethodArguments( + writeFunctionalInterfaceIndy( + controller.getMethodVisitor(), + samMethodName, + invokedTypeDescriptor, createMethodDescriptor(abstractMethod), - referenceKind, + implMethodKind, methodRefMethod.getDeclaringClass(), methodRefMethod, parametersWithExactType, - false + serializable ); - controller.getMethodVisitor().visitInvokeDynamicInsn(methodName, methodDesc, bootstrapMethod, bootstrapArgs); if (isClassExpression) { controller.getOperandStack().push(functionalType); @@ -255,7 +272,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 +314,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 +363,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 +471,75 @@ public class StaticTypesMethodReferenceExpressionWriter extends MethodReferenceE throw new MultipleCompilationErrorsException(controller.getSourceUnit().getErrorCollector()); } + private void ensureDeserializeLambdaSupport(final MethodReferenceExpression methodReferenceExpression, final ClassNode functionalType, + final MethodNode abstractMethod, final MethodNode methodRefMethod, final Parameter[] parametersWithExactType, + final int implMethodKind, final boolean capturing, + final ClassNode capturedTargetType, final String invokedTypeDescriptor) { + String helperName = methodReferenceExpression.getNodeMetaData(METHOD_REFERENCE_DESERIALIZE_METHOD_NAME); + if (helperName == null) { + helperName = createDeserializeLambdaMethodName(); + methodReferenceExpression.putNodeMetaData(METHOD_REFERENCE_DESERIALIZE_METHOD_NAME, helperName); + } + + Parameter[] parameters = createDeserializeMethodParameters(); + if (controller.getClassNode().hasMethod(helperName, parameters)) { + return; + } + + String samMethodDescriptor = createMethodDescriptor(abstractMethod); + MethodNode helperMethod = addDeserializeLambdaMethodForMethodReference(helperName, abstractMethod, methodRefMethod, + parametersWithExactType, implMethodKind, capturing, capturedTargetType, invokedTypeDescriptor, samMethodDescriptor); + addDeserializeDispatcherEntry(controller, parameters, + createSerializedLambdaFingerprint(samMethodDescriptor, controller.getClassNode(), implMethodKind, + methodRefMethod.getDeclaringClass(), methodRefMethod, + parametersWithExactType, functionalType, abstractMethod, capturing ? 1 : 0), + helperMethod); + } + + 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) { diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index 3c6b5e1b90..76a78dcdba 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1158,6 +1158,142 @@ 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 + + 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 +1798,281 @@ 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 + + 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() {
