Handle distinct symbolic reference to a dynamically-computed constants
A class file can have multiple dynamically computed constants using the
exact same BSM and arguments.
Keep track of the individual symbolic references instead of comparing
BSM and arguments.
Fixes: b/272346803
Change-Id: I068c4009628cba5ca88c0d049023d7505f06adcd
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
index 6c379bb..9b9b2b1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstDynamic.java
@@ -46,17 +46,21 @@
private final ConstantDynamicReference reference;
public CfConstDynamic(
+ int symbolicReferenceId,
DexString name,
DexType type,
DexMethodHandle bootstrapMethod,
List<DexValue> bootstrapMethodArguments) {
+ assert symbolicReferenceId >= 0;
assert name != null;
assert type != null;
assert bootstrapMethod != null;
assert bootstrapMethodArguments != null;
assert bootstrapMethodArguments.isEmpty();
- reference = new ConstantDynamicReference(name, type, bootstrapMethod, bootstrapMethodArguments);
+ reference =
+ new ConstantDynamicReference(
+ symbolicReferenceId, name, type, bootstrapMethod, bootstrapMethodArguments);
}
@Override
@@ -86,7 +90,10 @@
}
public static CfConstDynamic fromAsmConstantDynamic(
- ConstantDynamic insn, JarApplicationReader application, DexType clazz) {
+ int symbolicReferenceId,
+ ConstantDynamic insn,
+ JarApplicationReader application,
+ DexType clazz) {
String constantName = insn.getName();
String constantDescriptor = insn.getDescriptor();
DexMethodHandle bootstrapMethodHandle =
@@ -99,6 +106,7 @@
bootstrapMethodArguments.add(dexValue);
}
return new CfConstDynamic(
+ symbolicReferenceId,
application.getString(constantName),
application.getTypeFromDescriptor(constantDescriptor),
bootstrapMethodHandle,
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index 45fed00..153f85f 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -79,6 +79,8 @@
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -86,6 +88,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
+import java.util.function.Supplier;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ConstantDynamic;
@@ -334,6 +337,7 @@
private boolean usrJsrInliner;
private final Origin origin;
private final DebugParsingOptions debugParsingOptions;
+ private Reference2IntMap<ConstantDynamic> constantDynamicSymbolicReferences;
ClassCodeVisitor(
DexClass clazz,
@@ -351,6 +355,13 @@
this.debugParsingOptions = debugParsingOptions;
}
+ private Reference2IntMap<ConstantDynamic> getConstantDynamicSymbolicReferences() {
+ if (constantDynamicSymbolicReferences == null) {
+ constantDynamicSymbolicReferences = new Reference2IntArrayMap<>();
+ }
+ return constantDynamicSymbolicReferences;
+ }
+
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
@@ -360,7 +371,13 @@
if (code != null) {
DexMethod method = application.getMethod(clazz.type, name, desc);
MethodCodeVisitor methodVisitor =
- new MethodCodeVisitor(application, method, code, origin, debugParsingOptions);
+ new MethodCodeVisitor(
+ application,
+ method,
+ code,
+ origin,
+ debugParsingOptions,
+ this::getConstantDynamicSymbolicReferences);
if (!usrJsrInliner) {
return methodVisitor;
}
@@ -391,13 +408,16 @@
private final Origin origin;
private int minLine = Integer.MAX_VALUE;
private int maxLine = -1;
+ private final Supplier<Reference2IntMap<ConstantDynamic>>
+ constantDynamicSymbolicReferencesSupplier;
MethodCodeVisitor(
JarApplicationReader application,
DexMethod method,
LazyCfCode code,
Origin origin,
- DebugParsingOptions debugParsingOptions) {
+ DebugParsingOptions debugParsingOptions,
+ Supplier<Reference2IntMap<ConstantDynamic>> constantDynamicSymbolicReferencesSupplier) {
super(InternalOptions.ASM_VERSION);
this.debugParsingOptions = debugParsingOptions;
assert code != null;
@@ -406,6 +426,7 @@
this.code = code;
this.method = method;
this.origin = origin;
+ this.constantDynamicSymbolicReferencesSupplier = constantDynamicSymbolicReferencesSupplier;
}
private void addInstruction(CfInstruction instruction) {
@@ -1004,9 +1025,20 @@
new CfConstMethodHandle(
DexMethodHandle.fromAsmHandle((Handle) cst, application, method.holder)));
} else if (cst instanceof ConstantDynamic) {
+ // Each symbolic reference to a dynamically-computed constant has a unique ConstantDynamic
+ // instance from ASM, even when they are equal (i.e. all their components are equal). See
+ // ConstantDynamicMultipleConstantsWithDifferentSymbolicReferenceUsingSameBSMAndArgumentsTest
+ // for an example.
+ Reference2IntMap<ConstantDynamic> constantDynamicSymbolicReferences =
+ constantDynamicSymbolicReferencesSupplier.get();
+ int symbolicReferenceId = constantDynamicSymbolicReferences.getOrDefault(cst, -1);
+ if (symbolicReferenceId == -1) {
+ symbolicReferenceId = constantDynamicSymbolicReferences.size();
+ constantDynamicSymbolicReferences.put((ConstantDynamic) cst, symbolicReferenceId);
+ }
addInstruction(
CfConstDynamic.fromAsmConstantDynamic(
- (ConstantDynamic) cst, application, method.holder));
+ symbolicReferenceId, (ConstantDynamic) cst, application, method.holder));
} else {
throw new CompilationError("Unsupported constant: " + cst.toString());
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
index ed08c4d..478e4cf 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/constantdynamic/ConstantDynamicReference.java
@@ -15,24 +15,24 @@
import java.util.Objects;
public class ConstantDynamicReference implements StructuralItem<ConstantDynamicReference> {
+ private final int symbolicReferenceId;
private final DexString name;
private final DexType type;
private final DexMethodHandle bootstrapMethod;
private final List<DexValue> bootstrapMethodArguments;
private static void specify(StructuralSpecification<ConstantDynamicReference, ?> spec) {
- spec.withItem(ConstantDynamicReference::getName)
- .withItem(ConstantDynamicReference::getType)
- .withItem(ConstantDynamicReference::getBootstrapMethod)
- .withItemCollection(ConstantDynamicReference::getBootstrapMethodArguments);
+ spec.withInt(c -> c.symbolicReferenceId);
}
public ConstantDynamicReference(
+ int symbolicReferenceId,
DexString name,
DexType type,
DexMethodHandle bootstrapMethod,
List<DexValue> bootstrapMethodArguments) {
assert bootstrapMethodArguments.isEmpty();
+ this.symbolicReferenceId = symbolicReferenceId;
this.name = name;
this.type = type;
this.bootstrapMethod = bootstrapMethod;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 95105f8..301d7b4 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -65,7 +65,7 @@
// TODO(b/214901256): Sharing of synthetic classes may lead to duplicate method errors.
public final SyntheticKind NON_FIXED_INIT_TYPE_ARGUMENT =
generator.forNonSharableInstanceClass("$IA");
- public final SyntheticKind CONST_DYNAMIC = generator.forInstanceClass("$Condy");
+ public final SyntheticKind CONST_DYNAMIC = generator.forNonSharableInstanceClass("$Condy");
// Method synthetics.
public final SyntheticKind ENUM_UNBOXING_CHECK_NOT_ZERO_METHOD =
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsUsingSameSymbolicReferenceTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsUsingSameSymbolicReferenceTest.java
new file mode 100644
index 0000000..5ba0396
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsUsingSameSymbolicReferenceTest.java
@@ -0,0 +1,141 @@
+// Copyright (c) 2023, 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.desugar.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DesugarTestConfiguration;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ConstantDynamicMultipleConstantsUsingSameSymbolicReferenceTest extends TestBase {
+
+ @Parameter() public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ private static final Class<?> MAIN_CLASS = Main.class;
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("true");
+
+ @Test
+ public void testReference() throws Exception {
+ parameters.assumeJvmTestParameters();
+ assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+ testForJvm(parameters)
+ .addProgramClassFileData(getTransformedClasses())
+ .addProgramClasses(Main.class)
+ .run(parameters.getRuntime(), MAIN_CLASS)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testDesugaring() throws Exception {
+ testForDesugaring(parameters)
+ .addProgramClassFileData(getTransformedClasses())
+ .addProgramClasses(Main.class)
+ .run(parameters.getRuntime(), MAIN_CLASS)
+ .applyIf(
+ // When not desugaring the CF code requires JDK 11.
+ DesugarTestConfiguration::isNotDesugared,
+ r -> {
+ if (parameters.isCfRuntime()
+ && parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11)) {
+ r.assertSuccessWithOutput(EXPECTED_OUTPUT);
+ } else {
+ r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class);
+ }
+ })
+ .applyIf(
+ DesugarTestConfiguration::isDesugared, r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedClasses())
+ .addProgramClasses(Main.class)
+ .setMinApi(parameters)
+ .addKeepMainRule(MAIN_CLASS)
+ // TODO(b/198142613): There should not be a warnings on class references which are
+ // desugared away.
+ .applyIf(
+ parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+ b -> b.addDontWarn("java.lang.invoke.MethodHandles$Lookup"))
+ // TODO(b/198142625): Support CONSTANT_Dynamic output for class files.
+ .applyIf(
+ parameters.isCfRuntime(),
+ r -> {
+ assertThrows(
+ CompilationFailedException.class,
+ () ->
+ r.compileWithExpectedDiagnostics(
+ diagnostics -> {
+ diagnostics.assertOnlyErrors();
+ diagnostics.assertErrorsMatch(
+ diagnosticMessage(
+ containsString(
+ "Unsupported dynamic constant (not desugaring)")));
+ }));
+ },
+ r ->
+ r.run(parameters.getRuntime(), MAIN_CLASS)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ // When ASM writes two dynamic constants with the same BSM and arguments they are canonicialized
+ // into the same symbolic reference.
+ private byte[] getTransformedClasses() throws IOException {
+ return transformer(A.class)
+ .setVersion(CfVersion.V11)
+ .transformConstStringToConstantDynamic(
+ "condy1", A.class, "myConstant", false, "constantName", Object.class)
+ .transformConstStringToConstantDynamic(
+ "condy2", A.class, "myConstant", false, "constantName", Object.class)
+ .transform();
+ }
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ // This prints "true" due to the ASM canonicalization of the ConstantDynamic when writing.
+ System.out.println(A.getConstant1() == A.getConstant2());
+ }
+ }
+
+ public static class A {
+
+ public static Object getConstant1() {
+ return "condy1"; // Will be transformed to Constant_DYNAMIC.
+ }
+
+ public static Object getConstant2() {
+ return "condy2"; // Will be transformed to Constant_DYNAMIC.
+ }
+
+ private static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+ return new Object();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsWithDifferentSymbolicReferenceUsingSameBSMAndArgumentsTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsWithDifferentSymbolicReferenceUsingSameBSMAndArgumentsTest.java
index 9947216..3bf967d 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsWithDifferentSymbolicReferenceUsingSameBSMAndArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicMultipleConstantsWithDifferentSymbolicReferenceUsingSameBSMAndArgumentsTest.java
@@ -38,7 +38,6 @@
private static final String MAIN_CLASS = "A";
private static final String EXPECTED_OUTPUT = StringUtils.lines("false");
- private static final String UNEXPECTED_OUTPUT = StringUtils.lines("true");
@Test
public void testReference() throws Exception {
@@ -68,7 +67,7 @@
}
})
.applyIf(
- DesugarTestConfiguration::isDesugared, r -> r.assertSuccessWithOutput(UNEXPECTED_OUTPUT));
+ DesugarTestConfiguration::isDesugared, r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
}
@Test
@@ -101,7 +100,7 @@
},
r ->
r.run(parameters.getRuntime(), MAIN_CLASS)
- .assertSuccessWithOutput(UNEXPECTED_OUTPUT));
+ .assertSuccessWithOutput(EXPECTED_OUTPUT));
}
/*
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicRegress272346803Test.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicRegress272346803Test.java
index 4d8d477..bec0148 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicRegress272346803Test.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicRegress272346803Test.java
@@ -39,7 +39,6 @@
private static final Class<?> MAIN_CLASS = Main.class;
private static final String EXPECTED_OUTPUT = StringUtils.lines("A", "B");
- private static final String UNEXPECTED_OUTPUT = StringUtils.lines("A", "A");
@Test
public void testReference() throws Exception {
@@ -107,7 +106,7 @@
},
r ->
r.run(parameters.getRuntime(), MAIN_CLASS)
- .assertSuccessWithOutput(UNEXPECTED_OUTPUT));
+ .assertSuccessWithOutput(EXPECTED_OUTPUT));
}
private List<byte[]> getTransformedClasses() throws IOException {
@@ -126,7 +125,7 @@
// When R8 optimize this code the getter for the two constant dynamics will be inlined into
// Main.main. This leaves the synthetic constant dynamic classes with just two static fields.
- // The synthetic sharing then share these two synthetics leaving only one constant.
+ // The synthetic sharing cannot share these two synthetics as that would leave only one constant.
// See b/272346803 for details.
public static class Main {