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/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()),