Removal of companion class instantiation after method staticizing

Change-Id: I82efe364703c5df76ae351c43d2a6e8db23b777a
diff --git a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
index 97839cf..5d40177 100644
--- a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
+++ b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
@@ -4,6 +4,11 @@
 
 package com.android.tools.r8.graph.bytecodemetadata;
 
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
+import java.util.Collections;
+import java.util.Set;
+
 /**
  * A piece of information that can be attached to instructions in {@link
  * com.android.tools.r8.graph.CfCode} and {@link com.android.tools.r8.graph.DexCode}.
@@ -11,6 +16,16 @@
 public class BytecodeInstructionMetadata {
 
   /**
+   * Set for field-get instructions that are only used to invoke one or more instance methods.
+   *
+   * <p>This is used to skip field-get instructions that will be pruned as a result of method
+   * staticizing in the {@link TrivialFieldAccessReprocessor}. A common use case for this is Kotlin
+   * companion fields, which will generally not be read after the methods of a given companion class
+   * have been staticized.
+   */
+  private final Set<DexMethod> isReadForInvokeReceiver;
+
+  /**
    * Set for instance and static field read instructions which are only used to write the same
    * field.
    *
@@ -19,7 +34,9 @@
    */
   private final boolean isReadForWrite;
 
-  BytecodeInstructionMetadata(boolean isReadForWrite) {
+  private BytecodeInstructionMetadata(
+      Set<DexMethod> isReadForInvokeReceiver, boolean isReadForWrite) {
+    this.isReadForInvokeReceiver = isReadForInvokeReceiver;
     this.isReadForWrite = isReadForWrite;
   }
 
@@ -31,16 +48,32 @@
     return null;
   }
 
+  public boolean isReadForInvokeReceiver() {
+    return isReadForInvokeReceiver != null;
+  }
+
+  public Set<DexMethod> getReadForInvokeReceiver() {
+    return isReadForInvokeReceiver;
+  }
+
   public boolean isReadForWrite() {
     return isReadForWrite;
   }
 
   public static class Builder {
 
+    private Set<DexMethod> isReadForInvokeReceiver = Collections.emptySet();
     private boolean isReadForWrite;
 
     private boolean isEmpty() {
-      return !isReadForWrite;
+      return isReadForInvokeReceiver.isEmpty() && !isReadForWrite;
+    }
+
+    public Builder setIsReadForInvokeReceiver(Set<DexMethod> isReadForInvokeReceiver) {
+      assert isReadForInvokeReceiver != null;
+      assert !isReadForInvokeReceiver.isEmpty();
+      this.isReadForInvokeReceiver = isReadForInvokeReceiver;
+      return this;
     }
 
     public Builder setIsReadForWrite() {
@@ -50,7 +83,7 @@
 
     public BytecodeInstructionMetadata build() {
       assert !isEmpty();
-      return new BytecodeInstructionMetadata(isReadForWrite);
+      return new BytecodeInstructionMetadata(isReadForInvokeReceiver, isReadForWrite);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
index e5a1aec..3ebd37a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
@@ -28,6 +28,7 @@
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final FieldAssignmentTracker fieldAssignmentTracker;
   private final FieldBitAccessAnalysis fieldBitAccessAnalysis;
+  private final FieldReadForInvokeReceiverAnalysis fieldReadForInvokeReceiverAnalysis;
   private final FieldReadForWriteAnalysis fieldReadForWriteAnalysis;
 
   public FieldAccessAnalysis(AppView<AppInfoWithLiveness> appView) {
@@ -36,6 +37,7 @@
     this.fieldBitAccessAnalysis =
         options.enableFieldBitAccessAnalysis ? new FieldBitAccessAnalysis() : null;
     this.fieldAssignmentTracker = new FieldAssignmentTracker(appView);
+    this.fieldReadForInvokeReceiverAnalysis = new FieldReadForInvokeReceiverAnalysis(appView);
     this.fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
   }
 
@@ -44,10 +46,12 @@
       AppView<? extends AppInfoWithClassHierarchy> appView,
       FieldAssignmentTracker fieldAssignmentTracker,
       FieldBitAccessAnalysis fieldBitAccessAnalysis,
+      FieldReadForInvokeReceiverAnalysis fieldReadForInvokeReceiverAnalysis,
       FieldReadForWriteAnalysis fieldReadForWriteAnalysis) {
     this.appView = appView;
     this.fieldAssignmentTracker = fieldAssignmentTracker;
     this.fieldBitAccessAnalysis = fieldBitAccessAnalysis;
+    this.fieldReadForInvokeReceiverAnalysis = fieldReadForInvokeReceiverAnalysis;
     this.fieldReadForWriteAnalysis = fieldReadForWriteAnalysis;
   }
 
@@ -91,6 +95,10 @@
               fieldBitAccessAnalysis.recordFieldAccess(
                   fieldInstruction, field.getDefinition(), feedback);
             }
+            if (fieldReadForInvokeReceiverAnalysis != null) {
+              fieldReadForInvokeReceiverAnalysis.recordFieldAccess(
+                  fieldInstruction, field, bytecodeMetadataProviderBuilder, code.context());
+            }
             if (fieldReadForWriteAnalysis != null) {
               fieldReadForWriteAnalysis.recordFieldAccess(
                   fieldInstruction, field, bytecodeMetadataProviderBuilder);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
new file mode 100644
index 0000000..b2aded5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2022, 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.analysis.fieldaccess;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class FieldReadForInvokeReceiverAnalysis {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  FieldReadForInvokeReceiverAnalysis(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public void recordFieldAccess(
+      FieldInstruction instruction,
+      ProgramField field,
+      BytecodeMetadataProvider.Builder bytecodeMetadataProviderBuilder,
+      ProgramMethod context) {
+    if (!instruction.isStaticGet()) {
+      return;
+    }
+
+    StaticGet staticGet = instruction.asStaticGet();
+    Set<DexMethod> methods = getMethods(staticGet.outValue(), context);
+    if (methods == null || methods.isEmpty()) {
+      return;
+    }
+
+    bytecodeMetadataProviderBuilder.addMetadata(
+        instruction, builder -> builder.setIsReadForInvokeReceiver(methods));
+  }
+
+  private Set<DexMethod> getMethods(Value value, ProgramMethod context) {
+    WorkList<Instruction> users = WorkList.newIdentityWorkList();
+    if (!enqueueUsersForAnalysis(value, users)) {
+      return null;
+    }
+    Set<DexMethod> methods = Sets.newIdentityHashSet();
+    while (users.hasNext()) {
+      Instruction user = users.next();
+      if (user.isAssume()) {
+        if (enqueueUsersForAnalysis(user.outValue(), users)) {
+          // OK.
+          continue;
+        }
+      } else if (user.isInvokeMethodWithReceiver()) {
+        InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+        for (int argumentIndex = 1; argumentIndex < invoke.arguments().size(); argumentIndex++) {
+          if (invoke.getArgument(argumentIndex).getAliasedValue() == value) {
+            return null;
+          }
+        }
+        assert invoke.getReceiver().getAliasedValue() == value;
+
+        ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
+        if (singleTarget == null) {
+          return null;
+        }
+
+        methods.add(singleTarget.getReference());
+      }
+      return null;
+    }
+    return methods;
+  }
+
+  private boolean enqueueUsersForAnalysis(Value value, WorkList<Instruction> users) {
+    if (value.hasDebugUsers() || value.hasPhiUsers()) {
+      return false;
+    }
+    users.addIfNotSeen(value.uniqueUsers());
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index f392dec..7f36c0c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.ir.analysis.fieldaccess;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.code.CfOrDexInstanceFieldRead;
 import com.android.tools.r8.code.CfOrDexStaticFieldRead;
@@ -53,6 +55,8 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final PostMethodProcessor.Builder postMethodProcessorBuilder;
 
+  private final Map<DexEncodedField, ProgramMethodSet> dependencies = new ConcurrentHashMap<>();
+
   /** Updated concurrently from {@link #processClass(DexProgramClass)}. */
   private final Map<DexEncodedField, AbstractAccessContexts> readFields = new ConcurrentHashMap<>();
 
@@ -233,6 +237,7 @@
           fieldAccessInfoCollection.get(field.getReference()).asMutable().clearReads();
           methodsToReprocess.addAll(
               contexts.asConcrete().getAccessesWithContexts().values().iterator().next());
+          methodsToReprocess.addAll(dependencies.getOrDefault(field, ProgramMethodSet.empty()));
         });
   }
 
@@ -250,6 +255,7 @@
           assert !writtenFields.containsKey(field);
           methodsToReprocess.addAll(
               contexts.asConcrete().getAccessesWithContexts().values().iterator().next());
+          methodsToReprocess.addAll(dependencies.getOrDefault(field, ProgramMethodSet.empty()));
         });
   }
 
@@ -326,11 +332,20 @@
         return;
       }
 
-      if (metadata != null && metadata.isReadForWrite()) {
-        // Ignore this read. If the field ends up only being written, then we will still reprocess
-        // the method with the read-for-write instruction, since the method contains a write that
-        // requires reprocessing.
-        return;
+      if (metadata != null) {
+        if (isUnusedReadAfterMethodStaticizing(metadata)) {
+          // Ignore this read.
+          dependencies
+              .computeIfAbsent(field.getDefinition(), ignoreKey(ProgramMethodSet::createConcurrent))
+              .add(getContext());
+          return;
+        }
+        if (metadata.isReadForWrite()) {
+          // Ignore this read. If the field ends up only being written, then we will still reprocess
+          // the method with the read-for-write instruction, since the method contains a write that
+          // requires reprocessing.
+          return;
+        }
       }
 
       // Record access.
@@ -352,6 +367,28 @@
       }
     }
 
+    private boolean isUnusedReadAfterMethodStaticizing(BytecodeInstructionMetadata metadata) {
+      if (!metadata.isReadForInvokeReceiver()) {
+        return false;
+      }
+      Set<DexMethod> readForInvokeReceiver = metadata.getReadForInvokeReceiver();
+      for (DexMethod methodReference : readForInvokeReceiver) {
+        DexMethod rewrittenMethodReference =
+            appView.graphLens().getRenamedMethodSignature(methodReference, appView.codeLens());
+        DexProgramClass holder =
+            asProgramClassOrNull(appView.definitionFor(rewrittenMethodReference.getHolderType()));
+        ProgramMethod method = rewrittenMethodReference.lookupOnProgramClass(holder);
+        if (method == null) {
+          assert false;
+          return false;
+        }
+        if (!method.getDefinition().isStatic()) {
+          return false;
+        }
+      }
+      return true;
+    }
+
     private void recordAccessThatCannotBeOptimized(
         DexClassAndField field, DexEncodedField definition) {
       constantFields.remove(definition);
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
index 82bc605..2e3512d 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
@@ -86,7 +86,7 @@
     OptimizationFeedbackMock feedback = new OptimizationFeedbackMock();
     FieldBitAccessAnalysis fieldBitAccessAnalysis = new FieldBitAccessAnalysis();
     FieldAccessAnalysis fieldAccessAnalysis =
-        new FieldAccessAnalysis(appView, null, fieldBitAccessAnalysis, null);
+        new FieldAccessAnalysis(appView, null, fieldBitAccessAnalysis, null, null);
 
     DexProgramClass clazz = appView.appInfo().classes().iterator().next();
     assertEquals(TestClass.class.getTypeName(), clazz.type.toSourceString());
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
new file mode 100644
index 0000000..f5fb4f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2022, 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.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CompanionConstructorShakingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(options -> options.enableClassStaticizer = false)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject hostClassSubject = inspector.clazz(Host.class);
+              assertThat(hostClassSubject, isAbsent());
+
+              ClassSubject companionClassSubject = inspector.clazz(Host.Companion.class);
+              assertThat(companionClassSubject, isPresent());
+              assertEquals(1, companionClassSubject.allMethods().size());
+
+              MethodSubject greetMethodSubject =
+                  companionClassSubject.uniqueMethodWithName("greet");
+              assertThat(greetMethodSubject, isStatic());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Host.companion.greet();
+    }
+  }
+
+  static class Host {
+
+    static final Companion companion = new Companion();
+
+    @NeverClassInline
+    @NoHorizontalClassMerging
+    static class Companion {
+
+      @NeverInline
+      void greet() {
+        System.out.println("Hello world!");
+      }
+    }
+  }
+}