Reland "Allow unicode range 0x10000..0x10ffff in identifiers."

Which includes:

- Iterating by code points instead of by chars where we're checking
  whether it's a valid Dex SimpleNameChar.
- Extending the InvalidMethodNames test with more thorough JVM and Art
  testing and additional characters.

Bug:
Change-Id: I87b908d5ca0e8ce6f7ca673f8522b06ce1456d10
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index ad187ec..9036fd7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -25,7 +25,7 @@
 
   public DexString(String string) {
     this.size = string.length();
-    this.content = encode(string);
+    this.content = encodeToMutf8(string);
   }
 
   @Override
@@ -118,7 +118,7 @@
   }
 
   // Inspired from /dex/src/main/java/com/android/dex/Mutf8.java
-  private static byte[] encode(String string) {
+  public static byte[] encodeToMutf8(String string) {
     byte[] result = new byte[countBytes(string)];
     int offset = 0;
     for (int i = 0; i < string.length(); i++) {
@@ -207,15 +207,12 @@
     if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') {
       return false;
     }
-    for (int i = 1; i < string.length() - 1; i++) {
-      char ch = string.charAt(i);
-      if (ch == '/') {
-        continue;
+    int cp;
+    for (int i = 1; i < string.length() - 1; i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (cp != '/' && !IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      if (IdentifierUtils.isDexIdentifierPart(ch)) {
-        continue;
-      }
-      return false;
     }
     return true;
   }
@@ -232,12 +229,12 @@
             string.equals(Constants.CLASS_INITIALIZER_NAME))) {
       return true;
     }
-    for (int i = 0; i < string.length(); i++) {
-      char ch = string.charAt(i);
-      if (IdentifierUtils.isDexIdentifierPart(ch)) {
-        continue;
+    int cp;
+    for (int i = 0; i < string.length(); i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      return false;
     }
     return true;
   }
@@ -249,18 +246,19 @@
     int start = 0;
     int end = string.length();
     if (string.charAt(0) == '<') {
-      if (string.charAt(string.length() - 1) == '>') {
+      if (string.charAt(end - 1) == '>') {
         start = 1;
-        end = string.length() - 1;
+        --end;
       } else {
         return false;
       }
     }
-    for (int i = start; i < end; i++) {
-      if (IdentifierUtils.isDexIdentifierPart(string.charAt(i))) {
-        continue;
+    int cp;
+    for (int i = start; i < end; i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      return false;
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 23a92a0..deab69f 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -69,11 +69,11 @@
   private int lineOffset = 0;
   private String line;
 
-  private char peek() {
-    return peek(0);
+  private int peekCodePoint() {
+    return lineOffset < line.length() ? line.codePointAt(lineOffset) : '\n';
   }
 
-  private char peek(int distance) {
+  private char peekChar(int distance) {
     return lineOffset + distance < line.length()
         ? line.charAt(lineOffset + distance)
         : '\n';
@@ -83,7 +83,17 @@
     return lineOffset < line.length();
   }
 
-  private char next() {
+  private int nextCodePoint() {
+    try {
+      int cp = line.codePointAt(lineOffset);
+      lineOffset += Character.charCount(cp);
+      return cp;
+    } catch (ArrayIndexOutOfBoundsException e) {
+      throw new ParseException("Unexpected end of line");
+    }
+  }
+
+  private char nextChar() {
     try {
       return line.charAt(lineOffset++);
     } catch (ArrayIndexOutOfBoundsException e) {
@@ -128,8 +138,8 @@
 
   // Helpers for common pattern
   private void skipWhitespace() {
-    while (Character.isWhitespace(peek())) {
-      next();
+    while (Character.isWhitespace(peekCodePoint())) {
+      nextCodePoint();
     }
   }
 
@@ -137,7 +147,7 @@
     if (!hasNext()) {
       throw new ParseException("Expected '" + c + "'", true);
     }
-    if (next() != c) {
+    if (nextChar() != c) {
       throw new ParseException("Expected '" + c + "'");
     }
     return c;
@@ -197,7 +207,7 @@
       // In the last round we're only here to flush the last line read (which may trigger adding a
       // new MemberNaming) and flush activeMemberNaming, so skip parsing.
       if (!lastRound) {
-        if (!Character.isWhitespace(peek())) {
+        if (!Character.isWhitespace(peekCodePoint())) {
           lastRound = true;
           continue;
         }
@@ -212,9 +222,9 @@
           expect(':');
         }
         signature = parseSignature();
-        if (peek() == ':') {
+        if (peekChar(0) == ':') {
           // This is a mapping or inlining definition
-          next();
+          nextChar();
           originalRange = maybeParseRangeOrInt();
           if (originalRange == null) {
             throw new ParseException("No number follows the colon after the method signature.");
@@ -294,22 +304,22 @@
 
   private void skipIdentifier(boolean allowInit) {
     boolean isInit = false;
-    if (allowInit && peek() == '<') {
+    if (allowInit && peekChar(0) == '<') {
       // swallow the leading < character
-      next();
+      nextChar();
       isInit = true;
     }
-    if (!IdentifierUtils.isDexIdentifierStart(peek())) {
+    if (!IdentifierUtils.isDexIdentifierStart(peekCodePoint())) {
       throw new ParseException("Identifier expected");
     }
-    next();
-    while (IdentifierUtils.isDexIdentifierPart(peek())) {
-      next();
+    nextCodePoint();
+    while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
+      nextCodePoint();
     }
     if (isInit) {
       expect('>');
     }
-    if (IdentifierUtils.isDexIdentifierPart(peek())) {
+    if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
       throw new ParseException("End of identifier expected");
     }
   }
@@ -330,8 +340,8 @@
   private String parseMethodName() {
     int startPosition = lineOffset;
     skipIdentifier(true);
-    while (peek() == '.') {
-      next();
+    while (peekChar(0) == '.') {
+      nextChar();
       skipIdentifier(true);
     }
     return substring(startPosition);
@@ -340,13 +350,13 @@
   private String parseType(boolean allowArray) {
     int startPosition = lineOffset;
     skipIdentifier(false);
-    while (peek() == '.') {
-      next();
+    while (peekChar(0) == '.') {
+      nextChar();
       skipIdentifier(false);
     }
     if (allowArray) {
-      while (peek() == '[') {
-        next();
+      while (peekChar(0) == '[') {
+        nextChar();
         expect(']');
       }
     }
@@ -358,15 +368,15 @@
     expect(' ');
     String name = parseMethodName();
     Signature signature;
-    if (peek() == '(') {
-      next();
+    if (peekChar(0) == '(') {
+      nextChar();
       String[] arguments;
-      if (peek() == ')') {
+      if (peekChar(0) == ')') {
         arguments = new String[0];
       } else {
         List<String> items = new LinkedList<>();
         items.add(parseType(true));
-        while (peek() != ')') {
+        while (peekChar(0) != ')') {
           expect(',');
           items.add(parseType(true));
         }
@@ -386,9 +396,9 @@
   }
 
   private boolean acceptArrow() {
-    if (peek() == '-' && peek(1) == '>') {
-      next();
-      next();
+    if (peekChar(0) == '-' && peekChar(1) == '>') {
+      nextChar();
+      nextChar();
       return true;
     }
     return false;
@@ -396,22 +406,26 @@
 
   private boolean acceptString(String s) {
     for (int i = 0; i < s.length(); i++) {
-      if (peek(i) != s.charAt(i)) {
+      if (peekChar(i) != s.charAt(i)) {
         return false;
       }
     }
     for (int i = 0; i < s.length(); i++) {
-      next();
+      nextChar();
     }
     return true;
   }
 
+  private boolean isSimpleDigit(char c) {
+    return '0' <= c && c <= '9';
+  }
+
   private Object maybeParseRangeOrInt() {
-    if (!Character.isDigit(peek())) {
+    if (!isSimpleDigit(peekChar(0))) {
       return null;
     }
     int from = parseNumber();
-    if (peek() != ':') {
+    if (peekChar(0) != ':') {
       return from;
     }
     expect(':');
@@ -421,13 +435,13 @@
 
   private int parseNumber() {
     int result = 0;
-    if (!Character.isDigit(peek())) {
+    if (!isSimpleDigit(peekChar(0))) {
       throw new ParseException("Number expected");
     }
     do {
       result *= 10;
-      result += Character.getNumericValue(next());
-    } while (Character.isDigit(peek()));
+      result += Character.getNumericValue(nextChar());
+    } while (isSimpleDigit(peekChar(0)));
     return result;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5cb4ce8..35c8e47 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1104,14 +1104,15 @@
       return Integer.parseInt(s);
     }
 
-    private final Predicate<Character> CLASS_NAME_PREDICATE =
-        character -> IdentifierUtils.isDexIdentifierPart(character)
-            || character == '.'
-            || character == '*'
-            || character == '?'
-            || character == '%'
-            || character == '['
-            || character == ']';
+    private final Predicate<Integer> CLASS_NAME_PREDICATE =
+        codePoint ->
+            IdentifierUtils.isDexIdentifierPart(codePoint)
+                || codePoint == '.'
+                || codePoint == '*'
+                || codePoint == '?'
+                || codePoint == '%'
+                || codePoint == '['
+                || codePoint == ']';
 
     private String acceptClassName() {
       return acceptString(CLASS_NAME_PREDICATE);
@@ -1123,7 +1124,7 @@
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
+        int current = contents.codePointAt(end);
         if (currentBackreference != null) {
           if (current == '>') {
             try {
@@ -1138,10 +1139,10 @@
                   origin, getPosition()));
             }
             currentBackreference = null;
-          } else if (Character.isDigit(current)
+          } else if (('0' <= current && current <= '9')
               // Only collect integer literal for the backreference.
               || (current == '-' && currentBackreference.length() == 0)) {
-            currentBackreference.append(current);
+            currentBackreference.append((char) current);
           } else if (kind == IdentifierType.CLASS_NAME) {
             throw reporter.fatalError(new StringDiagnostic(
                 "Use of generics not allowed for java type.", origin, getPosition()));
@@ -1150,12 +1151,12 @@
             // collection of the backreference.
             currentBackreference = null;
           }
-          end++;
+          end += Character.charCount(current);
         } else if (CLASS_NAME_PREDICATE.test(current) || current == '>') {
-          end++;
+          end += Character.charCount(current);
         } else if (current == '<') {
           currentBackreference = new StringBuilder();
-          end++;
+          ++end;
         } else {
           break;
         }
@@ -1177,14 +1178,15 @@
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
+        int current = contents.codePointAt(end);
         if (current == '.' && !eof(end + 1) && peekCharAt(end + 1) == '.') {
           // The grammar is ambiguous. End accepting before .. token used in return ranges.
           break;
         }
-        if ((start == end && IdentifierUtils.isDexIdentifierStart(current)) ||
-            ((start < end) && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
-          end++;
+        if ((start == end && IdentifierUtils.isDexIdentifierStart(current))
+            || ((start < end)
+                && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
+          end += Character.charCount(current);
         } else {
           break;
         }
@@ -1216,18 +1218,21 @@
     }
 
     private String acceptPattern() {
-      return acceptString(character ->
-          IdentifierUtils.isDexIdentifierPart(character) || character == '!' || character == '*');
+      return acceptString(
+          codePoint ->
+              IdentifierUtils.isDexIdentifierPart(codePoint)
+                  || codePoint == '!'
+                  || codePoint == '*');
     }
 
-    private String acceptString(Predicate<Character> characterAcceptor) {
+    private String acceptString(Predicate<Integer> codepointAcceptor) {
       skipWhitespace();
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
-        if (characterAcceptor.test(current)) {
-          end++;
+        int current = contents.codePointAt(end);
+        if (codepointAcceptor.test(current)) {
+          end += Character.charCount(current);
         } else {
           break;
         }
diff --git a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
index 3097a07..5fb617b 100644
--- a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -5,43 +5,27 @@
 package com.android.tools.r8.utils;
 
 public class IdentifierUtils {
-  public static boolean isDexIdentifierStart(char ch) {
+  public static boolean isDexIdentifierStart(int cp) {
     // Dex does not have special restrictions on the first char of an identifier.
-    return isDexIdentifierPart(ch);
+    return isDexIdentifierPart(cp);
   }
 
-  public static boolean isDexIdentifierPart(char ch) {
-    return isSimpleNameChar(ch);
+  public static boolean isDexIdentifierPart(int cp) {
+    return isSimpleNameChar(cp);
   }
 
-  private static boolean isSimpleNameChar(char ch) {
-    if (ch >= 'A' && ch <= 'Z') {
-      return true;
-    }
-    if (ch >= 'a' && ch <= 'z') {
-      return true;
-    }
-    if (ch >= '0' && ch <= '9') {
-      return true;
-    }
-    if (ch == '$' || ch == '-' || ch == '_') {
-      return true;
-    }
-    if (ch >= 0x00a1 && ch <= 0x1fff) {
-      return true;
-    }
-    if (ch >= 0x2010 && ch <= 0x2027) {
-      return true;
-    }
-    if (ch >= 0x2030 && ch <= 0xd7ff) {
-      return true;
-    }
-    if (ch >= 0xe000 && ch <= 0xffef) {
-      return true;
-    }
-    if (ch >= 0x10000 && ch <= 0x10ffff) {
-      return true;
-    }
-    return false;
+  private static boolean isSimpleNameChar(int cp) {
+    // See https://source.android.com/devices/tech/dalvik/dex-format#string-syntax.
+    return ('A' <= cp && cp <= 'Z')
+        || ('a' <= cp && cp <= 'z')
+        || ('0' <= cp && cp <= '9')
+        || cp == '$'
+        || cp == '-'
+        || cp == '_'
+        || (0x00a1 <= cp && cp <= 0x1fff)
+        || (0x2010 <= cp && cp <= 0x2027)
+        || (0x2030 <= cp && cp <= 0xd7ff)
+        || (0xe000 <= cp && cp <= 0xffef)
+        || (0x10000 <= cp && cp <= 0x10ffff);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
index 7caac8e..bad64ac 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
@@ -4,14 +4,25 @@
 package com.android.tools.r8.jasmin;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Bytes;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.zip.Adler32;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -20,64 +31,183 @@
 @RunWith(Parameterized.class)
 public class InvalidFieldNames extends JasminTestBase {
 
-  public boolean runsOnJVM;
-  public String name;
+  private static final String CLASS_NAME = "Test";
+  private static String FIELD_VALUE = "42";
 
-  public InvalidFieldNames(String name, boolean runsOnJVM) {
-    this.name = name;
-    this.runsOnJVM = runsOnJVM;
-  }
-
-  private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
-    if (runsOnJVM) {
-      String javaResult = runOnJava(builder, main);
-      assertEquals(expected, javaResult);
-    }
-    String artResult = null;
-    try {
-      artResult = runOnArtD8(builder, main);
-      fail();
-    } catch (CompilationError t) {
-      assertTrue(t.getMessage().contains(name));
-    }
-    assertNull("Invalid dex field names should be rejected.", artResult);
-  }
-
-  @Parameters
+  @Parameters(name = "\"{0}\", jvm: {1}, art: {2}")
   public static Collection<Object[]> data() {
-    return Arrays.asList(new Object[][]{
-        {"\u00a0", !ToolHelper.isJava9Runtime()},
-        {"\u2000", !ToolHelper.isJava9Runtime()},
-        {"\u200f", !ToolHelper.isJava9Runtime()},
-        {"\u2028", !ToolHelper.isJava9Runtime()},
-        {"\u202f", !ToolHelper.isJava9Runtime()},
-        {"\ud800", !ToolHelper.isJava9Runtime()},
-        {"\udfff", !ToolHelper.isJava9Runtime()},
-        {"\ufff0", !ToolHelper.isJava9Runtime()},
-        {"\uffff", !ToolHelper.isJava9Runtime()},
-        {"a/b", false},
-        {"<a", false},
-        {"a>", !ToolHelper.isJava9Runtime()},
-        {"a<b>", !ToolHelper.isJava9Runtime()},
-        {"<a>b", !ToolHelper.isJava9Runtime()}
-    });
+    return Arrays.asList(
+        new Object[][] {
+          {new TestStringParameter("azAZ09$_"), true, true},
+          {new TestStringParameter("_"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("a-b"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u00a0"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u00a1"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u1fff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2000"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u200f"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u2010"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2027"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2028"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u202f"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u2030"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ud7ff"), !ToolHelper.isJava9Runtime(), true},
+
+          // Standalone high and low surrogates.
+          {new TestStringParameter("\ud800"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udbff"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udc00"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udfff"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\ue000"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\uffef"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ufff0"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\uffff"), !ToolHelper.isJava9Runtime(), false},
+
+          // Single and double code points above 0x10000.
+          {new TestStringParameter("\ud800\udc00"), true, true},
+          {new TestStringParameter("\ud800\udcfa"), true, true},
+          {new TestStringParameter("\ud800\udcfb"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ud800\udc00\ud800\udcfa"), true, true},
+          {new TestStringParameter("\ud800\udc00\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("a/b"), false, false},
+          {new TestStringParameter("<a"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("a>"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("a<b>"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("<a>b"), !ToolHelper.isJava9Runtime(), false}
+        });
   }
 
-  @Test
-  public void invalidFieldNames() throws Exception {
-    JasminBuilder builder = new JasminBuilder();
-    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+  // TestStringParameter is a String with modified toString() which prints \\uXXXX for
+  // characters outside 0x20..0x7e.
+  static class TestStringParameter {
+    private final String value;
 
-    clazz.addStaticField(name, "I", "42");
+    TestStringParameter(String value) {
+      this.value = value;
+    }
+
+    String getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return StringUtils.toASCIIString(value);
+    }
+  }
+
+  private String name;
+  private boolean validForJVM;
+  private boolean validForArt;
+
+  public InvalidFieldNames(TestStringParameter name, boolean validForJVM, boolean validForArt) {
+    this.name = name.getValue();
+    this.validForJVM = validForJVM;
+    this.validForArt = validForArt;
+  }
+
+  private byte[] trimLastZeroByte(byte[] bytes) {
+    assert bytes.length > 0 && bytes[bytes.length - 1] == 0;
+    byte[] result = new byte[bytes.length - 1];
+    System.arraycopy(bytes, 0, result, 0, result.length);
+    return result;
+  }
+
+  private JasminBuilder createJasminBuilder() {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass(CLASS_NAME);
+
+    clazz.addStaticField(name, "I", FIELD_VALUE);
 
     clazz.addMainMethod(
         ".limit stack 2",
         ".limit locals 1",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
-        "  getstatic Test/" + name + " I",
+        "  getstatic " + CLASS_NAME + "/" + name + " I",
         "  invokevirtual java/io/PrintStream.print(I)V",
         "  return");
+    return builder;
+  }
 
-    runTest(builder, clazz.name, "42");
+  private AndroidApp createAppWithSmali() throws Exception {
+
+    SmaliBuilder smaliBuilder = new SmaliBuilder(CLASS_NAME);
+    String originalSourceFile = CLASS_NAME + FileUtils.JAVA_EXTENSION;
+    smaliBuilder.setSourceFile(originalSourceFile);
+
+    // We're using a valid placeholder string which will be replaced by the actual name.
+    byte[] nameMutf8 = trimLastZeroByte(DexString.encodeToMutf8(name));
+
+    String placeholderString = Strings.repeat("A", nameMutf8.length);
+
+    smaliBuilder.addStaticField(placeholderString, "I", FIELD_VALUE);
+    MethodSignature mainSignature =
+        smaliBuilder.addMainMethod(
+            1,
+            "sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+            "sget v0, LTest;->" + placeholderString + ":I",
+            "invoke-virtual {p0, v0}, Ljava/io/PrintStream;->print(I)V",
+            "return-void");
+    byte[] dexCode = smaliBuilder.compile();
+
+    // Replace placeholder by mutf8-encoded name
+    byte[] placeholderBytes = trimLastZeroByte(DexString.encodeToMutf8(placeholderString));
+    assert placeholderBytes.length == nameMutf8.length;
+    int index = Bytes.indexOf(dexCode, placeholderBytes);
+    if (index >= 0) {
+      System.arraycopy(nameMutf8, 0, dexCode, index, nameMutf8.length);
+    }
+    assert Bytes.indexOf(dexCode, placeholderBytes) < 0;
+
+    // Update checksum
+    Adler32 adler = new Adler32();
+    adler.update(dexCode, Constants.SIGNATURE_OFFSET, dexCode.length - Constants.SIGNATURE_OFFSET);
+    int checksum = (int) adler.getValue();
+    for (int i = 0; i < 4; ++i) {
+      dexCode[Constants.CHECKSUM_OFFSET + i] = (byte) (checksum >> (8 * i) & 0xff);
+    }
+
+    return AndroidApp.builder()
+        .addDexProgramData(dexCode, new PathOrigin(Paths.get(originalSourceFile)))
+        .build();
+  }
+
+  @Test
+  public void invalidFieldNames() throws Exception {
+    JasminBuilder jasminBuilder = createJasminBuilder();
+
+    if (validForJVM) {
+      String javaResult = runOnJava(jasminBuilder, CLASS_NAME);
+      assertEquals(FIELD_VALUE, javaResult);
+    } else {
+      try {
+        runOnJava(jasminBuilder, CLASS_NAME);
+        fail("Should have failed on JVM.");
+      } catch (AssertionError e) {
+        // Silent on expected failure.
+      }
+    }
+
+    if (validForArt) {
+      String artResult = runOnArtD8(jasminBuilder, CLASS_NAME);
+      assertEquals(FIELD_VALUE, artResult);
+    } else {
+      // Make sure the compiler fails.
+      try {
+        runOnArtD8(jasminBuilder, CLASS_NAME);
+        fail("D8 should have rejected this case.");
+      } catch (CompilationError t) {
+        assertTrue(t.getMessage().contains(name));
+      }
+
+      // Make sure ART also fail, if D8 rejects it.
+      try {
+        runOnArt(createAppWithSmali(), CLASS_NAME);
+        fail("Art should have failed.");
+      } catch (AssertionError e) {
+        // Silent on expected failure.
+      }
+    }
   }
 }