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"));
   }