Merge "Extend tests for b/116092333"
diff --git a/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java b/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
index 1e16b73..5651dde 100644
--- a/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
+++ b/src/main/java/com/android/tools/r8/CompatProguardCommandBuilder.java
@@ -27,11 +27,11 @@
   }
 
   public CompatProguardCommandBuilder(
-      boolean forceProguardCompatibility, boolean enableVerticalClassMerging) {
+      boolean forceProguardCompatibility, boolean disableVerticalClassMerging) {
     if (forceProguardCompatibility) {
       internalForceProguardCompatibility();
     }
-    setEnableVerticalClassMerging(enableVerticalClassMerging);
+    setDisableVerticalClassMerging(disableVerticalClassMerging);
     setIgnoreDexInArchive(true);
   }
 
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 4c5e0b9..964f699 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -252,8 +252,7 @@
     internal.enableInlining = false;
     assert internal.enableClassInlining;
     internal.enableClassInlining = false;
-    // TODO(christofferqa): Remove negation when enabling vertical class merging by default.
-    assert !internal.enableVerticalClassMerging;
+    assert internal.enableVerticalClassMerging;
     internal.enableVerticalClassMerging = false;
     assert internal.enableClassStaticizer;
     internal.enableClassStaticizer = false;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 0970a0c..dc94041 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -81,7 +81,7 @@
     private final List<ProguardConfigurationSource> proguardConfigs = new ArrayList<>();
     private boolean disableTreeShaking = false;
     private boolean disableMinification = false;
-    private boolean enableVerticalClassMerging = false;
+    private boolean disableVerticalClassMerging = false;
     private boolean forceProguardCompatibility = false;
     private StringConsumer proguardMapConsumer = null;
 
@@ -116,8 +116,8 @@
       this.forceProguardCompatibility = true;
     }
 
-    void setEnableVerticalClassMerging(boolean enableVerticalClassMerging) {
-      this.enableVerticalClassMerging = enableVerticalClassMerging;
+    void setDisableVerticalClassMerging(boolean disableVerticalClassMerging) {
+      this.disableVerticalClassMerging = disableVerticalClassMerging;
     }
 
     @Override
@@ -443,7 +443,7 @@
               desugaring,
               configuration.isShrinking(),
               configuration.isObfuscating(),
-              enableVerticalClassMerging,
+              disableVerticalClassMerging,
               forceProguardCompatibility,
               proguardMapConsumer,
               proguardCompatibilityRulesOutput,
@@ -509,7 +509,7 @@
   private final ProguardConfiguration proguardConfiguration;
   private final boolean enableTreeShaking;
   private final boolean enableMinification;
-  private final boolean enableVerticalClassMerging;
+  private final boolean disableVerticalClassMerging;
   private final boolean forceProguardCompatibility;
   private final StringConsumer proguardMapConsumer;
   private final Path proguardCompatibilityRulesOutput;
@@ -573,7 +573,7 @@
       boolean enableDesugaring,
       boolean enableTreeShaking,
       boolean enableMinification,
-      boolean enableVerticalClassMerging,
+      boolean disableVerticalClassMerging,
       boolean forceProguardCompatibility,
       StringConsumer proguardMapConsumer,
       Path proguardCompatibilityRulesOutput,
@@ -587,7 +587,7 @@
     this.proguardConfiguration = proguardConfiguration;
     this.enableTreeShaking = enableTreeShaking;
     this.enableMinification = enableMinification;
-    this.enableVerticalClassMerging = enableVerticalClassMerging;
+    this.disableVerticalClassMerging = disableVerticalClassMerging;
     this.forceProguardCompatibility = forceProguardCompatibility;
     this.proguardMapConsumer = proguardMapConsumer;
     this.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
@@ -600,7 +600,7 @@
     proguardConfiguration = null;
     enableTreeShaking = false;
     enableMinification = false;
-    enableVerticalClassMerging = false;
+    disableVerticalClassMerging = false;
     forceProguardCompatibility = false;
     proguardMapConsumer = null;
     proguardCompatibilityRulesOutput = null;
@@ -650,6 +650,7 @@
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
 
+    assert internal.enableVerticalClassMerging || !proguardConfiguration.isOptimizing();
     if (internal.debug) {
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
@@ -713,8 +714,9 @@
     // EXPERIMENTAL flags.
     assert !internal.forceProguardCompatibility;
     internal.forceProguardCompatibility = forceProguardCompatibility;
-    assert !internal.enableVerticalClassMerging;
-    internal.enableVerticalClassMerging = enableVerticalClassMerging;
+    if (disableVerticalClassMerging) {
+      internal.enableVerticalClassMerging = false;
+    }
 
     internal.enableInheritanceClassInDexDistributor = isOptimizeMultidexForLinearAlloc();
 
diff --git a/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java b/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
index ec15120..5416350 100644
--- a/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
+++ b/src/main/java/com/android/tools/r8/compatproguard/CompatProguard.java
@@ -40,8 +40,8 @@
     public final List<String> proguardConfig;
     public boolean printHelpAndExit;
 
-    // Flags to enable experimental features.
-    public boolean enableVerticalClassMerging;
+    // Flags to disable experimental features.
+    public boolean disableVerticalClassMerging;
 
     CompatProguardOptions(
         List<String> proguardConfig,
@@ -52,7 +52,7 @@
         boolean includeDataResources,
         String mainDexList,
         boolean printHelpAndExit,
-        boolean verticalClassMerging) {
+        boolean disableVerticalClassMerging) {
       this.output = output;
       this.minApi = minApi;
       this.forceProguardCompatibility = forceProguardCompatibility;
@@ -61,7 +61,7 @@
       this.mainDexList = mainDexList;
       this.proguardConfig = proguardConfig;
       this.printHelpAndExit = printHelpAndExit;
-      this.enableVerticalClassMerging = verticalClassMerging;
+      this.disableVerticalClassMerging = disableVerticalClassMerging;
     }
 
     public static CompatProguardOptions parse(String[] args) {
@@ -72,8 +72,8 @@
       boolean multiDex = false;
       String mainDexList = null;
       boolean printHelpAndExit = false;
-      // Flags to enable experimental features.
-      boolean verticalClassMerging = false;
+      // Flags to disable experimental features.
+      boolean disableVerticalClassMerging = false;
       // These flags are currently ignored.
       boolean minimalMainDex = false;
       boolean coreLibrary = false;
@@ -101,8 +101,8 @@
               mainDexList = args[++i];
             } else if (arg.startsWith("--main-dex-list=")) {
               mainDexList = arg.substring("--main-dex-list=".length());
-            } else if (arg.equals("--vertical-class-merging")) {
-              verticalClassMerging = true;
+            } else if (arg.equals("--no-vertical-class-merging")) {
+              disableVerticalClassMerging = true;
             } else if (arg.equals("--minimal-main-dex")) {
               minimalMainDex = true;
             } else if (arg.equals("--core-library")) {
@@ -138,7 +138,7 @@
           includeDataResources,
           mainDexList,
           printHelpAndExit,
-          verticalClassMerging);
+          disableVerticalClassMerging);
     }
 
     public static void print() {
@@ -177,7 +177,7 @@
     }
     CompatProguardCommandBuilder builder =
         new CompatProguardCommandBuilder(
-            options.forceProguardCompatibility, options.enableVerticalClassMerging);
+            options.forceProguardCompatibility, options.disableVerticalClassMerging);
     builder
         .setOutput(Paths.get(options.output), OutputMode.DexIndexed, options.includeDataResources)
         .addProguardConfiguration(options.proguardConfig, CommandLineOrigin.INSTANCE)
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java
index 2bfe88d..2eb732d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/PrimitiveTypeLatticeElement.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.NumericType;
 
 /**
  * A {@link TypeLatticeElement} that abstracts primitive types.
@@ -57,6 +58,24 @@
     }
   }
 
+  public static PrimitiveTypeLatticeElement fromNumericType(NumericType numericType) {
+    switch(numericType) {
+      case BYTE:
+      case CHAR:
+      case SHORT:
+      case INT:
+        return IntTypeLatticeElement.getInstance();
+      case FLOAT:
+        return FloatTypeLatticeElement.getInstance();
+      case LONG:
+        return LongTypeLatticeElement.getInstance();
+      case DOUBLE:
+        return DoubleTypeLatticeElement.getInstance();
+      default:
+        throw new Unreachable("Invalid numeric type '" + numericType + "'");
+    }
+  }
+
   public static TypeLatticeElement join(
       PrimitiveTypeLatticeElement t1, PrimitiveTypeLatticeElement t2) {
     if (t1 == t2) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java b/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
index f1c0049..d676216 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
@@ -20,6 +20,9 @@
 import com.android.tools.r8.code.LongToFloat;
 import com.android.tools.r8.code.LongToInt;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 
@@ -146,6 +149,11 @@
   }
 
   @Override
+  public TypeLatticeElement evaluate(AppInfo appInfo) {
+    return PrimitiveTypeLatticeElement.fromNumericType(to);
+  }
+
+  @Override
   public void buildCf(CfBuilder builder) {
     builder.add(new CfNumberConversion(from, to));
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 382dd64..6a302c2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IROrdering;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
@@ -39,7 +40,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -117,7 +117,7 @@
       if (isSelfRecursive()) {
         builder.append(", recursive");
       }
-      builder.append(", invoke count " + invokeCount);
+      builder.append(", invoke count ").append(invokeCount);
       builder.append(").\n");
       if (callees.size() > 0) {
         builder.append("Callees:\n");
@@ -140,7 +140,7 @@
   }
 
   private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
-  private final Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> shuffle;
+  private final IROrdering shuffle;
 
   private final Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
   private final Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
@@ -220,7 +220,7 @@
    *
    * <p>
    */
-  private Set<DexEncodedMethod> extractLeaves() {
+  private Collection<DexEncodedMethod> extractLeaves() {
     if (isEmpty()) {
       return Collections.emptySet();
     }
@@ -230,8 +230,10 @@
       leaf.callers.forEach(caller -> caller.callees.remove(leaf));
       nodes.remove(leaf.method);
     });
-    return shuffle.apply(leaves.stream().map(leaf -> leaf.method)
-        .collect(Collectors.toCollection(LinkedHashSet::new)));
+    Set<DexEncodedMethod> methods =
+        leaves.stream().map(x -> x.method).collect(Collectors.toCollection(LinkedHashSet::new));
+    // TODO(b/116282409): Resolve why shuffling makes art.none.r8.Art800_smaliTest flaky.
+    return methods;
   }
 
   public static class CycleEliminator {
@@ -457,7 +459,7 @@
       ExecutorService executorService)
       throws ExecutionException {
     while (!isEmpty()) {
-      Set<DexEncodedMethod> methods = extractLeaves();
+      Collection<DexEncodedMethod> methods = extractLeaves();
       assert methods.size() > 0;
       List<Future<?>> futures = new ArrayList<>();
       for (DexEncodedMethod method : methods) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 406412b..f4a1e4e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -777,7 +777,9 @@
       inliner.performInlining(
           method, code, isProcessedConcurrently, callSiteInformation);
     }
-    stringOptimizer.computeConstStringLength(code, appInfo.dexItemFactory);
+    if (!options.debug) {
+      stringOptimizer.computeConstStringLength(code, appInfo.dexItemFactory);
+    }
     if (devirtualizer != null) {
       devirtualizer.devirtualizeInvokeInterface(code, method.method.getHolder());
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index e356bb1..0abbfde 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -51,15 +51,14 @@
       List<Value> ins = invoke.arguments();
       assert ins.size() == 1;
       Value in = ins.get(0);
-      if (in.definition == null || !in.definition.isConstString()) {
+      if (in.definition == null
+          || !in.definition.isConstString()
+          || !in.definition.outValue().isConstant()) {
         continue;
       }
       ConstString constString = in.definition.asConstString();
       int length = constString.getValue().toString().length();
       ConstNumber constNumber = code.createIntConstant(length);
-      if (invoke.outValue().hasLocalInfo()) {
-        constNumber.outValue().setLocalInfo(invoke.outValue().getLocalInfo());
-      }
       it.replaceCurrentInstruction(constNumber);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index 4c64cf6..5128b96 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -18,14 +18,14 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstInstruction;
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.Value;
-import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public final class IdentifierNameStringUtils {
@@ -295,6 +295,66 @@
     return itemBasedString;
   }
 
+  // Perform a conservative evaluation of the constant content of an array from its construction
+  // until its use at a given instruction.
+  private static ConstInstruction[] evaluateConstArrayContentFromConstructionToUse(
+      NewArrayEmpty newArray, int size, Instruction user) {
+    ConstInstruction[] values = new ConstInstruction[size];
+    int remaining = size;
+    Set<Instruction> users = newArray.outValue().uniqueUsers();
+    // Follow the path from the array construction to the requested use collecting the constants
+    // put into the array. Conservatively bail out if the content of the array cannot be statically
+    // computed.
+    BasicBlock block = newArray.getBlock();
+    InstructionListIterator iterator = block.listIterator();
+    iterator.nextUntil(i -> i == newArray);
+    do {
+      while (iterator.hasNext()) {
+        Instruction instruction = iterator.next();
+        // Ignore instructions which do not use the array.
+        if (!users.contains(instruction)) {
+          continue;
+        }
+        if (instruction == user) {
+          // Return the array content if all elements are known when hitting the user for which
+          // the content was requested.
+          return remaining == 0 ? values : null;
+        }
+        // Any other kinds of use besides array-put mean that the array escapes and could be
+        // altered.
+        if (!instruction.isArrayPut()) {
+          return null;
+        }
+        ArrayPut arrayPut = instruction.asArrayPut();
+        if (!(arrayPut.value().isConstant() && arrayPut.index().isConstNumber())) {
+          return null;
+        }
+        int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
+        if (index < 0 || index >= values.length) {
+          return null;
+        }
+        // Allow several writes to the same array element.
+        if (values[index] == null) {
+          remaining--;
+        }
+        ConstInstruction value = arrayPut.value().getConstInstruction();
+        values[index] = value;
+      }
+      if (!block.exit().isGoto()) {
+        return null;
+      }
+      block = block.exit().asGoto().getTarget();
+      // Don't allow any other control flow into the sequence of blocks filling the array from
+      // construction to requested use. This will also includes loopback and guarantee that
+      // this will terminate without marking visited blocks.
+      if (block.getPredecessors().size() != 1) {
+        return null;
+      }
+      iterator = block.listIterator();
+    } while (iterator != null);
+    return null;
+  }
+
   /**
    * Visits all {@link ArrayPut}'s with the given {@param classListValue} as array and {@link Class}
    * as value. Then collects all corresponding {@link DexType}s so as to determine reflective cases.
@@ -305,64 +365,56 @@
    */
   private static DexTypeList retrieveDexTypeListFromClassList(
       InvokeMethod invoke, Value classListValue) {
-    // Make sure this Value refers to an array.
-    if (!classListValue.definition.isInvokeNewArray()
-        && !classListValue.definition.isNewArrayEmpty()) {
+
+    // The code
+    //   A.class.getMethod("m", String.class, String.class)
+    // results in the following Java byte code from javac:
+    //
+    // LDC LA;.class
+    // LDC "m"
+    // ICONST_2
+    // ANEWARRAY java/lang/Class
+    // DUP
+    // ICONST_0
+    // LDC Ljava/lang/String;.class
+    // AASTORE
+    // DUP
+    // ICONST_1
+    // LDC Ljava/lang/String;.class
+    // AASTORE
+    // INVOKEVIRTUAL java/lang/Class.getMethod \
+    //     (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
+
+    // Make sure this Value refers to a new array.
+    if (!classListValue.definition.isNewArrayEmpty()) {
       return null;
     }
-    // The only pattern we consider is: new Class[] { A.class, B.class, ... }, which looks like
-    //   new-array va ...
-    //   const-class vc ...
-    //   const/4 vi ...
-    //   aput-object vc va vi
-    //   ... repeat putting const class into one location at a time ...
-    //   invoke-static ... va // Use that array at {@param invoke}.
-    BasicBlock block = classListValue.definition.getBlock();
-    InstructionIterator iterator = block.iterator();
-    iterator.nextUntil(instr -> instr == classListValue.definition);
-    Set<Instruction> users = classListValue.definition.outValue().uniqueUsers();
-    int maxIndex = -1;
-    Map<Integer, DexType> typeMap = new Int2ObjectArrayMap<>();
-    while (iterator.hasNext()) {
-      Instruction instr = iterator.next();
-      // Iterate the instructions up to the current {@param invoke}.
-      if (instr == invoke) {
-        break;
-      }
-      if (!users.contains(instr)) {
-        continue;
-      }
-      // Any other kinds of users mean that elements could be escaped and altered.
-      if (!instr.isArrayPut()) {
-        return null;
-      }
-      ArrayPut arrayPut = instr.asArrayPut();
-      assert arrayPut.array() == classListValue;
-      // Ignore statically unknown index.
-      if (!(arrayPut.value().isConstClass() && arrayPut.index().isConstNumber())) {
-        return null;
-      }
-      int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
-      // Filter out out-of-bound index or non-deterministic index.
-      if (index < 0 || typeMap.containsKey(index)) {
-        return null;
-      }
-      maxIndex = maxIndex < index ? index : maxIndex;
-      DexType type = arrayPut.value().getConstInstruction().asConstClass().getValue();
-      typeMap.put(index, type);
-    }
-    if (maxIndex < 0) {
+
+    int size =
+        classListValue
+            .definition
+            .asNewArrayEmpty()
+            .size()
+            .getConstInstruction()
+            .asConstNumber()
+            .getIntValue();
+    if (size == 0) {
       return DexTypeList.empty();
     }
-    // Make sure we were able to collect *all* {@link ConstClass}'s.
-    for (int i = 0; i <= maxIndex; i++) {
-      if (!typeMap.containsKey(i)) {
+
+    ConstInstruction[] arrayContent =
+        evaluateConstArrayContentFromConstructionToUse(
+            classListValue.definition.asNewArrayEmpty(), size, invoke);
+
+    if (arrayContent == null) {
+      return null;
+    }
+    DexType[] types = new DexType[size];
+    for (int i = 0; i < size; i++) {
+      if (!arrayContent[i].isConstClass()) {
         return null;
       }
-    }
-    DexType[] types = new DexType [maxIndex + 1];
-    for (int i = 0; i <= maxIndex; i++) {
-      types[i] = typeMap.get(i);
+      types[i] = arrayContent[i].asConstClass().getValue();
     }
     return new DexTypeList(types);
   }
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
index e03890c..43eab1d 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNameMinifier.java
@@ -18,6 +18,7 @@
 
   protected final AppInfoWithLiveness appInfo;
   protected final RootSet rootSet;
+  protected final InternalOptions options;
   protected final ImmutableList<String> dictionary;
 
   protected final Map<MemberType, DexString> renaming = new IdentityHashMap<>();
@@ -29,6 +30,7 @@
   MemberNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, InternalOptions options) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
+    this.options = options;
     this.dictionary = options.proguardConfiguration.getObfuscationDictionary();
     this.useUniqueMemberNames = options.proguardConfiguration.isUseUniqueClassMemberNames();
     this.overloadAggressively =
diff --git a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
index 1f206a3..6b109c5 100644
--- a/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/MethodNameMinifier.java
@@ -168,8 +168,9 @@
       Map<Wrapper<DexMethod>, DexString> renamingAtThisLevel = new HashMap<>();
       NamingState<DexProto, ?> state =
           computeStateIfAbsent(type, k -> getState(holder.superType).createChild());
-      holder.forEachMethod(method ->
-          assignNameToMethod(method, state, renamingAtThisLevel, doPrivates));
+      for (DexEncodedMethod method : holder.allMethodsSorted()) {
+        assignNameToMethod(method, state, renamingAtThisLevel, doPrivates);
+      }
       if (!doPrivates && !useUniqueMemberNames) {
         renamingAtThisLevel.forEach(
             (key, candidate) -> {
@@ -236,8 +237,10 @@
       DexClass clazz = appInfo.definitionFor(iface);
       if (clazz != null) {
         Set<NamingState<DexProto, ?>> collectedStates = getReachableStates(iface, frontierMap);
-        clazz.forEachMethod(method -> addStatesToGlobalMapForMethod(
-            method, collectedStates, globalStateMap, sourceMethodsMap, originStates, iface));
+        for (DexEncodedMethod method : shuffleMethods(clazz.methods())) {
+          addStatesToGlobalMapForMethod(
+              method, collectedStates, globalStateMap, sourceMethodsMap, originStates, iface);
+        }
       }
     });
     // Collect the live call sites for multi-interface lambda expression renaming. For code with
@@ -469,7 +472,9 @@
                 : parent.createChild());
     if (holder != null) {
       boolean keepAll = holder.isLibraryClass() || holder.accessFlags.isAnnotation();
-      holder.forEachMethod(method -> reserveNamesForMethod(method, keepAll, state));
+      for (DexEncodedMethod method : shuffleMethods(holder.methods())) {
+        reserveNamesForMethod(method, keepAll, state);
+      }
     }
     return state;
   }
@@ -528,4 +533,10 @@
       return proto;
     }
   }
+
+  // Shuffles the given methods if assertions are enabled and deterministic debugging is disabled.
+  // Used to ensure that the generated output is deterministic.
+  private Iterable<DexEncodedMethod> shuffleMethods(Iterable<DexEncodedMethod> methods) {
+    return options.testing.irOrdering.order(methods);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/IROrdering.java b/src/main/java/com/android/tools/r8/utils/IROrdering.java
new file mode 100644
index 0000000..4b4ded3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/IROrdering.java
@@ -0,0 +1,62 @@
+// 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.utils;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public interface IROrdering {
+
+  Iterable<DexEncodedMethod> order(Iterable<DexEncodedMethod> methods);
+
+  Collection<DexEncodedMethod> order(Collection<DexEncodedMethod> methods);
+
+  class IdentityIROrdering implements IROrdering {
+
+    private static final IdentityIROrdering INSTANCE = new IdentityIROrdering();
+
+    private IdentityIROrdering() {}
+
+    public static IdentityIROrdering getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public Iterable<DexEncodedMethod> order(Iterable<DexEncodedMethod> methods) {
+      return methods;
+    }
+
+    @Override
+    public Collection<DexEncodedMethod> order(Collection<DexEncodedMethod> methods) {
+      return methods;
+    }
+  }
+
+  class NondeterministicIROrdering implements IROrdering {
+
+    private static final NondeterministicIROrdering INSTANCE = new NondeterministicIROrdering();
+
+    private NondeterministicIROrdering() {}
+
+    public static NondeterministicIROrdering getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public List<DexEncodedMethod> order(Iterable<DexEncodedMethod> methods) {
+      List<DexEncodedMethod> toShuffle = Lists.newArrayList(methods);
+      Collections.shuffle(toShuffle);
+      return toShuffle;
+    }
+
+    @Override
+    public List<DexEncodedMethod> order(Collection<DexEncodedMethod> methods) {
+      return order((Iterable<DexEncodedMethod>) methods);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 421d55c..16f181d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -22,6 +22,8 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.IROrdering.IdentityIROrdering;
+import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -30,7 +32,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.function.Function;
 
 public class InternalOptions {
 
@@ -97,7 +98,7 @@
   public boolean passthroughDexCode = false;
 
   // Optimization-related flags. These should conform to -dontoptimize.
-  public boolean enableVerticalClassMerging = false;
+  public boolean enableVerticalClassMerging = true;
   public boolean enableDevirtualization = true;
   public boolean enableNonNullTracking = true;
   public boolean enableInlining =
@@ -293,6 +294,12 @@
 
   public Path proguardCompatibilityRulesOutput = null;
 
+  public static boolean assertionsEnabled() {
+    boolean assertionsEnabled = false;
+    assert assertionsEnabled = true; // Intentional side-effect.
+    return assertionsEnabled;
+  }
+
   public void warningMissingEnclosingMember(DexType clazz, Origin origin, int version) {
     TypeVersionPair pair = new TypeVersionPair(version, clazz);
     synchronized (missingEnclosingMembers) {
@@ -442,8 +449,10 @@
 
   public static class TestingOptions {
 
-    public Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> irOrdering =
-        Function.identity();
+    public IROrdering irOrdering =
+        InternalOptions.assertionsEnabled() && !InternalOptions.DETERMINISTIC_DEBUGGING
+            ? NondeterministicIROrdering.getInstance()
+            : IdentityIROrdering.getInstance();
 
     public boolean alwaysUsePessimisticRegisterAllocation = false;
     public boolean invertConditionals = false;
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 01fa54d..2816a9c 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -411,7 +411,13 @@
   /** Compile an application with R8 using the supplied proguard configuration. */
   protected AndroidApp compileWithR8(AndroidApp app, String proguardConfig)
       throws IOException, CompilationFailedException {
-    return compileWithR8(app, proguardConfig, null);
+    return compileWithR8(app, proguardConfig, null, Backend.DEX);
+  }
+
+  /** Compile an application with R8 using the supplied proguard configuration. */
+  protected AndroidApp compileWithR8(AndroidApp app, String proguardConfig, Backend backend)
+      throws IOException, CompilationFailedException {
+    return compileWithR8(app, proguardConfig, null, backend);
   }
 
   /** Compile an application with R8 using the supplied proguard configuration. */
@@ -676,6 +682,11 @@
   }
 
   /** Run application on Art or Java with the specified main class. */
+  protected String runOnVM(AndroidApp app, Class mainClass, Backend backend) throws IOException {
+    return runOnVM(app, mainClass.getName(), backend);
+  }
+
+  /** Run application on Art or Java with the specified main class. */
   protected String runOnVM(AndroidApp app, String mainClass, Backend backend) throws IOException {
     switch (backend) {
       case CF:
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index c63f425..06e6a4f 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -95,7 +95,16 @@
   }
 
   @Test
-  public void testInstanceMethodRelaxation() throws Exception {
+  public void testInstanceMethodRelaxationWithVerticalClassMerging() throws Exception {
+    testInstanceMethodRelaxation(true);
+  }
+
+  @Test
+  public void testInstanceMethodRelaxationWithoutVerticalClassMerging() throws Exception {
+    testInstanceMethodRelaxation(false);
+  }
+
+  private void testInstanceMethodRelaxation(boolean enableVerticalClassMerging) throws Exception {
     Class mainClass = TestMain.class;
     R8Command.Builder builder = loadProgramFiles(backend, mainClass.getPackage());
 
@@ -122,27 +131,34 @@
         ),
         Origin.unknown());
 
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app =
+        ToolHelper.runR8(
+            builder.build(),
+            options -> options.enableVerticalClassMerging = enableVerticalClassMerging);
     compareReferenceJVMAndProcessed(app, mainClass);
 
+    // When vertical class merging is enabled, Itf1 is merged into Sub1 and Itf2 is merged into
+    // Sub2, and as a result of these merges, neither Sub1 nor Sub2 end up in the output because of
+    // inlining.
     CodeInspector codeInspector = new CodeInspector(app);
-    assertPublic(codeInspector, Base.class,
-        new MethodSignature("foo", STRING, ImmutableList.of()));
+    assertPublic(codeInspector, Base.class, new MethodSignature("foo", STRING, ImmutableList.of()));
 
     // Base#foo?() can't be publicized due to Itf<1>#foo<1>().
-    assertNotPublic(codeInspector, Base.class,
-        new MethodSignature("foo1", STRING, ImmutableList.of()));
-    assertNotPublic(codeInspector, Base.class,
-        new MethodSignature("foo2", STRING, ImmutableList.of()));
+    assertNotPublic(
+        codeInspector, Base.class, new MethodSignature("foo1", STRING, ImmutableList.of()));
+    assertNotPublic(
+        codeInspector, Base.class, new MethodSignature("foo2", STRING, ImmutableList.of()));
 
-    // Sub?#bar1(int) can be publicized as they don't bother each other.
-    assertPublic(codeInspector, Sub1.class,
-        new MethodSignature("bar1", STRING, ImmutableList.of("int")));
-    assertPublic(codeInspector, Sub2.class,
-        new MethodSignature("bar1", STRING, ImmutableList.of("int")));
+    if (!enableVerticalClassMerging) {
+      // Sub?#bar1(int) can be publicized as they don't bother each other.
+      assertPublic(
+          codeInspector, Sub1.class, new MethodSignature("bar1", STRING, ImmutableList.of("int")));
+      assertPublic(
+          codeInspector, Sub2.class, new MethodSignature("bar1", STRING, ImmutableList.of("int")));
 
-    // Sub2#bar2(int) is unique throughout the hierarchy, hence publicized.
-    assertPublic(codeInspector, Sub2.class,
-        new MethodSignature("bar2", STRING, ImmutableList.of("int")));
+      // Sub2#bar2(int) is unique throughout the hierarchy, hence publicized.
+      assertPublic(
+          codeInspector, Sub2.class, new MethodSignature("bar2", STRING, ImmutableList.of("int")));
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
index cf95846..ede9fee 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
@@ -95,4 +95,13 @@
     CompatProguardOptions options = parseArgs("--no-data-resources");
     assertFalse(options.includeDataResources);
   }
+
+  @Test
+  public void testDisableVerticalClassMerging() throws Exception {
+    CompatProguardOptions enabledOptions = parseArgs();
+    assertFalse(enabledOptions.disableVerticalClassMerging);
+
+    CompatProguardOptions disabledOptions = parseArgs("--no-vertical-class-merging");
+    assertTrue(disabledOptions.disableVerticalClassMerging);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java b/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java
new file mode 100644
index 0000000..c90e0fb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compatproguard/reflection/ReflectionTest.java
@@ -0,0 +1,151 @@
+// 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.compatproguard.reflection;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class A {
+
+  public void method0() {
+    System.out.print(0);
+  }
+
+  public void method1(String s) {
+    System.out.print(s);
+  }
+
+  public void method2(String s1, String s2) {
+    System.out.print(s1 + s2);
+  }
+
+  public void method3(int i1, int i2) {
+    System.out.print(i1 + i2);
+  }
+}
+
+class Main {
+  public static void main(String[] args) throws Exception {
+    A a = new A();
+
+    Method m;
+    m = A.class.getMethod("method0");
+    m.invoke(a);
+    m = A.class.getMethod("method1", String.class);
+    m.invoke(a, "1");
+    m = A.class.getMethod("method2", String.class, String.class);
+    m.invoke(a, "2", "3");
+    m = A.class.getDeclaredMethod("method0");
+    m.invoke(a);
+    m = A.class.getDeclaredMethod("method1", String.class);
+    m.invoke(a, "1");
+    m = A.class.getDeclaredMethod("method2", String.class, String.class);
+    m.invoke(a, "2", "3");
+    m = A.class.getDeclaredMethod("method3", int.class, int.class);
+    m.invoke(a, 2, 2);
+
+    try {
+      m = A.class.getMethod("method0");
+      m.invoke(a);
+      m = A.class.getMethod("method1", String.class);
+      m.invoke(a, "1");
+      m = A.class.getMethod("method2", String.class, String.class);
+      m.invoke(a, "2", "3");
+      m = A.class.getDeclaredMethod("method0");
+      m.invoke(a);
+      m = A.class.getDeclaredMethod("method1", String.class);
+      m.invoke(a, "1");
+      m = A.class.getDeclaredMethod("method2", String.class, String.class);
+      m.invoke(a, "2", "3");
+      m = A.class.getDeclaredMethod("method3", int.class, int.class);
+      m.invoke(a, 2, 2);
+    } catch (Exception e) {
+    }
+
+    Class[] argumentTypes;
+    argumentTypes = new Class[2];
+    argumentTypes[1] = int.class;
+    argumentTypes[0] = int.class;
+    argumentTypes[0] = String.class;
+    argumentTypes[1] = String.class;
+    m = A.class.getDeclaredMethod("method2", argumentTypes);
+    m.invoke(a, "2", "3");
+    m = A.class.getDeclaredMethod("method2", argumentTypes);
+    m.invoke(a, "4", "5");
+
+    argumentTypes[1] = int.class;
+    argumentTypes[0] = int.class;
+    m = A.class.getDeclaredMethod("method3", argumentTypes);
+    m.invoke(a, 3, 3);
+    m = A.class.getDeclaredMethod("method3", argumentTypes);
+    m.invoke(a, 3, 4);
+
+    try {
+      argumentTypes = new Class[2];
+      argumentTypes[1] = int.class;
+      argumentTypes[0] = int.class;
+      argumentTypes[0] = String.class;
+      argumentTypes[1] = String.class;
+      m = A.class.getDeclaredMethod("method2", argumentTypes);
+      m.invoke(a, "2", "3");
+      m = A.class.getDeclaredMethod("method2", argumentTypes);
+      m.invoke(a, "4", "7");
+
+      argumentTypes[1] = int.class;
+      argumentTypes[0] = int.class;
+      m = A.class.getDeclaredMethod("method3", argumentTypes);
+      m.invoke(a, 3, 3);
+      m = A.class.getDeclaredMethod("method3", argumentTypes);
+      m.invoke(a, 3, 4);
+    } catch (Exception e) {
+    }
+  }
+}
+
+@RunWith(Parameterized.class)
+public class ReflectionTest extends TestBase {
+
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public ReflectionTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    AndroidApp output =
+        compileWithR8(
+            readClasses(A.class, Main.class), keepMainProguardConfiguration(Main.class), backend);
+    CodeInspector inspector = new CodeInspector(output);
+    assertThat(inspector.clazz(A.class).method("void", "method0", ImmutableList.of()), isRenamed());
+    assertThat(
+        inspector.clazz(A.class).method("void", "method1", ImmutableList.of("java.lang.String")),
+        isRenamed());
+    assertThat(
+        inspector
+            .clazz(A.class)
+            .method("void", "method2", ImmutableList.of("java.lang.String", "java.lang.String")),
+        isRenamed());
+
+    assertEquals(runOnJava(Main.class), runOnVM(output, Main.class, backend));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 6ae151e..b63d4ba 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -10,17 +10,11 @@
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
-import com.beust.jcommander.internal.Lists;
-import java.io.IOException;
+import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
 import org.junit.Test;
 
 public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
@@ -30,13 +24,7 @@
     String proguardMap;
   }
 
-  private static Set<DexEncodedMethod> shuffle(Set<DexEncodedMethod> methods) {
-    List<DexEncodedMethod> toShuffle = Lists.newArrayList(methods);
-    Collections.shuffle(toShuffle);
-    return new LinkedHashSet<>(toShuffle);
-  }
-
-  private CompilationResult doRun() throws IOException, CompilationFailedException {
+  private CompilationResult doRun() throws CompilationFailedException {
     R8Command command =
         R8Command.builder()
             .addProgramFiles(Paths.get(GMSCORE_V7_DIR, GMSCORE_APK))
@@ -49,7 +37,7 @@
             command,
             options -> {
               // For this test just do random shuffle.
-              options.testing.irOrdering = R8GMSCoreDeterministicTest::shuffle;
+              options.testing.irOrdering = NondeterministicIROrdering.getInstance();
               // Only use one thread to process to process in the order decided by the callback.
               options.numberOfThreads = 1;
               // Ignore the missing classes.
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
index fdb86a1..a2f8d3e 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.NumberConversion;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -60,6 +61,7 @@
   private static final TypeLatticeElement NULL = NullLatticeElement.getInstance();
   private static final TypeLatticeElement SINGLE = SingleTypeLatticeElement.getInstance();
   private static final TypeLatticeElement INT = IntTypeLatticeElement.getInstance();
+  private static final TypeLatticeElement LONG = LongTypeLatticeElement.getInstance();
 
   private final String dirName;
   private final String smaliFileName;
@@ -287,7 +289,18 @@
         method.buildIR(appInfo, GraphLense.getIdentityLense(), TEST_OPTIONS, Origin.unknown());
     TypeAnalysis analysis = new TypeAnalysis(appInfo, method);
     analysis.widening(method, irCode);
-    forEachOutValue(irCode, (v, l) -> verifyTypeEnvironment(expectedLattices, v, l));
+    forEachOutValue(irCode, (v, l) -> {
+      verifyTypeEnvironment(expectedLattices, v, l);
+      // There are double-to-int, long-to-int, and int-to-long conversions in this example.
+      if (v.definition != null && v.definition.isNumberConversion()) {
+        NumberConversion instr = v.definition.asNumberConversion();
+        if (instr.to.isWide()) {
+          assertEquals(LONG, l);
+        } else {
+          assertEquals(INT, l);
+        }
+      }
+    });
   }
 
   // One more complicated example.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTest.java
new file mode 100644
index 0000000..35b2788
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTest.java
@@ -0,0 +1,14 @@
+// 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.ir.optimize.string;
+
+class StringLengthDebugTest {
+  public static void main(String[] args) {
+    String x = "ABC";
+    int l1 = x.length();
+    System.out.println(l1);
+    int l2 = "XYZ".length();
+    System.out.println(l2);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTestRunner.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTestRunner.java
new file mode 100644
index 0000000..21033e5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthDebugTestRunner.java
@@ -0,0 +1,85 @@
+// 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.ir.optimize.string;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
+import com.android.tools.r8.debug.D8DebugTestConfig;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.FrameInspector;
+import com.android.tools.r8.debug.DebugTestConfig;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class StringLengthDebugTestRunner extends DebugTestBase {
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V5_1_1)
+  public void test() throws Throwable {
+    Class<?> main = StringLengthDebugTest.class;
+    DebugTestConfig config = new D8DebugTestConfig()
+        .compileAndAdd(temp, ToolHelper.getClassFileForTestClass(main));
+    runDebugTest(config, main.getCanonicalName(),
+        breakpoint(main.getCanonicalName(), "main"),
+        run(),
+        stepOver(),
+        // String x = "ABC";
+        inspect(
+            i -> {
+              FrameInspector frame = i.getFrame(0);
+              frame.checkLocal("x");
+              frame.checkNoLocal("l1");
+              frame.checkNoLocal("l2");
+            }
+        ),
+        // int l1 = x.length();
+        stepInto(),
+        inspect(
+            i -> {
+              FrameInspector frame = i.getFrame(0);
+              assertEquals(String.class.getCanonicalName(), frame.getClassName());
+              assertEquals("length", frame.getMethodName());
+            }
+        ),
+        stepOut(),
+        stepOver(),
+        inspect(
+            i -> {
+              FrameInspector frame = i.getFrame(0);
+              frame.checkLocal("x");
+              frame.checkLocal("l1");
+              frame.checkNoLocal("l2");
+            }
+        ),
+        // System.out.println(l1);
+        stepOver(),
+        // int l2 = "XYZ".length();
+        stepInto(),
+        inspect(
+            i -> {
+              FrameInspector frame = i.getFrame(0);
+              assertEquals(String.class.getCanonicalName(), frame.getClassName());
+              assertEquals("length", frame.getMethodName());
+            }
+        ),
+        stepOut(),
+        stepOver(),
+        inspect(
+            i -> {
+              FrameInspector frame = i.getFrame(0);
+              frame.checkLocal("x");
+              frame.checkLocal("l1");
+              frame.checkLocal("l2");
+            }
+        ),
+        run()
+    );
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
index 307dd26..6e18f74 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
@@ -9,6 +9,8 @@
 import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8Command;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ForceInline;
 import com.android.tools.r8.NeverInline;
@@ -63,8 +65,7 @@
     System.out.println(s1.length());
 
     String s2 = simpleInlinable();
-    // Depends on inlining, and thus R8 can whereas D8 can't.
-    // For R8: constCount++, for D8: stringLengthCount++
+    // Depends on inlining: constCount++
     System.out.println(s2.length());
     String s3 = simpleInlinable();
     System.out.println(s3);
@@ -81,6 +82,10 @@
     System.out.println(s5.codePointCount(0, s5.length()));
     System.out.println(s5);
 
+    // Make sure this is not optimized in DEBUG mode.
+    int l = "ABC".length();
+    System.out.println(l);
+
     try {
       npe();
     } catch (NullPointerException npe) {
@@ -199,9 +204,15 @@
     }
 
     AndroidApp app = buildAndroidApp(classes);
-    AndroidApp processedApp = compileWithD8(app);
-    // No inlining, thus the 2nd length() can't be computed.
-    test(processedApp, 1, 3);
+    D8Command.Builder builder = ToolHelper.prepareD8CommandBuilder(app);
+    builder.setMode(CompilationMode.RELEASE);
+    AndroidApp processedApp = ToolHelper.runD8(builder);
+    test(processedApp, 1, 4);
+
+    builder = ToolHelper.prepareD8CommandBuilder(app);
+    builder.setMode(CompilationMode.DEBUG);
+    processedApp = ToolHelper.runD8(builder);
+    test(processedApp, 6, 0);
   }
 
   @Test
@@ -224,6 +235,6 @@
     builder.addProguardConfiguration(ImmutableList.of(pgConf), Origin.unknown());
 
     AndroidApp processedApp = ToolHelper.runR8(builder.build());
-    test(processedApp, 0, 4);
+    test(processedApp, 0, 5);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index d938819..6d24386 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -79,7 +79,16 @@
 public class ParameterTypeTest extends TestBase {
 
   @Test
-  public void test_fromJavac() throws Exception {
+  public void test_fromJavacWithVerticalClassMerging() throws Exception {
+    test_fromJavac(true);
+  }
+
+  @Test
+  public void test_fromJavacWithoutVerticalClassMerging() throws Exception {
+    test_fromJavac(false);
+  }
+
+  private void test_fromJavac(boolean enableVerticalClassMerging) throws Exception {
     String mainName = B112452064TestMain.class.getCanonicalName();
     ProcessResult javaResult = ToolHelper.runJava(ToolHelper.getClassPathForTests(), mainName);
     assertEquals(0, javaResult.exitCode);
@@ -102,6 +111,7 @@
     builder.addProguardConfiguration(config, Origin.unknown());
     AndroidApp processedApp = ToolHelper.runR8(builder.build(), options -> {
       options.enableInlining = false;
+      options.enableVerticalClassMerging = enableVerticalClassMerging;
     });
 
     Path outDex = temp.getRoot().toPath().resolve("dex.zip");
@@ -117,7 +127,11 @@
     MethodSubject foo = superInterface1.method("void", "foo", ImmutableList.of());
     assertThat(foo, isRenamed());
     ClassSubject superInterface2 = inspector.clazz(B112452064SuperInterface2.class);
-    assertThat(superInterface2, isRenamed());
+    if (enableVerticalClassMerging) {
+      assertThat(superInterface2, not(isPresent()));
+    } else {
+      assertThat(superInterface2, isRenamed());
+    }
     MethodSubject bar = superInterface1.method("void", "bar", ImmutableList.of());
     assertThat(bar, not(isPresent()));
     ClassSubject subInterface = inspector.clazz(B112452064SubInterface.class);