Extend bridge analyzer to bridges that target static methods

This extends the bridge analyzer to cover the main method of synthetic
lambda classes, which typically target static javac synthetic lambda$
methods (except when method handles are used).

Bug: b/309575527
Change-Id: Ic87992568c97b7a0e65eb4cbc512a6117d824ef2
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeAnalyzer.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeAnalyzer.java
index 780eff6..98b8c4b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeAnalyzer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeAnalyzer.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.utils.internal.BooleanUtils;
@@ -36,11 +35,15 @@
   /** Returns a {@link BridgeInfo} object describing this method if it is recognized as a bridge. */
   public static BridgeInfo analyzeMethod(AppView<?> appView, DexEncodedMethod method, IRCode code) {
     DexItemFactory factory = appView.dexItemFactory();
+    // The main method of lambda classes forward all arguments except the receiver (i.e., the lambda
+    // instance) to the synthetic javac lambda$ method using an invoke-static. If the receiver is
+    // unused, we require that argument i is passed as argument i-1 to the invoke-static for i>0.
+    int forwardArgumentOffset = method.isInstance() && code.getThis().isUnused() ? -1 : 0;
     // Scan through the instructions one-by-one. We expect a sequence of Argument instructions,
     // followed by a (possibly empty) sequence of CheckCast/Box/Unbox operations followed by a
     // single InvokeMethod instruction, followed by an optional CheckCast/Box/Unbox instruction,
     // followed by a Return instruction.
-    InvokeMethodWithReceiver uniqueInvoke = null;
+    InvokeMethod uniqueInvoke = null;
     // The unique cast, box, or unbox operation prior to the return instruction.
     Instruction uniqueReturnCheckCastBoxOrUnbox = null;
     InstructionListIterator instructionIterator = code.entryBlock().listIterator();
@@ -54,7 +57,8 @@
 
         case CHECK_CAST:
           {
-            if (!analyzeCheckCastBoxOrUnbox(method, instruction, uniqueInvoke)) {
+            if (!analyzeCheckCastBoxOrUnbox(
+                method, instruction, uniqueInvoke, forwardArgumentOffset)) {
               return failure();
             }
             // If we have moved past the single invoke instruction, then record that this cast is
@@ -73,7 +77,8 @@
           {
             InvokeMethod invoke = instruction.asInvokeMethod();
             if (isBoxingInvoke(invoke, factory) || isUnboxingInvoke(invoke, factory)) {
-              if (!analyzeCheckCastBoxOrUnbox(method, instruction, uniqueInvoke)) {
+              if (!analyzeCheckCastBoxOrUnbox(
+                  method, instruction, uniqueInvoke, forwardArgumentOffset)) {
                 return failure();
               }
               // If we have moved past the single invoke instruction, then record that this boxing
@@ -85,8 +90,6 @@
                 uniqueReturnCheckCastBoxOrUnbox = invoke;
               }
               break;
-            } else if (invoke.isInvokeStatic()) {
-              return failure();
             } else {
               // Intentionally fall through.
             }
@@ -99,8 +102,8 @@
             if (uniqueInvoke != null) {
               return failure();
             }
-            InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
-            if (!analyzeInvoke(invoke, factory)) {
+            InvokeMethod invoke = instruction.asInvokeMethod();
+            if (!analyzeInvoke(invoke, factory, forwardArgumentOffset)) {
               return failure();
             }
             // Record that we have seen the single invoke instruction.
@@ -140,14 +143,29 @@
 
     assert uniqueInvoke != null;
     assert uniqueInvoke.isInvokeDirect()
+        || uniqueInvoke.isInvokeStatic()
         || uniqueInvoke.isInvokeSuper()
         || uniqueInvoke.isInvokeVirtual();
     switch (uniqueInvoke.getType()) {
       case DIRECT:
+        if (forwardArgumentOffset != 0) {
+          return null;
+        }
         return new DirectBridgeInfo(uniqueInvoke.getInvokedMethod());
+      case STATIC:
+        return forwardArgumentOffset == 0
+            ? new StaticBridgeInfo(uniqueInvoke.getInvokedMethod())
+            : new StaticBridgeExcludingReceiverInfo(
+                uniqueInvoke.getInvokedMethod(), uniqueInvoke.getInterfaceBit());
       case SUPER:
+        if (forwardArgumentOffset != 0) {
+          return null;
+        }
         return new SuperBridgeInfo(uniqueInvoke.getInvokedMethod());
       case VIRTUAL:
+        if (forwardArgumentOffset != 0) {
+          return null;
+        }
         return new VirtualBridgeInfo(uniqueInvoke.getInvokedMethod());
       default:
         throw new Unreachable();
@@ -155,14 +173,18 @@
   }
 
   private static boolean analyzeCheckCastBoxOrUnbox(
-      DexEncodedMethod method, Instruction checkCastBoxOrUnbox, InvokeMethod invoke) {
+      DexEncodedMethod method,
+      Instruction checkCastBoxOrUnbox,
+      InvokeMethod invoke,
+      int forwardArgumentOffset) {
     return invoke == null
-        ? analyzeCheckCastBoxOrUnboxBeforeInvoke(checkCastBoxOrUnbox)
+        ? analyzeCheckCastBoxOrUnboxBeforeInvoke(checkCastBoxOrUnbox, forwardArgumentOffset)
         : analyzeCheckCastBoxOrUnboxAfterInvoke(method, checkCastBoxOrUnbox, invoke);
   }
 
   @SuppressWarnings("ReferenceEquality")
-  private static boolean analyzeCheckCastBoxOrUnboxBeforeInvoke(Instruction checkCastBoxOrUnbox) {
+  private static boolean analyzeCheckCastBoxOrUnboxBeforeInvoke(
+      Instruction checkCastBoxOrUnbox, int forwardArgumentOffset) {
     Value object = checkCastBoxOrUnbox.getFirstOperand().getAliasedValue();
     // It must be processing one of the arguments.
     if (!object.isArgument()) {
@@ -185,28 +207,28 @@
     if (invoke == null) {
       return false;
     }
-    if (invoke.arguments().size() <= argumentIndex) {
+    // The cast value must be used in the same argument position, unless the `this` value is unused.
+    int expectedForwardArgumentIndex = argumentIndex + forwardArgumentOffset;
+    if (invoke.arguments().size() <= expectedForwardArgumentIndex
+        || invoke.getArgument(expectedForwardArgumentIndex) != outValue) {
       return false;
     }
-    // The cast value must be used in the same argument position.
-    if (invoke.getArgument(argumentIndex) != outValue) {
-      return false;
-    }
-    int parameterIndex = argumentIndex - BooleanUtils.intValue(invoke.isInvokeMethodWithReceiver());
+    int forwardParameterIndex =
+        expectedForwardArgumentIndex - BooleanUtils.intValue(invoke.isInvokeMethodWithReceiver());
     // It is not allowed to cast the receiver.
-    if (parameterIndex == -1) {
+    if (forwardParameterIndex == -1) {
       return false;
     }
     // The type of the cast must match the type of the parameter.
     if (checkCastBoxOrUnbox.isCheckCast()) {
       if (checkCastBoxOrUnbox.asCheckCast().getType()
-          != invoke.getInvokedMethod().getParameter(parameterIndex)) {
+          != invoke.getInvokedMethod().getParameter(forwardParameterIndex)) {
         return false;
       }
     } else {
       InvokeMethod boxOrUnbox = checkCastBoxOrUnbox.asInvokeMethod();
       if (boxOrUnbox.getInvokedMethod().getReturnType()
-          != invoke.getInvokedMethod().getParameter(parameterIndex)) {
+          != invoke.getInvokedMethod().getParameter(forwardParameterIndex)) {
         return false;
       }
     }
@@ -241,17 +263,20 @@
         && castValue.singleUniqueUser().isReturn();
   }
 
-  private static boolean analyzeInvoke(InvokeMethodWithReceiver invoke, DexItemFactory factory) {
+  private static boolean analyzeInvoke(
+      InvokeMethod invoke, DexItemFactory factory, int forwardArgumentOffset) {
     // All of the forwarded arguments of the enclosing method must be in the same argument position.
     for (int argumentIndex = 0; argumentIndex < invoke.arguments().size(); argumentIndex++) {
       Value argument = invoke.getArgument(argumentIndex).getAliasedValue();
       if (argument.isPhi()) {
         return false;
-      } else if (argument.isArgument()
-          && argumentIndex != argument.getDefinition().asArgument().getIndex()) {
-        return false;
+      } else if (argument.isArgument()) {
+        int incomingArgumentIndex = argument.getDefinition().asArgument().getIndex();
+        if (argumentIndex != incomingArgumentIndex + forwardArgumentOffset) {
+          return false;
+        }
       } else if (isCheckCastBoxOrUnbox(argument.getDefinition(), factory)) {
-        int expectedArgumentIndex =
+        int incomingArgumentIndex =
             argument
                 .getDefinition()
                 .getFirstOperand()
@@ -259,7 +284,7 @@
                 .getDefinition()
                 .asArgument()
                 .getIndex();
-        if (argumentIndex != expectedArgumentIndex) {
+        if (argumentIndex != incomingArgumentIndex + forwardArgumentOffset) {
           return false;
         }
       }
@@ -272,11 +297,12 @@
                   .getAliasedValue(new BridgeAnalyzerAliasedValueConfiguration(factory))
                   .isArgument()
               && invoke
-                      .getArgument(argumentIndex)
-                      .getAliasedValue(new BridgeAnalyzerAliasedValueConfiguration(factory))
-                      .getDefinition()
-                      .asArgument()
-                      .getIndex()
+                          .getArgument(argumentIndex)
+                          .getAliasedValue(new BridgeAnalyzerAliasedValueConfiguration(factory))
+                          .getDefinition()
+                          .asArgument()
+                          .getIndex()
+                      + forwardArgumentOffset
                   == argumentIndex);
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
index fdb5092..7489c43 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
@@ -3,12 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.info.bridge;
 
+import com.android.tools.r8.graph.DexMethod;
+
 /**
  * A piece of optimization info that is computed for bridge methods. The info stores details about
  * the behavior of the bridge.
  */
 public abstract class BridgeInfo {
 
+  // The targeted method.
+  private final DexMethod invokedMethod;
+
+  public BridgeInfo(DexMethod invokedMethod) {
+    this.invokedMethod = invokedMethod;
+  }
+
   public boolean isDirectBridgeInfo() {
     return false;
   }
@@ -17,6 +26,26 @@
     return null;
   }
 
+  public boolean isStaticBridgeInfo() {
+    return false;
+  }
+
+  public StaticBridgeInfo asStaticBridgeInfo() {
+    return null;
+  }
+
+  public boolean isStaticBridgeExcludingReceiverInfo() {
+    return false;
+  }
+
+  public StaticBridgeExcludingReceiverInfo asStaticBridgeExcludingReceiverInfo() {
+    return null;
+  }
+
+  public DexMethod getInvokedMethod() {
+    return invokedMethod;
+  }
+
   public boolean isSuperBridgeInfo() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/DirectBridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/DirectBridgeInfo.java
index 3579edf..b704d35 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/DirectBridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/DirectBridgeInfo.java
@@ -26,15 +26,8 @@
  */
 public class DirectBridgeInfo extends BridgeInfo {
 
-  // The targeted method.
-  private final DexMethod invokedMethod;
-
   public DirectBridgeInfo(DexMethod invokedMethod) {
-    this.invokedMethod = invokedMethod;
-  }
-
-  public DexMethod getInvokedMethod() {
-    return invokedMethod;
+    super(invokedMethod);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeExcludingReceiverInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeExcludingReceiverInfo.java
new file mode 100644
index 0000000..af71dc8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeExcludingReceiverInfo.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2026, 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.ir.optimize.info.bridge;
+
+import com.android.tools.r8.graph.DexMethod;
+
+/**
+ * Optimization info computed for non-static bridge methods that use an invoke-static instruction,
+ * where the `this` value is not forwarded.
+ *
+ * <p>This typically occurs in D8/R8 synthesized lambda classes that call javac synthetic lambda
+ * methods.
+ */
+public class StaticBridgeExcludingReceiverInfo extends BridgeInfo {
+
+  private final boolean isInterface;
+
+  public StaticBridgeExcludingReceiverInfo(DexMethod invokedMethod, boolean isInterface) {
+    super(invokedMethod);
+    this.isInterface = isInterface;
+  }
+
+  public boolean getInterfaceBit() {
+    return isInterface;
+  }
+
+  @Override
+  public boolean isStaticBridgeExcludingReceiverInfo() {
+    return true;
+  }
+
+  @Override
+  public StaticBridgeExcludingReceiverInfo asStaticBridgeExcludingReceiverInfo() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeInfo.java
new file mode 100644
index 0000000..58c2c90
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/StaticBridgeInfo.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2026, 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.ir.optimize.info.bridge;
+
+import com.android.tools.r8.graph.DexMethod;
+
+/** Optimization info computed for bridge methods that use an invoke-static instruction. */
+public class StaticBridgeInfo extends BridgeInfo {
+
+  public StaticBridgeInfo(DexMethod invokedMethod) {
+    super(invokedMethod);
+  }
+
+  @Override
+  public boolean isStaticBridgeInfo() {
+    return true;
+  }
+
+  @Override
+  public StaticBridgeInfo asStaticBridgeInfo() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/SuperBridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/SuperBridgeInfo.java
index 54b74f4..ad30beb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/SuperBridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/SuperBridgeInfo.java
@@ -26,15 +26,8 @@
  */
 public class SuperBridgeInfo extends BridgeInfo {
 
-  // The targeted method.
-  private final DexMethod invokedMethod;
-
   public SuperBridgeInfo(DexMethod invokedMethod) {
-    this.invokedMethod = invokedMethod;
-  }
-
-  public DexMethod getInvokedMethod() {
-    return invokedMethod;
+    super(invokedMethod);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
index a07abdc..0697ce6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
@@ -26,15 +26,8 @@
  */
 public class VirtualBridgeInfo extends BridgeInfo {
 
-  // The targeted method.
-  private final DexMethod invokedMethod;
-
   public VirtualBridgeInfo(DexMethod invokedMethod) {
-    this.invokedMethod = invokedMethod;
-  }
-
-  public DexMethod getInvokedMethod() {
-    return invokedMethod;
+    super(invokedMethod);
   }
 
   @Override