String concatenation desugaring.

Implements desugaring of string concatenations handles with invokedynamic
call bootstrapped with makeConcat or makeConcatWithConstants methods of
StringConcatFactory class.

Note, this implementation does not implement support of `constants` passed
to makeConcatWithConstants bootstrap method, since they don't seem to be used.

Bug: 67230058
Change-Id: I890c9390c1b8514e3d2ae69af0d2a8fceafea15d
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 2f80250..aeadd6e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -94,6 +94,7 @@
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
   public final DexString stringArrayDescriptor = createString("[Ljava/lang/String;");
   public final DexString objectDescriptor = createString("Ljava/lang/Object;");
+  public final DexString objectArrayDescriptor = createString("[Ljava/lang/Object;");
   public final DexString classDescriptor = createString("Ljava/lang/Class;");
   public final DexString enumDescriptor = createString("Ljava/lang/Enum;");
   public final DexString annotationDescriptor = createString("Ljava/lang/annotation/Annotation;");
@@ -132,6 +133,7 @@
   public final DexType stringType = createType(stringDescriptor);
   public final DexType stringArrayType = createType(stringArrayDescriptor);
   public final DexType objectType = createType(objectDescriptor);
+  public final DexType objectArrayType = createType(objectArrayDescriptor);
   public final DexType enumType = createType(enumDescriptor);
   public final DexType annotationType = createType(annotationDescriptor);
   public final DexType throwableType = createType(throwableDescriptor);
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 6450e7f..2312320 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
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import com.android.tools.r8.ir.desugar.StringConcatRewriter;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.Inliner;
@@ -65,6 +66,7 @@
   private final Timing timing;
   public final AppInfo appInfo;
   private final Outliner outliner;
+  private final StringConcatRewriter stringConcatRewriter;
   private final LambdaRewriter lambdaRewriter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final InternalOptions options;
@@ -94,6 +96,7 @@
     this.options = options;
     this.printer = printer;
     this.codeRewriter = new CodeRewriter(appInfo, libraryMethodsReturningReceiver(), options);
+    this.stringConcatRewriter = new StringConcatRewriter(options.itemFactory);
     this.lambdaRewriter = options.enableDesugaring ? new LambdaRewriter(this) : null;
     this.interfaceMethodRewriter =
         (options.enableDesugaring && enableInterfaceMethodDesugaring())
@@ -551,6 +554,8 @@
       codeRewriter.rewriteThrowableAddAndGetSuppressed(code);
     }
 
+    stringConcatRewriter.desugarStringConcats(method.method, code);
+
     if (lambdaRewriter != null) {
       lambdaRewriter.desugarLambdas(method, code);
       assert code.isConsistentSSA();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index 2004742..357a813 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -46,7 +46,6 @@
   private static final String LOOKUP_TYPE_DESCR = "Ljava/lang/invoke/MethodHandles$Lookup;";
   private static final String METHODTYPE_TYPE_DESCR = "Ljava/lang/invoke/MethodType;";
   private static final String METHODHANDLE_TYPE_DESCR = "Ljava/lang/invoke/MethodHandle;";
-  private static final String OBJECT_ARRAY_TYPE_DESCR = "[Ljava/lang/Object;";
   private static final String SERIALIZABLE_TYPE_DESCR = "Ljava/io/Serializable;";
   private static final String SERIALIZED_LAMBDA_TYPE_DESCR = "Ljava/lang/invoke/SerializedLambda;";
 
@@ -103,7 +102,6 @@
     DexType lookupType = factory.createType(LOOKUP_TYPE_DESCR);
     DexType methodTypeType = factory.createType(METHODTYPE_TYPE_DESCR);
     DexType methodHandleType = factory.createType(METHODHANDLE_TYPE_DESCR);
-    DexType objectArrayType = factory.createType(OBJECT_ARRAY_TYPE_DESCR);
 
     this.metafactoryMethod = factory.createMethod(metafactoryType,
         factory.createProto(callSiteType, lookupType, factory.stringType, methodTypeType,
@@ -112,7 +110,7 @@
 
     this.metafactoryAltMethod = factory.createMethod(metafactoryType,
         factory.createProto(callSiteType, lookupType, factory.stringType, methodTypeType,
-            objectArrayType),
+            factory.objectArrayType),
         factory.createString(METAFACTORY_ALT_METHOD_NAME));
 
     this.constructorName = factory.createString(Constants.INSTANCE_INITIALIZER_NAME);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/StringConcatRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/StringConcatRewriter.java
new file mode 100644
index 0000000..9d34e80
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/StringConcatRewriter.java
@@ -0,0 +1,442 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeCustom;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.MoveType;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/** String concatenation desugaring rewriter. */
+public class StringConcatRewriter {
+  private static final String CONCAT_FACTORY_TYPE_DESCR = "Ljava/lang/invoke/StringConcatFactory;";
+  private static final String CALLSITE_TYPE_DESCR = "Ljava/lang/invoke/CallSite;";
+  private static final String LOOKUP_TYPE_DESCR = "Ljava/lang/invoke/MethodHandles$Lookup;";
+  private static final String METHOD_TYPE_TYPE_DESCR = "Ljava/lang/invoke/MethodType;";
+
+  private static final String MAKE_CONCAT = "makeConcat";
+  private static final String MAKE_CONCAT_WITH_CONSTANTS = "makeConcatWithConstants";
+  private static final String TO_STRING = "toString";
+  private static final String APPEND = "append";
+
+  private final DexItemFactory factory;
+
+  private final DexMethod makeConcat;
+  private final DexMethod makeConcatWithConstants;
+
+  private final DexMethod stringBuilderInit;
+  private final DexMethod stringBuilderToString;
+
+  private final Map<DexType, DexMethod> paramTypeToAppendMethod = new IdentityHashMap<>();
+  private final DexMethod defaultAppendMethod;
+
+  public StringConcatRewriter(DexItemFactory factory) {
+    assert factory != null;
+    this.factory = factory;
+
+    DexType factoryType = factory.createType(CONCAT_FACTORY_TYPE_DESCR);
+    DexType callSiteType = factory.createType(CALLSITE_TYPE_DESCR);
+    DexType lookupType = factory.createType(LOOKUP_TYPE_DESCR);
+    DexType methodTypeType = factory.createType(METHOD_TYPE_TYPE_DESCR);
+
+    makeConcat = factory.createMethod(factoryType,
+        factory.createProto(callSiteType, lookupType, factory.stringType, methodTypeType),
+        factory.createString(MAKE_CONCAT));
+
+    makeConcatWithConstants = factory.createMethod(factoryType,
+        factory.createProto(callSiteType, lookupType, factory.stringType, methodTypeType,
+            factory.stringType, factory.objectArrayType),
+        factory.createString(MAKE_CONCAT_WITH_CONSTANTS));
+
+    stringBuilderInit = factory.createMethod(
+        factory.stringBuilderType, factory.createProto(factory.voidType),
+        factory.createString(Constants.INSTANCE_INITIALIZER_NAME));
+
+    stringBuilderToString = factory.createMethod(
+        factory.stringBuilderType, factory.createProto(factory.stringType),
+        factory.createString(TO_STRING));
+
+    // Mapping of type parameters to methods of StringBuilder.
+    DexType stringBuilderType = factory.stringBuilderType;
+    paramTypeToAppendMethod.put(factory.booleanType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.booleanType), APPEND));
+    paramTypeToAppendMethod.put(factory.charType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.charType), APPEND));
+    paramTypeToAppendMethod.put(factory.byteType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.intType), APPEND));
+    paramTypeToAppendMethod.put(factory.shortType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.intType), APPEND));
+    paramTypeToAppendMethod.put(factory.intType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.intType), APPEND));
+    paramTypeToAppendMethod.put(factory.longType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.longType), APPEND));
+    paramTypeToAppendMethod.put(factory.floatType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.floatType), APPEND));
+    paramTypeToAppendMethod.put(factory.doubleType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.doubleType), APPEND));
+    paramTypeToAppendMethod.put(factory.stringType, factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.stringType), APPEND));
+    defaultAppendMethod = factory.createMethod(
+        stringBuilderType, factory.createProto(stringBuilderType, factory.objectType), APPEND);
+  }
+
+  /**
+   * Find and desugar all string concatenations implemented via `invokedynamic` call
+   * to either of StringConcatFactory bootstrap methods.
+   */
+  public void desugarStringConcats(DexMethod method, IRCode code) {
+    ListIterator<BasicBlock> blocks = code.listIterator();
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      InstructionListIterator instructions = block.listIterator();
+      while (instructions.hasNext()) {
+        Instruction instruction = instructions.next();
+        if (!instruction.isInvokeCustom()) {
+          continue;
+        }
+
+        InvokeCustom invokeCustom = instruction.asInvokeCustom();
+        DexCallSite callSite = invokeCustom.getCallSite();
+
+        // We are interested in bootstrap methods StringConcatFactory::makeConcat
+        // and StringConcatFactory::makeConcatWithConstants, both are static.
+        if (!callSite.bootstrapMethod.type.isInvokeStatic()) {
+          continue;
+        }
+
+        DexMethod bootstrapMethod = callSite.bootstrapMethod.asMethod();
+        // We rely on both rewrite methods called below performing their work in
+        // a way which keeps both `instructions` and `blocks` iterators in
+        // valid state so that we can continue iteration.
+        if (bootstrapMethod == this.makeConcat) {
+          rewriteMakeConcat(method, code, blocks, instructions, invokeCustom);
+        } else if (bootstrapMethod == this.makeConcatWithConstants) {
+          rewriteMakeConcatWithConstants(method, code, blocks, instructions, invokeCustom);
+        }
+      }
+    }
+  }
+
+  /**
+   * Rewrite concatenation with StringConcatFactory::makeConcat​(...). There is no
+   * format string (`recipe`), all arguments are just concatenated in order.
+   */
+  private void rewriteMakeConcat(DexMethod method, IRCode code, ListIterator<BasicBlock> blocks,
+      InstructionListIterator instructions, InvokeCustom invokeCustom) {
+    DexProto proto = invokeCustom.getCallSite().methodProto;
+    DexType[] parameters = proto.parameters.values;
+    int paramCount = parameters.length;
+    List<Value> arguments = invokeCustom.inValues();
+
+    // Signature of the callsite proto defines the effective types of the arguments.
+    if (paramCount != arguments.size()) {
+      throw error(method, "inconsistent arguments: expected " +
+          paramCount + ", actual " + arguments.size());
+    }
+
+    // Collect chunks.
+    ConcatBuilder builder = new ConcatBuilder(code, blocks, instructions);
+    for (int i = 0; i < paramCount; i++) {
+      builder.addChunk(arguments.get(i),
+          paramTypeToAppendMethod.getOrDefault(parameters[i], defaultAppendMethod));
+    }
+
+    // Desugar the instruction.
+    builder.desugar();
+  }
+
+  /**
+   * Rewrite concatenation with StringConcatFactory::makeConcat​WithConstants(...).
+   * There is a format string (`recipe`) specifying where exactly the arguments are
+   * to be inserted into a template string `recipe`.
+   *
+   * NOTE: `makeConcat​WithConstants` also supports passing compilation time `constants`
+   * as bootstrap method arguments, but it does not seem to be used in current Java 9
+   * compiler. This method does not support desugaring of cases with such bootstrap method
+   * arguments are provided.
+   */
+  private void rewriteMakeConcatWithConstants(
+      DexMethod method, IRCode code, ListIterator<BasicBlock> blocks,
+      InstructionListIterator instructions, InvokeCustom invokeCustom) {
+    DexCallSite callSite = invokeCustom.getCallSite();
+    DexProto proto = callSite.methodProto;
+    DexType[] parameters = proto.parameters.values;
+    int paramCount = parameters.length;
+    List<Value> callArgs = invokeCustom.inValues();
+    List<DexValue> bootstrapArgs = callSite.bootstrapArgs;
+
+    // Signature of the callsite proto defines the effective types of the arguments.
+    if (paramCount != callArgs.size()) {
+      throw error(method, "inconsistent arguments: expected " +
+          paramCount + ", actual " + callArgs.size());
+    }
+
+    // Get `recipe` string.
+    if (bootstrapArgs.size() == 0) {
+      throw error(method, "bootstrap method misses `recipe` argument");
+    }
+
+    // Constant arguments to `recipe`.
+    List<DexValue> constArgs = new ArrayList<>();
+    for (int i = 1; i < bootstrapArgs.size(); i++) {
+      constArgs.add(bootstrapArgs.get(i));
+    }
+
+    // Extract recipe.
+    DexValue recipeValue = bootstrapArgs.get(0);
+    if (!(recipeValue instanceof DexValue.DexValueString)) {
+      throw error(method, "bootstrap method argument `recipe` must be a string");
+    }
+    String recipe = ((DexValue.DexValueString) recipeValue).getValue().toString();
+
+    // Collect chunks and patch the instruction.
+    ConcatBuilder builder = new ConcatBuilder(code, blocks, instructions);
+    StringBuilder acc = new StringBuilder();
+    int argIndex = 0;
+    int constArgIndex = 0;
+    int length = recipe.length();
+    for (int i = 0; i < length; i++) {
+      char c = recipe.charAt(i);
+      if (c == '\u0001') {
+        // Reference to an argument.
+        if (acc.length() > 0) {
+          builder.addChunk(acc.toString(), paramTypeToAppendMethod.get(factory.stringType));
+          acc.setLength(0);
+        }
+        if (argIndex >= paramCount) {
+          throw error(method, "too many argument references in `recipe`");
+        }
+        builder.addChunk(callArgs.get(argIndex),
+            paramTypeToAppendMethod.getOrDefault(parameters[argIndex], defaultAppendMethod));
+        argIndex++;
+
+      } else if (c == '\u0002') {
+        if (constArgIndex >= constArgs.size()) {
+          throw error(method, "too many constant references in `recipe`");
+        }
+
+        // Reference to a constant. Since it's a constant we just convert it to
+        // string and append to `acc`, this way we will avoid calling toString()
+        // on every call.
+        acc.append(convertToString(method, constArgs.get(constArgIndex++)));
+
+      } else {
+        acc.append(c);
+      }
+    }
+
+    if (argIndex != paramCount) {
+      throw error(method, "too few argument references in `recipe`, "
+          + "expected " + paramCount + ", referenced: " + argIndex);
+    }
+    if (constArgIndex != constArgs.size()) {
+      throw error(method, "too few constant references in `recipe`, "
+          + "expected " + constArgs.size() + ", referenced: " + constArgIndex);
+    }
+
+    // Final part.
+    if (acc.length() > 0) {
+      builder.addChunk(acc.toString(), paramTypeToAppendMethod.get(factory.stringType));
+    }
+
+    // Desugar the instruction.
+    builder.desugar();
+  }
+
+  private static String convertToString(DexMethod method, DexValue value) {
+    if (value instanceof DexValue.DexValueString) {
+      return ((DexValue.DexValueString) value).getValue().toString();
+    }
+    throw error(method,
+        "const arg referenced from `recipe` is not supported: " + value.getClass().getName());
+  }
+
+  private final class ConcatBuilder {
+    private final IRCode code;
+    private final ListIterator<BasicBlock> blocks;
+    private final InstructionListIterator instructions;
+    private final Instruction invokeCustom;
+    private final BasicBlock currentBlock;
+    private final List<Chunk> chunks = new ArrayList<>();
+
+    private ConcatBuilder(
+        IRCode code, ListIterator<BasicBlock> blocks, InstructionListIterator instructions) {
+      this.code = code;
+      this.blocks = blocks;
+      this.instructions = instructions;
+
+      invokeCustom = instructions.peekPrevious();
+      assert invokeCustom.isInvokeCustom();
+      currentBlock = invokeCustom.getBlock();
+    }
+
+    private void appendInstruction(Instruction instruction) {
+      instruction.setPosition(invokeCustom.getPosition());
+      instructions.add(instruction);
+    }
+
+    final void addChunk(Value value, DexMethod method) {
+      chunks.add(new ArgumentChunk(value, method));
+    }
+
+    final void addChunk(String str, DexMethod method) {
+      chunks.add(new ConstantChunk(str, method));
+    }
+
+    /**
+     * Patch current `invoke-custom` instruction with:
+     * <pre>
+     *   prologue:
+     *      |   new-instance v0, StringBuilder
+     *      |   invoke-direct {v0}, void StringBuilder.<init>()
+     *
+     *   populate each chunk:
+     *      |   (optional) load the constant, e.g.: const-string v1, ""
+     *      |   invoke-virtual {v0, v1}, StringBuilder StringBuilder.append([type])
+     *
+     *   epilogue:
+     *      |   invoke-virtual {v0}, String StringBuilder.toString()
+     *
+     * </pre>
+     */
+    final void desugar() {
+      // Move the iterator before the invoke-custom we are about to patch.
+      instructions.previous();
+
+      // new-instance v0, StringBuilder
+      Value sbInstance = code.createValue(MoveType.OBJECT);
+      appendInstruction(new NewInstance(factory.stringBuilderType, sbInstance));
+
+      // invoke-direct {v0}, void StringBuilder.<init>()
+      appendInstruction(new InvokeDirect(stringBuilderInit,
+          null /* no return value */, Collections.singletonList(sbInstance)));
+
+      // Add calls to append(...) methods
+      for (Chunk chunk : chunks) {
+        chunk.addAppendCall(sbInstance);
+      }
+
+      // invoke-virtual {v0}, String StringBuilder.toString()
+      Instruction nextInstruction = instructions.next();
+      assert invokeCustom == nextInstruction;
+
+      // The value representing the string: we reuse the value from the
+      // original invoke-custom instruction, and thus all its usages.
+      Value concatValue = invokeCustom.outValue();
+      if (concatValue == null) {
+        // The out value might be empty in case it was optimized out.
+        concatValue = code.createValue(MoveType.OBJECT);
+      }
+
+      // Replace the instruction.
+      instructions.replaceCurrentInstruction(new InvokeVirtual(
+          stringBuilderToString, concatValue, Collections.singletonList(sbInstance)));
+
+      if (!currentBlock.hasCatchHandlers()) {
+        return;
+      }
+
+      // Since the block has handlers we should split the block at exception throwing
+      // instructions. Splitting blocks while adding instructions seems more complicated.
+      //
+      // NOTE: we collect new blocks first and copy catch handlers from the original
+      // one after all blocks are split. Copying handlers just after splitting involves
+      // extra complexity since split() method expects that the block being split is
+      // located right before the iterator point and new blocks created while copying
+      // handles break this expectation.
+      List<BasicBlock> newBlocks = new ArrayList<>();
+      InstructionListIterator it = currentBlock.listIterator();
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+        if (instruction.instructionTypeCanThrow() && it.hasNext()) {
+          // We split block in case we see throwing instruction which
+          // is not the last instruction of the block.
+          BasicBlock newBlock = it.split(code, blocks);
+          newBlocks.add(newBlock);
+          // Follow with the next block.
+          it = newBlock.listIterator();
+        }
+      }
+      // Copy catch handlers after all blocks are split.
+      for (BasicBlock newBlock : newBlocks) {
+        newBlock.copyCatchHandlers(code, blocks, currentBlock);
+      }
+    }
+
+    private abstract class Chunk {
+      final DexMethod method;
+
+      Chunk(DexMethod method) {
+        this.method = method;
+      }
+
+      abstract Value getOrCreateValue();
+
+      final void addAppendCall(Value sbInstance) {
+        appendInstruction(new InvokeVirtual(
+            method, null /* don't care about return value */,
+            Lists.newArrayList(sbInstance, getOrCreateValue())));
+      }
+    }
+
+    private final class ArgumentChunk extends Chunk {
+      final Value value;
+
+      ArgumentChunk(Value value, DexMethod method) {
+        super(method);
+        this.value = value;
+      }
+
+      @Override
+      Value getOrCreateValue() {
+        return value;
+      }
+    }
+
+    private final class ConstantChunk extends Chunk {
+      final String str;
+
+      ConstantChunk(String str, DexMethod method) {
+        super(method);
+        this.str = str;
+      }
+
+      @Override
+      Value getOrCreateValue() {
+        Value value = code.createValue(MoveType.OBJECT);
+        appendInstruction(new ConstString(value, factory.createString(str)));
+        return value;
+      }
+    }
+  }
+
+  private static CompilationError error(DexMethod method, String message) {
+    return new CompilationError(
+        "String concatenation desugaring error (method: " +
+            method.qualifiedName() + "): " + message);
+  }
+}
diff --git a/src/test/examplesAndroidO/stringconcat/StringConcat.java b/src/test/examplesAndroidO/stringconcat/StringConcat.java
new file mode 100644
index 0000000..9b726b3
--- /dev/null
+++ b/src/test/examplesAndroidO/stringconcat/StringConcat.java
@@ -0,0 +1,269 @@
+// 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 stringconcat;
+
+public class StringConcat {
+  private static void check(String actual, String expected) {
+    if (expected.equals(actual)) {
+      return;
+    }
+    throw new AssertionError(
+        "Test method failed: expected=[" + expected + "], actual=[" + actual + "]");
+  }
+
+  // --------- used 'makeConcat' signatures ---------
+
+  private static String makeConcat() {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(String s) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(char[] s) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(Object o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(boolean o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(char o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(byte o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(short o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(int o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(long o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(float o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(double o) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcat(Object o, String st, boolean z,
+      char c, byte b, short s, int i, long l, float f, double d) {
+    throw new AssertionError("unreachable");
+  }
+
+  // --------- used 'makeConcatWithConstants' signatures ---------
+
+  private static String makeConcatWithConstants(String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(String s, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(char[] s, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(Object o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(boolean o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(char o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(byte o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(short o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(int o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(long o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(float o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(double o, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(Object o, String st, boolean z,
+      char c, byte b, short s, int i, long l, float f, double d, String recipe) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(int i, String s, String recipe, String sConst) {
+    throw new AssertionError("unreachable");
+  }
+
+  private static String makeConcatWithConstants(
+      int i, String s, String recipe, String sConstA, String sConstB, String sConstC) {
+    throw new AssertionError("unreachable");
+  }
+
+  // ------------------------------------------------
+
+  private static void testEmpty() {
+    check(makeConcat(), "");
+    makeConcat();
+
+    check(makeConcatWithConstants("RECIPE:"), "");
+    check(makeConcatWithConstants("RECIPE:12-34"), "12-34");
+    makeConcatWithConstants("RECIPE:a");
+  }
+
+  private static void testSingleValueString() {
+    check(makeConcat("str"), "str");
+    check(makeConcat((String) null), "null");
+
+    check(makeConcatWithConstants("()", "RECIPE:prefix\u0001suffix"), "prefix()suffix");
+    check(makeConcatWithConstants("()", "RECIPE:prefix\u0001"), "prefix()");
+    check(makeConcatWithConstants("()", "RECIPE:\u0001suffix"), "()suffix");
+    check(makeConcatWithConstants("()", "RECIPE:\u0001"), "()");
+  }
+
+  private static void testSingleValueArray() {
+    // Unchecked since Array.toString() is non-deterministic.
+    makeConcat(new char[] { 'a', 'b' });
+    makeConcatWithConstants(new char[] { 'a', 'b' }, "RECIPE:prefix\u0001suffix");
+  }
+
+  private static void testSingleValueObject() {
+    check(makeConcat((Object) "object"), "object");
+    check(makeConcat((Object) 1.234), "1.234");
+    check(makeConcat((Object) null), "null");
+
+    check(
+        makeConcatWithConstants((Object) "object", "RECIPE:prefix\u0001suffix"),
+        "prefixobjectsuffix");
+    check(
+        makeConcatWithConstants((Object) 1.234, "RECIPE:prefix\u0001suffix"),
+        "prefix1.234suffix");
+    check(
+        makeConcatWithConstants((Object) null, "RECIPE:prefix\u0001suffix"),
+        "prefixnullsuffix");
+  }
+
+  private static void testSingleValuePrimitive() {
+    check(makeConcat(true), "true");
+    check(makeConcat((char) 65), "A");
+    check(makeConcat((byte) 1), "1");
+    check(makeConcat((short) 2), "2");
+    check(makeConcat(3), "3");
+    check(makeConcat((long) 4), "4");
+    check(makeConcat((float) 5), "5.0");
+    check(makeConcat((double) 6), "6.0");
+
+    check(makeConcatWithConstants(true, "RECIPE:prefix\u0001suffix"), "prefixtruesuffix");
+    check(makeConcatWithConstants((char) 65, "RECIPE:prefix\u0001suffix"), "prefixAsuffix");
+    check(makeConcatWithConstants((byte) 1, "RECIPE:prefix\u0001suffix"), "prefix1suffix");
+    check(makeConcatWithConstants((short) 2, "RECIPE:prefix\u0001suffix"), "prefix2suffix");
+    check(makeConcatWithConstants(3, "RECIPE:prefix\u0001suffix"), "prefix3suffix");
+    check(makeConcatWithConstants((long) 4, "RECIPE:prefix\u0001suffix"), "prefix4suffix");
+    check(makeConcatWithConstants((float) 5, "RECIPE:prefix\u0001suffix"), "prefix5.0suffix");
+    check(makeConcatWithConstants((double) 6, "RECIPE:prefix\u0001suffix"), "prefix6.0suffix");
+  }
+
+  private static void testAllTypes(Object o, String st, boolean z,
+      char c, byte b, short s, int i, long l, float f, double d) {
+    check(makeConcat(o, st, z, c, b, s, i, l, f, d), "nullstrtrueA12345.06.0");
+    check(makeConcatWithConstants(o, st, z, c, b, s, i, l, f, d,
+        "RECIPE:[\u0001-\u0001>\u0001===\u0001\u0001\u0001alpha\u0001beta\u0001\u0001]\u0001"),
+        "[null-str>true===A12alpha3beta45.0]6.0");
+  }
+
+  private static void testInExceptionContext(Object o, String st, boolean z,
+      char c, byte b, short s, int i, long l, float f, double d) {
+    check(makeConcat((long) 4), "4");
+    try {
+      check(makeConcat(o, st, z, c, b, s, i, l, f, d), "nullstrtrueA12345.06.0");
+      check(makeConcatWithConstants(o, st, z, c, b, s, i, l, f, d,
+          "RECIPE:\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"),
+          "nullstrtrueA12345.06.0");
+      try {
+        check(makeConcat("try-try"), "try-try");
+        throw new IndexOutOfBoundsException();
+      } catch (NullPointerException re) {
+        throw new AssertionError("UNREACHABLE");
+      } catch (Exception re) {
+        check(makeConcatWithConstants(o, st, z, c, b, s, i, l, f, d,
+            "RECIPE:(\u0001, \u0001, \u0001, \u0001, \u0001, "
+                + "\u0001, \u0001, \u0001, \u0001, \u0001)"),
+            "(null, str, true, A, 1, 2, 3, 4, 5.0, 6.0)");
+        throw new IndexOutOfBoundsException();
+      }
+    } catch (IndexOutOfBoundsException re) {
+      check(makeConcat("bar"), "bar");
+      check(makeConcatWithConstants("foo", "RECIPE:bar -> \u0001"), "bar -> foo");
+      try {
+        check(makeConcatWithConstants("inside", "RECIPE:try \u0001 try"), "try inside try");
+        throw new NullPointerException();
+      } catch (IndexOutOfBoundsException e) {
+        throw new AssertionError("UNREACHABLE");
+      } catch (NullPointerException npe) {
+        check(makeConcat(o, st, z, c, b, s, i, l, f, d), "nullstrtrueA12345.06.0");
+      }
+    } catch (Exception re) {
+      throw new AssertionError("UNREACHABLE");
+    }
+  }
+
+  private static void testConcatWitConstants() {
+    check(
+        makeConcatWithConstants(
+            123, "abc", "RECIPE:arg=\u0001; const=\u0002; arg=\u0001", "str"
+        ),
+        "arg=123; const=str; arg=abc");
+    check(
+        makeConcatWithConstants(
+            123, "abc", "RECIPE:\u0002arg=\u0001\u0002arg=\u0001\u0002",
+            "prefix-", "-infix-", "-suffix"
+        ),
+        "prefix-arg=123-infix-arg=abc-suffix");
+  }
+
+  // ------------------------------------------------
+
+  public static void main(String[] args) {
+    testEmpty();
+    testSingleValueString();
+    testSingleValueArray();
+    testSingleValueObject();
+    testSingleValuePrimitive();
+    testAllTypes(null, "str", true, (char) 65,
+        (byte) 1, (short) 2, 3, (long) 4, (float) 5, (double) 6);
+    testInExceptionContext(null, "str", true, (char) 65,
+        (byte) 1, (short) 2, 3, (long) 4, (float) 5, (double) 6);
+    testConcatWitConstants();
+  }
+}
diff --git a/src/test/examplesAndroidO/stringconcat/TestGenerator.java b/src/test/examplesAndroidO/stringconcat/TestGenerator.java
new file mode 100644
index 0000000..6a837a4
--- /dev/null
+++ b/src/test/examplesAndroidO/stringconcat/TestGenerator.java
@@ -0,0 +1,222 @@
+// 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 stringconcat;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.invoke.CallSite;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class TestGenerator {
+  private static final String RECIPE_PREFIX = "RECIPE:";
+
+  private static final String STRING_CONCAT_FACTORY = "java/lang/invoke/StringConcatFactory";
+
+  private static final Handle MAKE_CONCAT_WITH_CONSTANTS = new Handle(
+      Opcodes.H_INVOKESTATIC, STRING_CONCAT_FACTORY, "makeConcatWithConstants",
+      MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class,
+          MethodType.class, String.class, Object[].class).toMethodDescriptorString(),
+      false);
+
+  private static final Handle MAKE_CONCAT = new Handle(
+      Opcodes.H_INVOKESTATIC, STRING_CONCAT_FACTORY, "makeConcat",
+      MethodType.methodType(CallSite.class, MethodHandles.Lookup.class,
+          String.class, MethodType.class).toMethodDescriptorString(),
+      false);
+
+  public static void main(String[] args) throws IOException {
+    assert args.length == 1;
+    generateTests(Paths.get(args[0],
+        TestGenerator.class.getPackage().getName(),
+        StringConcat.class.getSimpleName() + ".class"));
+  }
+
+  private static void generateTests(Path classNamePath) throws IOException {
+    ClassReader cr = new ClassReader(new FileInputStream(classNamePath.toFile()));
+    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
+    cr.accept(
+        new ClassVisitor(Opcodes.ASM6, cw) {
+          @Override
+          public MethodVisitor visitMethod(int access,
+              final String methodName, String desc, String signature, String[] exceptions) {
+            MethodVisitor mv = super.visitMethod(access, methodName, desc, signature, exceptions);
+            return new MethodVisitor(Opcodes.ASM6, mv) {
+              private List<Object> recentConstants = new ArrayList<>();
+
+              @Override
+              public void visitLdcInsn(Object cst) {
+                if (!recentConstants.isEmpty() ||
+                    (cst instanceof String && ((String) cst).startsWith(RECIPE_PREFIX))) {
+                  // Add the constant, don't push anything on stack.
+                  recentConstants.add(cst);
+                  return;
+                }
+                super.visitLdcInsn(cst);
+              }
+
+              @Override
+              public void visitMethodInsn(
+                  int opcode, String owner, String name, String desc, boolean itf) {
+                // Replace calls to 'makeConcat(...)' with appropriate `invokedynamic`.
+                if (opcode == Opcodes.INVOKESTATIC && name.equals("makeConcat")) {
+                  mv.visitInvokeDynamicInsn(MAKE_CONCAT.getName(), desc, MAKE_CONCAT);
+                  recentConstants.clear();
+                  return;
+                }
+
+                // Replace calls to 'makeConcat(...)' with appropriate `invokedynamic`.
+                if (opcode == Opcodes.INVOKESTATIC && name.equals("makeConcatWithConstants")) {
+                  if (recentConstants.isEmpty()) {
+                    throw new AssertionError("No constants detected in `" +
+                        methodName + "`: call to " + name + desc);
+                  }
+                  recentConstants.set(0,
+                      ((String) recentConstants.get(0)).substring(RECIPE_PREFIX.length()));
+
+                  mv.visitInvokeDynamicInsn(MAKE_CONCAT_WITH_CONSTANTS.getName(),
+                      removeLastParams(desc, recentConstants.size()), MAKE_CONCAT_WITH_CONSTANTS,
+                      recentConstants.toArray(new Object[recentConstants.size()]));
+                  recentConstants.clear();
+                  return;
+                }
+
+                // Otherwise fall back to default implementation.
+                super.visitMethodInsn(opcode, owner, name, desc, itf);
+              }
+
+              private String removeLastParams(String descr, int paramsToRemove) {
+                MethodType methodType =
+                    MethodType.fromMethodDescriptorString(
+                        descr, this.getClass().getClassLoader());
+                return methodType
+                    .dropParameterTypes(
+                        methodType.parameterCount() - paramsToRemove,
+                        methodType.parameterCount())
+                    .toMethodDescriptorString();
+              }
+
+              @Override
+              public void visitInsn(int opcode) {
+                switch (opcode) {
+                  case Opcodes.ICONST_0:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(0);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_1:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(1);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_2:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(2);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_3:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(3);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_4:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(4);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_5:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(5);
+                      return;
+                    }
+                    break;
+                  case Opcodes.ICONST_M1:
+                    if (!recentConstants.isEmpty()) {
+                      recentConstants.add(-1);
+                      return;
+                    }
+                    break;
+                  default:
+                    recentConstants.clear();
+                    break;
+                }
+                super.visitInsn(opcode);
+              }
+
+              @Override
+              public void visitIntInsn(int opcode, int operand) {
+                recentConstants.clear();
+                super.visitIntInsn(opcode, operand);
+              }
+
+              @Override
+              public void visitVarInsn(int opcode, int var) {
+                recentConstants.clear();
+                super.visitVarInsn(opcode, var);
+              }
+
+              @Override
+              public void visitTypeInsn(int opcode, String type) {
+                recentConstants.clear();
+                super.visitTypeInsn(opcode, type);
+              }
+
+              @Override
+              public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+                recentConstants.clear();
+                super.visitFieldInsn(opcode, owner, name, desc);
+              }
+
+              @Override
+              public void visitJumpInsn(int opcode, Label label) {
+                recentConstants.clear();
+                super.visitJumpInsn(opcode, label);
+              }
+
+              @Override
+              public void visitIincInsn(int var, int increment) {
+                recentConstants.clear();
+                super.visitIincInsn(var, increment);
+              }
+
+              @Override
+              public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+                recentConstants.clear();
+                super.visitTableSwitchInsn(min, max, dflt, labels);
+              }
+
+              @Override
+              public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+                recentConstants.clear();
+                super.visitLookupSwitchInsn(dflt, keys, labels);
+              }
+
+              @Override
+              public void visitMultiANewArrayInsn(String desc, int dims) {
+                recentConstants.clear();
+                super.visitMultiANewArrayInsn(desc, dims);
+              }
+            };
+          }
+        }, 0);
+    new FileOutputStream(classNamePath.toFile()).write(cw.toByteArray());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index fc4eefe..a8e6651 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -248,11 +248,22 @@
     return failsOn(failsOn, name);
   }
 
+  boolean skipRunningOnJvm(String name) {
+    return name.equals("stringconcat");
+  }
+
   boolean minSdkErrorExpected(String testName) {
     return minSdkErrorExpected.contains(testName);
   }
 
   @Test
+  public void stringConcat() throws Throwable {
+    test("stringconcat", "stringconcat", "StringConcat")
+        .withMinApiLevel(AndroidApiLevel.K.getLevel())
+        .run();
+  }
+
+  @Test
   public void invokeCustom() throws Throwable {
     test("invokecustom", "invokecustom", "InvokeCustom")
         .withMinApiLevel(AndroidApiLevel.O.getLevel())
@@ -385,7 +396,7 @@
         Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
         qualifiedMainClass,
         null);
-    if (!expectedToFail) {
+    if (!expectedToFail && !skipRunningOnJvm(testName)) {
       ToolHelper.ProcessResult javaResult =
           ToolHelper.runJava(
               Arrays.stream(jars).map(path -> path.toString()).collect(Collectors.toList()),