Determine synthetic context from end of string
Bug: b/324270842
Change-Id: Ibd8ad7f15c1a72bd641b9b6b968c2834048107b7
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index dc0a4ac..00c6b10 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -62,9 +62,8 @@
final String getPrefixForExternalSyntheticType(AppView<?> appView) {
if (!appView.options().intermediate && context.isSyntheticInputClass() && !kind.isGlobal()) {
// If the input class was a synthetic and the build is non-intermediate, unwind the synthetic
- // name back to the original context (if present in the textual type).
- return SyntheticNaming.getOuterContextFromExternalSyntheticType(
- getKind(), getHolder().getType());
+ // name back to the original context.
+ return context.getSynthesizingContextType().toBinaryName();
}
return SyntheticNaming.getPrefixForExternalSyntheticType(getKind(), getHolder().getType());
}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index abd4a13..bb72018 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -576,7 +576,9 @@
private static boolean shouldAnnotateSynthetics(InternalOptions options) {
// Only intermediate builds have annotated synthetics to allow later sharing.
// Also, CF builds are marked in the writer using an attribute.
- return options.intermediate && options.isGeneratingDex();
+ return options.intermediate
+ && options.isGeneratingDex()
+ && !options.testing.disableSyntheticMarkerAttributeWriting;
}
private <T extends SyntheticDefinition<?, T, ?>>
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 7f56b25..50c9f23 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -300,33 +300,19 @@
assert synthetics.committed.isEmpty();
assert synthetics.pending.isEmpty();
CommittedSyntheticsCollection.Builder builder = synthetics.committed.builder();
- // TODO(b/158159959): Consider populating the input synthetics when identified.
- for (DexProgramClass clazz : appView.appInfo().classes()) {
- SyntheticMarker marker = SyntheticMarker.stripMarkerFromClass(clazz, appView);
- if (!appView.options().intermediate && marker.getContext() != null) {
- DexClass contextClass =
- appView
- .appInfo()
- .definitionForWithoutExistenceAssert(
- marker.getContext().getSynthesizingContextType());
- if (contextClass == null || contextClass.isNotProgramClass()) {
- appView
- .reporter()
- .error(
- new StringDiagnostic(
- "Attempt at compiling intermediate artifact without its context",
- clazz.getOrigin()));
- }
- }
- if (marker.isSyntheticMethods()) {
- clazz.forEachProgramMethod(
- method ->
- builder.addMethod(
- new SyntheticMethodDefinition(marker.getKind(), marker.getContext(), method)));
- } else if (marker.isSyntheticClass()) {
- builder.addClass(
- new SyntheticProgramClassDefinition(marker.getKind(), marker.getContext(), clazz));
- }
+ if (appView.options().intermediate) {
+ appView
+ .appInfo()
+ .classes()
+ .forEach(
+ clazz -> {
+ SyntheticMarker marker = SyntheticMarker.stripMarkerFromClass(clazz, appView);
+ if (marker.isValidMarker()) {
+ addSyntheticInput(clazz, marker, marker.getContext(), builder);
+ }
+ });
+ } else {
+ collectSyntheticInputsTransitive(appView, builder);
}
CommittedSyntheticsCollection committed = builder.collectSyntheticInputs().build();
if (committed.isEmpty()) {
@@ -350,6 +336,77 @@
}
}
+ private static void addSyntheticInput(
+ DexProgramClass clazz,
+ SyntheticMarker marker,
+ SynthesizingContext context,
+ CommittedSyntheticsCollection.Builder builder) {
+ if (marker.isSyntheticMethods()) {
+ clazz.forEachProgramMethod(
+ method ->
+ builder.addMethod(new SyntheticMethodDefinition(marker.getKind(), context, method)));
+ } else if (marker.isSyntheticClass()) {
+ builder.addClass(new SyntheticProgramClassDefinition(marker.getKind(), context, clazz));
+ }
+ }
+
+ private static void collectSyntheticInputsTransitive(
+ AppView<?> appView, CommittedSyntheticsCollection.Builder builder) {
+ Map<DexType, SynthesizingContext> cache = new IdentityHashMap<>();
+ appView
+ .appInfo()
+ .classes()
+ .forEach(clazz -> collectSyntheticInputTransitive(clazz, cache, builder, appView));
+ }
+
+ private static SynthesizingContext collectSyntheticInputTransitive(
+ DexProgramClass clazz,
+ Map<DexType, SynthesizingContext> cache,
+ CommittedSyntheticsCollection.Builder builder,
+ AppView<?> appView) {
+ SyntheticMarker marker = SyntheticMarker.stripMarkerFromClass(clazz, appView);
+ if (!marker.isValidMarker()) {
+ return cache.get(clazz.getType());
+ }
+ return cache.computeIfAbsent(
+ clazz.getType(),
+ type -> {
+ SynthesizingContext context =
+ ensureSyntheticInputContext(clazz, marker, cache, builder, appView);
+ addSyntheticInput(clazz, marker, context, builder);
+ return context;
+ });
+ }
+
+ private static SynthesizingContext ensureSyntheticInputContext(
+ DexProgramClass clazz,
+ SyntheticMarker marker,
+ Map<DexType, SynthesizingContext> cache,
+ CommittedSyntheticsCollection.Builder builder,
+ AppView<?> appView) {
+ assert !appView.options().intermediate;
+ // The context should be flattened such that it is not itself a synthetic.
+ DexProgramClass contextClass =
+ DexProgramClass.asProgramClassOrNull(
+ appView
+ .appInfo()
+ .definitionForWithoutExistenceAssert(
+ marker.getContext().getSynthesizingContextType()));
+ if (contextClass == null) {
+ appView
+ .reporter()
+ .error(
+ new StringDiagnostic(
+ "Attempt at compiling intermediate artifact without its context",
+ clazz.getOrigin()));
+ return marker.getContext();
+ } else {
+ SynthesizingContext contextOfContext =
+ collectSyntheticInputTransitive(contextClass, cache, builder, appView);
+ return contextOfContext != null ? contextOfContext : marker.getContext();
+ }
+ }
+
// Predicates and accessors.
@Override
@@ -1165,6 +1222,9 @@
public void writeAttributeIfIntermediateSyntheticClass(
ClassWriter writer, DexProgramClass clazz, AppView<?> appView) {
+ if (appView.options().testing.disableSyntheticMarkerAttributeWriting) {
+ return;
+ }
if (!appView.options().intermediate || !appView.options().isGeneratingClassFiles()) {
return;
}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index 9e8ec26..7670696 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -205,6 +205,11 @@
this.context = context;
}
+ public boolean isValidMarker() {
+ assert getContext() != null || this == NO_MARKER;
+ return getContext() != null;
+ }
+
public boolean isSyntheticMethods() {
return kind != null && kind.isSingleSyntheticMethod();
}
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 a9f9957..fad4880 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -424,8 +424,10 @@
assert !kind.isGlobal();
String binaryName = type.toBinaryName();
int index =
- binaryName.indexOf(
- kind.isFixedSuffixSynthetic() ? kind.descriptor : EXTERNAL_SYNTHETIC_CLASS_SEPARATOR);
+ binaryName.lastIndexOf(
+ kind.isFixedSuffixSynthetic()
+ ? kind.descriptor
+ : EXTERNAL_SYNTHETIC_CLASS_SEPARATOR + kind.getDescriptor());
if (index < 0) {
throw new Unreachable(
"Unexpected failure to determine the context of synthetic class: " + binaryName);
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 8a940af..e90aa6c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2404,6 +2404,8 @@
public boolean emitDebugLocalStartBeforeDefaultEvent = false;
+ public boolean disableSyntheticMarkerAttributeWriting = false;
+
// Option for testing outlining with interface array arguments, see b/132420510.
public boolean allowOutlinerInterfaceArrayArguments = false;
diff --git a/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsAndStrippedMarkerTest.java b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsAndStrippedMarkerTest.java
new file mode 100644
index 0000000..6bd5b5a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsAndStrippedMarkerTest.java
@@ -0,0 +1,233 @@
+// Copyright (c) 2024, 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.synthesis;
+
+import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticBackportClass;
+import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticLambdaClass;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanBox;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+/** This test is a regression test for the issue identified in b/324270842#comment37 */
+@RunWith(Parameterized.class)
+public class RepeatedCompilationNestedSyntheticsAndStrippedMarkerTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public Backend intermediateBackend;
+
+ @Parameterized.Parameters(name = "{0}, intermediate: {1}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withDefaultDexRuntime().withMinimumApiLevel().build(),
+ Backend.values());
+ }
+
+ @Test
+ public void test() throws Exception {
+ assertEquals(Backend.DEX, parameters.getBackend());
+
+ ClassReference syntheticLambdaClass = syntheticLambdaClass(UsesBackport.class, 0);
+ ImmutableSet<String> expectedClassOutputs =
+ ImmutableSet.of(descriptor(UsesBackport.class), syntheticLambdaClass.getDescriptor());
+
+ Map<String, byte[]> firstCompilation = new HashMap<>();
+ testForD8(Backend.CF)
+ // High API level such that only the lambda is desugared.
+ .setMinApi(AndroidApiLevel.S)
+ .setIntermediate(true)
+ .addClasspathClasses(I.class)
+ .addProgramClasses(UsesBackport.class)
+ .addOptionsModification(o -> o.testing.disableSyntheticMarkerAttributeWriting = true)
+ .setProgramConsumer(
+ new ClassFileConsumer() {
+ @Override
+ public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+ byte[] bytes = data.copyByteData();
+ assertEquals(Collections.emptyList(), SyntheticMarkerCfTest.readAttributes(bytes));
+ firstCompilation.put(descriptor, bytes);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {}
+ })
+ .compile();
+ assertEquals(expectedClassOutputs, firstCompilation.keySet());
+
+ Map<String, byte[]> secondCompilation = new HashMap<>();
+ ImmutableSet.Builder<String> allDescriptors = ImmutableSet.builder();
+ BooleanBox matched = new BooleanBox(false);
+ for (Entry<String, byte[]> entry : firstCompilation.entrySet()) {
+ byte[] bytes = entry.getValue();
+ Origin origin =
+ new Origin(Origin.root()) {
+ @Override
+ public String part() {
+ return entry.getKey();
+ }
+ };
+ testForD8(intermediateBackend)
+ .setMinApi(parameters)
+ .setIntermediate(true)
+ .addClasspathClasses(I.class)
+ .apply(b -> b.getBuilder().addClassProgramData(bytes, origin))
+ .apply(
+ b ->
+ b.getBuilder()
+ .setDesugarGraphConsumer(
+ new DesugarGraphConsumer() {
+
+ @Override
+ public void accept(Origin dependent, Origin dependency) {
+ assertThat(
+ dependency.toString(), containsString(binaryName(I.class)));
+ assertThat(
+ dependent.toString(),
+ containsString(syntheticLambdaClass.getBinaryName()));
+ matched.set(true);
+ }
+
+ @Override
+ public void finished() {}
+ }))
+ .applyIf(
+ intermediateBackend == Backend.CF,
+ b ->
+ b.setProgramConsumer(
+ new ClassFileConsumer() {
+ @Override
+ public void accept(
+ ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+ secondCompilation.put(descriptor, data.copyByteData());
+ allDescriptors.add(descriptor);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {}
+ }),
+ b ->
+ b.setProgramConsumer(
+ new DexFilePerClassFileConsumer() {
+
+ @Override
+ public void accept(
+ String primaryClassDescriptor,
+ ByteDataView data,
+ Set<String> descriptors,
+ DiagnosticsHandler handler) {
+ secondCompilation.put(primaryClassDescriptor, data.copyByteData());
+ allDescriptors.addAll(descriptors);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {}
+ }))
+ .compile();
+ }
+ assertTrue(matched.get());
+ // The dex file per class file output should maintain the exact same set of primary descriptors.
+ if (intermediateBackend == Backend.DEX) {
+ assertEquals(expectedClassOutputs, secondCompilation.keySet());
+ }
+ // The total set of classes should also include the backport. The backport should be
+ // hygienically placed under the synthetic lambda (not the context of the lambda!).
+ assertEquals(
+ ImmutableSet.<String>builder()
+ .addAll(expectedClassOutputs)
+ .add(syntheticBackportClass(syntheticLambdaClass, 0).getDescriptor())
+ .build(),
+ allDescriptors.build());
+
+ List<byte[]> secondCompilationWitoutOuterContext =
+ secondCompilation.entrySet().stream()
+ .filter(e -> !e.getKey().equals(descriptor(UsesBackport.class)))
+ .map(Entry::getValue)
+ .collect(Collectors.toList());
+ Path out =
+ testForD8(Backend.DEX)
+ .setMinApi(parameters)
+ .addProgramClasses(I.class, TestClass.class)
+ .applyIf(
+ intermediateBackend == Backend.CF,
+ b -> b.addProgramClassFileData(secondCompilationWitoutOuterContext),
+ b -> b.addProgramDexFileData(secondCompilationWitoutOuterContext))
+ .compile()
+ .writeToZip();
+
+ byte[] secondCompilationOfOuterContext = secondCompilation.get(descriptor(UsesBackport.class));
+ testForD8(Backend.DEX)
+ .setMinApi(parameters)
+ .addProgramFiles(out)
+ .applyIf(
+ intermediateBackend == Backend.CF,
+ b -> b.addProgramClassFileData(secondCompilationOfOuterContext),
+ b -> b.addProgramDexFileData(secondCompilationOfOuterContext))
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("1")
+ .inspect(
+ inspector -> {
+ Set<String> descriptors =
+ inspector.allClasses().stream()
+ .map(c -> c.getFinalReference().getDescriptor())
+ .collect(Collectors.toSet());
+ // The initial lambda stays as the only item under UsesBackport.
+ ClassReference lambdaClass = syntheticLambdaClass(UsesBackport.class, 0);
+ // The nested backport has context in the lambda since the lambda was not marked.
+ ClassReference backportClass = syntheticBackportClass(lambdaClass, 0);
+ assertEquals(
+ ImmutableSet.of(
+ descriptor(I.class),
+ descriptor(TestClass.class),
+ descriptor(UsesBackport.class),
+ backportClass.getDescriptor(),
+ lambdaClass.getDescriptor()),
+ descriptors);
+ });
+ }
+
+ interface I {
+ int compare(boolean b1, boolean b2);
+ }
+
+ static class UsesBackport {
+ public static I foo() {
+ return Boolean::compare;
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println(UsesBackport.foo().compare(true, false));
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
index 697b2b5..74ba13b 100644
--- a/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticMarkerCfTest.java
@@ -137,7 +137,7 @@
}
}
- private static List<Attribute> readAttributes(byte[] bytes) {
+ public static List<Attribute> readAttributes(byte[] bytes) {
List<Attribute> attributes = new ArrayList<>();
ClassReader reader = new ClassReader(bytes);
reader.accept(