Decouple Stack Manipulation logic in IndyStringDesugaring to LangModelHelper.

#java11 #desugar #refactor

- The abstracted logic is to be a shared API with Desugaring interactions with Android APIs.

PiperOrigin-RevId: 299264709
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/ClassName.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/ClassName.java
index 7cb47c7..75fa1c5 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/ClassName.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/ClassName.java
@@ -16,9 +16,14 @@
 
 package com.google.devtools.build.android.desugar.langmodel;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Collection;
 import org.objectweb.asm.Type;
 
 /**
@@ -28,6 +33,59 @@
 @AutoValue
 public abstract class ClassName implements TypeMappable<ClassName> {
 
+  public static final String IN_PROCESS_LABEL = "__desugar__/";
+
+  private static final String TYPE_ADAPTER_PACKAGE_ROOT = "desugar/runtime/typeadapter/";
+
+  /**
+   * The primitive type as specified at
+   * https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.3
+   */
+  private static final ImmutableMap<String, Type> PRIMITIVES_TYPES =
+      ImmutableMap.<String, Type>builder()
+          .put("V", Type.VOID_TYPE)
+          .put("Z", Type.BOOLEAN_TYPE)
+          .put("C", Type.CHAR_TYPE)
+          .put("B", Type.BYTE_TYPE)
+          .put("S", Type.SHORT_TYPE)
+          .put("I", Type.INT_TYPE)
+          .put("F", Type.FLOAT_TYPE)
+          .put("J", Type.LONG_TYPE)
+          .put("D", Type.DOUBLE_TYPE)
+          .build();
+
+  /**
+   * The primitive type as specified at
+   * https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.3
+   */
+  private static final ImmutableMap<ClassName, ClassName> PRIMITIVES_TO_BOXED_TYPES =
+      ImmutableMap.<ClassName, ClassName>builder()
+          .put(ClassName.create(Type.VOID_TYPE), ClassName.create("java/lang/Void"))
+          .put(ClassName.create(Type.BOOLEAN_TYPE), ClassName.create("java/lang/Boolean"))
+          .put(ClassName.create(Type.CHAR_TYPE), ClassName.create("java/lang/Character"))
+          .put(ClassName.create(Type.BYTE_TYPE), ClassName.create("java/lang/Byte"))
+          .put(ClassName.create(Type.SHORT_TYPE), ClassName.create("java/lang/Short"))
+          .put(ClassName.create(Type.INT_TYPE), ClassName.create("java/lang/Integer"))
+          .put(ClassName.create(Type.FLOAT_TYPE), ClassName.create("java/lang/Float"))
+          .put(ClassName.create(Type.LONG_TYPE), ClassName.create("java/lang/Long"))
+          .put(ClassName.create(Type.DOUBLE_TYPE), ClassName.create("java/lang/Double"))
+          .build();
+
+  private static final ImmutableBiMap<String, String> DELIVERY_TYPE_MAPPINGS =
+      ImmutableBiMap.<String, String>builder()
+          .put("java/", "j$/")
+          .put("javadesugar/", "jd$/")
+          .build();
+
+  public static final TypeMapper IN_PROCESS_LABEL_STRIPPER =
+      new TypeMapper(ClassName::verbatimName);
+
+  public static final TypeMapper DELIVERY_TYPE_MAPPER =
+      new TypeMapper(ClassName::verbatimToDelivery);
+
+  public static final TypeMapper VERBATIM_TYPE_MAPPER =
+      new TypeMapper(ClassName::deliveryToVerbatim);
+
   /**
    * The textual binary name used to index the class name, as defined at,
    * https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.2.1
@@ -35,7 +93,7 @@
   public abstract String binaryName();
 
   public static ClassName create(String binaryName) {
-    checkState(
+    checkArgument(
         !binaryName.contains("."),
         "Expected a binary/internal class name ('/'-delimited) instead of a qualified name."
             + " Actual: (%s)",
@@ -52,7 +110,24 @@
   }
 
   public final Type toAsmObjectType() {
-    return Type.getObjectType(binaryName());
+    return isPrimitive() ? PRIMITIVES_TYPES.get(binaryName()) : Type.getObjectType(binaryName());
+  }
+
+  public final ClassName toBoxedType() {
+    checkState(isPrimitive(), "Expected a primitive type for type boxing, but got %s", this);
+    return PRIMITIVES_TO_BOXED_TYPES.get(this);
+  }
+
+  public final boolean isPrimitive() {
+    return PRIMITIVES_TYPES.containsKey(binaryName());
+  }
+
+  public final boolean isWideType() {
+    return "D".equals(binaryName()) || "J".equals(binaryName());
+  }
+
+  public final boolean isBoxedType() {
+    return PRIMITIVES_TO_BOXED_TYPES.containsValue(this);
   }
 
   public final String qualifiedName() {
@@ -63,22 +138,126 @@
     return ClassName.create(binaryName() + '$' + innerClassSimpleName);
   }
 
+  public final String getPackageName() {
+    String binaryName = binaryName();
+    int i = binaryName.lastIndexOf('/');
+    return i < 0 ? "" : binaryName.substring(0, i + 1);
+  }
+
   public final String simpleName() {
     String binaryName = binaryName();
     int i = binaryName.lastIndexOf('/');
     return i < 0 ? binaryName : binaryName.substring(i + 1);
   }
 
+  public final ClassName withSimpleNameSuffix(String suffix) {
+    return ClassName.create(binaryName() + suffix);
+  }
+
   public final String classFilePathName() {
     return binaryName() + ".class";
   }
 
+  public final boolean hasInProcessLabel() {
+    return hasPackagePrefix(IN_PROCESS_LABEL);
+  }
+
+  private ClassName stripInProcessLabel() {
+    return stripPackagePrefix(IN_PROCESS_LABEL);
+  }
+
+  private ClassName stripInProcessLabelIfAny() {
+    return hasInProcessLabel() ? stripPackagePrefix(IN_PROCESS_LABEL) : this;
+  }
+
+  /** Strips out in-process labels if any. */
+  public final ClassName verbatimName() {
+    return stripInProcessLabelIfAny().deliveryToVerbatim();
+  }
+
+  public final ClassName typeAdapterOwner() {
+    return verbatimName().withSimpleNameSuffix("Adapter").prependPrefix(TYPE_ADAPTER_PACKAGE_ROOT);
+  }
+
+  public final ClassName typeConverterOwner() {
+    return verbatimName()
+        .withSimpleNameSuffix("Converter")
+        .prependPrefix(TYPE_ADAPTER_PACKAGE_ROOT);
+  }
+
+  public final ClassName verbatimToDelivery() {
+    return DELIVERY_TYPE_MAPPINGS.keySet().stream()
+        .filter(this::hasPackagePrefix)
+        .map(prefix -> replacePackagePrefix(prefix, DELIVERY_TYPE_MAPPINGS.get(prefix)))
+        .findAny()
+        .orElse(this);
+  }
+
+  public final ClassName deliveryToVerbatim() {
+    ImmutableBiMap<String, String> verbatimTypeMappings = DELIVERY_TYPE_MAPPINGS.inverse();
+    return verbatimTypeMappings.keySet().stream()
+        .filter(this::hasPackagePrefix)
+        .map(prefix -> replacePackagePrefix(prefix, verbatimTypeMappings.get(prefix)))
+        .findAny()
+        .orElse(this);
+  }
+
   public final ClassName prependPrefix(String prefix) {
+    checkPackagePrefixFormat(prefix);
     return ClassName.create(prefix + binaryName());
   }
 
+  public final boolean hasPackagePrefix(String prefix) {
+    return binaryName().startsWith(prefix);
+  }
+
+  public final boolean hasAnyPackagePrefix(String... prefixes) {
+    return Arrays.stream(prefixes).anyMatch(this::hasPackagePrefix);
+  }
+
+  public final boolean hasAnyPackagePrefix(Collection<String> prefixes) {
+    return prefixes.stream().anyMatch(this::hasPackagePrefix);
+  }
+
+  public final ClassName stripPackagePrefix(String prefix) {
+    return replacePackagePrefix(/* originalPrefix= */ prefix, /* targetPrefix= */ "");
+  }
+
+  public final ClassName replacePackagePrefix(String originalPrefix, String targetPrefix) {
+    checkState(
+        hasPackagePrefix(originalPrefix),
+        "Expected %s to have a package prefix of (%s) before stripping.",
+        this,
+        originalPrefix);
+    checkPackagePrefixFormat(targetPrefix);
+    return ClassName.create(targetPrefix + binaryName().substring(originalPrefix.length()));
+  }
+
   @Override
   public ClassName acceptTypeMapper(TypeMapper typeMapper) {
     return typeMapper.map(this);
   }
+
+  private static void checkPackagePrefixFormat(String prefix) {
+    checkArgument(
+        prefix.isEmpty() || prefix.endsWith("/"),
+        "Expected (%s) to be a package prefix of ending with '/'.",
+        prefix);
+    checkArgument(
+        !prefix.contains("."),
+        "Expected a '/'-delimited binary name instead of a '.'-delimited qualified name for %s",
+        prefix);
+  }
+
+  public boolean isInProcessCoreType() {
+    return hasInProcessLabel() && stripInProcessLabel().isVerbatimCoreType();
+  }
+
+  public boolean isVerbatimCoreType() {
+    return hasAnyPackagePrefix(DELIVERY_TYPE_MAPPINGS.keySet());
+  }
+
+  public boolean isDeliveryCoreType() {
+    return hasAnyPackagePrefix(DELIVERY_TYPE_MAPPINGS.values());
+  }
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/InvocationSiteTransformationRecord.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/InvocationSiteTransformationRecord.java
new file mode 100644
index 0000000..b95ea0d
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/InvocationSiteTransformationRecord.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.devtools.build.android.desugar.langmodel;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+
+/** A record that tracks the method invocation transformations. */
+@AutoValue
+public abstract class InvocationSiteTransformationRecord {
+
+  public abstract ImmutableSet<MethodInvocationSite> record();
+
+  public static InvocationSiteTransformationRecordBuilder builder() {
+    return new AutoValue_InvocationSiteTransformationRecord.Builder();
+  }
+
+  /** The builder for {@link InvocationSiteTransformationRecord}. */
+  @AutoValue.Builder
+  public abstract static class InvocationSiteTransformationRecordBuilder {
+
+    abstract ImmutableSet.Builder<MethodInvocationSite> recordBuilder();
+
+    public final InvocationSiteTransformationRecordBuilder addTransformation(
+        MethodInvocationSite originalMethodInvocationSite) {
+      recordBuilder().add(originalMethodInvocationSite);
+      return this;
+    }
+
+    public abstract InvocationSiteTransformationRecord build();
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/LangModelHelper.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/LangModelHelper.java
index d339ed5..a67657b 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/LangModelHelper.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/LangModelHelper.java
@@ -16,10 +16,16 @@
 
 package com.google.devtools.build.android.desugar.langmodel;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Streams;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
@@ -28,22 +34,6 @@
 public final class LangModelHelper {
 
   /**
-   * The primitive type as specified at
-   * https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.3
-   */
-  private static final ImmutableMap<Type, Type> PRIMITIVES_TO_BOXED_TYPES =
-      ImmutableMap.<Type, Type>builder()
-          .put(Type.INT_TYPE, Type.getObjectType("java/lang/Integer"))
-          .put(Type.BOOLEAN_TYPE, Type.getObjectType("java/lang/Boolean"))
-          .put(Type.BYTE_TYPE, Type.getObjectType("java/lang/Byte"))
-          .put(Type.CHAR_TYPE, Type.getObjectType("java/lang/Character"))
-          .put(Type.SHORT_TYPE, Type.getObjectType("java/lang/Short"))
-          .put(Type.DOUBLE_TYPE, Type.getObjectType("java/lang/Double"))
-          .put(Type.FLOAT_TYPE, Type.getObjectType("java/lang/Float"))
-          .put(Type.LONG_TYPE, Type.getObjectType("java/lang/Long"))
-          .build();
-
-  /**
    * The lookup table for dup instructional opcodes. The row key is the count of words on stack top
    * to be duplicated. The column key is gap in word count between the original word section on
    * stack top and the post-duplicated word section underneath. See from
@@ -60,15 +50,6 @@
           .put(2, 2, Opcodes.DUP2_X2)
           .build();
 
-  /** Whether the given type is a primitive type */
-  public static boolean isPrimitive(Type type) {
-    return PRIMITIVES_TO_BOXED_TYPES.containsKey(type);
-  }
-
-  public static Type toBoxedType(Type primitiveType) {
-    return PRIMITIVES_TO_BOXED_TYPES.get(primitiveType);
-  }
-
   /**
    * Returns the operation code for pop operations with a single instruction support by their type
    * sizes on stack top
@@ -163,5 +144,224 @@
     }
   }
 
+  /**
+   * Emits bytecode to allocate a new object array, stores the bottom values in the operand stack to
+   * the new array, and replaces the bottom values with a reference to the newly-allocated array.
+   *
+   * <p>Operand Stack:
+   * <li>Before instructions: [Stack Top]..., value_n, value_n-1, ..., value_2, value_1, value_0
+   * <li>After instructions: [Stack Top] ..., value_n, arrayref
+   *
+   *     <p>where n is the size of {@code expectedTypesOnOperandStack} and is expected to be equal
+   *     to array length referenced by arrayref
+   *
+   * @param mv The current method visitor that is visiting the class.
+   * @param mappers Applies to an operand stack value if tested positive on {@code filter}.
+   * @param expectedTypesOnOperandStack The expected types at the bottom of the operand stack. The
+   *     end of the list corresponds to the the bottom of the operand stack.
+   */
+  public static ImmutableList<ClassName> collapseStackValuesToObjectArray(
+      MethodVisitor mv,
+      ImmutableList<Function<ClassName, Optional<MethodInvocationSite>>> mappers,
+      ImmutableList<ClassName> expectedTypesOnOperandStack) {
+    // Creates an array of java/lang/Object to store the values on top of the operand stack that
+    // are subject to string concatenation.
+    int numOfValuesOnOperandStack = expectedTypesOnOperandStack.size();
+    visitPushInstr(mv, numOfValuesOnOperandStack);
+    mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
+
+    // To preserve the order of the operands to be string-concatenated, we slot the values on
+    // the top of the stack to the end of the array.
+    List<ClassName> actualTypesOnObjectArray = new ArrayList<>(expectedTypesOnOperandStack);
+    for (int i = numOfValuesOnOperandStack - 1; i >= 0; i--) {
+      ClassName operandTypeName = expectedTypesOnOperandStack.get(i);
+      Type operandType = operandTypeName.toAsmObjectType();
+      // Pre-duplicates the array reference for next loop iteration use.
+      // Post-operation stack bottom to top:
+      //     ..., value_i-1, arrayref, value_i, arrayref.
+      mv.visitInsn(
+          getTypeSizeAlignedDupOpcode(
+              ImmutableList.of(Type.getType(Object.class)), ImmutableList.of(operandType)));
+
+      // Pushes the array index and adjusts the order of the values on stack top in the order
+      // of <bottom/> arrayref, index, value <top/> before emitting an aastore instruction.
+      // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aastore
+      // Post-operation stack bottom to top:
+      //     ..., value_i-1, arrayref, value_i, arrayref, i.
+      visitPushInstr(mv, i);
+      // Cross-duplicates the array reference and index.
+      // Post-operation stack bottom to top:
+      //     ..., value_i-1, arrayref, arrayref, i, value_i, arrayref, i.
+      mv.visitInsn(
+          getTypeSizeAlignedDupOpcode(
+              ImmutableList.of(Type.getType(Object.class), Type.getType(int.class)),
+              ImmutableList.of(operandType)));
+
+      // Pops arrayref, index, leaving the stack top as value_i.
+      // Post-operation stack bottom to top:
+      //     ..., value_i-1, arrayref, arrayref, i, value_i.
+      mv.visitInsn(
+          getTypeSizeAlignedPopOpcode(
+              ImmutableList.of(Type.getType(Object.class), Type.getType(int.class))));
+
+      int targetArrayIndex = i;
+      mappers.stream()
+          .map(mapper -> mapper.apply(actualTypesOnObjectArray.get(targetArrayIndex)))
+          .flatMap(Streams::stream)
+          .forEach(
+              typeConversionSite -> {
+                typeConversionSite.accept(mv);
+                actualTypesOnObjectArray.set(targetArrayIndex, typeConversionSite.returnTypeName());
+              });
+
+      // Post-operation stack bottom to top:
+      //     ..., value_i-1, arrayref.
+      mv.visitInsn(Opcodes.AASTORE);
+    }
+    return ImmutableList.copyOf(actualTypesOnObjectArray);
+  }
+
+  /**
+   * Emits bytecode to replace the object array reference at the operand stack bottom with its array
+   * element values.
+   *
+   * <p>Operand Stack:
+   * <li>Before instructions: [Stack Top] ..., value_n, arrayref
+   * <li>After instructions: [Stack Top]..., value_n, value_n-1, ..., value_2, value_1, value_0
+   *
+   *     <p>where n is the array length referenced by arrayref and is expected to be equal to the
+   *     size of {@code expectedTypesOnOperandStack} expanded on the operand stack.
+   *
+   * @param mv The current method visitor that is visiting the class.
+   * @param expectedTypesOnOperandStack The expected types at the bottom of the operand stack. The
+   *     end of the list corresponds to the the bottom of the operand stack.
+   */
+  public static void expandObjectArrayToStackValues(
+      MethodVisitor mv, ImmutableList<ClassName> expectedTypesOnOperandStack) {
+    int numOfValuesExpandedOnOperandStack = expectedTypesOnOperandStack.size();
+    for (int i = 0; i < numOfValuesExpandedOnOperandStack; i++) {
+      ClassName operandTypeName = expectedTypesOnOperandStack.get(i);
+      // Pre-duplicates the array reference for next loop iteration use.
+      // Post-operation stack bottom to top:
+      //     ..., arrayref, arrayref
+      mv.visitInsn(Opcodes.DUP);
+
+      // Pushes the current array index on stack.
+      // Post-operation stack bottom to top:
+      //     ..., arrayref, arrayref, i
+      visitPushInstr(mv, i);
+
+      // Post-operation stack bottom to top:
+      //     ..., arrayref, obj_value_i
+      mv.visitInsn(Opcodes.AALOAD);
+
+      // Post-operation stack bottom to top:
+      //     ..., arrayref, cast_and_unboxed_value_i
+      if (operandTypeName.isPrimitive()) {
+        ClassName boxedTypeName = operandTypeName.toBoxedType();
+        mv.visitTypeInsn(Opcodes.CHECKCAST, boxedTypeName.binaryName());
+        createBoxedTypeToPrimitiveInvocationSite(boxedTypeName).accept(mv);
+      } else if (!ClassName.create(Object.class).equals(operandTypeName)) {
+        mv.visitTypeInsn(Opcodes.CHECKCAST, operandTypeName.binaryName());
+      }
+
+      //     ..., cast_and_unboxed_value_i, arrayref
+      if (operandTypeName.isWideType()) {
+        mv.visitInsn(Opcodes.DUP2_X1);
+        mv.visitInsn(Opcodes.POP2);
+      } else {
+        mv.visitInsn(Opcodes.SWAP);
+      }
+    }
+
+    // pops out the original arrayref.
+    mv.visitInsn(Opcodes.POP);
+  }
+
+  public static Optional<MethodInvocationSite> anyPrimitiveToStringInvocationSite(
+      ClassName className) {
+    return className.isPrimitive()
+        ? Optional.of(createPrimitiveToStringInvocationSite(className))
+        : Optional.empty();
+  }
+
+  /** Convenient factory method for converting a primitive type to string call site. */
+  private static MethodInvocationSite createPrimitiveToStringInvocationSite(
+      ClassName primitiveTypeName) {
+    checkArgument(
+        primitiveTypeName.isPrimitive(),
+        "Expected a primitive type for a type boxing call site, but got %s",
+        primitiveTypeName);
+    return MethodInvocationSite.builder()
+        .setInvocationKind(MemberUseKind.INVOKESTATIC)
+        .setMethod(
+            MethodKey.create(
+                primitiveTypeName.toBoxedType(),
+                "toString",
+                Type.getMethodDescriptor(
+                    Type.getType(String.class), primitiveTypeName.toAsmObjectType())))
+        .setIsInterface(false)
+        .build();
+  }
+
+  /** Convenient factory method for converting a primitive type to string call site. */
+  public static Optional<MethodInvocationSite> anyPrimitiveToBoxedTypeInvocationSite(
+      ClassName className) {
+    return className.isPrimitive()
+        ? Optional.of(createPrimitiveToBoxedTypeInvocationSite(className))
+        : Optional.empty();
+  }
+
+  private static MethodInvocationSite createPrimitiveToBoxedTypeInvocationSite(
+      ClassName primitiveTypeName) {
+    checkArgument(
+        primitiveTypeName.isPrimitive(),
+        "Expected a primitive type for a type boxing call site, but got %s",
+        primitiveTypeName);
+    return MethodInvocationSite.builder()
+        .setInvocationKind(MemberUseKind.INVOKESTATIC)
+        .setMethod(
+            MethodKey.create(
+                primitiveTypeName.toBoxedType(),
+                "valueOf",
+                Type.getMethodDescriptor(
+                    primitiveTypeName.toBoxedType().toAsmObjectType(),
+                    primitiveTypeName.toAsmObjectType())))
+        .setIsInterface(false)
+        .build();
+  }
+
+  private static MethodInvocationSite createBoxedTypeToPrimitiveInvocationSite(
+      ClassName boxedType) {
+    String boxedTypeBinaryName = boxedType.binaryName();
+    final MethodKey typeUnboxingMethod;
+    if ("java/lang/Boolean".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "booleanValue", "()Z");
+    } else if ("java/lang/Character".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "charValue", "()C");
+    } else if ("java/lang/Byte".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "byteValue", "()B");
+    } else if ("java/lang/Short".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "shortValue", "()S");
+    } else if ("java/lang/Integer".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "intValue", "()I");
+    } else if ("java/lang/Float".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "floatValue", "()F");
+    } else if ("java/lang/Long".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "longValue", "()J");
+    } else if ("java/lang/Double".contentEquals(boxedTypeBinaryName)) {
+      typeUnboxingMethod = MethodKey.create(boxedType, "doubleValue", "()D");
+    } else {
+      throw new IllegalArgumentException(
+          String.format(
+              "Expected a boxed type to create a type boxing call site, but got %s", boxedType));
+    }
+    return MethodInvocationSite.builder()
+        .setInvocationKind(MemberUseKind.INVOKEVIRTUAL)
+        .setMethod(typeUnboxingMethod)
+        .setIsInterface(false)
+        .build();
+  }
+
   private LangModelHelper() {}
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodDeclInfo.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodDeclInfo.java
index 942276c..7580fa9 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodDeclInfo.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodDeclInfo.java
@@ -88,6 +88,10 @@
     return methodKey().getReturnType();
   }
 
+  public final ClassName returnTypeName() {
+    return methodKey().getReturnTypeName();
+  }
+
   public final ImmutableList<Type> argumentTypes() {
     return ImmutableList.copyOf(methodKey().getArgumentTypes());
   }
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodInvocationSite.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodInvocationSite.java
new file mode 100644
index 0000000..dffbb3b
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodInvocationSite.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.devtools.build.android.desugar.langmodel;
+
+import com.google.auto.value.AutoValue;
+import org.objectweb.asm.MethodVisitor;
+
+/** A value object that represents an method invocation site. */
+@AutoValue
+public abstract class MethodInvocationSite implements TypeMappable<MethodInvocationSite> {
+
+  public abstract MemberUseKind invocationKind();
+
+  public abstract MethodKey method();
+
+  public abstract boolean isInterface();
+
+  public static MethodInvocationSiteBuilder builder() {
+    return new AutoValue_MethodInvocationSite.Builder();
+  }
+
+  /** Convenient factory method for use in the callback of {@link MethodVisitor#visitMethodInsn}. */
+  public static MethodInvocationSite create(
+      int opcode, String owner, String name, String descriptor, boolean isInterface) {
+    return builder()
+        .setInvocationKind(MemberUseKind.fromValue(opcode))
+        .setMethod(MethodKey.create(ClassName.create(owner), name, descriptor))
+        .setIsInterface(isInterface)
+        .build();
+  }
+
+  public abstract MethodInvocationSiteBuilder toBuilder();
+
+  public final int invokeOpcode() {
+    return invocationKind().getOpcode();
+  }
+
+  public final ClassName owner() {
+    return method().owner();
+  }
+
+  public final String name() {
+    return method().name();
+  }
+
+  public final String descriptor() {
+    return method().descriptor();
+  }
+
+  public final ClassName returnTypeName() {
+    return method().getReturnTypeName();
+  }
+
+  public final boolean isStaticInvocation() {
+    return invocationKind() == MemberUseKind.INVOKESTATIC;
+  }
+
+  public final boolean isConstructorInvocation() {
+    return method().isConstructor();
+  }
+
+  public final MethodVisitor accept(MethodVisitor mv) {
+    mv.visitMethodInsn(invokeOpcode(), owner().binaryName(), name(), descriptor(), isInterface());
+    return mv;
+  }
+
+  @Override
+  public MethodInvocationSite acceptTypeMapper(TypeMapper typeMapper) {
+    return toBuilder().setMethod(method().acceptTypeMapper(typeMapper)).build();
+  }
+
+  public final MethodInvocationSite toAdapterInvocationSite() {
+    return MethodInvocationSite.builder()
+        .setInvocationKind(MemberUseKind.INVOKESTATIC)
+        .setMethod(method().toArgumentTypeAdapter(isStaticInvocation()))
+        .setIsInterface(false)
+        .build();
+  }
+
+  /** The builder for {@link MethodInvocationSite}. */
+  @AutoValue.Builder
+  public abstract static class MethodInvocationSiteBuilder {
+
+    public abstract MethodInvocationSiteBuilder setInvocationKind(MemberUseKind value);
+
+    public abstract MethodInvocationSiteBuilder setMethod(MethodKey value);
+
+    public abstract MethodInvocationSiteBuilder setIsInterface(boolean value);
+
+    public abstract MethodInvocationSite build();
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodKey.java b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodKey.java
index ca868c5..0bd249c 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodKey.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/langmodel/MethodKey.java
@@ -17,6 +17,7 @@
 package com.google.devtools.build.android.desugar.langmodel;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
@@ -58,6 +59,23 @@
     return ImmutableList.copyOf(getArgumentTypeArray());
   }
 
+  /** The formal parameter type names of a method. */
+  public ImmutableList<ClassName> getArgumentTypeNames() {
+    return getArgumentTypes().stream().map(ClassName::create).collect(toImmutableList());
+  }
+
+  public MethodKey toArgumentTypeAdapter(boolean fromStaticOrigin) {
+    ClassName typeAdapterOwner = owner().typeAdapterOwner();
+    checkState(
+        !isConstructor(), "Argument type adapter for constructor is not supported: %s. ", this);
+
+    return MethodKey.create(
+            typeAdapterOwner,
+            name(),
+            fromStaticOrigin ? descriptor() : instanceMethodToStaticDescriptor())
+        .acceptTypeMapper(ClassName.DELIVERY_TYPE_MAPPER);
+  }
+
   /** The synthetic constructor for a private constructor. */
   public final MethodKey bridgeOfConstructor(ClassName nestCompanion) {
     checkState(isConstructor(), "Expect to use for a constructor but is %s", this);
@@ -79,7 +97,7 @@
 
   /** The synthetic bridge method for a private instance method in a class. */
   public final MethodKey bridgeOfClassInstanceMethod() {
-    return create(owner(), nameWithSuffix("bridge"), instanceMethodToStaticDescriptor(this));
+    return create(owner(), nameWithSuffix("bridge"), this.instanceMethodToStaticDescriptor());
   }
 
   /** The substitute method for a private static method in an interface. */
@@ -90,20 +108,16 @@
 
   /** The substitute method for a private instance method in an interface. */
   public final MethodKey substituteOfInterfaceInstanceMethod() {
-    return create(owner(), name(), instanceMethodToStaticDescriptor(this));
+    return create(owner(), name(), this.instanceMethodToStaticDescriptor());
   }
 
   /** The descriptor of the static version of a given instance method. */
-  private static String instanceMethodToStaticDescriptor(MethodKey methodKey) {
-    checkState(!methodKey.isConstructor(), "Expect a Non-constructor method: %s", methodKey);
-    ImmutableList<Type> argumentTypes = methodKey.getArgumentTypes();
+  private String instanceMethodToStaticDescriptor() {
+    checkState(!isConstructor(), "Expect a Non-constructor method: %s", this);
+    ImmutableList<Type> argumentTypes = getArgumentTypes();
     ImmutableList<Type> bridgeMethodArgTypes =
-        ImmutableList.<Type>builder()
-            .add(methodKey.ownerAsmObjectType())
-            .addAll(argumentTypes)
-            .build();
-    return Type.getMethodDescriptor(
-        methodKey.getReturnType(), bridgeMethodArgTypes.toArray(new Type[0]));
+        ImmutableList.<Type>builder().add(ownerAsmObjectType()).addAll(argumentTypes).build();
+    return Type.getMethodDescriptor(getReturnType(), bridgeMethodArgTypes.toArray(new Type[0]));
   }
 
   @Override
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/strconcat/IndyStringConcatDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/strconcat/IndyStringConcatDesugaring.java
index f86e7a9..21ca226 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/strconcat/IndyStringConcatDesugaring.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/strconcat/IndyStringConcatDesugaring.java
@@ -16,8 +16,7 @@
 
 package com.google.devtools.build.android.desugar.strconcat;
 
-import static com.google.devtools.build.android.desugar.langmodel.LangModelHelper.isPrimitive;
-import static com.google.devtools.build.android.desugar.langmodel.LangModelHelper.toBoxedType;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.devtools.build.android.desugar.langmodel.LangModelHelper.visitPushInstr;
 
 import com.google.common.collect.ImmutableList;
@@ -27,6 +26,7 @@
 import com.google.devtools.build.android.desugar.langmodel.LangModelHelper;
 import com.google.devtools.build.android.desugar.langmodel.MemberUseKind;
 import com.google.devtools.build.android.desugar.langmodel.MethodKey;
+import java.util.Arrays;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.MethodVisitor;
@@ -105,62 +105,13 @@
         // StringConcatFactory#makeConcatWithConstants
         classMemberUseCounter.incrementMemberUseCount(bootstrapMethodInvocation);
 
-        // Creates an array of java/lang/Object to store the values on top of the operand stack that
-        // are subject to string concatenation.
-        Type[] typesOfValuesOnOperandStack = Type.getArgumentTypes(descriptor);
-        int numOfValuesOnOperandStack = typesOfValuesOnOperandStack.length;
-        visitPushInstr(mv, numOfValuesOnOperandStack);
-        visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
+        LangModelHelper.collapseStackValuesToObjectArray(
+            this,
+            ImmutableList.of(LangModelHelper::anyPrimitiveToStringInvocationSite),
+            Arrays.stream(Type.getArgumentTypes(descriptor))
+                .map(ClassName::create)
+                .collect(toImmutableList()));
 
-        // To preserve the order of the operands to be string-concatenated, we slot the values on
-        // the top of the stack to the end of the array.
-        for (int i = numOfValuesOnOperandStack - 1; i >= 0; i--) {
-          Type operandType = typesOfValuesOnOperandStack[i];
-          // Pre-duplicates the array reference for next loop iteration use.
-          // Post-operation stack bottom to top:
-          //     ..., value_i-1, arrayref, value_i, arrayref.
-          visitInsn(
-              LangModelHelper.getTypeSizeAlignedDupOpcode(
-                  ImmutableList.of(Type.getType(Object.class)), ImmutableList.of(operandType)));
-
-          // Pushes the array index and adjusts the order of the values on stack top in the order
-          // of <bottom/> arrayref, index, value <top/> before emitting an aastore instruction.
-          // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aastore
-          // Post-operation stack bottom to top:
-          //     ..., value_i-1, arrayref, value_i, arrayref, i.
-          visitPushInstr(mv, i);
-          // Cross-duplicates the array reference and index.
-          // Post-operation stack bottom to top:
-          //     ..., value_i-1, arrayref, arrayref, i, value_i, arrayref, i.
-          visitInsn(
-              LangModelHelper.getTypeSizeAlignedDupOpcode(
-                  ImmutableList.of(Type.getType(Object.class), Type.getType(int.class)),
-                  ImmutableList.of(operandType)));
-
-          // Pops arrayref, index, leaving the stack top as value_i.
-          // Post-operation stack bottom to top:
-          //     ..., value_i-1, arrayref, arrayref, i, value_i.
-          visitInsn(
-              LangModelHelper.getTypeSizeAlignedPopOpcode(
-                  ImmutableList.of(Type.getType(Object.class), Type.getType(int.class))));
-
-          if (isPrimitive(operandType)) {
-            // Explicitly computes the string value of primitive types, so that they can be stored
-            // in the Object[] array.
-            // Post-operation stack bottom to top:
-            //     ..., value_i-1, arrayref, arrayref, i, processed_value_i.
-            Type boxedType = toBoxedType(operandType);
-            visitMethodInsn(
-                Opcodes.INVOKESTATIC,
-                boxedType.getInternalName(),
-                "toString",
-                Type.getMethodDescriptor(Type.getType(String.class), operandType),
-                /* isInterface= */ false);
-          }
-          // Post-operation stack bottom to top:
-          //     ..., value_i-1, arrayref.
-          visitInsn(Opcodes.AASTORE);
-        }
         String recipe = (String) bootstrapMethodArguments[0];
         visitLdcInsn(recipe);
 
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/testing/junit/DesugarRuleBuilder.java b/src/tools/android/java/com/google/devtools/build/android/desugar/testing/junit/DesugarRuleBuilder.java
index e7005c7..f4abb75 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/testing/junit/DesugarRuleBuilder.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/testing/junit/DesugarRuleBuilder.java
@@ -163,10 +163,20 @@
   }
 
   /**
+   * Add JVM-flag-specified Java source files subject to be compiled during test execution. It is
+   * expected the value associated with `jvmFlagKey` to be a space-separated Strings. E.g. on the
+   * command line you would set it like: -Dinput_srcs="path1 path2 path3", and use <code>
+   *  .addSourceInputsFromJvmFlag("input_srcs").</code> in your test class.
+   */
+  public DesugarRuleBuilder addJarInputsFromJvmFlag(String jvmFlagKey) {
+    return addInputs(getRuntimePathsFromJvmFlag(jvmFlagKey));
+  }
+
+  /**
    * A helper method that reads file paths into an array from the JVM flag value associated with
    * {@param jvmFlagKey}.
    */
-  private static Path[] getRuntimePathsFromJvmFlag(String jvmFlagKey) {
+  public static Path[] getRuntimePathsFromJvmFlag(String jvmFlagKey) {
     return Splitter.on(" ").trimResults().splitToList(System.getProperty(jvmFlagKey)).stream()
         .map(Paths::get)
         .toArray(Path[]::new);