Test debug stepping in <clinit>

This disables most of the <clinit> optimization in debug mode, as static
initialization through asignemnt has debug position in the <clinit>
code generated by javac.

For now the release mode still performs all optimizations.

Bug: 65133411
Change-Id: I11813a4c1d687f0a3eabaef2e67563bd6f902994
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 1a8602c..1893949 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -82,6 +82,17 @@
     this.debugInfo = debugInfo;
   }
 
+  public boolean hasDebugPositions() {
+    if (debugInfo != null) {
+      for (DexDebugEvent event : debugInfo.events) {
+        if (event instanceof DexDebugEvent.Default) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
     if (debugInfo == null) {
       return null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 8c071c8..b76177b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -209,6 +209,11 @@
     code = null;
   }
 
+  public boolean hasDebugPositions() {
+    assert code != null && code.isDexCode();
+    return code.asDexCode().hasDebugPositions();
+  }
+
   public String qualifiedName() {
     return method.qualifiedName();
   }
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 24e0396..94a88b5 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
@@ -343,7 +343,8 @@
   }
 
   private void removeEmptyClassInitializer(DexProgramClass clazz) {
-    if (clazz.hasTrivialClassInitializer()) {
+    if (clazz.hasTrivialClassInitializer()
+        && !clazz.getClassInitializer().hasDebugPositions()) {
       clazz.removeStaticMethod(clazz.getClassInitializer());
     }
   }
@@ -493,7 +494,9 @@
     codeRewriter.foldConstants(code);
     codeRewriter.rewriteSwitch(code);
     codeRewriter.simplifyIf(code);
-    codeRewriter.collectClassInitializerDefaults(method, code);
+    if (!options.debug) {
+      codeRewriter.collectClassInitializerDefaults(method, code);
+    }
     if (Log.ENABLED) {
       Log.debug(getClass(), "Intermediate (SSA) flow graph for %s:\n%s",
           method.toSourceString(), code);
diff --git a/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
new file mode 100644
index 0000000..fa6078a
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2017, 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.
+
+public class ClassInitializerAssignmentInitialization {
+
+  static int x = 1;
+  static int y;
+
+  static int z = 2;
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerEmpty.java b/src/test/debugTestResources/ClassInitializerEmpty.java
new file mode 100644
index 0000000..9062d62
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerEmpty.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2017, 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.
+
+public class ClassInitializerEmpty {
+
+  static {
+  }
+
+  public static void main(String[] args) {
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerMixedInitialization.java b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
new file mode 100644
index 0000000..504db6c
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2017, 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.
+
+public class ClassInitializerMixedInitialization {
+
+  static boolean b;
+  static int x = 1;
+  static int y;
+
+  static {
+    x = 2;
+    if (b) {
+      y = 1;
+    } else {
+      y = 2;
+    }
+    x = 3;
+  }
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
new file mode 100644
index 0000000..78fe9a0
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, 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.
+
+public class ClassInitializerStaticBlockInitialization {
+
+  static boolean b;
+  static int x;
+  static int y;
+
+  static {
+    x = 1;
+    x = 2;
+    if (b) {
+      y = 1;
+    } else {
+      y = 2;
+    }
+    x = 3;
+  }
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index cb8e307..fcbc1ef 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -14,15 +14,11 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
-import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
-import com.android.tools.r8.shaking.RootSetBuilder;
-import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -93,6 +89,10 @@
       return compareTo(other) > 0;
     }
 
+    public boolean isOlderThanOrEqual(DexVm other) {
+      return compareTo(other) <= 0;
+    }
+
     private DexVm(String shortName) {
       this.shortName = shortName;
     }
diff --git a/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
new file mode 100644
index 0000000..e90bca7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2017, 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.debug;
+
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Test;
+
+public class ClassInitializationTest extends DebugTestBase {
+
+  @Test
+  public void testStaticAssingmentInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerAssignmentInitialization.java";
+    final String CLASS = "ClassInitializerAssignmentInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 7),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 10),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkStaticField(CLASS, "x", null, Value.createInt(1)),
+        checkStaticField(CLASS, "y", null, Value.createInt(0)),
+        checkStaticField(CLASS, "z", null, Value.createInt(2)),
+        run());
+  }
+
+  @Test
+  public void testBreakpointInEmptyClassInitializer() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerEmpty.java";
+    final String CLASS = "ClassInitializerEmpty";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 8),
+        run());
+  }
+
+  @Test
+  public void testStaticBlockInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerStaticBlockInitialization.java";
+    final String CLASS = "ClassInitializerStaticBlockInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 12),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 13),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 14),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 17),
+        stepOver(),
+        checkLine(SOURCE_FILE, 19),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkLine(SOURCE_FILE, 23),
+        checkStaticField(CLASS, "x", null, Value.createInt(3)),
+        run());
+  }
+
+  @Test
+  public void testStaticMixedInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerMixedInitialization.java";
+    final String CLASS = "ClassInitializerMixedInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 8),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 12),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 13),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 16),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 18),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(2)),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkLine(SOURCE_FILE, 22),
+        checkStaticField(CLASS, "x", null, Value.createInt(3)),
+        checkStaticField(CLASS, "y", null, Value.createInt(2)),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 7635e52..fa78fd0 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -10,9 +10,12 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongList;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -62,8 +65,10 @@
 import org.junit.rules.TestName;
 
 /**
+ * Base class for debugging tests.
  *
- * Base class for debugging tests
+ * The protocol messages are described here:
+ * https://docs.oracle.com/javase/8/docs/platform/jpda/jdwp/jdwp-protocol.html
  */
 public abstract class DebugTestBase {
 
@@ -347,6 +352,27 @@
     });
   }
 
+  protected final JUnit3Wrapper.Command checkStaticFieldClinitSafe(
+      String className, String fieldName, String fieldSignature, Value expectedValue) {
+    return inspect(t -> {
+      // TODO(65148874): The current Art from AOSP master hangs when requesting static fields
+      // when breaking in <clinit>. Last known good version is 7.0.0.
+      Assume.assumeTrue(
+          "Skipping test " + testName.getMethodName() + " because ART version is not supported",
+          isRunningJava() || ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_7_0_0));
+      checkStaticField(className, fieldName, fieldSignature, expectedValue);
+    });
+  }
+
+  protected final JUnit3Wrapper.Command checkStaticField(
+      String className, String fieldName, String fieldSignature, Value expectedValue) {
+    return inspect(t -> {
+      Value value = t.getStaticField(className, fieldName, fieldSignature);
+      Assert.assertEquals("Incorrect value for static '" + className + "." + fieldName + "'",
+          expectedValue, value);
+    });
+  }
+
   protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) {
     return t -> inspector.accept(t.debuggeeState);
   }
@@ -561,6 +587,7 @@
     public static class DebuggeeState implements FrameInspector {
 
       private class DebuggeeFrame implements FrameInspector {
+
         private final long frameId;
         private final Location location;
 
@@ -819,6 +846,77 @@
       public String getMethodSignature() {
         return getTopFrame().getMethodSignature();
       }
+
+      public Value getStaticField(String className, String fieldName, String fieldSignature) {
+        String classSignature = DescriptorUtils.javaTypeToDescriptor(className);
+        byte typeTag = TypeTag.CLASS;
+        long classId = getMirror().getClassID(classSignature);
+        Assert.assertFalse("No class named " + className + " found", classId == -1);
+
+        // The class is available, lookup and read the field.
+        long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+        return getField(getMirror(), classId, fieldId);
+      }
+
+      private long findField(VmMirror mirror, long classId, String fieldName,
+          String fieldSignature) {
+
+        boolean withGenericSignature = true;
+        CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+            ReferenceTypeCommandSet.FieldsWithGenericCommand);
+        commandPacket.setNextValueAsReferenceTypeID(classId);
+        ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+        if (replyPacket.getErrorCode() != Error.NONE) {
+          // Retry with older command ReferenceType.Fields.
+          withGenericSignature = false;
+          commandPacket.setCommand(ReferenceTypeCommandSet.FieldsCommand);
+          replyPacket = mirror.performCommand(commandPacket);
+          assert replyPacket.getErrorCode() == Error.NONE;
+        }
+
+        int fieldsCount = replyPacket.getNextValueAsInt();
+        LongList matchingFieldIds = new LongArrayList();
+        for (int i = 0; i < fieldsCount; ++i) {
+          long currentFieldId = replyPacket.getNextValueAsFieldID();
+          String currentFieldName = replyPacket.getNextValueAsString();
+          String currentFieldSignature = replyPacket.getNextValueAsString();
+          if (withGenericSignature) {
+            replyPacket.getNextValueAsString(); // Skip generic signature.
+          }
+          replyPacket.getNextValueAsInt(); // Skip modifiers.
+
+          // Filter fields based on name (and signature if there is).
+          if (fieldName.equals(currentFieldName)) {
+            if (fieldSignature == null || fieldSignature.equals(currentFieldSignature)) {
+              matchingFieldIds.add(currentFieldId);
+            }
+          }
+        }
+        Assert.assertTrue(replyPacket.isAllDataRead());
+
+        Assert.assertFalse("No field named " + fieldName + " found", matchingFieldIds.isEmpty());
+        // There must be only one matching field.
+        Assert.assertEquals("More than 1 field found: please specify a signature", 1,
+            matchingFieldIds.size());
+        return matchingFieldIds.getLong(0);
+      }
+
+      private Value getField(VmMirror mirror, long classId, long fieldId) {
+
+        CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+            ReferenceTypeCommandSet.GetValuesCommand);
+        commandPacket.setNextValueAsReferenceTypeID(classId);
+        commandPacket.setNextValueAsInt(1);
+        commandPacket.setNextValueAsFieldID(fieldId);
+        ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+        assert replyPacket.getErrorCode() == Error.NONE;
+
+        int fieldsCount = replyPacket.getNextValueAsInt();
+        assert fieldsCount == 1;
+        Value result = replyPacket.getNextValueAsValue();
+        Assert.assertTrue(replyPacket.isAllDataRead());
+        return result;
+      }
     }
 
     private static boolean inScope(long index, Variable var) {
@@ -1017,7 +1115,8 @@
           }
           Assert.assertTrue(replyPacket.isAllDataRead());
 
-          Assert.assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
+          Assert
+              .assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
           // There must be only one matching method
           Assert.assertEquals("More than 1 method found: please specify a signature", 1,
               matchingMethodIds.size());