Extend recognized set of signature-polymorphic methods

Bug: 66370036
Change-Id: Ib79a305263254a8b4e60acdc1d38e163e94a14dd
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index aeadd6e..153c2e6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -100,6 +100,7 @@
   public final DexString annotationDescriptor = createString("Ljava/lang/annotation/Annotation;");
   public final DexString throwableDescriptor = createString("Ljava/lang/Throwable;");
   public final DexString objectsDescriptor = createString("Ljava/util/Objects;");
+  public final DexString varHandleDescriptor = createString("Ljava/lang/invoke/VarHandle;");
 
   public final DexString constructorMethodName = createString(Constants.INSTANCE_INITIALIZER_NAME);
   public final DexString classConstructorMethodName = createString(Constants.CLASS_INITIALIZER_NAME);
@@ -141,6 +142,8 @@
   public final DexType stringBuilderType = createType("Ljava/lang/StringBuilder;");
   public final DexType stringBufferType = createType("Ljava/lang/StringBuffer;");
 
+  public final DexType varHandleType = createType(varHandleDescriptor);
+
   public final StringBuildingMethods stringBuilderMethods = new StringBuildingMethods(stringBuilderType);
   public final StringBuildingMethods stringBufferMethods = new StringBuildingMethods(stringBufferType);
   public final ObjectsMethods objectsMethods = new ObjectsMethods();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 4dc7e0a..258d8e6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -1058,11 +1058,20 @@
 
   public void addInvoke(Type type, DexItem item, DexProto callSiteProto, List<Value> arguments)
       throws ApiLevelException {
-    if (type == Invoke.Type.POLYMORPHIC && !options.canUseInvokePolymorphic()) {
-      throw new ApiLevelException(
-          AndroidApiLevel.O,
-          "MethodHandle.invoke and MethodHandle.invokeExact",
-          null /* sourceString */);
+    if (type == Invoke.Type.POLYMORPHIC) {
+      assert item instanceof DexMethod;
+      if (!options.canUseInvokePolymorphic()) {
+        throw new ApiLevelException(
+            AndroidApiLevel.O,
+            "MethodHandle.invoke and MethodHandle.invokeExact",
+            null /* sourceString */);
+      } else if (!options.canUseInvokePolymorphicOnVarHandle()
+          && ((DexMethod) item).getHolder() == options.itemFactory.varHandleType) {
+        throw new ApiLevelException(
+            AndroidApiLevel.P,
+            "Call to polymorphic signature of VarHandle",
+            null /* sourceString */);
+      }
     }
     add(Invoke.create(type, item, callSiteProto, null, arguments));
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 7f640c3..97dce33 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -135,7 +135,7 @@
   private static final String REFLECT_ARRAY_NEW_INSTANCE_NAME = "newInstance";
   private static final String REFLECT_ARRAY_NEW_INSTANCE_DESC =
       "(Ljava/lang/Class;[I)Ljava/lang/Object;";
-  private static final String METHODHANDLE_INVOKE_OR_INVOKEEXACT_DESC =
+  private static final String POLYMORPHIC_SIGNATURE_DESC =
       "([Ljava/lang/Object;)Ljava/lang/Object;";
 
   // Language types.
@@ -2539,8 +2539,7 @@
           DexProto callSiteProto = null;
           DexMethod targetMethod = method;
           if (invokeType == Invoke.Type.POLYMORPHIC) {
-            targetMethod = application.getMethod(
-                insn.owner, insn.name, METHODHANDLE_INVOKE_OR_INVOKEEXACT_DESC);
+            targetMethod = application.getMethod(insn.owner, insn.name, POLYMORPHIC_SIGNATURE_DESC);
             callSiteProto = application.getProto(insn.desc);
           }
           builder.addInvoke(invokeType, targetMethod, callSiteProto, types, registers);
@@ -2970,7 +2969,52 @@
   }
 
   private boolean isCallToPolymorphicSignatureMethod(MethodInsnNode method) {
-    return method.owner.equals("java/lang/invoke/MethodHandle")
-        && (method.name.equals("invoke") || method.name.equals("invokeExact"));
+    if (method.owner.equals("java/lang/invoke/MethodHandle")) {
+      switch (method.name) {
+        case "invoke":
+        case "invokeExact":
+          return true;
+        default :
+          return false;
+      }
+    } else if (method.owner.equals("java/lang/invoke/VarHandle")) {
+      switch (method.name) {
+        case "compareAndExchange":
+        case "compareAndExchangeAcquire":
+        case "compareAndExchangeRelease":
+        case "compareAndSet":
+        case "get":
+        case "getAcquire":
+        case "getAndAdd":
+        case "getAndAddAcquire":
+        case "getAndAddRelease":
+        case "getAndBitwiseAnd":
+        case "getAndBitwiseAndAcquire":
+        case "getAndBitwiseAndRelease":
+        case "getAndBitwiseOr":
+        case "getAndBitwiseOrAcquire":
+        case "getAndBitwiseOrRelease":
+        case "getAndBitwiseXor":
+        case "getAndBitwiseXorAcquire":
+        case "getAndBitwiseXorRelease":
+        case "getAndSet":
+        case "getAndSetAcquire":
+        case "getAndSetRelease":
+        case "getOpaque":
+        case "getVolatile":
+        case "set":
+        case "setOpaque":
+        case "setRelease":
+        case "setVolatile":
+        case "weakCompareAndSet":
+        case "weakCompareAndSetAcquire":
+        case "weakCompareAndSetPlain":
+        case "weakCompareAndSetRelease":
+          return true;
+        default :
+          return false;
+      }
+    }
+    return false;
   }
 }
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 dd0a60c..8d7171f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -408,6 +408,10 @@
     }
   }
 
+  public boolean canUseInvokePolymorphicOnVarHandle() {
+    return minApiLevel >= AndroidApiLevel.P.getLevel();
+  }
+
   public boolean canUseInvokePolymorphic() {
     return minApiLevel >= AndroidApiLevel.O.getLevel();
   }
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/D8RunExamplesJava9Test.java
new file mode 100644
index 0000000..2eb9eaf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesJava9Test.java
@@ -0,0 +1,72 @@
+// 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;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.InternalCompilerError;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.UnaryOperator;
+
+public class D8RunExamplesJava9Test extends RunExamplesJava9Test<D8Command.Builder> {
+
+  class D8TestRunner extends TestRunner<D8TestRunner> {
+
+    D8TestRunner(String testName, String packageName, String mainClass) {
+      super(testName, packageName, mainClass);
+    }
+
+    @Override
+    D8TestRunner withMinApiLevel(int minApiLevel) {
+      return withBuilderTransformation(builder -> builder.setMinApiLevel(minApiLevel));
+    }
+
+    D8TestRunner withClasspath(Path... classpath) {
+      return withBuilderTransformation(b -> {
+        try {
+          return b.addClasspathFiles(classpath);
+        } catch (IOException e) {
+          throw new AssertionError(e);
+        }
+      });
+    }
+
+
+    @Override
+    void build(Path inputFile, Path out) throws Throwable {
+      D8Command.Builder builder = D8Command.builder();
+      for (UnaryOperator<D8Command.Builder> transformation : builderTransformations) {
+        builder = transformation.apply(builder);
+      }
+      // TODO(mikaelpeltier) Add new android.jar build from aosp and use it
+      builder.addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(AndroidApiLevel.P.getLevel())));
+      D8Command command = builder.addProgramFiles(inputFile).setOutputPath(out).build();
+      try {
+        ToolHelper.runD8(command, this::combinedOptionConsumer);
+      } catch (Unimplemented | CompilationError | InternalCompilerError re) {
+        throw re;
+      } catch (RuntimeException re) {
+        throw re.getCause() == null ? re : re.getCause();
+      }
+    }
+
+    D8TestRunner withIntermediate(boolean intermediate) {
+      return withBuilderTransformation(builder -> builder.setIntermediate(intermediate));
+    }
+
+    @Override
+    D8TestRunner self() {
+      return this;
+    }
+  }
+
+  @Override
+  D8TestRunner test(String testName, String packageName, String mainClass) {
+    return new D8TestRunner(testName, packageName, mainClass);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
new file mode 100644
index 0000000..9414a73
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesJava9Test.java
@@ -0,0 +1,53 @@
+// 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;
+
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+import org.junit.Test;
+
+public class R8RunExamplesJava9Test extends RunExamplesJava9Test<R8Command.Builder> {
+
+  class R8TestRunner extends TestRunner<R8TestRunner> {
+
+    R8TestRunner(String testName, String packageName, String mainClass) {
+      super(testName, packageName, mainClass);
+    }
+
+    @Override
+    R8TestRunner withMinApiLevel(int minApiLevel) {
+      return withBuilderTransformation(builder -> builder.setMinApiLevel(minApiLevel));
+    }
+
+    @Override
+    void build(Path inputFile, Path out) throws Throwable {
+      R8Command.Builder builder = R8Command.builder();
+      for (UnaryOperator<R8Command.Builder> transformation : builderTransformations) {
+        builder = transformation.apply(builder);
+      }
+      // TODO(mikaelpeltier) Add new android.jar build from aosp and use it
+      builder.addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(AndroidApiLevel.P.getLevel())));
+      R8Command command = builder.addProgramFiles(inputFile).setOutputPath(out).build();
+      ToolHelper.runR8(command, this::combinedOptionConsumer);
+    }
+
+    @Override
+    R8TestRunner self() {
+      return this;
+    }
+  }
+
+  @Override
+  R8TestRunner test(String testName, String packageName, String mainClass) {
+    return new R8TestRunner(testName, packageName, mainClass);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
new file mode 100644
index 0000000..37a1697
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -0,0 +1,272 @@
+// 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;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
+import com.android.tools.r8.utils.DexInspector.InstructionSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OffOrAuto;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+public abstract class RunExamplesJava9Test
+      <B extends BaseCommand.Builder<? extends BaseCommand, B>> {
+  static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_JAVA9_BUILD_DIR;
+
+  abstract class TestRunner<C extends TestRunner<C>> {
+    final String testName;
+    final String packageName;
+    final String mainClass;
+
+    Integer androidJarVersion = null;
+
+    final List<Consumer<InternalOptions>> optionConsumers = new ArrayList<>();
+    final List<Consumer<DexInspector>> dexInspectorChecks = new ArrayList<>();
+    final List<UnaryOperator<B>> builderTransformations = new ArrayList<>();
+
+    TestRunner(String testName, String packageName, String mainClass) {
+      this.testName = testName;
+      this.packageName = packageName;
+      this.mainClass = mainClass;
+    }
+
+    abstract C self();
+
+    C withDexCheck(Consumer<DexInspector> check) {
+      dexInspectorChecks.add(check);
+      return self();
+    }
+
+    C withClassCheck(Consumer<FoundClassSubject> check) {
+      return withDexCheck(inspector -> inspector.forAllClasses(check));
+    }
+
+    C withMethodCheck(Consumer<FoundMethodSubject> check) {
+      return withClassCheck(clazz -> clazz.forAllMethods(check));
+    }
+
+    <T extends InstructionSubject> C withInstructionCheck(
+        Predicate<InstructionSubject> filter, Consumer<T> check) {
+      return withMethodCheck(method -> {
+        if (method.isAbstract()) {
+          return;
+        }
+        Iterator<T> iterator = method.iterateInstructions(filter);
+        while (iterator.hasNext()) {
+          check.accept(iterator.next());
+        }
+      });
+    }
+
+    C withOptionConsumer(Consumer<InternalOptions> consumer) {
+      optionConsumers.add(consumer);
+      return self();
+    }
+
+    C withMainDexClass(String... classes) {
+      return withBuilderTransformation(builder -> builder.addMainDexClasses(classes));
+    }
+
+    C withInterfaceMethodDesugaring(OffOrAuto behavior) {
+      return withOptionConsumer(o -> o.interfaceMethodDesugaring = behavior);
+    }
+
+    C withTryWithResourcesDesugaring(OffOrAuto behavior) {
+      return withOptionConsumer(o -> o.tryWithResourcesDesugaring = behavior);
+    }
+
+    C withBuilderTransformation(UnaryOperator<B> builderTransformation) {
+      builderTransformations.add(builderTransformation);
+      return self();
+    }
+
+    void combinedOptionConsumer(InternalOptions options) {
+      for (Consumer<InternalOptions> consumer : optionConsumers) {
+        consumer.accept(options);
+      }
+    }
+
+    Path build() throws Throwable {
+      Path inputFile = getInputJar();
+      Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
+
+      build(inputFile, out);
+      return out;
+    }
+
+    Path getInputJar() {
+      return Paths.get(EXAMPLE_DIR, packageName + JAR_EXTENSION);
+    }
+
+    void run() throws Throwable {
+      if (minSdkErrorExpected(testName)) {
+        thrown.expect(ApiLevelException.class);
+      }
+
+      String qualifiedMainClass = packageName + "." + mainClass;
+      Path inputFile = getInputJar();
+      Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
+
+      build(inputFile, out);
+
+      if (!ToolHelper.artSupported()) {
+        return;
+      }
+
+      if (!dexInspectorChecks.isEmpty()) {
+        DexInspector inspector = new DexInspector(out);
+        for (Consumer<DexInspector> check : dexInspectorChecks) {
+          check.accept(inspector);
+        }
+      }
+
+      execute(testName, qualifiedMainClass, new Path[]{inputFile}, new Path[]{out});
+    }
+
+    abstract C withMinApiLevel(int minApiLevel);
+
+    C withAndroidJar(int androidJarVersion) {
+      assert this.androidJarVersion == null;
+      this.androidJarVersion = androidJarVersion;
+      return self();
+    }
+
+    abstract void build(Path inputFile, Path out) throws Throwable;
+  }
+
+  private static List<String> minSdkErrorExpected =
+      ImmutableList.of("varhandle-error-due-to-min-sdk");
+
+  private static Map<DexVm.Version, List<String>> failsOn =
+      ImmutableMap.of(
+          DexVm.Version.V4_4_4, ImmutableList.of(
+              // Dex version not supported
+              "varhandle"
+          ),
+          DexVm.Version.V5_1_1, ImmutableList.of(
+              // Dex version not supported
+              "varhandle"
+          ),
+          DexVm.Version.V6_0_1, ImmutableList.of(
+              // Dex version not supported
+              "varhandle"
+          ),
+          DexVm.Version.V7_0_0, ImmutableList.of(
+              // Dex version not supported
+              "varhandle"
+          ),
+          DexVm.Version.DEFAULT, ImmutableList.of(
+              // TODO(mikaelpeltier): Update runtime when the support will be ready
+              "varhandle"
+          )
+      );
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
+    DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
+    return failsOn.containsKey(vmVersion)
+        && failsOn.get(vmVersion).contains(name);
+  }
+
+  boolean expectedToFail(String name) {
+    return failsOn(failsOn, name);
+  }
+
+  boolean minSdkErrorExpected(String testName) {
+    return minSdkErrorExpected.contains(testName);
+  }
+
+  @Test
+  public void varHAndle() throws Throwable {
+    test("varhandle", "varhandle", "VarHandleTests")
+        .withMinApiLevel(AndroidApiLevel.P.getLevel())
+        .run();
+  }
+
+  @Test
+  public void varHandleErrorDueToMinSdk() throws Throwable {
+    test("varhandle-error-due-to-min-sdk", "varhandle", "VarHandleTests")
+        .withMinApiLevel(AndroidApiLevel.O.getLevel())
+        .run();
+  }
+
+  abstract RunExamplesJava9Test<B>.TestRunner<?> test(String testName, String packageName,
+      String mainClass);
+
+  void execute(
+      String testName,
+      String qualifiedMainClass, Path[] jars, Path[] dexes)
+      throws IOException {
+
+    boolean expectedToFail = expectedToFail(testName);
+    if (expectedToFail) {
+      thrown.expect(Throwable.class);
+    }
+    String output = ToolHelper.runArtNoVerificationErrors(
+        Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
+        qualifiedMainClass,
+        null);
+    if (!expectedToFail) {
+      ToolHelper.ProcessResult javaResult =
+          ToolHelper.runJava(
+              Arrays.stream(jars).map(path -> path.toString()).collect(Collectors.toList()),
+              qualifiedMainClass);
+      assertEquals("JVM run failed", javaResult.exitCode, 0);
+      assertTrue(
+          "JVM output does not match art output.\n\tjvm: "
+              + javaResult.stdout
+              + "\n\tart: "
+              + output.replace("\r", ""),
+          output.equals(javaResult.stdout.replace("\r", "")));
+    }
+  }
+
+  protected DexInspector getMainDexInspector(Path zip)
+      throws ZipException, IOException, ExecutionException {
+    try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+      try (InputStream in =
+          zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
+        return new DexInspector(AndroidApp.fromDexProgramData(ByteStreams.toByteArray(in)));
+      }
+    }
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 80c1e76..12d1d5a 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -64,6 +64,7 @@
   public static final String EXAMPLES_ANDROID_N_BUILD_DIR = BUILD_DIR + "test/examplesAndroidN/";
   public static final String EXAMPLES_ANDROID_O_BUILD_DIR = BUILD_DIR + "test/examplesAndroidO/";
   public static final String EXAMPLES_ANDROID_P_BUILD_DIR = BUILD_DIR + "test/examplesAndroidP/";
+  public static final String EXAMPLES_JAVA9_BUILD_DIR = BUILD_DIR + "test/examplesJava9/";
   public static final String SMALI_BUILD_DIR = BUILD_DIR + "test/smali/";
 
   public static final String LINE_SEPARATOR = StringUtils.LINE_SEPARATOR;