Merge "Add DupDupDupPeephole for replacing 3 x dup with dup + dup2"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 3ac587e..b5eafcd 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -723,9 +723,6 @@
 
     internal.enableInheritanceClassInDexDistributor = isOptimizeMultidexForLinearAlloc();
 
-    // TODO(b/117916645): Re-enable horizontal class merging by deleting this line.
-    internal.enableHorizontalClassMerging = false;
-
     return internal;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 9ceaf09..10007ca 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -431,6 +431,24 @@
     return type.isExternalizable(appInfo);
   }
 
+  public boolean classInitializationMayHaveSideEffects(AppInfo appInfo) {
+    if (hasNonTrivialClassInitializer()) {
+      return true;
+    }
+    if (defaultValuesForStaticFieldsMayTriggerAllocation()) {
+      return true;
+    }
+    for (DexType iface : interfaces.values) {
+      if (iface.classInitializationMayHaveSideEffects(appInfo)) {
+        return true;
+      }
+    }
+    if (superType != null && superType.classInitializationMayHaveSideEffects(appInfo)) {
+      return true;
+    }
+    return false;
+  }
+
   public boolean defaultValuesForStaticFieldsMayTriggerAllocation() {
     return Arrays.stream(staticFields())
         .anyMatch(field -> !field.getStaticValue().mayTriggerAllocation());
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 5e08b64..f49115a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -108,6 +108,11 @@
     return implementedInterfaces(appInfo).contains(appInfo.dexItemFactory.serializableType);
   }
 
+  public boolean classInitializationMayHaveSideEffects(AppInfo appInfo) {
+    DexClass clazz = appInfo.definitionFor(this);
+    return clazz == null || clazz.classInitializationMayHaveSideEffects(appInfo);
+  }
+
   public boolean isUnknown() {
     return hierarchyLevel == UNKNOWN_LEVEL;
   }
@@ -187,6 +192,10 @@
     return self == other;
   }
 
+  public Set<DexType> allImmediateSubtypes() {
+    return directSubtypes;
+  }
+
   /**
    * Apply the given function to all classes that directly extend this class.
    * <p>
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 82e3bff..0f6dbd7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -491,16 +491,18 @@
     if (!reportedMissing.add(missing)) {
       return;
     }
+    DexMethod originalReferencedFrom =
+        converter.graphLense().getOriginalMethodSignature(referencedFrom);
     StringBuilder builder = new StringBuilder();
     builder
         .append("Type `")
         .append(missing.toSourceString())
         .append("` was not found, ")
         .append("it is required for default or static interface methods desugaring of `")
-        .append(referencedFrom.toSourceString())
+        .append(originalReferencedFrom.toSourceString())
         .append("`");
     options.reporter.warning(
-        new StringDiagnostic(builder.toString(), getMethodOrigin(referencedFrom)));
+        new StringDiagnostic(builder.toString(), getMethodOrigin(originalReferencedFrom)));
   }
 
   private Origin getMethodOrigin(DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 479c3e4..ed92f64 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -128,31 +128,14 @@
     if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
       return true;
     }
-    return classInitializationHasNoSideffects(targetHolder);
-  }
-
-  /**
-   * Check for class initializer side effects when loading this class, as inlining might remove the
-   * load operation.
-   * <p>
-   * See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
-   * <p>
-   * For simplicity, we are conservative and consider all interfaces, not only the ones with default
-   * methods.
-   */
-  private boolean classInitializationHasNoSideffects(DexType classToCheck) {
-    DexClass clazz = inliner.appView.appInfo().definitionFor(classToCheck);
-    if ((clazz == null)
-        || clazz.hasNonTrivialClassInitializer()
-        || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
-      return false;
-    }
-    for (DexType iface : clazz.interfaces.values) {
-      if (!classInitializationHasNoSideffects(iface)) {
-        return false;
-      }
-    }
-    return clazz.superType == null || classInitializationHasNoSideffects(clazz.superType);
+    // Check for class initializer side effects when loading this class, as inlining might remove
+    // the load operation.
+    //
+    // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
+    //
+    // For simplicity, we are conservative and consider all interfaces, not only the ones with
+    // default methods.
+    return !clazz.classInitializationMayHaveSideEffects(appView.appInfo());
   }
 
   private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index 906451e..1cb5d93 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -90,9 +90,6 @@
     if (appView.appInfo().neverMerge.contains(clazz.type)) {
       return false;
     }
-    if (clazz.accessFlags.isInterface()) {
-      return false;
-    }
     if (clazz.staticFields().length + clazz.directMethods().length + clazz.virtualMethods().length
         == 0) {
       return false;
@@ -121,6 +118,15 @@
                     || appView.appInfo().noSideEffects.keySet().contains(method))) {
       return false;
     }
+    if (clazz.classInitializationMayHaveSideEffects(appView.appInfo())) {
+      // This could have a negative impact on inlining.
+      //
+      // See {@link com.android.tools.r8.ir.optimize.DefaultInliningOracle#canInlineStaticInvoke}
+      // for further details.
+      //
+      // Note that this will be true for all classes that inherit from or implement a library class.
+      return false;
+    }
     return true;
   }
 
@@ -266,7 +272,9 @@
 
       DexMethod originalMethod =
           methodMapping.inverse().getOrDefault(sourceMethod.method, sourceMethod.method);
-      methodMapping.put(originalMethod, sourceMethodAfterMove.method);
+      methodMapping.forcePut(originalMethod, sourceMethodAfterMove.method);
+
+      existingMethods.add(equivalence.wrap(sourceMethodAfterMove.method));
     }
 
     return result;
@@ -297,7 +305,9 @@
 
       DexField originalField =
           fieldMapping.inverse().getOrDefault(sourceField.field, sourceField.field);
-      fieldMapping.put(originalField, sourceFieldAfterMove.field);
+      fieldMapping.forcePut(originalField, sourceFieldAfterMove.field);
+
+      existingFields.add(equivalence.wrap(sourceFieldAfterMove.field));
     }
 
     return result;
diff --git a/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
new file mode 100644
index 0000000..4a331a7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2018, 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This test checks that the static class inliner does not merge classes in situations where a merge
+ * operation could lead to a regression in code size because of reduced inlining opportunities.
+ */
+@RunWith(Parameterized.class)
+public class InliningAfterStaticClassMergerTest extends TestBase {
+
+  // A class that implements a library class.
+  static class StaticMergeCandidateA implements Cloneable {
+
+    // Cannot be inlined into TestClass.main() because the static initialization of this class could
+    // have side-effects; in order for R8 to be conservative, library classes are treated as if
+    // their static initialization could have side-effects.
+    public static String m() {
+      return "StaticMergeCandidateA.m()";
+    }
+  }
+
+  static class StaticMergeCandidateB {
+
+    // Can be inlined into TestClass.main() because the static initialization of this class has no
+    // side-effects.
+    public static String m() {
+      return "StaticMergeCandidateB.m()";
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(StaticMergeCandidateA.m());
+      System.out.print(StaticMergeCandidateB.m());
+    }
+  }
+
+  private Backend backend;
+
+  public InliningAfterStaticClassMergerTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  @Test
+  public void testMethodsAreInlined() throws Exception {
+    String expected =
+        StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()");
+
+    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
+
+    CodeInspector inspector =
+        testForR8(backend)
+            .addProgramClasses(
+                InliningAfterStaticClassMergerTest.TestClass.class,
+                InliningAfterStaticClassMergerTest.StaticMergeCandidateA.class,
+                InliningAfterStaticClassMergerTest.StaticMergeCandidateB.class)
+            .addKeepMainRule(TestClass.class)
+            .addOptionsModification(options -> options.enableMinification = false)
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expected)
+            .inspector();
+
+    // Check that StaticMergeCandidateB has been removed.
+    List<FoundClassSubject> classes =
+        inspector.allClasses().stream()
+            .filter(clazz -> clazz.getOriginalName().contains("StaticMergeCandidate"))
+            .collect(Collectors.toList());
+    assertEquals(1, classes.size());
+    assertEquals(StaticMergeCandidateA.class.getTypeName(), classes.get(0).getOriginalName());
+
+    // Check that StaticMergeCandidateB.m() has not been moved into StaticMergeCandidateA, because
+    // that would disable inlining of it.
+    assertEquals(1, classes.get(0).allMethods().size());
+
+    // Check that the test class only has a main method.
+    ClassSubject classSubject = inspector.clazz(TestClass.class);
+    assertThat(classSubject, isPresent());
+    assertEquals(1, classSubject.allMethods().size());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
index fff28c9..2d2788c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
@@ -78,12 +78,10 @@
         inspector.allClasses().stream()
             .filter(clazz -> clazz.getOriginalName().contains("StaticMergeCandidate"))
             .collect(Collectors.toList());
-    // TODO(b/117916645): Revert back to 1 here once the issue is fixed.
-    assertEquals(2, classes.size());
+    assertEquals(1, classes.size());
 
     // Check that the remaining static merge candidate has two methods.
     FoundClassSubject remaining = classes.get(0);
-    // TODO(b/117916645): Revert back to 2 here once the issue is fixed.
-    assertEquals(1, remaining.allMethods().size());
+    assertEquals(2, remaining.allMethods().size());
   }
 }