Fix BootstrapMethodError from invalid class merging
Bug: b/397737234
Change-Id: Icacf51f06017c50b3448610e8493a225c225c40d
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 1d8da6e..14b0866 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -34,6 +34,7 @@
import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
+import com.android.tools.r8.horizontalclassmerging.policies.NoMethodHandleFromLambda;
import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
import com.android.tools.r8.horizontalclassmerging.policies.NoRecords;
import com.android.tools.r8.horizontalclassmerging.policies.NoResourceClasses;
@@ -138,6 +139,9 @@
new NoCheckDiscard(appView),
new NoKeepRules(appView),
new NoClassInitializerWithObservableSideEffects());
+ if (appView.hasLiveness() && appView.options().isGeneratingClassFiles()) {
+ builder.add(new NoMethodHandleFromLambda(appView.withLiveness()));
+ }
}
private static void addSingleClassPoliciesForMergingNonSyntheticClasses(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoMethodHandleFromLambda.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoMethodHandleFromLambda.java
new file mode 100644
index 0000000..3d6ae08
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoMethodHandleFromLambda.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+import com.android.tools.r8.utils.TraversalContinuation;
+
+/**
+ * When the app contains a method handle such as T::new, then we need to treat T.<init> as pinned.
+ * If we merge T with another class, we could end up changing the signature of T.<init>, which
+ * breaks the T::new method handle.
+ *
+ * <p>During tree shaking, in {@link com.android.tools.r8.shaking.Enqueuer#traceCallSite}, we
+ * disable closed world reasoning for methods that are referenced from a lambda method handle.
+ *
+ * <p>We fix this issue when compiling to class files by disallowing class merging of classes that
+ * have a method where closed world reasoning is disallowed.
+ */
+public class NoMethodHandleFromLambda extends SingleClassPolicy {
+
+ private final AppView<AppInfoWithLiveness> appView;
+
+ public NoMethodHandleFromLambda(AppView<AppInfoWithLiveness> appView) {
+ assert appView.options().isGeneratingClassFiles();
+ this.appView = appView;
+ }
+
+ @Override
+ public boolean canMerge(DexProgramClass clazz) {
+ return clazz
+ .traverseProgramMethods(
+ method -> {
+ KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+ return TraversalContinuation.continueIf(
+ keepInfo.isClosedWorldReasoningAllowed(appView.options()));
+ })
+ .shouldContinue();
+ }
+
+ @Override
+ public String getName() {
+ return "NoConstructorMethodHandleFromLambda";
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index a5293a9..8cee863 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1295,7 +1295,9 @@
traceInvokeDirectFromLambda(method, context, registry);
break;
case INVOKE_CONSTRUCTOR:
+ assert appView.dexItemFactory().isConstructor(method);
traceNewInstanceFromLambda(method.holder, context);
+ traceInvokeDirectFromLambda(method, context, registry);
break;
default:
throw new Unreachable();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithLambdaAllocationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithLambdaAllocationTest.java
index 3bf22e7..77c48954 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithLambdaAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/DefaultFieldValueJoinerWithLambdaAllocationTest.java
@@ -44,7 +44,7 @@
.run(parameters.getRuntime(), Main.class)
.applyIf(
parameters.isCfRuntime(),
- rr -> rr.assertFailureWithErrorThatThrows(BootstrapMethodError.class),
+ rr -> rr.assertSuccessWithOutputLines("1", "1", "2", "2"),
rr -> rr.assertSuccessWithOutputLines("0", "1", "0", "2"));
}