Merge commit '190440afd5c304438851011526ebe11bcde62cce' into 1.7.x-dev Change-Id: I74db49506e6b2beb6eb94ec95a5a704d0a29f728
diff --git a/.gitignore b/.gitignore index b74632a..bc99d6a 100644 --- a/.gitignore +++ b/.gitignore
@@ -2,6 +2,7 @@ buildSrc/out/ .idea/ .gradle/ +.gradle_user_home android-data*/ tests/2016-12-19/art.tar.gz tests/2016-12-19/art
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java index a6389da..31e0dbd 100644 --- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java +++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -12,6 +12,7 @@ import com.android.tools.r8.origin.Origin; import com.android.tools.r8.origin.PathOrigin; import com.android.tools.r8.utils.DescriptorUtils; +import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.ZipUtils; import com.google.common.io.ByteStreams; import java.io.Closeable; @@ -60,7 +61,7 @@ assert isArchive(archive); origin = new PathOrigin(archive); try { - zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8); + zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8); } catch (IOException e) { if (!Files.exists(archive)) { throw new NoSuchFileException(archive.toString());
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java index c5065f2..d5dd240 100644 --- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java +++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -9,6 +9,7 @@ import com.android.tools.r8.origin.Origin; import com.android.tools.r8.origin.PathOrigin; import com.android.tools.r8.utils.DescriptorUtils; +import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.ZipUtils; import com.google.common.io.ByteStreams; import java.io.IOException; @@ -62,7 +63,7 @@ Path archive, Predicate<String> include) { return fromSupplier( new PathOrigin(archive), - () -> new ZipFile(archive.toFile(), StandardCharsets.UTF_8), + () -> FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8), include); }
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java index 318305d..50e5be1 100644 --- a/src/main/java/com/android/tools/r8/BaseCommand.java +++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -166,15 +166,14 @@ public B addProgramFiles(Collection<Path> files) { guard( () -> { - files.forEach( - path -> { - try { - app.addProgramFile(path); - programFiles.add(path); - } catch (CompilationError e) { - error(new ProgramInputOrigin(path), e); - } - }); + for (Path path : files) { + try { + app.addProgramFile(path); + programFiles.add(path); + } catch (CompilationError e) { + error(new ProgramInputOrigin(path), e); + } + } }); return self(); } @@ -201,14 +200,13 @@ public B addLibraryFiles(Collection<Path> files) { guard( () -> { - files.forEach( - path -> { - try { - app.addLibraryFile(path); - } catch (CompilationError e) { - error(new LibraryInputOrigin(path), e); - } - }); + for (Path path : files) { + try { + app.addLibraryFile(path); + } catch (CompilationError e) { + error(new LibraryInputOrigin(path), e); + } + } }); return self(); }
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java index f17bab9..7834adb 100644 --- a/src/main/java/com/android/tools/r8/L8.java +++ b/src/main/java/com/android/tools/r8/L8.java
@@ -113,10 +113,6 @@ options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options.itemFactory); app = new L8TreePruner(options).prune(app, rewritePrefix); - if (app.classes().size() == 0) { - // TODO(b/134732760): report error instead of fatalError. - throw options.reporter.fatalError("Empty desugared library."); - } AppInfo appInfo = new AppInfo(app); AppView<?> appView = AppView.createForL8(appInfo, options, rewritePrefix);
diff --git a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java index a48bbd1..e77cba3 100644 --- a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java +++ b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
@@ -20,9 +20,9 @@ import com.android.tools.r8.ir.code.StackValue; import com.android.tools.r8.ir.code.Value; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; @@ -223,7 +223,7 @@ computingVerificationTypes = true; types = new HashMap<>(); List<ConstNumber> nullsUsedInPhis = new ArrayList<>(); - Set<Value> worklist = new HashSet<>(); + Set<Value> worklist = Sets.newIdentityHashSet(); { InstructionIterator it = code.instructionIterator(); Instruction instruction = null;
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java index 7321b53..68e9e69 100644 --- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java +++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -569,7 +569,7 @@ // For each input archive file, add all class files within. for (Path input : inputs) { if (FileUtils.isArchive(input)) { - try (ZipFile zipFile = new ZipFile(input.toFile(), StandardCharsets.UTF_8)) { + try (ZipFile zipFile = FileUtils.createZipFile(input.toFile(), StandardCharsets.UTF_8)) { final Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java index 460885c..3425946 100644 --- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java +++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -4,6 +4,7 @@ package com.android.tools.r8.graph; import com.android.tools.r8.errors.CompilationError; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.desugar.LambdaDescriptor; import com.android.tools.r8.origin.Origin; import com.android.tools.r8.utils.SetUtils; @@ -131,7 +132,7 @@ public AppInfoWithSubtyping(DexApplication application) { super(application); - typeInfo = Collections.synchronizedMap(new IdentityHashMap<>()); + typeInfo = new ConcurrentHashMap<>(); // Recompute subtype map if we have modified the graph. populateSubtypeMap(application.asDirect(), application.dexItemFactory); } @@ -140,7 +141,7 @@ super(previous); missingClasses.addAll(previous.missingClasses); subtypeMap.putAll(previous.subtypeMap); - typeInfo = Collections.synchronizedMap(new IdentityHashMap<>(previous.typeInfo)); + typeInfo = new ConcurrentHashMap<>(previous.typeInfo); assert app() instanceof DirectMappedDexApplication; } @@ -211,6 +212,7 @@ } private TypeInfo getTypeInfo(DexType type) { + assert type != null; return typeInfo.computeIfAbsent(type, TypeInfo::new); } @@ -274,11 +276,11 @@ } assert !seenTypes.contains(next); seenTypes.add(next); - TypeInfo superInfo = getTypeInfo(superType); TypeInfo nextInfo = getTypeInfo(next); if (superType == null) { assert nextInfo.hierarchyLevel == ROOT_LEVEL; } else { + TypeInfo superInfo = getTypeInfo(superType); assert superInfo.hierarchyLevel == nextInfo.hierarchyLevel - 1 || (superInfo.hierarchyLevel == ROOT_LEVEL && nextInfo.hierarchyLevel == INTERFACE_LEVEL); @@ -714,8 +716,17 @@ return !isSubtype(type1, type2) && !isSubtype(type2, type1); } - public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(DexType type) { - return computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(type, true); + public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(ClassTypeLatticeElement type) { + Set<DexType> interfaces = type.getInterfaces(); + if (!interfaces.isEmpty()) { + for (DexType interfaceType : interfaces) { + if (computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(interfaceType, false)) { + return true; + } + } + return false; + } + return computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(type.getClassType(), true); } private boolean computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent( @@ -727,8 +738,10 @@ } DexClass clazz = definitionFor(type); if (clazz == null) { - mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, true); - return true; + // This is strictly not conservative but is needed to avoid that we treat Object as having + // a subtype that has a non-default finalize() implementation. + mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, false); + return false; } if (clazz.isProgramClass()) { if (lookUpwards) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java index 6f5df28..7ce2685 100644 --- a/src/main/java/com/android/tools/r8/graph/DexClass.java +++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -924,4 +924,13 @@ assert verifyNoDuplicateMethods(); return true; } + + public boolean hasStaticSynchronizedMethods() { + for (DexEncodedMethod encodedMethod : directMethods()) { + if (encodedMethod.accessFlags.isStatic() && encodedMethod.accessFlags.isSynchronized()) { + return true; + } + } + return false; + } }
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 0cc264a..f60e877 100644 --- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java +++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -62,12 +62,12 @@ public static final String throwableDescriptorString = "Ljava/lang/Throwable;"; - private final ConcurrentHashMap<DexString, DexString> strings = new ConcurrentHashMap<>(); - private final ConcurrentHashMap<DexString, DexType> types = new ConcurrentHashMap<>(); - private final ConcurrentHashMap<DexField, DexField> fields = new ConcurrentHashMap<>(); - private final ConcurrentHashMap<DexProto, DexProto> protos = new ConcurrentHashMap<>(); - private final ConcurrentHashMap<DexMethod, DexMethod> methods = new ConcurrentHashMap<>(); - private final ConcurrentHashMap<DexMethodHandle, DexMethodHandle> methodHandles = + private final Map<DexString, DexString> strings = new ConcurrentHashMap<>(); + private final Map<DexString, DexType> types = new ConcurrentHashMap<>(); + private final Map<DexField, DexField> fields = new ConcurrentHashMap<>(); + private final Map<DexProto, DexProto> protos = new ConcurrentHashMap<>(); + private final Map<DexMethod, DexMethod> methods = new ConcurrentHashMap<>(); + private final Map<DexMethodHandle, DexMethodHandle> methodHandles = new ConcurrentHashMap<>(); // DexDebugEvent Canonicalization. @@ -730,10 +730,13 @@ public class EnumMethods { - public DexMethod valueOf; - public DexMethod ordinal; - public DexMethod name; - public DexMethod toString; + public final DexMethod valueOf; + public final DexMethod ordinal; + public final DexMethod name; + public final DexMethod toString; + + public final DexMethod finalize = + createMethod(enumType, createProto(voidType), finalizeMethodName); private EnumMethods() { valueOf = @@ -1152,7 +1155,7 @@ } } - private static <T extends DexItem> T canonicalize(ConcurrentHashMap<T, T> map, T item) { + private static <T extends DexItem> T canonicalize(Map<T, T> map, T item) { assert item != null; assert !DexItemFactory.isInternalSentinel(item); T previous = map.putIfAbsent(item, item);
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java index aa6b651..af70929 100644 --- a/src/main/java/com/android/tools/r8/graph/DexValue.java +++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -56,6 +56,22 @@ return null; } + public DexValueType asDexValueType() { + return null; + } + + public DexValueArray asDexValueArray() { + return null; + } + + public boolean isDexValueType() { + return false; + } + + public boolean isDexValueArray() { + return false; + } + public static DexValue fromAsmBootstrapArgument( Object value, JarApplicationReader application, DexType clazz) { if (value instanceof Integer) { @@ -835,6 +851,16 @@ DexMethod method, int instructionOffset) { value.collectIndexedItems(indexedItems, method, instructionOffset); } + + @Override + public DexValueType asDexValueType() { + return this; + } + + @Override + public boolean isDexValueType() { + return true; + } } static public class DexValueField extends NestedDexValue<DexField> { @@ -979,6 +1005,16 @@ public String toString() { return "Array " + Arrays.toString(values); } + + @Override + public boolean isDexValueArray() { + return true; + } + + @Override + public DexValueArray asDexValueArray() { + return this; + } } static public class DexValueAnnotation extends DexValue {
diff --git a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java index 68c7da1..6ff5c75 100644 --- a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java +++ b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
@@ -5,8 +5,10 @@ import com.android.tools.r8.dex.IndexedItemCollection; import com.android.tools.r8.dex.MixedSectionCollection; +import com.android.tools.r8.utils.ArrayUtils; import java.util.Arrays; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; /** @@ -193,4 +195,12 @@ } return new ParameterAnnotationsList(filtered, missingParameterAnnotations); } + + public ParameterAnnotationsList rewrite(Function<DexAnnotationSet, DexAnnotationSet> mapper) { + DexAnnotationSet[] rewritten = ArrayUtils.map(DexAnnotationSet[].class, values, mapper); + if (rewritten != values) { + return new ParameterAnnotationsList(rewritten, missingParameterAnnotations); + } + return this; + } }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java new file mode 100644 index 0000000..d654c87 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
@@ -0,0 +1,261 @@ +// Copyright (c) 2019, 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.analysis; + +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.ir.code.ArrayPut; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.Instruction; +import com.android.tools.r8.ir.code.InvokeDirect; +import com.android.tools.r8.ir.code.InvokeMethod; +import com.android.tools.r8.ir.code.InvokeNewArray; +import com.android.tools.r8.ir.code.NewArrayEmpty; +import com.android.tools.r8.ir.code.NewArrayFilledData; +import com.android.tools.r8.ir.code.StaticPut; +import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.utils.LongInterval; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.Set; + +public class ValueMayDependOnEnvironmentAnalysis { + + private final AppView<?> appView; + private final IRCode code; + private final DexType context; + + public ValueMayDependOnEnvironmentAnalysis(AppView<?> appView, IRCode code) { + this.appView = appView; + this.code = code; + this.context = code.method.method.holder; + } + + public boolean valueMayDependOnEnvironment(Value value) { + Value root = value.getAliasedValue(); + if (root.isConstant()) { + return false; + } + if (isConstantArrayThroughoutMethod(root)) { + return false; + } + if (isNewInstanceWithoutEnvironmentDependentFields(root)) { + return false; + } + return true; + } + + public boolean isConstantArrayThroughoutMethod(Value value) { + Value root = value.getAliasedValue(); + if (root.isPhi()) { + // Would need to track the aliases, just give up. + return false; + } + + Instruction definition = root.definition; + + // Check that it is a constant array with a known size at this point in the IR. + long size; + if (definition.isInvokeNewArray()) { + InvokeNewArray invokeNewArray = definition.asInvokeNewArray(); + for (Value argument : invokeNewArray.arguments()) { + if (!argument.isConstant()) { + return false; + } + } + size = invokeNewArray.arguments().size(); + } else if (definition.isNewArrayEmpty()) { + NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty(); + Value sizeValue = newArrayEmpty.size().getAliasedValue(); + if (!sizeValue.hasValueRange()) { + return false; + } + LongInterval sizeRange = sizeValue.getValueRange(); + if (!sizeRange.isSingleValue()) { + return false; + } + size = sizeRange.getSingleValue(); + } else { + // Some other array creation. + return false; + } + + if (size < 0) { + // Check for NegativeArraySizeException. + return false; + } + + if (size == 0) { + // Empty arrays are always constant. + return true; + } + + // Allow array-put and new-array-filled-data instructions that immediately follow the array + // creation. + Set<Instruction> consumedInstructions = Sets.newIdentityHashSet(); + + for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) { + if (instruction.isArrayPut()) { + ArrayPut arrayPut = instruction.asArrayPut(); + Value array = arrayPut.array().getAliasedValue(); + if (array != root) { + // This ends the chain of array-put instructions that are allowed immediately after the + // array creation. + break; + } + + LongInterval indexRange = arrayPut.index().getValueRange(); + if (!indexRange.isSingleValue()) { + return false; + } + + long index = indexRange.getSingleValue(); + if (index < 0 || index >= size) { + return false; + } + + if (!arrayPut.value().isConstant()) { + return false; + } + + consumedInstructions.add(arrayPut); + continue; + } + + if (instruction.isNewArrayFilledData()) { + NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData(); + Value array = newArrayFilledData.src(); + if (array != root) { + break; + } + + consumedInstructions.add(newArrayFilledData); + continue; + } + + if (instruction.instructionMayHaveSideEffects(appView, context)) { + // This ends the chain of array-put instructions that are allowed immediately after the + // array creation. + break; + } + } + + // Check that the array is not mutated before the end of this method. + // + // Currently, we only allow the array to flow into static-put instructions that are not + // followed by an instruction that may have side effects. Instructions that do not have any + // side effects are ignored because they cannot mutate the array. + return !valueMayBeMutatedBeforeMethodExit(root, consumedInstructions); + } + + private boolean isNewInstanceWithoutEnvironmentDependentFields(Value value) { + assert !value.hasAliasedValue(); + + if (value.isPhi() || !value.definition.isNewInstance()) { + return false; + } + + // Find the single constructor invocation. + InvokeMethod constructorInvoke = null; + for (Instruction instruction : value.uniqueUsers()) { + if (!instruction.isInvokeDirect()) { + continue; + } + + InvokeDirect invoke = instruction.asInvokeDirect(); + if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())) { + continue; + } + + if (invoke.getReceiver().getAliasedValue() != value) { + continue; + } + + if (constructorInvoke == null) { + constructorInvoke = invoke; + } else { + // Not a single constructor invocation, give up. + return false; + } + } + + if (constructorInvoke == null) { + // Didn't find a constructor invocation, give up. + return false; + } + + // Check that it is a trivial initializer (otherwise, the constructor could do anything). + DexEncodedMethod constructor = appView.definitionFor(constructorInvoke.getInvokedMethod()); + if (constructor == null) { + return false; + } + + TrivialInitializer initializerInfo = + constructor.getOptimizationInfo().getTrivialInitializerInfo(); + if (initializerInfo == null || !initializerInfo.isTrivialInstanceInitializer()) { + return false; + } + + // Check that none of the arguments to the constructor depend on the environment. + for (int i = 1; i < constructorInvoke.arguments().size(); i++) { + Value argument = constructorInvoke.arguments().get(i); + if (valueMayDependOnEnvironment(argument)) { + return false; + } + } + + // Finally, check that the object does not escape. + return !valueMayBeMutatedBeforeMethodExit(value, ImmutableSet.of(constructorInvoke)); + } + + private boolean valueMayBeMutatedBeforeMethodExit(Value value, Set<Instruction> whitelist) { + assert !value.hasAliasedValue(); + + if (value.numberOfPhiUsers() > 0) { + // Could be mutated indirectly. + return true; + } + + Set<Instruction> visited = Sets.newIdentityHashSet(); + for (Instruction user : value.uniqueUsers()) { + if (whitelist.contains(user)) { + continue; + } + + if (user.isStaticPut()) { + StaticPut staticPut = user.asStaticPut(); + if (visited.contains(staticPut)) { + // Already visited previously. + continue; + } + for (Instruction instruction : code.getInstructionsReachableFrom(staticPut)) { + if (!visited.add(instruction)) { + // Already visited previously. + continue; + } + if (instruction.isStaticPut()) { + StaticPut otherStaticPut = instruction.asStaticPut(); + if (otherStaticPut.getField().holder == staticPut.getField().holder + && instruction.instructionInstanceCanThrow(appView, context).cannotThrow()) { + continue; + } + return true; + } + if (instruction.instructionMayTriggerMethodInvocation(appView, context)) { + return true; + } + } + continue; + } + + // Other user than static-put, just give up. + return false; + } + + return false; + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java index 2d67908..1ef58fb 100644 --- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java +++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
@@ -9,6 +9,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.Nullability; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; import com.android.tools.r8.ir.code.BasicBlock; @@ -245,9 +246,14 @@ private void updateFieldOptimizationInfo(DexEncodedField field, Value value) { TypeLatticeElement fieldType = TypeLatticeElement.fromDexType(field.field.type, Nullability.maybeNull(), appView); - TypeLatticeElement valueType = value.getTypeLattice(); - if (valueType.strictlyLessThan(fieldType, appView)) { - feedback.markFieldHasDynamicType(field, valueType); + TypeLatticeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView); + if (dynamicUpperBoundType.strictlyLessThan(fieldType, appView)) { + feedback.markFieldHasDynamicUpperBoundType(field, dynamicUpperBoundType); + } + ClassTypeLatticeElement dynamicLowerBoundType = value.getDynamicLowerBoundType(appView); + if (dynamicLowerBoundType != null) { + assert dynamicLowerBoundType.lessThanOrEqual(dynamicUpperBoundType, appView); + feedback.markFieldHasDynamicLowerBoundType(field, dynamicLowerBoundType); } } }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java index 4071c99..35c483b 100644 --- a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java +++ b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
@@ -8,6 +8,7 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexEncodedField; import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.ir.analysis.ValueMayDependOnEnvironmentAnalysis; import com.android.tools.r8.ir.code.ArrayPut; import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.Instruction; @@ -42,6 +43,8 @@ OptionalBool controlFlowMayDependOnEnvironment = OptionalBool.unknown(); boolean mayHaveSideEffects = false; + ValueMayDependOnEnvironmentAnalysis environmentAnalysis = + new ValueMayDependOnEnvironmentAnalysis(appView, code); for (Instruction instruction : code.instructions()) { // Array stores to a newly created array are only observable if they may throw, or if the // array content may depend on the environment. @@ -50,14 +53,14 @@ Value array = arrayPut.array().getAliasedValue(); if (array.isPhi() || !array.definition.isCreatingArray() - || arrayPut.index().mayDependOnEnvironment(appView, code) - || arrayPut.value().mayDependOnEnvironment(appView, code) + || environmentAnalysis.valueMayDependOnEnvironment(arrayPut.index()) + || environmentAnalysis.valueMayDependOnEnvironment(arrayPut.value()) || arrayPut.instructionInstanceCanThrow(appView, context).isThrowing()) { return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; } if (controlFlowMayDependOnEnvironment.isUnknown()) { controlFlowMayDependOnEnvironment = - OptionalBool.of(code.controlFlowMayDependOnEnvironment(appView)); + OptionalBool.of(code.controlFlowMayDependOnEnvironment(environmentAnalysis)); } if (controlFlowMayDependOnEnvironment.isTrue()) { return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; @@ -76,7 +79,7 @@ } if (controlFlowMayDependOnEnvironment.isUnknown()) { controlFlowMayDependOnEnvironment = - OptionalBool.of(code.controlFlowMayDependOnEnvironment(appView)); + OptionalBool.of(code.controlFlowMayDependOnEnvironment(environmentAnalysis)); } if (controlFlowMayDependOnEnvironment.isTrue()) { return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; @@ -92,7 +95,7 @@ return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; } for (Value argument : invokeNewArray.arguments()) { - if (argument.mayDependOnEnvironment(appView, code)) { + if (environmentAnalysis.valueMayDependOnEnvironment(argument)) { return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; } } @@ -111,7 +114,7 @@ DexEncodedField field = appView.appInfo().resolveField(staticPut.getField()); if (field == null || field.field.holder != context - || staticPut.value().mayDependOnEnvironment(appView, code) + || environmentAnalysis.valueMayDependOnEnvironment(staticPut.value()) || instruction.instructionInstanceCanThrow(appView, context).isThrowing()) { return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED; }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Assume.java b/src/main/java/com/android/tools/r8/ir/code/Assume.java index 9ba29fd..37de422 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Assume.java +++ b/src/main/java/com/android/tools/r8/ir/code/Assume.java
@@ -16,6 +16,7 @@ import com.android.tools.r8.ir.conversion.DexBuilder; import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget; import com.android.tools.r8.ir.optimize.InliningConstraints; +import java.util.Objects; import java.util.Set; public class Assume<An extends Assumption> extends Instruction { @@ -46,14 +47,18 @@ } public static Assume<DynamicTypeAssumption> createAssumeDynamicTypeInstruction( - TypeLatticeElement type, - ClassTypeLatticeElement lowerBoundType, + TypeLatticeElement dynamicUpperBoundType, + ClassTypeLatticeElement dynamicLowerBoundType, Value dest, Value src, Instruction origin, AppView<?> appView) { return new Assume<>( - new DynamicTypeAssumption(type, lowerBoundType), dest, src, origin, appView); + new DynamicTypeAssumption(dynamicUpperBoundType, dynamicLowerBoundType), + dest, + src, + origin, + appView); } @Override @@ -170,7 +175,7 @@ return true; } if (assumption.isAssumeDynamicType()) { - outType = asAssumeDynamicType().assumption.getType(); + outType = asAssumeDynamicType().assumption.getDynamicUpperBoundType(); } if (appView.appInfo().hasSubtyping()) { if (outType.isClassType() @@ -288,19 +293,24 @@ @Override public String toString() { - // During branch simplification, the origin `if` could be simplified. - // It means the assumption became "truth." - assert origin.hasBlock() || isAssumeNonNull(); + // `origin` could become obsolete: + // 1) during branch simplification, the origin `if` could be simplified, which means the + // assumption became "truth." + // 2) invoke-interface could be devirtualized, while its dynamic type and/or non-null receiver + // are still valid. String originString = - origin.hasBlock() ? " (origin: `" + origin.toString() + "`)" : " (origin simplified)"; + origin.hasBlock() ? " (origin: `" + origin.toString() + "`)" : " (obsolete origin)"; if (isAssumeNone() || isAssumeNonNull()) { return super.toString() + originString; } if (isAssumeDynamicType()) { DynamicTypeAssumption assumption = asAssumeDynamicType().getAssumption(); return super.toString() - + "; upper bound: " + assumption.type - + (assumption.lowerBoundType != null ? "; lower bound: " + assumption.lowerBoundType : "") + + "; upper bound: " + + assumption.dynamicUpperBoundType + + (assumption.dynamicLowerBoundType != null + ? "; lower bound: " + assumption.dynamicLowerBoundType + : "") + originString; } return super.toString(); @@ -348,20 +358,21 @@ public static class DynamicTypeAssumption extends Assumption { - private final TypeLatticeElement type; - private final ClassTypeLatticeElement lowerBoundType; + private final TypeLatticeElement dynamicUpperBoundType; + private final ClassTypeLatticeElement dynamicLowerBoundType; - private DynamicTypeAssumption(TypeLatticeElement type, ClassTypeLatticeElement lowerBoundType) { - this.type = type; - this.lowerBoundType = lowerBoundType; + private DynamicTypeAssumption( + TypeLatticeElement dynamicUpperBoundType, ClassTypeLatticeElement dynamicLowerBoundType) { + this.dynamicUpperBoundType = dynamicUpperBoundType; + this.dynamicLowerBoundType = dynamicLowerBoundType; } - public TypeLatticeElement getType() { - return type; + public TypeLatticeElement getDynamicUpperBoundType() { + return dynamicUpperBoundType; } - public ClassTypeLatticeElement getLowerBoundType() { - return lowerBoundType; + public ClassTypeLatticeElement getDynamicLowerBoundType() { + return dynamicLowerBoundType; } @Override @@ -371,7 +382,7 @@ @Override public boolean verifyCorrectnessOfValues(Value dest, Value src, AppView<?> appView) { - assert type.lessThanOrEqualUpToNullability(src.getTypeLattice(), appView); + assert dynamicUpperBoundType.lessThanOrEqualUpToNullability(src.getTypeLattice(), appView); return true; } @@ -384,12 +395,13 @@ return false; } DynamicTypeAssumption assumption = (DynamicTypeAssumption) other; - return type == assumption.type; + return dynamicUpperBoundType == assumption.dynamicUpperBoundType + && dynamicLowerBoundType == assumption.dynamicLowerBoundType; } @Override public int hashCode() { - return type.hashCode(); + return Objects.hash(dynamicUpperBoundType, dynamicLowerBoundType); } }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java index 78018f9..a4c8d4b 100644 --- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java +++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -26,6 +26,7 @@ import com.google.common.base.Equivalence.Wrapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; @@ -35,7 +36,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -854,7 +854,7 @@ } public Set<Value> cleanForRemoval() { - Set<Value> affectedValues = new HashSet<>(); + Set<Value> affectedValues = Sets.newIdentityHashSet(); for (BasicBlock block : successors) { affectedValues.addAll(block.getPhis()); block.removePredecessor(this, affectedValues);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java index 63f6636..708e947 100644 --- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java +++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -20,7 +20,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; @@ -617,7 +616,7 @@ if (normalExits.isEmpty()) { assert inlineeCanThrow; DominatorTree dominatorTree = new DominatorTree(code, MAY_HAVE_UNREACHABLE_BLOCKS); - Set<Value> affectedValues = new HashSet<>(); + Set<Value> affectedValues = Sets.newIdentityHashSet(); blocksToRemove.addAll(invokePredecessor.unlink(invokeBlock, dominatorTree, affectedValues)); new TypeAnalysis(appView).narrowing(affectedValues); }
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java index b95376e..72e4c9d 100644 --- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java +++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -8,13 +8,16 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexClass; import com.android.tools.r8.graph.DexEncodedField; +import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexField; +import com.android.tools.r8.graph.DexItemFactory; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.analysis.AbstractError; import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet; import com.android.tools.r8.ir.analysis.fieldvalueanalysis.ConcreteMutableFieldSet; import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet; import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet; +import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; import com.android.tools.r8.shaking.AppInfoWithLiveness; import java.util.Collections; import java.util.List; @@ -163,4 +166,39 @@ assert isFieldPut(); return EmptyFieldSet.getInstance(); } + + /** + * Returns {@code true} if this instruction may store an instance of a class that has a non- + * default finalize() method in a field. In that case, it is not safe to remove this instruction, + * since that could change the lifetime of the value. + */ + boolean isStoringObjectWithFinalizer(AppInfoWithLiveness appInfo) { + assert isFieldPut(); + TypeLatticeElement type = value().getTypeLattice(); + TypeLatticeElement baseType = + type.isArrayType() ? type.asArrayTypeLatticeElement().getArrayBaseTypeLattice() : type; + if (baseType.isClassType()) { + Value root = value().getAliasedValue(); + if (!root.isPhi() && root.definition.isNewInstance()) { + DexClass clazz = appInfo.definitionFor(root.definition.asNewInstance().clazz); + if (clazz == null) { + return true; + } + if (clazz.superType == null) { + return false; + } + DexItemFactory dexItemFactory = appInfo.dexItemFactory(); + DexEncodedMethod resolutionResult = + appInfo + .resolveMethod(clazz.type, dexItemFactory.objectMethods.finalize) + .asSingleTarget(); + return resolutionResult != null && resolutionResult.isProgramMethod(appInfo); + } + + return appInfo.mayHaveFinalizeMethodDirectlyOrIndirectly( + baseType.asClassTypeLatticeElement()); + } + + return false; + } }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java index 99a4956..955182b 100644 --- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java +++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -10,6 +10,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.analysis.TypeChecker; +import com.android.tools.r8.ir.analysis.ValueMayDependOnEnvironmentAnalysis; import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.Nullability; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; @@ -35,6 +36,7 @@ import java.util.Deque; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; @@ -152,8 +154,9 @@ worklist.addAll(sorted.reverse()); while (!worklist.isEmpty()) { BasicBlock block = worklist.poll(); - Set<Value> live = new HashSet<>(); - Set<Value> liveLocals = new HashSet<>(); + // Note that the iteration order of live values matters when inserting spill/restore moves. + Set<Value> live = new LinkedHashSet<>(); + Set<Value> liveLocals = Sets.newIdentityHashSet(); Deque<Value> liveStack = new ArrayDeque<>(); Set<BasicBlock> exceptionalSuccessors = block.getCatchHandlers().getUniqueTargets(); for (BasicBlock succ : block.getSuccessors()) { @@ -259,7 +262,8 @@ return liveAtEntrySets; } - public boolean controlFlowMayDependOnEnvironment(AppView<?> appView) { + public boolean controlFlowMayDependOnEnvironment( + ValueMayDependOnEnvironmentAnalysis environmentAnalysis) { for (BasicBlock block : blocks) { if (block.hasCatchHandlers()) { // Whether an instruction throws may generally depend on the environment. @@ -267,16 +271,16 @@ } if (block.exit().isIf()) { If ifInstruction = block.exit().asIf(); - if (ifInstruction.lhs().mayDependOnEnvironment(appView, this)) { + if (environmentAnalysis.valueMayDependOnEnvironment(ifInstruction.lhs())) { return true; } if (!ifInstruction.isZeroTest() - && ifInstruction.rhs().mayDependOnEnvironment(appView, this)) { + && environmentAnalysis.valueMayDependOnEnvironment(ifInstruction.rhs())) { return true; } } else if (block.exit().isSwitch()) { Switch switchInstruction = block.exit().asSwitch(); - if (switchInstruction.value().mayDependOnEnvironment(appView, this)) { + if (environmentAnalysis.valueMayDependOnEnvironment(switchInstruction.value())) { return true; } } @@ -631,7 +635,7 @@ } private boolean consistentDefUseChains() { - Set<Value> values = new HashSet<>(); + Set<Value> values = Sets.newIdentityHashSet(); for (BasicBlock block : blocks) { int predecessorCount = block.getPredecessors().size();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java index 4dcb749..32484c3 100644 --- a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java +++ b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
@@ -110,7 +110,8 @@ DexEncodedField encodedField = appInfoWithLiveness.resolveField(getField()); assert encodedField != null : "NoSuchFieldError (resolution failure) should be caught."; - return appInfoWithLiveness.isFieldRead(encodedField); + return appInfoWithLiveness.isFieldRead(encodedField) + || isStoringObjectWithFinalizer(appInfoWithLiveness); } // In D8, we always have to assume that the field can be read, and thus have side effects.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java index 5bddf13..1129616 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java +++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -34,8 +34,8 @@ import com.android.tools.r8.utils.StringUtils.BraceType; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; -import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -46,7 +46,7 @@ protected final List<Value> inValues = new ArrayList<>(); private BasicBlock block = null; private int number = -1; - private Set<Value> debugValues = null; + private LinkedHashSet<Value> debugValues = null; private Position position = null; protected Instruction(Value outValue) { @@ -141,7 +141,7 @@ public void addDebugValue(Value value) { assert value.hasLocalInfo(); if (debugValues == null) { - debugValues = new HashSet<>(); + debugValues = new LinkedHashSet<>(); } if (debugValues.add(value)) { value.addDebugUser(this);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java index 48baef4..ab51239 100644 --- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java +++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -25,6 +25,7 @@ import com.android.tools.r8.ir.conversion.DexBuilder; import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget; import com.android.tools.r8.ir.optimize.InliningConstraints; +import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo; import com.android.tools.r8.shaking.AppInfoWithLiveness; import java.util.ArrayList; import java.util.Collection; @@ -218,14 +219,21 @@ assert false : "Expected to be able to find the enclosing class of a method definition"; return true; } - boolean targetMayHaveSideEffects; + if (appViewWithLiveness.appInfo().noSideEffects.containsKey(target.method)) { - targetMayHaveSideEffects = false; - } else { - targetMayHaveSideEffects = target.getOptimizationInfo().mayHaveSideEffects(); + return false; } - return targetMayHaveSideEffects; + MethodOptimizationInfo optimizationInfo = target.getOptimizationInfo(); + if (target.isInstanceInitializer()) { + TrivialInitializer trivialInitializerInfo = optimizationInfo.getTrivialInitializerInfo(); + if (trivialInitializerInfo != null + && trivialInitializerInfo.isTrivialInstanceInitializer()) { + return false; + } + } + + return target.getOptimizationInfo().mayHaveSideEffects(); } return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java index 84da0d1..9054d07 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Move.java +++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -122,4 +122,11 @@ public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, DexType context) { return false; } + + @Override + public boolean verifyTypes(AppView<?> appView) { + super.verifyTypes(appView); + assert src().getTypeLattice().equals(outValue().getTypeLattice()); + return true; + } }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java index 7e6fffe..7a38882 100644 --- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java +++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -11,7 +11,10 @@ import com.android.tools.r8.dex.Constants; import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.graph.ResolutionResult; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query; @@ -168,6 +171,18 @@ return true; } + // Verify that the object does not have a finalizer. + DexItemFactory dexItemFactory = appView.dexItemFactory(); + ResolutionResult finalizeResolutionResult = + appView.appInfo().resolveMethod(clazz, dexItemFactory.objectMethods.finalize); + if (finalizeResolutionResult.hasSingleTarget()) { + DexMethod finalizeMethod = finalizeResolutionResult.asSingleTarget().method; + if (finalizeMethod != dexItemFactory.enumMethods.finalize + && finalizeMethod != dexItemFactory.objectMethods.finalize) { + return true; + } + } + return false; }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java index 95dac01..ae145b3 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Phi.java +++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -19,6 +19,7 @@ import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.Reporter; import com.android.tools.r8.utils.StringUtils; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -342,7 +343,7 @@ @Override public boolean isValueOnStack() { - assert verifyIsStackPhi(new HashSet<>()); + assert verifyIsStackPhi(Sets.newIdentityHashSet()); return isStackPhi; }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java index 5d8fb67..63e5c10 100644 --- a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java +++ b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
@@ -15,16 +15,12 @@ import com.android.tools.r8.dex.Constants; import com.android.tools.r8.errors.Unreachable; import com.android.tools.r8.graph.AppView; -import com.android.tools.r8.graph.DexClass; import com.android.tools.r8.graph.DexEncodedField; -import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexField; -import com.android.tools.r8.graph.DexItemFactory; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption; import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query; -import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; import com.android.tools.r8.ir.conversion.CfBuilder; import com.android.tools.r8.ir.conversion.DexBuilder; import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget; @@ -132,40 +128,6 @@ return true; } - /** - * Returns {@code true} if this instruction may store a value that has a non-default finalize() - * method in a static field. In that case, it is not safe to remove this instruction, since that - * could change the lifetime of the value. - */ - private boolean isStoringObjectWithFinalizer(AppInfoWithLiveness appInfo) { - TypeLatticeElement type = value().getTypeLattice(); - TypeLatticeElement baseType = - type.isArrayType() ? type.asArrayTypeLatticeElement().getArrayBaseTypeLattice() : type; - if (baseType.isClassType()) { - Value root = value().getAliasedValue(); - if (!root.isPhi() && root.definition.isNewInstance()) { - DexClass clazz = appInfo.definitionFor(root.definition.asNewInstance().clazz); - if (clazz == null) { - return true; - } - if (clazz.superType == null) { - return false; - } - DexItemFactory dexItemFactory = appInfo.dexItemFactory(); - DexEncodedMethod resolutionResult = - appInfo - .resolveMethod(clazz.type, dexItemFactory.objectMethods.finalize) - .asSingleTarget(); - return resolutionResult != null && resolutionResult.isProgramMethod(appInfo); - } - - return appInfo.mayHaveFinalizeMethodDirectlyOrIndirectly( - baseType.asClassTypeLatticeElement().getClassType()); - } - - return false; - } - @Override public boolean canBeDeadCode(AppView<?> appView, IRCode code) { // static-put can be dead as long as it cannot have any of the following:
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java index 33b06c5..74add42 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Value.java +++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -9,6 +9,8 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DebugLocalInfo; import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexEncodedField; +import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; @@ -28,7 +30,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -37,7 +38,7 @@ import java.util.Set; import java.util.function.Predicate; -public class Value { +public class Value implements Comparable<Value> { public void constrainType( ValueTypeConstraint constraint, DexMethod method, Origin origin, Reporter reporter) { @@ -431,16 +432,20 @@ public Set<Instruction> aliasedUsers() { Set<Instruction> users = SetUtils.newIdentityHashSet(uniqueUsers()); - collectAliasedUsersViaAssume(uniqueUsers(), users); + Set<Instruction> visited = Sets.newIdentityHashSet(); + collectAliasedUsersViaAssume(visited, uniqueUsers(), users); return users; } private static void collectAliasedUsersViaAssume( - Set<Instruction> usersToTest, Set<Instruction> collectedUsers) { + Set<Instruction> visited, Set<Instruction> usersToTest, Set<Instruction> collectedUsers) { for (Instruction user : usersToTest) { + if (!visited.add(user)) { + continue; + } if (user.isAssume()) { collectedUsers.addAll(user.outValue().uniqueUsers()); - collectAliasedUsersViaAssume(user.outValue().uniqueUsers(), collectedUsers); + collectAliasedUsersViaAssume(visited, user.outValue().uniqueUsers(), collectedUsers); } } } @@ -510,17 +515,6 @@ return false; } - public boolean mayDependOnEnvironment(AppView<?> appView, IRCode code) { - Value root = getAliasedValue(); - if (root.isConstant()) { - return false; - } - if (root.isConstantArrayThroughoutMethod(appView, code)) { - return false; - } - return true; - } - public boolean usedInMonitorOperation() { for (Instruction instruction : uniqueUsers()) { if (instruction.isMonitor()) { @@ -760,6 +754,11 @@ } @Override + public int compareTo(Value value) { + return Integer.compare(this.number, value.number); + } + + @Override public int hashCode() { return number; } @@ -831,149 +830,33 @@ return definition.isOutConstant() && !hasLocalInfo(); } - public boolean isConstantArrayThroughoutMethod(AppView<?> appView, IRCode code) { + public DexEncodedField getEnumField(AppView<?> appView) { + if (!appView.enableWholeProgramOptimizations()) { + return null; + } + Value root = getAliasedValue(); - if (root.isPhi()) { - // Would need to track the aliases, just give up. - return false; + if (root.isPhi() || !root.definition.isStaticGet()) { + return null; } - DexType context = code.method.method.holder; - Instruction definition = root.definition; - - // Check that it is a constant array with a known size at this point in the IR. - long size; - if (definition.isInvokeNewArray()) { - InvokeNewArray invokeNewArray = definition.asInvokeNewArray(); - for (Value argument : invokeNewArray.arguments()) { - if (!argument.isConstant()) { - return false; - } - } - size = invokeNewArray.arguments().size(); - } else if (definition.isNewArrayEmpty()) { - NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty(); - Value sizeValue = newArrayEmpty.size().getAliasedValue(); - if (!sizeValue.hasValueRange()) { - return false; - } - LongInterval sizeRange = sizeValue.getValueRange(); - if (!sizeRange.isSingleValue()) { - return false; - } - size = sizeRange.getSingleValue(); - } else { - // Some other array creation. - return false; + StaticGet staticGet = root.definition.asStaticGet(); + DexField field = staticGet.getField(); + if (field.type != field.holder) { + return null; } - if (size < 0) { - // Check for NegativeArraySizeException. - return false; + DexEncodedField encodedField = appView.definitionFor(field); + if (encodedField == null) { + return null; } - if (size == 0) { - // Empty arrays are always constant. - return true; + DexClass clazz = appView.definitionFor(encodedField.field.holder); + if (clazz == null || !clazz.isEnum()) { + return null; } - // Allow array-put and new-array-filled-data instructions that immediately follow the array - // creation. - Set<Instruction> consumedInstructions = Sets.newIdentityHashSet(); - - for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) { - if (instruction.isArrayPut()) { - ArrayPut arrayPut = instruction.asArrayPut(); - Value array = arrayPut.array().getAliasedValue(); - if (array != root) { - // This ends the chain of array-put instructions that are allowed immediately after the - // array creation. - break; - } - - LongInterval indexRange = arrayPut.index().getValueRange(); - if (!indexRange.isSingleValue()) { - return false; - } - - long index = indexRange.getSingleValue(); - if (index < 0 || index >= size) { - return false; - } - - if (!arrayPut.value().isConstant()) { - return false; - } - - consumedInstructions.add(arrayPut); - continue; - } - - if (instruction.isNewArrayFilledData()) { - NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData(); - Value array = newArrayFilledData.src(); - if (array != root) { - break; - } - - consumedInstructions.add(newArrayFilledData); - continue; - } - - if (instruction.instructionMayHaveSideEffects(appView, context)) { - // This ends the chain of array-put instructions that are allowed immediately after the - // array creation. - break; - } - } - - // Check that the array is not mutated before the end of this method. - // - // Currently, we only allow the array to flow into static-put instructions that are not - // followed by an instruction that may have side effects. Instructions that do not have any - // side effects are ignored because they cannot mutate the array. - Set<Instruction> visitedFromStaticPut = Sets.newIdentityHashSet(); - for (Instruction user : root.uniqueUsers()) { - if (consumedInstructions.contains(user)) { - continue; - } - - if (user.isStaticPut()) { - StaticPut staticPut = user.asStaticPut(); - if (visitedFromStaticPut.contains(staticPut)) { - // Already visited previously. - continue; - } - for (Instruction instruction : code.getInstructionsReachableFrom(staticPut)) { - if (!visitedFromStaticPut.add(instruction)) { - // Already visited previously. - continue; - } - if (instruction.isStaticPut()) { - StaticPut otherStaticPut = instruction.asStaticPut(); - if (otherStaticPut.getField().holder == staticPut.getField().holder - && instruction.instructionInstanceCanThrow(appView, context).cannotThrow()) { - continue; - } - return false; - } - if (instruction.instructionMayTriggerMethodInvocation(appView, context)) { - return false; - } - } - continue; - } - - // Other user than static-put, just give up. - return false; - } - - if (root.numberOfPhiUsers() > 0) { - // Could be mutated indirectly. - return false; - } - - return true; + return encodedField; } public boolean isPhi() { @@ -1030,7 +913,7 @@ if (isPhi()) { Phi self = this.asPhi(); if (seen == null) { - seen = new HashSet<>(); + seen = Sets.newIdentityHashSet(); } if (seen.contains(self)) { return true; @@ -1101,7 +984,7 @@ public boolean isDead(AppView<?> appView, IRCode code, Predicate<Instruction> ignoreUser) { // Totally unused values are trivially dead. - return !isUsed() || isDead(appView, code, ignoreUser, new HashSet<>()); + return !isUsed() || isDead(appView, code, ignoreUser, Sets.newIdentityHashSet()); } /** @@ -1209,7 +1092,8 @@ if (aliasedValue != null) { // If there is an alias of the receiver, which is defined by an Assume<DynamicTypeAssumption> // instruction, then use the dynamic type as the refined receiver type. - lattice = aliasedValue.definition.asAssumeDynamicType().getAssumption().getType(); + lattice = + aliasedValue.definition.asAssumeDynamicType().getAssumption().getDynamicUpperBoundType(); // For precision, verify that the dynamic type is at least as precise as the static type. assert lattice.lessThanOrEqualUpToNullability(typeLattice, appView) @@ -1250,7 +1134,7 @@ Value aliasedValue = getSpecificAliasedValue(value -> value.definition.isAssumeDynamicType()); if (aliasedValue != null) { ClassTypeLatticeElement lattice = - aliasedValue.definition.asAssumeDynamicType().getAssumption().getLowerBoundType(); + aliasedValue.definition.asAssumeDynamicType().getAssumption().getDynamicLowerBoundType(); return lattice != null && typeLattice.isDefinitelyNotNull() && lattice.isNullable() ? lattice.asMeetWithNotNull().asClassTypeLatticeElement() : lattice;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java index 307215a..b9f72f7 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -6,8 +6,7 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexEncodedMethod; -import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator; -import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator.CycleEliminationResult; +import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult; import com.android.tools.r8.ir.conversion.CallSiteInformation.CallGraphBasedCallSiteInformation; import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.utils.Timing; @@ -76,7 +75,14 @@ caller.callees.remove(this); } - public void cleanForRemoval() { + public void cleanCalleesForRemoval() { + assert callers.isEmpty(); + for (Node callee : callees) { + callee.callers.remove(this); + } + } + + public void cleanCallersForRemoval() { assert callees.isEmpty(); for (Node caller : callers) { caller.callees.remove(this); @@ -103,6 +109,10 @@ return callers.contains(method); } + public boolean isRoot() { + return callers.isEmpty(); + } + public boolean isLeaf() { return callees.isEmpty(); } @@ -123,21 +133,24 @@ builder.append(callers.size()); builder.append(" callers"); builder.append(", invoke count ").append(numberOfCallSites); - builder.append(").\n"); + builder.append(")."); + builder.append(System.lineSeparator()); if (callees.size() > 0) { - builder.append("Callees:\n"); + builder.append("Callees:"); + builder.append(System.lineSeparator()); for (Node call : callees) { builder.append(" "); builder.append(call.method.toSourceString()); - builder.append("\n"); + builder.append(System.lineSeparator()); } } if (callers.size() > 0) { - builder.append("Callers:\n"); + builder.append("Callers:"); + builder.append(System.lineSeparator()); for (Node caller : callers) { builder.append(" "); builder.append(caller.method.toSourceString()); - builder.append("\n"); + builder.append(System.lineSeparator()); } } return builder.toString(); @@ -156,14 +169,6 @@ return new CallGraphBuilder(appView); } - /** - * Extract the next set of leaves (nodes with an call (outgoing) degree of 0) if any. - * - * <p>All nodes in the graph are extracted if called repeatedly until null is returned. Please - * note that there are no cycles in this graph (see {@link CycleEliminator#breakCycles}). - * - * <p> - */ static MethodProcessor createMethodProcessor( AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing) throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java index 8078fe0..546c021 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
@@ -4,59 +4,26 @@ package com.android.tools.r8.ir.conversion; -import com.android.tools.r8.errors.CompilationError; import com.android.tools.r8.graph.AppView; -import com.android.tools.r8.graph.DexCallSite; -import com.android.tools.r8.graph.DexClass; -import com.android.tools.r8.graph.DexEncodedField; import com.android.tools.r8.graph.DexEncodedMethod; -import com.android.tools.r8.graph.DexField; -import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexProgramClass; -import com.android.tools.r8.graph.DexType; -import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult; -import com.android.tools.r8.graph.ResolutionResult; -import com.android.tools.r8.graph.UseRegistry; -import com.android.tools.r8.ir.code.Invoke.Type; -import com.android.tools.r8.ir.conversion.CallGraph.Node; -import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator.CycleEliminationResult; -import com.android.tools.r8.logging.Log; import com.android.tools.r8.shaking.AppInfoWithLiveness; -import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.SetUtils; import com.android.tools.r8.utils.ThreadUtils; -import com.android.tools.r8.utils.Timing; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import java.util.ArrayDeque; +import com.google.common.base.Predicates; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.function.Consumer; -public class CallGraphBuilder { - - private final AppView<AppInfoWithLiveness> appView; - private final Map<DexMethod, Node> nodes = new IdentityHashMap<>(); - private final Map<DexMethod, Set<DexEncodedMethod>> possibleTargetsCache = - new ConcurrentHashMap<>(); +public class CallGraphBuilder extends CallGraphBuilderBase { CallGraphBuilder(AppView<AppInfoWithLiveness> appView) { - this.appView = appView; + super(appView); } - public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException { + @Override + void process(ExecutorService executorService) throws ExecutionException { List<Future<?>> futures = new ArrayList<>(); for (DexProgramClass clazz : appView.appInfo().classes()) { if (clazz.hasMethods()) { @@ -69,19 +36,6 @@ } } ThreadUtils.awaitFutures(futures); - - assert verifyAllMethodsWithCodeExists(); - - timing.begin("Cycle elimination"); - // Sort the nodes for deterministic cycle elimination. - Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values()); - CycleEliminator cycleEliminator = - new CycleEliminator(nodesWithDeterministicOrder, appView.options()); - CycleEliminationResult cycleEliminationResult = cycleEliminator.breakCycles(); - timing.end(); - assert cycleEliminator.breakCycles().numberOfRemovedEdges() == 0; // The cycles should be gone. - - return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult); } private void processClass(DexProgramClass clazz) { @@ -90,17 +44,13 @@ private void processMethod(DexEncodedMethod method) { if (method.hasCode()) { - method.registerCodeReferences(new InvokeExtractor(getOrCreateNode(method))); + method.registerCodeReferences( + new InvokeExtractor(getOrCreateNode(method), Predicates.alwaysTrue())); } } - private Node getOrCreateNode(DexEncodedMethod method) { - synchronized (nodes) { - return nodes.computeIfAbsent(method.method, ignore -> new Node(method)); - } - } - - private boolean verifyAllMethodsWithCodeExists() { + @Override + boolean verifyAllMethodsWithCodeExists() { for (DexProgramClass clazz : appView.appInfo().classes()) { for (DexEncodedMethod method : clazz.methods()) { assert !method.hasCode() || nodes.get(method.method) != null; @@ -108,454 +58,4 @@ } return true; } - - private class InvokeExtractor extends UseRegistry { - - private final Node caller; - - InvokeExtractor(Node caller) { - super(appView.dexItemFactory()); - this.caller = caller; - } - - private void addClassInitializerTarget(DexClass clazz) { - assert clazz != null; - if (clazz.isProgramClass() && clazz.hasClassInitializer()) { - addTarget(clazz.getClassInitializer(), false); - } - } - - private void addClassInitializerTarget(DexType type) { - assert type.isClassType(); - DexClass clazz = appView.definitionFor(type); - if (clazz != null) { - addClassInitializerTarget(clazz); - } - } - - private void addTarget(DexEncodedMethod callee, boolean likelySpuriousCallEdge) { - if (callee.accessFlags.isAbstract()) { - // Not a valid target. - return; - } - if (appView.appInfo().isPinned(callee.method)) { - // Since the callee is kept, we cannot inline it into the caller, and we also cannot collect - // any optimization info for the method. Therefore, we drop the call edge to reduce the - // total number of call graph edges, which should lead to fewer call graph cycles. - return; - } - assert callee.isProgramMethod(appView); - getOrCreateNode(callee).addCallerConcurrently(caller, likelySpuriousCallEdge); - } - - private void processInvoke(Type originalType, DexMethod originalMethod) { - DexEncodedMethod source = caller.method; - DexMethod context = source.method; - GraphLenseLookupResult result = - appView.graphLense().lookupMethod(originalMethod, context, originalType); - DexMethod method = result.getMethod(); - Type type = result.getType(); - if (type == Type.INTERFACE || type == Type.VIRTUAL) { - // For virtual and interface calls add all potential targets that could be called. - ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method.holder, method); - resolutionResult.forEachTarget(target -> processInvokeWithDynamicDispatch(type, target)); - } else { - DexEncodedMethod singleTarget = - appView.appInfo().lookupSingleTarget(type, method, context.holder); - if (singleTarget != null) { - assert !source.accessFlags.isBridge() || singleTarget != caller.method; - DexClass clazz = appView.definitionFor(singleTarget.method.holder); - assert clazz != null; - if (clazz.isProgramClass()) { - // For static invokes, the class could be initialized. - if (type == Type.STATIC) { - addClassInitializerTarget(clazz); - } - addTarget(singleTarget, false); - } - } - } - } - - private void processInvokeWithDynamicDispatch(Type type, DexEncodedMethod encodedTarget) { - DexMethod target = encodedTarget.method; - DexClass clazz = appView.definitionFor(target.holder); - if (clazz == null) { - assert false : "Unable to lookup holder of `" + target.toSourceString() + "`"; - return; - } - - if (!appView.options().testing.addCallEdgesForLibraryInvokes) { - if (clazz.isLibraryClass()) { - // Likely to have many possible targets. - return; - } - } - - boolean isInterface = type == Type.INTERFACE; - Set<DexEncodedMethod> possibleTargets = - possibleTargetsCache.computeIfAbsent( - target, - method -> { - ResolutionResult resolution = - appView.appInfo().resolveMethod(method.holder, method, isInterface); - if (resolution.isValidVirtualTarget(appView.options())) { - return resolution.lookupVirtualDispatchTargets(isInterface, appView.appInfo()); - } - return null; - }); - if (possibleTargets != null) { - boolean likelySpuriousCallEdge = - possibleTargets.size() >= appView.options().callGraphLikelySpuriousCallEdgeThreshold; - for (DexEncodedMethod possibleTarget : possibleTargets) { - if (possibleTarget.isProgramMethod(appView)) { - addTarget(possibleTarget, likelySpuriousCallEdge); - } - } - } - } - - private void processFieldAccess(DexField field) { - // Any field access implicitly calls the class initializer. - if (field.holder.isClassType()) { - DexEncodedField encodedField = appView.appInfo().resolveField(field); - if (encodedField != null && encodedField.isStatic()) { - addClassInitializerTarget(field.holder); - } - } - } - - @Override - public boolean registerInvokeVirtual(DexMethod method) { - processInvoke(Type.VIRTUAL, method); - return false; - } - - @Override - public boolean registerInvokeDirect(DexMethod method) { - processInvoke(Type.DIRECT, method); - return false; - } - - @Override - public boolean registerInvokeStatic(DexMethod method) { - processInvoke(Type.STATIC, method); - return false; - } - - @Override - public boolean registerInvokeInterface(DexMethod method) { - processInvoke(Type.INTERFACE, method); - return false; - } - - @Override - public boolean registerInvokeSuper(DexMethod method) { - processInvoke(Type.SUPER, method); - return false; - } - - @Override - public boolean registerInstanceFieldWrite(DexField field) { - processFieldAccess(field); - return false; - } - - @Override - public boolean registerInstanceFieldRead(DexField field) { - processFieldAccess(field); - return false; - } - - @Override - public boolean registerNewInstance(DexType type) { - if (type.isClassType()) { - addClassInitializerTarget(type); - } - return false; - } - - @Override - public boolean registerStaticFieldRead(DexField field) { - processFieldAccess(field); - return false; - } - - @Override - public boolean registerStaticFieldWrite(DexField field) { - processFieldAccess(field); - return false; - } - - @Override - public boolean registerTypeReference(DexType type) { - return false; - } - - @Override - public void registerCallSite(DexCallSite callSite) { - registerMethodHandle( - callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY); - } - } - - public static class CycleEliminator { - - public static final String CYCLIC_FORCE_INLINING_MESSAGE = - "Unable to satisfy force inlining constraints due to cyclic force inlining"; - - private static class CallEdge { - - private final Node caller; - private final Node callee; - - public CallEdge(Node caller, Node callee) { - this.caller = caller; - this.callee = callee; - } - - public void remove() { - callee.removeCaller(caller); - } - } - - public static class CycleEliminationResult { - - private Map<Node, Set<Node>> removedEdges; - - CycleEliminationResult(Map<Node, Set<Node>> removedEdges) { - this.removedEdges = removedEdges; - } - - void forEachRemovedCaller(Node callee, Consumer<Node> fn) { - removedEdges.getOrDefault(callee, ImmutableSet.of()).forEach(fn); - } - - public int numberOfRemovedEdges() { - int numberOfRemovedEdges = 0; - for (Set<Node> nodes : removedEdges.values()) { - numberOfRemovedEdges += nodes.size(); - } - return numberOfRemovedEdges; - } - } - - private final Collection<Node> nodes; - private final InternalOptions options; - - // DFS stack. - private Deque<Node> stack = new ArrayDeque<>(); - - // Set of nodes on the DFS stack. - private Set<Node> stackSet = Sets.newIdentityHashSet(); - - // Set of nodes that have been visited entirely. - private Set<Node> marked = Sets.newIdentityHashSet(); - - // Mapping from callee to the set of callers that were removed from the callee. - private Map<Node, Set<Node>> removedEdges = new IdentityHashMap<>(); - - private int currentDepth = 0; - private int maxDepth = 0; - - public CycleEliminator(Collection<Node> nodes, InternalOptions options) { - this.options = options; - - // Call to reorderNodes must happen after assigning options. - this.nodes = - options.testing.nondeterministicCycleElimination - ? reorderNodes(new ArrayList<>(nodes)) - : nodes; - } - - public CycleEliminationResult breakCycles() { - // Break cycles in this call graph by removing edges causing cycles. - for (Node node : nodes) { - assert currentDepth == 0; - traverse(node); - } - CycleEliminationResult result = new CycleEliminationResult(removedEdges); - if (Log.ENABLED) { - Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedEdges()); - Log.info(getClass(), "# max call graph depth: %s", maxDepth); - } - reset(); - return result; - } - - private void reset() { - assert currentDepth == 0; - assert stack.isEmpty(); - assert stackSet.isEmpty(); - marked.clear(); - maxDepth = 0; - removedEdges = new IdentityHashMap<>(); - } - - private void traverse(Node node) { - if (Log.ENABLED) { - if (currentDepth > maxDepth) { - maxDepth = currentDepth; - } - } - - if (marked.contains(node)) { - // Already visited all nodes that can be reached from this node. - return; - } - - push(node); - - // The callees must be sorted before calling traverse recursively. This ensures that cycles - // are broken the same way across multiple compilations. - Collection<Node> callees = node.getCalleesWithDeterministicOrder(); - - if (options.testing.nondeterministicCycleElimination) { - callees = reorderNodes(new ArrayList<>(callees)); - } - - Iterator<Node> calleeIterator = callees.iterator(); - while (calleeIterator.hasNext()) { - Node callee = calleeIterator.next(); - - // If we've exceeded the depth threshold, then treat it as if we have found a cycle. This - // ensures that we won't run into stack overflows when the call graph contains large call - // chains. This should have a negligible impact on code size as long as the threshold is - // large enough. - boolean foundCycle = stackSet.contains(callee); - boolean thresholdExceeded = - currentDepth >= options.callGraphCycleEliminatorMaxDepthThreshold - && edgeRemovalIsSafe(node, callee); - if (foundCycle || thresholdExceeded) { - // Found a cycle that needs to be eliminated. - if (edgeRemovalIsSafe(node, callee)) { - // Break the cycle by removing the edge node->callee. - if (options.testing.nondeterministicCycleElimination) { - callee.removeCaller(node); - } else { - // Need to remove `callee` from `node.callees` using the iterator to prevent a - // ConcurrentModificationException. This is not needed when nondeterministic cycle - // elimination is enabled, because we iterate a copy of `node.callees` in that case. - calleeIterator.remove(); - callee.getCallersWithDeterministicOrder().remove(node); - } - recordEdgeRemoval(node, callee); - - if (Log.ENABLED) { - Log.info( - CallGraph.class, - "Removed call edge from method '%s' to '%s'", - node.method.toSourceString(), - callee.method.toSourceString()); - } - } else { - assert foundCycle; - - // The cycle has a method that is marked as force inline. - LinkedList<Node> cycle = extractCycle(callee); - - if (Log.ENABLED) { - Log.info( - CallGraph.class, "Extracted cycle to find an edge that can safely be removed"); - } - - // Break the cycle by finding an edge that can be removed without breaking force - // inlining. If that is not possible, this call fails with a compilation error. - CallEdge edge = findCallEdgeForRemoval(cycle); - - // The edge will be null if this cycle has already been eliminated as a result of - // another cycle elimination. - if (edge != null) { - assert edgeRemovalIsSafe(edge.caller, edge.callee); - - // Break the cycle by removing the edge caller->callee. - edge.remove(); - recordEdgeRemoval(edge.caller, edge.callee); - - if (Log.ENABLED) { - Log.info( - CallGraph.class, - "Removed call edge from force inlined method '%s' to '%s' to ensure that " - + "force inlining will succeed", - node.method.toSourceString(), - callee.method.toSourceString()); - } - } - - // Recover the stack. - recoverStack(cycle); - } - } else { - currentDepth++; - traverse(callee); - currentDepth--; - } - } - pop(node); - marked.add(node); - } - - private void push(Node node) { - stack.push(node); - boolean changed = stackSet.add(node); - assert changed; - } - - private void pop(Node node) { - Node popped = stack.pop(); - assert popped == node; - boolean changed = stackSet.remove(node); - assert changed; - } - - private LinkedList<Node> extractCycle(Node entry) { - LinkedList<Node> cycle = new LinkedList<>(); - do { - assert !stack.isEmpty(); - cycle.add(stack.pop()); - } while (cycle.getLast() != entry); - return cycle; - } - - private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) { - Node callee = extractedCycle.getLast(); - for (Node caller : extractedCycle) { - if (!caller.hasCallee(callee)) { - // No need to break any edges since this cycle has already been broken previously. - assert !callee.hasCaller(caller); - return null; - } - if (edgeRemovalIsSafe(caller, callee)) { - return new CallEdge(caller, callee); - } - callee = caller; - } - throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE); - } - - private static boolean edgeRemovalIsSafe(Node caller, Node callee) { - // All call edges where the callee is a method that should be force inlined must be kept, - // to guarantee that the IR converter will process the callee before the caller. - return !callee.method.getOptimizationInfo().forceInline(); - } - - private void recordEdgeRemoval(Node caller, Node callee) { - removedEdges.computeIfAbsent(callee, ignore -> SetUtils.newIdentityHashSet(2)).add(caller); - } - - private void recoverStack(LinkedList<Node> extractedCycle) { - Iterator<Node> descendingIt = extractedCycle.descendingIterator(); - while (descendingIt.hasNext()) { - stack.push(descendingIt.next()); - } - } - - private Collection<Node> reorderNodes(List<Node> nodes) { - assert options.testing.nondeterministicCycleElimination; - if (!InternalOptions.DETERMINISTIC_DEBUGGING) { - Collections.shuffle(nodes); - } - return nodes; - } - } }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java new file mode 100644 index 0000000..1e0a60d --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
@@ -0,0 +1,536 @@ +// Copyright (c) 2019, 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.conversion; + +import com.android.tools.r8.errors.CompilationError; +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexCallSite; +import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexEncodedField; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexField; +import com.android.tools.r8.graph.DexMethod; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult; +import com.android.tools.r8.graph.ResolutionResult; +import com.android.tools.r8.graph.UseRegistry; +import com.android.tools.r8.ir.code.Invoke; +import com.android.tools.r8.ir.conversion.CallGraph.Node; +import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult; +import com.android.tools.r8.logging.Log; +import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.SetUtils; +import com.android.tools.r8.utils.Timing; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.Predicate; + +abstract class CallGraphBuilderBase { + final AppView<AppInfoWithLiveness> appView; + final Map<DexMethod, Node> nodes = new IdentityHashMap<>(); + private final Map<DexMethod, Set<DexEncodedMethod>> possibleTargetsCache = + new ConcurrentHashMap<>(); + + CallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) { + this.appView = appView; + } + + public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException { + process(executorService); + assert verifyAllMethodsWithCodeExists(); + + timing.begin("Cycle elimination"); + // Sort the nodes for deterministic cycle elimination. + Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values()); + CycleEliminator cycleEliminator = + new CycleEliminator(nodesWithDeterministicOrder, appView.options()); + CycleEliminationResult cycleEliminationResult = cycleEliminator.breakCycles(); + timing.end(); + assert cycleEliminator.breakCycles().numberOfRemovedEdges() == 0; // The cycles should be gone. + + return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult); + } + + abstract void process(ExecutorService executorService) throws ExecutionException; + + Node getOrCreateNode(DexEncodedMethod method) { + synchronized (nodes) { + return nodes.computeIfAbsent(method.method, ignore -> new Node(method)); + } + } + + abstract boolean verifyAllMethodsWithCodeExists(); + + class InvokeExtractor extends UseRegistry { + + private final Node caller; + private final Predicate<DexEncodedMethod> targetTester; + + InvokeExtractor(Node caller, Predicate<DexEncodedMethod> targetTester) { + super(appView.dexItemFactory()); + this.caller = caller; + this.targetTester = targetTester; + } + + private void addClassInitializerTarget(DexClass clazz) { + assert clazz != null; + if (clazz.isProgramClass() && clazz.hasClassInitializer()) { + addTarget(clazz.getClassInitializer(), false); + } + } + + private void addClassInitializerTarget(DexType type) { + assert type.isClassType(); + DexClass clazz = appView.definitionFor(type); + if (clazz != null) { + addClassInitializerTarget(clazz); + } + } + + private void addTarget(DexEncodedMethod callee, boolean likelySpuriousCallEdge) { + if (!targetTester.test(callee)) { + return; + } + if (callee.accessFlags.isAbstract()) { + // Not a valid target. + return; + } + if (appView.appInfo().isPinned(callee.method)) { + // Since the callee is kept, we cannot inline it into the caller, and we also cannot collect + // any optimization info for the method. Therefore, we drop the call edge to reduce the + // total number of call graph edges, which should lead to fewer call graph cycles. + return; + } + assert callee.isProgramMethod(appView); + getOrCreateNode(callee).addCallerConcurrently(caller, likelySpuriousCallEdge); + } + + private void processInvoke(Invoke.Type originalType, DexMethod originalMethod) { + DexEncodedMethod source = caller.method; + DexMethod context = source.method; + GraphLenseLookupResult result = + appView.graphLense().lookupMethod(originalMethod, context, originalType); + DexMethod method = result.getMethod(); + Invoke.Type type = result.getType(); + if (type == Invoke.Type.INTERFACE || type == Invoke.Type.VIRTUAL) { + // For virtual and interface calls add all potential targets that could be called. + ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method.holder, method); + resolutionResult.forEachTarget(target -> processInvokeWithDynamicDispatch(type, target)); + } else { + DexEncodedMethod singleTarget = + appView.appInfo().lookupSingleTarget(type, method, context.holder); + if (singleTarget != null) { + assert !source.accessFlags.isBridge() || singleTarget != caller.method; + DexClass clazz = appView.definitionFor(singleTarget.method.holder); + assert clazz != null; + if (clazz.isProgramClass()) { + // For static invokes, the class could be initialized. + if (type == Invoke.Type.STATIC) { + addClassInitializerTarget(clazz); + } + addTarget(singleTarget, false); + } + } + } + } + + private void processInvokeWithDynamicDispatch( + Invoke.Type type, DexEncodedMethod encodedTarget) { + DexMethod target = encodedTarget.method; + DexClass clazz = appView.definitionFor(target.holder); + if (clazz == null) { + assert false : "Unable to lookup holder of `" + target.toSourceString() + "`"; + return; + } + + if (!appView.options().testing.addCallEdgesForLibraryInvokes) { + if (clazz.isLibraryClass()) { + // Likely to have many possible targets. + return; + } + } + + boolean isInterface = type == Invoke.Type.INTERFACE; + Set<DexEncodedMethod> possibleTargets = + possibleTargetsCache.computeIfAbsent( + target, + method -> { + ResolutionResult resolution = + appView.appInfo().resolveMethod(method.holder, method, isInterface); + if (resolution.isValidVirtualTarget(appView.options())) { + return resolution.lookupVirtualDispatchTargets(isInterface, appView.appInfo()); + } + return null; + }); + if (possibleTargets != null) { + boolean likelySpuriousCallEdge = + possibleTargets.size() >= appView.options().callGraphLikelySpuriousCallEdgeThreshold; + for (DexEncodedMethod possibleTarget : possibleTargets) { + if (possibleTarget.isProgramMethod(appView)) { + addTarget(possibleTarget, likelySpuriousCallEdge); + } + } + } + } + + private void processFieldAccess(DexField field) { + // Any field access implicitly calls the class initializer. + if (field.holder.isClassType()) { + DexEncodedField encodedField = appView.appInfo().resolveField(field); + if (encodedField != null && encodedField.isStatic()) { + addClassInitializerTarget(field.holder); + } + } + } + + @Override + public boolean registerInvokeVirtual(DexMethod method) { + processInvoke(Invoke.Type.VIRTUAL, method); + return false; + } + + @Override + public boolean registerInvokeDirect(DexMethod method) { + processInvoke(Invoke.Type.DIRECT, method); + return false; + } + + @Override + public boolean registerInvokeStatic(DexMethod method) { + processInvoke(Invoke.Type.STATIC, method); + return false; + } + + @Override + public boolean registerInvokeInterface(DexMethod method) { + processInvoke(Invoke.Type.INTERFACE, method); + return false; + } + + @Override + public boolean registerInvokeSuper(DexMethod method) { + processInvoke(Invoke.Type.SUPER, method); + return false; + } + + @Override + public boolean registerInstanceFieldWrite(DexField field) { + processFieldAccess(field); + return false; + } + + @Override + public boolean registerInstanceFieldRead(DexField field) { + processFieldAccess(field); + return false; + } + + @Override + public boolean registerNewInstance(DexType type) { + if (type.isClassType()) { + addClassInitializerTarget(type); + } + return false; + } + + @Override + public boolean registerStaticFieldRead(DexField field) { + processFieldAccess(field); + return false; + } + + @Override + public boolean registerStaticFieldWrite(DexField field) { + processFieldAccess(field); + return false; + } + + @Override + public boolean registerTypeReference(DexType type) { + return false; + } + + @Override + public void registerCallSite(DexCallSite callSite) { + registerMethodHandle( + callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY); + } + } + + static class CycleEliminator { + + static final String CYCLIC_FORCE_INLINING_MESSAGE = + "Unable to satisfy force inlining constraints due to cyclic force inlining"; + + private static class CallEdge { + + private final Node caller; + private final Node callee; + + CallEdge(Node caller, Node callee) { + this.caller = caller; + this.callee = callee; + } + + public void remove() { + callee.removeCaller(caller); + } + } + + static class CycleEliminationResult { + + private Map<Node, Set<Node>> removedEdges; + + CycleEliminationResult(Map<Node, Set<Node>> removedEdges) { + this.removedEdges = removedEdges; + } + + void forEachRemovedCaller(Node callee, Consumer<Node> fn) { + removedEdges.getOrDefault(callee, ImmutableSet.of()).forEach(fn); + } + + int numberOfRemovedEdges() { + int numberOfRemovedEdges = 0; + for (Set<Node> nodes : removedEdges.values()) { + numberOfRemovedEdges += nodes.size(); + } + return numberOfRemovedEdges; + } + } + + private final Collection<Node> nodes; + private final InternalOptions options; + + // DFS stack. + private Deque<Node> stack = new ArrayDeque<>(); + + // Set of nodes on the DFS stack. + private Set<Node> stackSet = Sets.newIdentityHashSet(); + + // Set of nodes that have been visited entirely. + private Set<Node> marked = Sets.newIdentityHashSet(); + + // Mapping from callee to the set of callers that were removed from the callee. + private Map<Node, Set<Node>> removedEdges = new IdentityHashMap<>(); + + private int currentDepth = 0; + private int maxDepth = 0; + + CycleEliminator(Collection<Node> nodes, InternalOptions options) { + this.options = options; + + // Call to reorderNodes must happen after assigning options. + this.nodes = + options.testing.nondeterministicCycleElimination + ? reorderNodes(new ArrayList<>(nodes)) + : nodes; + } + + CycleEliminationResult breakCycles() { + // Break cycles in this call graph by removing edges causing cycles. + for (Node node : nodes) { + assert currentDepth == 0; + traverse(node); + } + CycleEliminationResult result = new CycleEliminationResult(removedEdges); + if (Log.ENABLED) { + Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedEdges()); + Log.info(getClass(), "# max call graph depth: %s", maxDepth); + } + reset(); + return result; + } + + private void reset() { + assert currentDepth == 0; + assert stack.isEmpty(); + assert stackSet.isEmpty(); + marked.clear(); + maxDepth = 0; + removedEdges = new IdentityHashMap<>(); + } + + private void traverse(Node node) { + if (Log.ENABLED) { + if (currentDepth > maxDepth) { + maxDepth = currentDepth; + } + } + + if (marked.contains(node)) { + // Already visited all nodes that can be reached from this node. + return; + } + + push(node); + + // The callees must be sorted before calling traverse recursively. This ensures that cycles + // are broken the same way across multiple compilations. + Collection<Node> callees = node.getCalleesWithDeterministicOrder(); + + if (options.testing.nondeterministicCycleElimination) { + callees = reorderNodes(new ArrayList<>(callees)); + } + + Iterator<Node> calleeIterator = callees.iterator(); + while (calleeIterator.hasNext()) { + Node callee = calleeIterator.next(); + + // If we've exceeded the depth threshold, then treat it as if we have found a cycle. This + // ensures that we won't run into stack overflows when the call graph contains large call + // chains. This should have a negligible impact on code size as long as the threshold is + // large enough. + boolean foundCycle = stackSet.contains(callee); + boolean thresholdExceeded = + currentDepth >= options.callGraphCycleEliminatorMaxDepthThreshold + && edgeRemovalIsSafe(node, callee); + if (foundCycle || thresholdExceeded) { + // Found a cycle that needs to be eliminated. + if (edgeRemovalIsSafe(node, callee)) { + // Break the cycle by removing the edge node->callee. + if (options.testing.nondeterministicCycleElimination) { + callee.removeCaller(node); + } else { + // Need to remove `callee` from `node.callees` using the iterator to prevent a + // ConcurrentModificationException. This is not needed when nondeterministic cycle + // elimination is enabled, because we iterate a copy of `node.callees` in that case. + calleeIterator.remove(); + callee.getCallersWithDeterministicOrder().remove(node); + } + recordEdgeRemoval(node, callee); + + if (Log.ENABLED) { + Log.info( + CallGraph.class, + "Removed call edge from method '%s' to '%s'", + node.method.toSourceString(), + callee.method.toSourceString()); + } + } else { + assert foundCycle; + + // The cycle has a method that is marked as force inline. + LinkedList<Node> cycle = extractCycle(callee); + + if (Log.ENABLED) { + Log.info( + CallGraph.class, "Extracted cycle to find an edge that can safely be removed"); + } + + // Break the cycle by finding an edge that can be removed without breaking force + // inlining. If that is not possible, this call fails with a compilation error. + CallEdge edge = findCallEdgeForRemoval(cycle); + + // The edge will be null if this cycle has already been eliminated as a result of + // another cycle elimination. + if (edge != null) { + assert edgeRemovalIsSafe(edge.caller, edge.callee); + + // Break the cycle by removing the edge caller->callee. + edge.remove(); + recordEdgeRemoval(edge.caller, edge.callee); + + if (Log.ENABLED) { + Log.info( + CallGraph.class, + "Removed call edge from force inlined method '%s' to '%s' to ensure that " + + "force inlining will succeed", + node.method.toSourceString(), + callee.method.toSourceString()); + } + } + + // Recover the stack. + recoverStack(cycle); + } + } else { + currentDepth++; + traverse(callee); + currentDepth--; + } + } + pop(node); + marked.add(node); + } + + private void push(Node node) { + stack.push(node); + boolean changed = stackSet.add(node); + assert changed; + } + + private void pop(Node node) { + Node popped = stack.pop(); + assert popped == node; + boolean changed = stackSet.remove(node); + assert changed; + } + + private LinkedList<Node> extractCycle(Node entry) { + LinkedList<Node> cycle = new LinkedList<>(); + do { + assert !stack.isEmpty(); + cycle.add(stack.pop()); + } while (cycle.getLast() != entry); + return cycle; + } + + private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) { + Node callee = extractedCycle.getLast(); + for (Node caller : extractedCycle) { + if (!caller.hasCallee(callee)) { + // No need to break any edges since this cycle has already been broken previously. + assert !callee.hasCaller(caller); + return null; + } + if (edgeRemovalIsSafe(caller, callee)) { + return new CallEdge(caller, callee); + } + callee = caller; + } + throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE); + } + + private static boolean edgeRemovalIsSafe(Node caller, Node callee) { + // All call edges where the callee is a method that should be force inlined must be kept, + // to guarantee that the IR converter will process the callee before the caller. + return !callee.method.getOptimizationInfo().forceInline(); + } + + private void recordEdgeRemoval(Node caller, Node callee) { + removedEdges.computeIfAbsent(callee, ignore -> SetUtils.newIdentityHashSet(2)).add(caller); + } + + private void recoverStack(LinkedList<Node> extractedCycle) { + Iterator<Node> descendingIt = extractedCycle.descendingIterator(); + while (descendingIt.hasNext()) { + stack.push(descendingIt.next()); + } + } + + private Collection<Node> reorderNodes(List<Node> nodes) { + assert options.testing.nondeterministicCycleElimination; + if (!InternalOptions.DETERMINISTIC_DEBUGGING) { + Collections.shuffle(nodes); + } + return nodes; + } + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java index 50fde0e..46a0cc4 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
@@ -5,6 +5,7 @@ package com.android.tools.r8.ir.conversion; import com.android.tools.r8.graph.DexEncodedField; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; public interface FieldOptimizationFeedback { @@ -13,7 +14,9 @@ void markFieldAsPropagated(DexEncodedField field); - void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type); + void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeLatticeElement type); + + void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type); void markFieldBitsRead(DexEncodedField field, int bitsRead); }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java index 995a583..7ef6cd5 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -620,8 +620,12 @@ } // Update the IR code if collected call site optimization info has something useful. + // While aggregation of parameter information at call sites would be more precise than static + // types, those could be still less precise at one single call site, where specific arguments + // will be passed during (double) inlining. Instead of adding assumptions and removing invalid + // ones, it's better not to insert assumptions for inlinee in the beginning. CallSiteOptimizationInfo callSiteOptimizationInfo = method.getCallSiteOptimizationInfo(); - if (appView.callSiteOptimizationInfoPropagator() != null) { + if (method == context && appView.callSiteOptimizationInfoPropagator() != null) { appView.callSiteOptimizationInfoPropagator() .applyCallSiteOptimizationInfo(ir, callSiteOptimizationInfo); }
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 dd3416f..7e15e72 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
@@ -205,7 +205,6 @@ .map(prefix -> "L" + DescriptorUtils.getPackageBinaryNameFromJavaType(prefix)) .map(options.itemFactory::createString) .collect(Collectors.toList()); - this.methodOptimizationInfoCollector = new MethodOptimizationInfoCollector(appView); if (options.isDesugaredLibraryCompilation()) { // Specific L8 Settings. // BackportedMethodRewriter is needed for retarget core library members and backports. @@ -241,6 +240,7 @@ this.stringSwitchRemover = null; this.desugaredLibraryAPIConverter = null; this.serviceLoaderRewriter = null; + this.methodOptimizationInfoCollector = null; return; } this.lambdaRewriter = options.enableDesugaring ? new LambdaRewriter(appView, this) : null; @@ -287,13 +287,16 @@ options.enableTreeShakingOfLibraryMethodOverrides ? new LibraryMethodOverrideAnalysis(appViewWithLiveness) : null; - this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, lambdaRewriter); - this.inliner = new Inliner(appViewWithLiveness, mainDexClasses, lensCodeRewriter); this.lambdaMerger = options.enableLambdaMerging ? new LambdaMerger(appViewWithLiveness) : null; + this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, lambdaRewriter); + this.inliner = + new Inliner(appViewWithLiveness, mainDexClasses, lambdaMerger, lensCodeRewriter); this.outliner = new Outliner(appViewWithLiveness, this); this.memberValuePropagation = options.enableValuePropagation ? new MemberValuePropagation(appViewWithLiveness) : null; + this.methodOptimizationInfoCollector = + new MethodOptimizationInfoCollector(appViewWithLiveness); if (options.isMinifying()) { this.identifierNameStringMarker = new IdentifierNameStringMarker(appViewWithLiveness); } else { @@ -329,6 +332,7 @@ this.d8NestBasedAccessDesugaring = options.shouldDesugarNests() ? new D8NestBasedAccessDesugaring(appView) : null; this.serviceLoaderRewriter = null; + this.methodOptimizationInfoCollector = null; } this.stringSwitchRemover = options.isStringSwitchConversionEnabled() @@ -429,7 +433,7 @@ private void synthesizeJava8UtilityClass( Builder<?> builder, ExecutorService executorService) throws ExecutionException { if (backportedMethodRewriter != null) { - backportedMethodRewriter.synthesizeUtilityClass(builder, executorService, options); + backportedMethodRewriter.synthesizeUtilityClasses(builder, executorService); } } @@ -685,6 +689,7 @@ CallSiteInformation.empty(), Outliner::noProcessing), executorService); + feedback.updateVisibleOptimizationInfo(); timing.end(); } @@ -852,6 +857,7 @@ // unused out-values. codeRewriter.rewriteMoveResult(code); deadCodeRemover.run(code); + codeRewriter.removeAssumeInstructions(code); consumer.accept(code, method); return null; })); @@ -879,7 +885,7 @@ private void collectLambdaMergingCandidates(DexApplication application) { if (lambdaMerger != null) { - lambdaMerger.collectGroupCandidates(application, appView.withLiveness()); + lambdaMerger.collectGroupCandidates(application); } } @@ -1116,6 +1122,11 @@ } } + if (lambdaMerger != null) { + lambdaMerger.rewriteCode(method, code); + assert code.isConsistentSSA(); + } + if (typeChecker != null && !typeChecker.check(code)) { assert appView.enableWholeProgramOptimizations(); assert options.testing.allowTypeErrors; @@ -1242,9 +1253,10 @@ if (stringSwitchRemover != null) { stringSwitchRemover.run(method, code); } - codeRewriter.rewriteSwitch(code); codeRewriter.processMethodsNeverReturningNormally(code); - codeRewriter.simplifyIf(code); + if (codeRewriter.simplifyControlFlow(code)) { + codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(code); + } if (options.enableRedundantConstNumberOptimization) { codeRewriter.redundantConstNumberRemoval(code); } @@ -1351,7 +1363,7 @@ previous = printMethod(code, "IR after twr close resource rewriter (SSA)", previous); if (lambdaMerger != null) { - lambdaMerger.processMethodCode(method, code); + lambdaMerger.analyzeCode(method, code); assert code.isConsistentSSA(); } @@ -1389,24 +1401,6 @@ classStaticizer.examineMethodCode(method, code); } - if (nonNullTracker != null) { - // TODO(b/139246447): Once we extend this optimization to, e.g., constants of primitive args, - // this may not be the right place to collect call site optimization info. - // Collecting call-site optimization info depends on the existence of non-null IRs. - // Arguments can be changed during the debug mode. - if (!isDebugMode && appView.callSiteOptimizationInfoPropagator() != null) { - appView.callSiteOptimizationInfoPropagator().collectCallSiteOptimizationInfo(code); - } - // Computation of non-null parameters on normal exits rely on the existence of non-null IRs. - methodOptimizationInfoCollector.computeNonNullParamOnNormalExits(feedback, code); - } - if (aliasIntroducer != null || nonNullTracker != null || dynamicTypeOptimization != null) { - codeRewriter.removeAssumeInstructions(code); - assert code.isConsistentSSA(); - } - // Assert that we do not have unremoved non-sense code in the output, e.g., v <- non-null NULL. - assert code.verifyNoNullabilityBottomTypes(); - assert code.verifyTypes(appView); if (appView.enableWholeProgramOptimizations()) { @@ -1418,31 +1412,30 @@ fieldBitAccessAnalysis.recordFieldAccesses(code, feedback); } + // Arguments can be changed during the debug mode. + if (!isDebugMode && appView.callSiteOptimizationInfoPropagator() != null) { + appView.callSiteOptimizationInfoPropagator().collectCallSiteOptimizationInfo(code); + } + // Compute optimization info summary for the current method unless it is pinned // (in that case we should not be making any assumptions about the behavior of the method). if (!appView.appInfo().withLiveness().isPinned(method.method)) { - methodOptimizationInfoCollector.identifyClassInlinerEligibility(method, code, feedback); - methodOptimizationInfoCollector.identifyParameterUsages(method, code, feedback); - methodOptimizationInfoCollector.identifyReturnsArgument(method, code, feedback); - methodOptimizationInfoCollector.identifyTrivialInitializer(method, code, feedback); - - if (options.enableInlining && inliner != null) { - methodOptimizationInfoCollector - .identifyInvokeSemanticsForInlining(method, code, appView, feedback); - } - methodOptimizationInfoCollector - .computeDynamicReturnType(dynamicTypeOptimization, feedback, method, code); + .collectMethodOptimizationInfo(method, code, feedback, dynamicTypeOptimization); FieldValueAnalysis.run(appView, code, feedback, method); - methodOptimizationInfoCollector - .computeInitializedClassesOnNormalExit(feedback, method, code); - methodOptimizationInfoCollector.computeMayHaveSideEffects(feedback, method, code); - methodOptimizationInfoCollector - .computeReturnValueOnlyDependsOnArguments(feedback, method, code); - methodOptimizationInfoCollector.computeNonNullParamOrThrow(feedback, method, code); } } + if (aliasIntroducer != null || nonNullTracker != null || dynamicTypeOptimization != null) { + codeRewriter.removeAssumeInstructions(code); + assert code.isConsistentSSA(); + } + + // Assert that we do not have unremoved non-sense code in the output, e.g., v <- non-null NULL. + assert code.verifyNoNullabilityBottomTypes(); + + assert code.verifyTypes(appView); + previous = printMethod(code, "IR after computation of optimization info summary (SSA)", previous);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java index 9a55240..a7933d2 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -7,6 +7,7 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.ir.conversion.CallGraph.Node; +import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator; import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.utils.Action; import com.android.tools.r8.utils.IROrdering; @@ -70,7 +71,13 @@ return waves; } - private static void extractLeaves(Set<Node> nodes, Consumer<Node> fn) { + /** + * Extract the next set of leaves (nodes with an outgoing call degree of 0) if any. + * + * <p>All nodes in the graph are extracted if called repeatedly until null is returned. Please + * note that there are no cycles in this graph (see {@link CycleEliminator#breakCycles}). + */ + static void extractLeaves(Set<Node> nodes, Consumer<Node> fn) { Set<Node> removed = Sets.newIdentityHashSet(); Iterator<Node> nodeIterator = nodes.iterator(); while (nodeIterator.hasNext()) { @@ -81,7 +88,7 @@ removed.add(node); } } - removed.forEach(Node::cleanForRemoval); + removed.forEach(Node::cleanCallersForRemoval); } /**
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java new file mode 100644 index 0000000..ae2c5c5 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java
@@ -0,0 +1,54 @@ +// Copyright (c) 2019, 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.conversion; + +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.utils.ThreadUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public class PartialCallGraphBuilder extends CallGraphBuilderBase { + private final Set<DexEncodedMethod> seeds; + + PartialCallGraphBuilder(AppView<AppInfoWithLiveness> appView, Set<DexEncodedMethod> seeds) { + super(appView); + assert seeds != null && !seeds.isEmpty(); + this.seeds = seeds; + } + + @Override + void process(ExecutorService executorService) throws ExecutionException { + List<Future<?>> futures = new ArrayList<>(); + for (DexEncodedMethod method : seeds) { + futures.add( + executorService.submit( + () -> { + processMethod(method); + return null; // we want a Callable not a Runnable to be able to throw + })); + } + ThreadUtils.awaitFutures(futures); + } + + private void processMethod(DexEncodedMethod method) { + if (method.hasCode()) { + method.registerCodeReferences( + new InvokeExtractor(getOrCreateNode(method), seeds::contains)); + } + } + + @Override + boolean verifyAllMethodsWithCodeExists() { + for (DexEncodedMethod method : seeds) { + assert !method.hasCode() || nodes.get(method.method) != null; + } + return true; + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java new file mode 100644 index 0000000..13566359 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -0,0 +1,33 @@ +// Copyright (c) 2019, 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.conversion; + +import com.android.tools.r8.ir.conversion.CallGraph.Node; +import com.google.common.collect.Sets; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Consumer; + +public class PostMethodProcessor { + + /** + * Extract the next set of roots (nodes with an incoming call degree of 0) if any. + * + * <p>All nodes in the graph are extracted if called repeatedly until null is returned. + */ + static void extractRoots(Set<Node> nodes, Consumer<Node> fn) { + Set<Node> removed = Sets.newIdentityHashSet(); + Iterator<Node> nodeIterator = nodes.iterator(); + while (nodeIterator.hasNext()) { + Node node = nodeIterator.next(); + if (node.isRoot()) { + fn.accept(node); + nodeIterator.remove(); + removed.add(node); + } + } + removed.forEach(Node::cleanCalleesForRemoval); + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java index be6ffb2..bf82f85 100644 --- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -100,7 +100,34 @@ InvokeMethod invoke = instruction.asInvokeMethod(); MethodProvider provider = getMethodProviderOrNull(invoke.getInvokedMethod()); if (provider == null) { - continue; + if (!rewritableMethods.matchesVirtualRewrite(invoke.getInvokedMethod())) { + continue; + } + // We need to force resolution, even on d8, to know if the invoke has to be rewritten. + DexEncodedMethod dexEncodedMethod = + quickLookUp(invoke.getInvokedMethod()); + if (dexEncodedMethod == null) { + continue; + } + provider = getMethodProviderOrNull(dexEncodedMethod.method); + if (provider == null) { + continue; + } + + // Since we are rewriting a virtual method into a static invoke in this case, the look-up + // logic gets confused. Final methods rewritten in such a way are always or invokes from a + // library class are rewritten into the static invoke, which is correct. However, + // overrides of the programmer are currently disabled. We still rewrite everything to make + // basic cases work. + // TODO(b/142846107): Support overrides of retarget virtual methods by uncommenting the + // following and implementing doSomethingSmart(). + + // DexClass receiverType = appView.definitionFor(invoke.getInvokedMethod().holder); + // if (!(dexEncodedMethod.isFinal() + // || (receiverType != null && receiverType.isLibraryClass()))) { + // doSomethingSmart(); + // continue; + // } } provider.rewriteInvoke(invoke, iterator, code, appView); @@ -113,6 +140,31 @@ } } + private DexEncodedMethod quickLookUp(DexMethod method) { + // Since retargeting cannot be on interface, we do a quick look-up excluding interfaces. + // On R8 resolution is immediate, on d8 it may look-up. + DexClass current = appView.definitionFor(method.holder); + if (current == null) { + return null; + } + DexEncodedMethod dexEncodedMethod = current.lookupVirtualMethod(method); + if (dexEncodedMethod != null) { + return dexEncodedMethod; + } + while (current.superType != factory.objectType) { + DexType superType = current.superType; + current = appView.definitionFor(superType); + if (current == null) { + return null; + } + dexEncodedMethod = current.lookupVirtualMethod(method); + if (dexEncodedMethod != null) { + return dexEncodedMethod; + } + } + return null; + } + private Collection<DexProgramClass> findSynthesizedFrom(Builder<?> builder, DexType holder) { for (DexProgramClass synthesizedClass : builder.getSynthesizedClasses()) { if (holder == synthesizedClass.getType()) { @@ -126,8 +178,7 @@ return clazz.descriptor.toString().startsWith(UTILITY_CLASS_DESCRIPTOR_PREFIX); } - public void synthesizeUtilityClass( - Builder<?> builder, ExecutorService executorService, InternalOptions options) + public void synthesizeUtilityClasses(Builder<?> builder, ExecutorService executorService) throws ExecutionException { if (holders.isEmpty()) { return; @@ -161,7 +212,7 @@ if (appView.definitionFor(method.holder) != null) { continue; } - Code code = provider.generateTemplateMethod(options, method); + Code code = provider.generateTemplateMethod(appView.options(), method); DexEncodedMethod dexEncodedMethod = new DexEncodedMethod( method, flags, DexAnnotationSet.empty(), ParameterAnnotationsList.empty(), code); @@ -181,7 +232,7 @@ DexAnnotationSet.empty(), DexEncodedField.EMPTY_ARRAY, DexEncodedField.EMPTY_ARRAY, - new DexEncodedMethod[] {dexEncodedMethod}, + new DexEncodedMethod[]{dexEncodedMethod}, DexEncodedMethod.EMPTY_ARRAY, factory.getSkipNameValidationForTesting(), referencingClasses); @@ -221,6 +272,10 @@ // Map backported method to a provider for creating the actual target method (with code). private final Map<DexMethod, MethodProvider> rewritable = new IdentityHashMap<>(); + // Map virtualRewrites hold a methodName->method mapping for virtual methods which are + // rewritten while the holder is non final but no superclass implement the method. In this case + // d8 needs to force resolution of given methods to see if the invoke needs to be rewritten. + private final Map<DexString, List<DexMethod>> virtualRewrites = new IdentityHashMap<>(); RewritableMethods(InternalOptions options, AppView<?> appView) { DexItemFactory factory = options.itemFactory; @@ -253,6 +308,19 @@ } } + boolean matchesVirtualRewrite(DexMethod method) { + List<DexMethod> dexMethods = virtualRewrites.get(method.name); + if (dexMethods == null) { + return false; + } + for (DexMethod dexMethod : dexMethods) { + if (method.match(dexMethod)) { + return true; + } + } + return false; + } + boolean isEmpty() { return rewritable.isEmpty(); } @@ -1007,7 +1075,8 @@ : new MethodGenerator( method, (options, methodArg) -> - CollectionMethodGenerators.generateListOf(options, methodArg, formalCount))); + CollectionMethodGenerators.generateListOf( + options, methodArg, formalCount))); } proto = factory.createProto(type, factory.objectArrayType); method = factory.createMethod(type, proto, name); @@ -1099,25 +1168,25 @@ // Optional{void,Int,Long,Double}.ifPresentOrElse(consumer,runnable) DexType[] optionalTypes = - new DexType[] { - optionalType, - factory.createType(factory.createString("Ljava/util/OptionalDouble;")), - factory.createType(factory.createString("Ljava/util/OptionalLong;")), - factory.createType(factory.createString("Ljava/util/OptionalInt;")) + new DexType[]{ + optionalType, + factory.createType(factory.createString("Ljava/util/OptionalDouble;")), + factory.createType(factory.createString("Ljava/util/OptionalLong;")), + factory.createType(factory.createString("Ljava/util/OptionalInt;")) }; DexType[] consumerTypes = - new DexType[] { - factory.consumerType, - factory.createType("Ljava/util/function/DoubleConsumer;"), - factory.createType("Ljava/util/function/LongConsumer;"), - factory.createType("Ljava/util/function/IntConsumer;") + new DexType[]{ + factory.consumerType, + factory.createType("Ljava/util/function/DoubleConsumer;"), + factory.createType("Ljava/util/function/LongConsumer;"), + factory.createType("Ljava/util/function/IntConsumer;") }; TemplateMethodFactory[] methodFactories = - new TemplateMethodFactory[] { - BackportedMethods::OptionalMethods_ifPresentOrElse, - BackportedMethods::OptionalMethods_ifPresentOrElseDouble, - BackportedMethods::OptionalMethods_ifPresentOrElseLong, - BackportedMethods::OptionalMethods_ifPresentOrElseInt + new TemplateMethodFactory[]{ + BackportedMethods::OptionalMethods_ifPresentOrElse, + BackportedMethods::OptionalMethods_ifPresentOrElseDouble, + BackportedMethods::OptionalMethods_ifPresentOrElseLong, + BackportedMethods::OptionalMethods_ifPresentOrElseInt }; for (int i = 0; i < optionalTypes.length; i++) { DexType optional = optionalTypes[i]; @@ -1151,6 +1220,10 @@ DexType newHolder = retargetCoreLibMember.get(methodName).get(inType); List<DexEncodedMethod> found = findDexEncodedMethodsWithName(methodName, typeClass); for (DexEncodedMethod encodedMethod : found) { + if (!encodedMethod.isStatic()) { + virtualRewrites.putIfAbsent(encodedMethod.method.name, new ArrayList<>()); + virtualRewrites.get(encodedMethod.method.name).add(encodedMethod.method); + } DexProto proto = encodedMethod.method.proto; DexMethod method = appView.dexItemFactory().createMethod(inType, proto, methodName); addProvider( @@ -1185,6 +1258,7 @@ } public abstract static class MethodProvider { + final DexMethod method; public MethodProvider(DexMethod method) { @@ -1244,6 +1318,7 @@ } private static final class InvokeRewriter extends MethodProvider { + private final MethodInvokeRewriter rewriter; InvokeRewriter(DexMethod method, MethodInvokeRewriter rewriter) { @@ -1251,8 +1326,9 @@ this.rewriter = rewriter; } - @Override public void rewriteInvoke(InvokeMethod invoke, InstructionListIterator iterator, - IRCode code, AppView<?> appView) { + @Override + public void rewriteInvoke( + InvokeMethod invoke, InstructionListIterator iterator, IRCode code, AppView<?> appView) { rewriter.rewrite(invoke, iterator, appView.dexItemFactory()); assert code.isConsistentSSA(); } @@ -1365,6 +1441,7 @@ } private interface MethodInvokeRewriter { + void rewrite(InvokeMethod invoke, InstructionListIterator iterator, DexItemFactory factory); } }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java index d81f3fe..688e751 100644 --- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -10,6 +10,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexItemFactory; import com.android.tools.r8.graph.DexMethod; +import com.android.tools.r8.graph.DexString; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.graph.MethodAccessFlags; import com.android.tools.r8.ir.code.Invoke; @@ -114,7 +115,12 @@ DexClass target = appView.definitionFor(method.holder); // NOTE: Never add a forwarding method to methods of classes unknown or coming from android.jar // even if this results in invalid code, these classes are never desugared. - assert target != null && !rewriter.isNonDesugaredLibraryClass(target); + assert target != null; + // In desugared library, emulated interface methods can be overridden by retarget lib members. + DexMethod forwardMethod = + target.isInterface() + ? rewriter.defaultAsMethodOfCompanionClass(method) + : retargetMethod(method); // New method will have the same name, proto, and also all the flags of the // default method, including bridge flag. DexMethod newMethod = dexItemFactory.createMethod(clazz.type, method.proto, method.name); @@ -125,7 +131,7 @@ ForwardMethodSourceCode.builder(newMethod); forwardSourceCodeBuilder .setReceiver(clazz.type) - .setTarget(rewriter.defaultAsMethodOfCompanionClass(method)) + .setTarget(forwardMethod) .setInvokeType(Invoke.Type.STATIC) .setIsInterface(false); // Holder is companion class, not an interface. return new DexEncodedMethod( @@ -136,6 +142,18 @@ new SynthesizedCode(forwardSourceCodeBuilder::build)); } + private DexMethod retargetMethod(DexMethod method) { + Map<DexString, Map<DexType, DexType>> retargetCoreLibMember = + appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember(); + Map<DexType, DexType> typeMap = retargetCoreLibMember.get(method.name); + assert typeMap != null; + assert typeMap.get(method.holder) != null; + return dexItemFactory.createMethod( + typeMap.get(method.holder), + dexItemFactory.prependTypeToProto(method.holder, method.proto), + method.name); + } + // For a given class `clazz` inspects all interfaces it implements directly or // indirectly and collect a set of all default methods to be implemented // in this class. @@ -215,17 +233,29 @@ // Remove from candidates methods defined in class or any of its superclasses. List<DexEncodedMethod> toBeImplemented = new ArrayList<>(candidates.size()); current = clazz; + Map<DexString, Map<DexType, DexType>> retargetCoreLibMember = + appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember(); while (true) { // In desugared library look-up, methods from library classes cannot hide methods from // emulated interfaces (the method being desugared implied the implementation is not - // present in the library class). + // present in the library class), except through retarget core lib member. if (desugaredLibraryLookup && current.isLibraryClass()) { Iterator<DexEncodedMethod> iterator = candidates.iterator(); while (iterator.hasNext()) { DexEncodedMethod candidate = iterator.next(); - if (rewriter.isEmulatedInterface(candidate.method.holder)) { - toBeImplemented.add(candidate); - iterator.remove(); + if (rewriter.isEmulatedInterface(candidate.method.holder) + && current.lookupVirtualMethod(candidate.method) != null) { + // A library class overrides an emulated interface method. This override is valid + // only if it goes through retarget core lib member, else it needs to be implemented. + Map<DexType, DexType> typeMap = retargetCoreLibMember.get(candidate.method.name); + if (typeMap != null && typeMap.containsKey(current.type)) { + // A rewrite needs to be performed, but instead of rewriting to the companion class, + // D8/R8 needs to rewrite to the retarget member. + toBeImplemented.add(current.lookupVirtualMethod(candidate.method)); + } else { + toBeImplemented.add(candidate); + iterator.remove(); + } } } }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java index d60ac1f..cbdef95 100644 --- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java +++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -28,6 +28,7 @@ import com.android.tools.r8.utils.BooleanUtils; import com.android.tools.r8.utils.DescriptorUtils; import com.android.tools.r8.utils.StringDiagnostic; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -64,11 +65,20 @@ private final DexItemFactory factory; private final DesugaredLibraryWrapperSynthesizer wrapperSynthesizor; private final Map<DexClass, Set<DexEncodedMethod>> callBackMethods = new HashMap<>(); + private final Set<DexMethod> trackedCallBackAPIs; + private final Set<DexMethod> trackedAPIs; public DesugaredLibraryAPIConverter(AppView<?> appView) { this.appView = appView; this.factory = appView.dexItemFactory(); this.wrapperSynthesizor = new DesugaredLibraryWrapperSynthesizer(appView, this); + if (appView.options().testing.trackDesugaredAPIConversions) { + trackedCallBackAPIs = Sets.newConcurrentHashSet(); + trackedAPIs = Sets.newConcurrentHashSet(); + } else { + trackedCallBackAPIs = null; + trackedAPIs = null; + } } public void desugar(IRCode code) { @@ -170,6 +180,9 @@ } private synchronized void generateCallBack(DexClass dexClass, DexEncodedMethod originalMethod) { + if (trackedCallBackAPIs != null) { + trackedCallBackAPIs.add(originalMethod.method); + } DexMethod methodToInstall = methodWithVivifiedTypeInSignature(originalMethod.method, dexClass.type, appView); if (dexClass.isInterface() @@ -219,6 +232,10 @@ public void generateWrappers( DexApplication.Builder<?> builder, IRConverter irConverter, ExecutorService executorService) throws ExecutionException { + if (appView.options().testing.trackDesugaredAPIConversions) { + generateTrackDesugaredAPIWarnings(trackedAPIs, ""); + generateTrackDesugaredAPIWarnings(trackedCallBackAPIs, "callback "); + } wrapperSynthesizor.finalizeWrappers(builder, irConverter, executorService); for (DexClass dexClass : callBackMethods.keySet()) { Set<DexEncodedMethod> dexEncodedMethods = callBackMethods.get(dexClass); @@ -227,6 +244,16 @@ } } + private void generateTrackDesugaredAPIWarnings(Set<DexMethod> tracked, String inner) { + StringBuilder sb = new StringBuilder(); + sb.append("Tracked ").append(inner).append("desugared API conversions: "); + for (DexMethod method : tracked) { + sb.append("\n"); + sb.append(method); + } + appView.options().reporter.warning(new StringDiagnostic(sb.toString())); + } + private void warnInvalidInvoke(DexType type, DexMethod invokedMethod, String debugString) { DexType desugaredType = appView.rewritePrefix.rewrittenType(type); appView @@ -260,6 +287,9 @@ InstructionListIterator iterator, ListIterator<BasicBlock> blockIterator) { DexMethod invokedMethod = invokeMethod.getInvokedMethod(); + if (trackedAPIs != null) { + trackedAPIs.add(invokedMethod); + } // Create return conversion if required. Instruction returnConversion = null;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java index 257d002..73214ba 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java
@@ -53,7 +53,7 @@ } public void markUsersForRemoval(Value value) { - for (Instruction user : value.uniqueUsers()) { + for (Instruction user : value.aliasedUsers()) { if (user.isAssumeDynamicType()) { assert value.numberOfAllUsers() == 1 : "Expected value flowing into Assume<DynamicTypeAssumption> instruction to have a "
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java index 31add54..f93d6c5 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
@@ -47,11 +47,8 @@ private enum Mode { COLLECT, // Set until the end of the 1st round of IR processing. CallSiteOptimizationInfo will // be updated in this mode only. - REVISIT, // Set once the all methods are processed. IRBuilder will add other instructions that + REVISIT // Set once the all methods are processed. IRBuilder will add other instructions that // reflect collected CallSiteOptimizationInfo. - FINISH; // Set once the 2nd round of IR processing is done. Other optimizations that need post - // IR processing, e.g., outliner, are still using IRBuilder, and this will isolate the - // impact of IR manipulation due to this optimization. } private final AppView<AppInfoWithLiveness> appView; @@ -256,14 +253,13 @@ } } } + mode = Mode.REVISIT; if (targetsToRevisit.isEmpty()) { - mode = Mode.FINISH; return; } if (revisitedMethods != null) { revisitedMethods.addAll(targetsToRevisit); } - mode = Mode.REVISIT; List<Future<?>> futures = new ArrayList<>(); for (DexEncodedMethod method : targetsToRevisit) { futures.add( @@ -274,6 +270,5 @@ })); } ThreadUtils.awaitFutures(futures); - mode = Mode.FINISH; } }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java index 6c2110b..fb75832 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -29,6 +29,7 @@ import com.android.tools.r8.graph.DexValue.DexValueNull; import com.android.tools.r8.graph.DexValue.DexValueShort; import com.android.tools.r8.graph.DexValue.DexValueString; +import com.android.tools.r8.ir.analysis.ValueMayDependOnEnvironmentAnalysis; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; import com.android.tools.r8.ir.code.ArrayPut; import com.android.tools.r8.ir.code.BasicBlock; @@ -327,6 +328,8 @@ private Collection<StaticPut> findFinalFieldPutsWhileCollectingUnnecessaryStaticPuts( IRCode code, DexClass clazz, Set<StaticPut> unnecessaryStaticPuts) { + ValueMayDependOnEnvironmentAnalysis environmentAnalysis = + new ValueMayDependOnEnvironmentAnalysis(appView, code); Map<DexField, StaticPut> finalFieldPuts = Maps.newIdentityHashMap(); Map<DexField, Set<StaticPut>> isWrittenBefore = Maps.newIdentityHashMap(); Set<DexField> isReadBefore = Sets.newIdentityHashSet(); @@ -436,7 +439,7 @@ Value outValue = instruction.outValue(); if (outValue.numberOfAllUsers() > 0) { if (instruction.isInvokeNewArray() - && outValue.isConstantArrayThroughoutMethod(appView, code)) { + && environmentAnalysis.isConstantArrayThroughoutMethod(outValue)) { // OK, this value is technically a constant. continue; }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java index 932c817..a47ba2c 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -14,6 +14,7 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DebugLocalInfo; import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexEncodedField; import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexItemFactory; @@ -82,12 +83,14 @@ import com.android.tools.r8.utils.InternalOptions.AssertionProcessing; import com.android.tools.r8.utils.InternalOutputMode; import com.android.tools.r8.utils.LongInterval; +import com.android.tools.r8.utils.SetUtils; import com.google.common.base.Equivalence; import com.google.common.base.Equivalence.Wrapper; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; @@ -839,9 +842,9 @@ return outliersAsIfSize; } - public void rewriteSwitch(IRCode code) { + private boolean rewriteSwitch(IRCode code) { if (!code.metadata().mayHaveIntSwitch()) { - return; + return false; } boolean needToRemoveUnreachableBlocks = false; @@ -985,15 +988,18 @@ } } - if (needToRemoveUnreachableBlocks) { - code.removeUnreachableBlocks(); - } - // Rewriting of switches introduces new branching structure. It relies on critical edges // being split on the way in but does not maintain this property. We therefore split // critical edges at exit. code.splitCriticalEdges(); + + Set<Value> affectedValues = + needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : ImmutableSet.of(); + if (!affectedValues.isEmpty()) { + new TypeAnalysis(appView).narrowing(affectedValues); + } assert code.isConsistentSSA(); + return !affectedValues.isEmpty(); } private SwitchCaseEliminator removeUnnecessarySwitchCases( @@ -1247,10 +1253,10 @@ assumeDynamicTypeRemover.finish(); if (!blocksToBeRemoved.isEmpty()) { code.removeBlocks(blocksToBeRemoved); - code.removeAllTrivialPhis(); + code.removeAllTrivialPhis(affectedValues); assert code.getUnreachableBlocks().isEmpty(); } else if (mayHaveRemovedTrivialPhi || assumeDynamicTypeRemover.mayHaveIntroducedTrivialPhi()) { - code.removeAllTrivialPhis(); + code.removeAllTrivialPhis(affectedValues); } if (!affectedValues.isEmpty()) { new TypeAnalysis(appView).narrowing(affectedValues); @@ -1407,7 +1413,10 @@ // Removing check-cast may result in a trivial phi: // v3 <- phi(v1, v1) if (needToRemoveTrivialPhis) { - code.removeAllTrivialPhis(); + code.removeAllTrivialPhis(affectedValues); + if (!affectedValues.isEmpty()) { + typeAnalysis.narrowing(affectedValues); + } } assert code.isConsistentSSA(); } @@ -1524,7 +1533,11 @@ value -> !value.isPhi() && value.definition.isAssumeDynamicType()); if (aliasedValue != null) { TypeLatticeElement dynamicType = - aliasedValue.definition.asAssumeDynamicType().getAssumption().getType(); + aliasedValue + .definition + .asAssumeDynamicType() + .getAssumption() + .getDynamicUpperBoundType(); if (dynamicType.isDefinitelyNull()) { result = InstanceOfResult.FALSE; } else if (dynamicType.lessThanOrEqual(instanceOfType, appView) @@ -2378,7 +2391,13 @@ assert code.isConsistentSSA(); } - public void simplifyIf(IRCode code) { + public boolean simplifyControlFlow(IRCode code) { + boolean anyAffectedValues = rewriteSwitch(code); + anyAffectedValues |= simplifyIf(code); + return anyAffectedValues; + } + + private boolean simplifyIf(IRCode code) { for (BasicBlock block : code.blocks) { // Skip removed (= unreachable) blocks. if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) { @@ -2394,27 +2413,26 @@ // Simplify if conditions when possible. If theIf = block.exit().asIf(); - List<Value> inValues = theIf.inValues(); + Value lhs = theIf.lhs(); + Value rhs = theIf.isZeroTest() ? null : theIf.rhs(); - if (inValues.get(0).isConstNumber() - && (theIf.isZeroTest() || inValues.get(1).isConstNumber())) { + if (lhs.isConstNumber() && (theIf.isZeroTest() || rhs.isConstNumber())) { // Zero test with a constant of comparison between between two constants. if (theIf.isZeroTest()) { - ConstNumber cond = inValues.get(0).getConstInstruction().asConstNumber(); + ConstNumber cond = lhs.getConstInstruction().asConstNumber(); BasicBlock target = theIf.targetFromCondition(cond); simplifyIfWithKnownCondition(code, block, theIf, target); } else { - ConstNumber left = inValues.get(0).getConstInstruction().asConstNumber(); - ConstNumber right = inValues.get(1).getConstInstruction().asConstNumber(); + ConstNumber left = lhs.getConstInstruction().asConstNumber(); + ConstNumber right = rhs.getConstInstruction().asConstNumber(); BasicBlock target = theIf.targetFromCondition(left, right); simplifyIfWithKnownCondition(code, block, theIf, target); } - } else if (inValues.get(0).hasValueRange() - && (theIf.isZeroTest() || inValues.get(1).hasValueRange())) { + } else if (lhs.hasValueRange() && (theIf.isZeroTest() || rhs.hasValueRange())) { // Zero test with a value range, or comparison between between two values, // each with a value ranges. if (theIf.isZeroTest()) { - LongInterval interval = inValues.get(0).getValueRange(); + LongInterval interval = lhs.getValueRange(); if (!interval.containsValue(0)) { // Interval doesn't contain zero at all. int sign = Long.signum(interval.getMin()); @@ -2448,8 +2466,8 @@ } } } else { - LongInterval leftRange = inValues.get(0).getValueRange(); - LongInterval rightRange = inValues.get(1).getValueRange(); + LongInterval leftRange = lhs.getValueRange(); + LongInterval rightRange = rhs.getValueRange(); // Two overlapping ranges. Check for single point overlap. if (!leftRange.overlapsWith(rightRange)) { // No overlap. @@ -2483,14 +2501,27 @@ } } } - } else if (theIf.isZeroTest() && !inValues.get(0).isConstNumber() - && (theIf.getType() == Type.EQ || theIf.getType() == Type.NE)) { - TypeLatticeElement l = inValues.get(0).getTypeLattice(); - if (l.isReference() && inValues.get(0).isNeverNull()) { - simplifyIfWithKnownCondition(code, block, theIf, 1); + } else if (theIf.getType() == Type.EQ || theIf.getType() == Type.NE) { + if (theIf.isZeroTest()) { + if (!lhs.isConstNumber()) { + TypeLatticeElement l = lhs.getTypeLattice(); + if (l.isReference() && lhs.isNeverNull()) { + simplifyIfWithKnownCondition(code, block, theIf, 1); + } else { + if (!l.isPrimitive() && !l.isNullable()) { + simplifyIfWithKnownCondition(code, block, theIf, 1); + } + } + } } else { - if (!l.isPrimitive() && !l.isNullable()) { - simplifyIfWithKnownCondition(code, block, theIf, 1); + DexEncodedField enumField = lhs.getEnumField(appView); + if (enumField != null) { + DexEncodedField otherEnumField = rhs.getEnumField(appView); + if (enumField == otherEnumField) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + } else if (otherEnumField != null) { + simplifyIfWithKnownCondition(code, block, theIf, 1); + } } } } @@ -2501,6 +2532,7 @@ new TypeAnalysis(appView).narrowing(affectedValues); } assert code.isConsistentSSA(); + return !affectedValues.isEmpty(); } private void simplifyIfWithKnownCondition( @@ -3500,8 +3532,8 @@ } private static NewInstance findNewInstance(Phi phi) { - Set<Phi> seen = new HashSet<>(); - Set<Value> values = new HashSet<>(); + Set<Phi> seen = Sets.newIdentityHashSet(); + Set<Value> values = Sets.newIdentityHashSet(); recursiveAddOperands(phi, seen, values); if (values.size() != 1) { throw new CompilationError("Failed to identify unique new-instance for <init>"); @@ -3539,7 +3571,7 @@ if (component.size() == 1 && component.iterator().next() == newInstanceValue) { continue; } - Set<Phi> trivialPhis = new HashSet<>(); + Set<Phi> trivialPhis = Sets.newIdentityHashSet(); for (Value value : component) { boolean isTrivial = true; Phi p = value.asPhi(); @@ -3569,7 +3601,7 @@ private int currentTime = 0; private final Reference2IntMap<Value> discoverTime = new Reference2IntOpenHashMap<>(); - private final Set<Value> unassignedSet = new HashSet<>(); + private final Set<Value> unassignedSet = Sets.newIdentityHashSet(); private final Deque<Value> unassignedStack = new ArrayDeque<>(); private final Deque<Value> preorderStack = new ArrayDeque<>(); private final List<Set<Value>> components = new ArrayList<>(); @@ -3603,7 +3635,7 @@ // If the current element is the top of the preorder stack, then we are at entry to a // strongly-connected component consisting of this element and every element above this // element on the stack. - Set<Value> component = new HashSet<>(unassignedStack.size()); + Set<Value> component = SetUtils.newIdentityHashSet(unassignedStack.size()); while (true) { Value member = unassignedStack.pop(); unassignedSet.remove(member);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java index e0d77fb..2649e62 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -428,9 +428,28 @@ } @Override - public boolean canInlineInstanceInitializer( - IRCode inlinee, + public boolean allowInliningOfInvokeInInlinee( + InlineAction action, + int inliningDepth, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) { + assert inliningDepth > 0; + + if (action.reason.mustBeInlined()) { + return true; + } + + int threshold = appView.options().applyInliningToInlineeMaxDepth; + if (inliningDepth <= threshold) { + return true; + } + + whyAreYouNotInliningReporter.reportWillExceedMaxInliningDepth(inliningDepth, threshold); + return false; + } + + @Override + public boolean canInlineInstanceInitializer( + IRCode inlinee, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) { // In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and // Newly Created Objects" it says: // @@ -685,13 +704,12 @@ @Override public void updateTypeInformationIfNeeded( IRCode inlinee, ListIterator<BasicBlock> blockIterator, BasicBlock block) { - if (appView.options().enableNonNullTracking) { + boolean assumersEnabled = + appView.options().enableNonNullTracking + || appView.options().enableDynamicTypeOptimization + || appView.options().testing.forceAssumeNoneInsertion; + if (assumersEnabled) { BasicBlock state = IteratorUtils.peekNext(blockIterator); - // Move the cursor back to where the first inlinee block was added. - while (blockIterator.hasPrevious() && blockIterator.previous() != block) { - // Do nothing. - } - assert IteratorUtils.peekNext(blockIterator) == block; Set<BasicBlock> inlineeBlocks = Sets.newIdentityHashSet(); inlineeBlocks.addAll(inlinee.blocks); @@ -699,18 +717,28 @@ // Introduce aliases only to the inlinee blocks. if (appView.options().testing.forceAssumeNoneInsertion) { insertAssumeInstructionsToInlinee( - new AliasIntroducer(appView), code, state, blockIterator, inlineeBlocks); + new AliasIntroducer(appView), code, block, blockIterator, inlineeBlocks); } // Add non-null IRs only to the inlinee blocks. - Consumer<BasicBlock> splitBlockConsumer = inlineeBlocks::add; - Assumer nonNullTracker = new NonNullTracker(appView, splitBlockConsumer); - insertAssumeInstructionsToInlinee( - nonNullTracker, code, state, blockIterator, inlineeBlocks); + if (appView.options().enableNonNullTracking) { + Consumer<BasicBlock> splitBlockConsumer = inlineeBlocks::add; + Assumer nonNullTracker = new NonNullTracker(appView, splitBlockConsumer); + insertAssumeInstructionsToInlinee( + nonNullTracker, code, block, blockIterator, inlineeBlocks); + } // Add dynamic type assumptions only to the inlinee blocks. - insertAssumeInstructionsToInlinee( - new DynamicTypeOptimization(appView), code, state, blockIterator, inlineeBlocks); + if (appView.options().enableDynamicTypeOptimization) { + insertAssumeInstructionsToInlinee( + new DynamicTypeOptimization(appView), code, block, blockIterator, inlineeBlocks); + } + + // Restore the old state of the iterator. + while (blockIterator.hasPrevious() && blockIterator.previous() != state) { + // Do nothing. + } + assert IteratorUtils.peekNext(blockIterator) == state; } // TODO(b/72693244): need a test where refined env in inlinee affects the caller. } @@ -718,17 +746,17 @@ private void insertAssumeInstructionsToInlinee( Assumer assumer, IRCode code, - BasicBlock state, + BasicBlock block, ListIterator<BasicBlock> blockIterator, Set<BasicBlock> inlineeBlocks) { - assumer.insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains); - assert !blockIterator.hasNext(); - - // Restore the old state of the iterator. - while (blockIterator.hasPrevious() && blockIterator.previous() != state) { + // Move the cursor back to where the first inlinee block was added. + while (blockIterator.hasPrevious() && blockIterator.previous() != block) { // Do nothing. } - assert IteratorUtils.peekNext(blockIterator) == state; + assert IteratorUtils.peekNext(blockIterator) == block; + + assumer.insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains); + assert !blockIterator.hasNext(); } @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java index 26f383b..e5c53b3 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
@@ -58,7 +58,7 @@ continue; } - TypeLatticeElement dynamicType; + TypeLatticeElement dynamicUpperBoundType; ClassTypeLatticeElement dynamicLowerBoundType; if (current.isInvokeMethod()) { InvokeMethod invoke = current.asInvokeMethod(); @@ -80,7 +80,7 @@ continue; } - dynamicType = optimizationInfo.getDynamicReturnType(); + dynamicUpperBoundType = optimizationInfo.getDynamicReturnType(); dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType(); } else if (current.isStaticGet()) { StaticGet staticGet = current.asStaticGet(); @@ -89,23 +89,23 @@ continue; } - dynamicType = encodedField.getOptimizationInfo().getDynamicType(); - // TODO(b/140234782): Extend to field values. - dynamicLowerBoundType = null; + dynamicUpperBoundType = encodedField.getOptimizationInfo().getDynamicUpperBoundType(); + dynamicLowerBoundType = encodedField.getOptimizationInfo().getDynamicLowerBoundType(); } else { continue; } Value outValue = current.outValue(); boolean isTrivial = - (dynamicType == null || !dynamicType.strictlyLessThan(outValue.getTypeLattice(), appView)) + (dynamicUpperBoundType == null + || !dynamicUpperBoundType.strictlyLessThan(outValue.getTypeLattice(), appView)) && dynamicLowerBoundType == null; if (isTrivial) { continue; } - if (dynamicType == null) { - dynamicType = outValue.getTypeLattice(); + if (dynamicUpperBoundType == null) { + dynamicUpperBoundType = outValue.getTypeLattice(); } // Split block if needed (only debug instructions are allowed after the throwing @@ -121,7 +121,12 @@ // Insert AssumeDynamicType instruction. Assume<DynamicTypeAssumption> assumeInstruction = Assume.createAssumeDynamicTypeInstruction( - dynamicType, dynamicLowerBoundType, specializedOutValue, outValue, current, appView); + dynamicUpperBoundType, + dynamicLowerBoundType, + specializedOutValue, + outValue, + current, + appView); assumeInstruction.setPosition( appView.options().debug ? current.getPosition() : Position.none()); if (insertionBlock == block) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java index fb1ef58..f52658f 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -84,6 +84,15 @@ IRCode inlinee, ListIterator<BasicBlock> blockIterator, BasicBlock block) {} @Override + public boolean allowInliningOfInvokeInInlinee( + InlineAction action, + int inliningDepth, + WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) { + // The purpose of force inlining is generally to inline a given invoke-instruction in the IR. + return false; + } + + @Override public boolean canInlineInstanceInitializer( IRCode code, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) { return true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java index fef9538..40d01c0 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -46,16 +46,20 @@ import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore; import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter; import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter; +import com.android.tools.r8.ir.optimize.lambda.LambdaMerger; import com.android.tools.r8.kotlin.Kotlin; import com.android.tools.r8.origin.Origin; import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.shaking.MainDexClasses; import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.IteratorUtils; import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.ThreadUtils; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.ListIterator; @@ -70,6 +74,7 @@ protected final AppView<AppInfoWithLiveness> appView; private final Set<DexMethod> blacklist; + private final LambdaMerger lambdaMerger; private final LensCodeRewriter lensCodeRewriter; final MainDexClasses mainDexClasses; @@ -82,10 +87,12 @@ public Inliner( AppView<AppInfoWithLiveness> appView, MainDexClasses mainDexClasses, + LambdaMerger lambdaMerger, LensCodeRewriter lensCodeRewriter) { Kotlin.Intrinsics intrinsics = appView.dexItemFactory().kotlin.intrinsics; this.appView = appView; this.blacklist = ImmutableSet.of(intrinsics.throwNpe, intrinsics.throwParameterIsNullException); + this.lambdaMerger = lambdaMerger; this.lensCodeRewriter = lensCodeRewriter; this.mainDexClasses = mainDexClasses; } @@ -585,6 +592,7 @@ ValueNumberGenerator generator, AppView<? extends AppInfoWithSubtyping> appView, Position callerPosition, + LambdaMerger lambdaMerger, LensCodeRewriter lensCodeRewriter) { DexItemFactory dexItemFactory = appView.dexItemFactory(); InternalOptions options = appView.options(); @@ -752,6 +760,9 @@ if (!target.isProcessed()) { lensCodeRewriter.rewrite(code, target); } + if (lambdaMerger != null) { + lambdaMerger.rewriteCodeForInlining(target, code, context); + } assert code.isConsistentSSA(); return new InlineeWithReason(code, reason); } @@ -870,8 +881,13 @@ ListIterator<BasicBlock> blockIterator = code.listIterator(); ClassInitializationAnalysis classInitializationAnalysis = new ClassInitializationAnalysis(appView, code); + Deque<BasicBlock> inlineeStack = new ArrayDeque<>(); + InternalOptions options = appView.options(); while (blockIterator.hasNext()) { BasicBlock block = blockIterator.next(); + if (!inlineeStack.isEmpty() && inlineeStack.peekFirst() == block) { + inlineeStack.pop(); + } if (blocksToRemove.contains(block)) { continue; } @@ -895,12 +911,19 @@ oracle.computeInlining( invoke, singleTarget, classInitializationAnalysis, whyAreYouNotInliningReporter); if (action == null) { - assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported(); + assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag(); + continue; + } + + if (!inlineeStack.isEmpty() + && !strategy.allowInliningOfInvokeInInlinee( + action, inlineeStack.size(), whyAreYouNotInliningReporter)) { + assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag(); continue; } if (!strategy.stillHasBudget(action, whyAreYouNotInliningReporter)) { - assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported(); + assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag(); continue; } @@ -910,10 +933,11 @@ code.valueNumberGenerator, appView, getPositionForInlining(invoke, context), + lambdaMerger, lensCodeRewriter); if (strategy.willExceedBudget( code, invoke, inlinee, block, whyAreYouNotInliningReporter)) { - assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported(); + assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag(); continue; } @@ -926,7 +950,7 @@ if (singleTarget.isInstanceInitializer() && !strategy.canInlineInstanceInitializer( inlinee.code, whyAreYouNotInliningReporter)) { - assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported(); + assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag(); continue; } @@ -936,6 +960,8 @@ assumeDynamicTypeRemover.markUsersForRemoval(outValue); } + boolean inlineeMayHaveInvokeMethod = inlinee.code.metadata().mayHaveInvokeMethod(); + // Inline the inlinee code in place of the invoke instruction // Back up before the invoke instruction. iterator.previous(); @@ -962,11 +988,26 @@ } context.copyMetadata(singleTarget); + + if (inlineeMayHaveInvokeMethod && options.applyInliningToInlinee) { + if (inlineeStack.size() + 1 > options.applyInliningToInlineeMaxDepth + && appView.appInfo().alwaysInline.isEmpty() + && appView.appInfo().forceInline.isEmpty()) { + continue; + } + // Record that we will be inside the inlinee until the next block. + BasicBlock inlineeEnd = IteratorUtils.peekNext(blockIterator); + inlineeStack.push(inlineeEnd); + // Move the cursor back to where the first inlinee block was added. + IteratorUtils.previousUntil(blockIterator, previous -> previous == block); + blockIterator.next(); + } } else if (current.isAssumeDynamicType()) { assumeDynamicTypeRemover.removeIfMarked(current.asAssumeDynamicType(), iterator); } } } + assert inlineeStack.isEmpty(); assumeDynamicTypeRemover.removeMarkedInstructions(blocksToRemove); assumeDynamicTypeRemover.finish(); classInitializationAnalysis.finish();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java index 5f9bb2a..e0b0a2c 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -17,6 +17,11 @@ interface InliningStrategy { + boolean allowInliningOfInvokeInInlinee( + InlineAction action, + int inliningDepth, + WhyAreYouNotInliningReporter whyAreYouNotInliningReporter); + boolean canInlineInstanceInitializer( IRCode code, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java index 62d5bd8..cea6258 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -3,6 +3,9 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.ir.optimize; +import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull; +import static com.android.tools.r8.ir.analysis.type.TypeLatticeElement.stringClassType; + import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DebugLocalInfo; import com.android.tools.r8.graph.DexDefinition; @@ -193,7 +196,8 @@ appView.dexItemFactory().stringType, typeLattice.asClassTypeLatticeElement().getClassType()) .isTrue(); - Value returnedValue = code.createValue(typeLattice, debugLocalInfo); + Value returnedValue = + code.createValue(stringClassType(appView, definitelyNotNull()), debugLocalInfo); ConstString instruction = new ConstString( returnedValue, constant, ThrowingInfo.defaultForConstString(appView.options()));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java index cea57a8..9752859 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -134,8 +134,8 @@ DexEncodedField encodedField = appView.appInfo().resolveField(field); if (encodedField != null) { FieldOptimizationInfo optimizationInfo = encodedField.getOptimizationInfo(); - if (optimizationInfo.getDynamicType() != null - && optimizationInfo.getDynamicType().isDefinitelyNotNull()) { + if (optimizationInfo.getDynamicUpperBoundType() != null + && optimizationInfo.getDynamicUpperBoundType().isDefinitelyNotNull()) { knownToBeNonNullValues.add(outValue); }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java index fdfa3ee..5088d99 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -373,7 +373,7 @@ public void rewrite(IRCode code) { Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet(); ListIterator<BasicBlock> blockIterator = code.listIterator(); - Set<Value> valuesToNarrow = new HashSet<>(); + Set<Value> valuesToNarrow = Sets.newIdentityHashSet(); while (blockIterator.hasNext()) { BasicBlock block = blockIterator.next(); if (blocksToBeRemoved.contains(block)) { @@ -422,12 +422,12 @@ } } } + code.removeBlocks(blocksToBeRemoved); + code.removeAllTrivialPhis(valuesToNarrow); + code.removeUnreachableBlocks(); if (!valuesToNarrow.isEmpty()) { new TypeAnalysis(appView).narrowing(valuesToNarrow); } - code.removeBlocks(blocksToBeRemoved); - code.removeAllTrivialPhis(); - code.removeUnreachableBlocks(); assert code.isConsistentSSA(); }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java index 95f29b9..a938ff2 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -226,6 +226,13 @@ List<DexEncodedMethod> directMethods = clazz.directMethods(); for (int i = 0; i < directMethods.size(); i++) { DexEncodedMethod method = directMethods.get(i); + + // If this is a private or static method that is targeted by an invoke-super instruction, then + // don't remove any unused arguments. + if (appView.appInfo().brokenSuperInvokes.contains(method.method)) { + continue; + } + RemovedArgumentsInfo unused = collectUnusedArguments(method); if (unused != null && unused.hasRemovedArguments()) { DexProto newProto = createProtoWithRemovedArguments(method, unused);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java index f550309..d576a22 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -246,7 +246,7 @@ // particularly important for bridge methods. codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(code); // If a method was inlined we may be able to prune additional branches. - codeRewriter.simplifyIf(code); + codeRewriter.simplifyControlFlow(code); // If a method was inlined we may see more trivial computation/conversion of String. boolean isDebugMode = appView.options().debug || method.getOptimizationInfo().isReachabilitySensitive();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java index 30721f5..e82afb7 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
@@ -4,6 +4,7 @@ package com.android.tools.r8.ir.optimize.info; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; public class DefaultFieldOptimizationInfo extends FieldOptimizationInfo { @@ -32,7 +33,12 @@ } @Override - public TypeLatticeElement getDynamicType() { + public ClassTypeLatticeElement getDynamicLowerBoundType() { + return null; + } + + @Override + public TypeLatticeElement getDynamicUpperBoundType() { return null; }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java index 58432ba..cc89f84 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
@@ -4,6 +4,7 @@ package com.android.tools.r8.ir.optimize.info; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; public abstract class FieldOptimizationInfo { @@ -18,7 +19,9 @@ */ public abstract int getReadBits(); - public abstract TypeLatticeElement getDynamicType(); + public abstract ClassTypeLatticeElement getDynamicLowerBoundType(); + + public abstract TypeLatticeElement getDynamicUpperBoundType(); public abstract boolean valueHasBeenPropagated();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java index 78369e3..8cbc644 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -46,6 +46,7 @@ import com.android.tools.r8.kotlin.Kotlin; import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.MethodSignatureEquivalence; import com.google.common.base.Equivalence.Wrapper; import com.google.common.collect.Sets; @@ -60,17 +61,16 @@ import java.util.function.Function; public class MethodOptimizationInfoCollector { - private final AppView<?> appView; + private final AppView<AppInfoWithLiveness> appView; private final InternalOptions options; private final DexItemFactory dexItemFactory; - public MethodOptimizationInfoCollector(AppView<?> appView) { + public MethodOptimizationInfoCollector(AppView<AppInfoWithLiveness> appView) { this.appView = appView; this.options = appView.options(); this.dexItemFactory = appView.dexItemFactory(); } - // TODO(b/141656615): Use this and then make all utils in this collector `private`. public void collectMethodOptimizationInfo( DexEncodedMethod method, IRCode code, @@ -91,7 +91,7 @@ computeNonNullParamOnNormalExits(feedback, code); } - public void identifyClassInlinerEligibility( + private void identifyClassInlinerEligibility( DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { // Method eligibility is calculated in similar way for regular method // and for the constructor. To be eligible method should only be using its @@ -126,7 +126,11 @@ boolean receiverUsedAsReturnValue = false; boolean seenSuperInitCall = false; - for (Instruction insn : receiver.uniqueUsers()) { + for (Instruction insn : receiver.aliasedUsers()) { + if (insn.isAssume()) { + continue; + } + if (insn.isMonitor()) { continue; } @@ -140,11 +144,11 @@ if (insn.isInstancePut()) { InstancePut instancePutInstruction = insn.asInstancePut(); // Only allow field writes to the receiver. - if (instancePutInstruction.object() != receiver) { + if (instancePutInstruction.object().getAliasedValue() != receiver) { return; } // Do not allow the receiver to escape via a field write. - if (instancePutInstruction.value() == receiver) { + if (instancePutInstruction.value().getAliasedValue() == receiver) { return; } } @@ -162,7 +166,8 @@ DexMethod invokedMethod = invokedDirect.getInvokedMethod(); if (dexItemFactory.isConstructor(invokedMethod) && invokedMethod.holder == clazz.superType - && invokedDirect.inValues().lastIndexOf(receiver) == 0 + && ListUtils.lastIndexMatching( + invokedDirect.inValues(), v -> v.getAliasedValue() == receiver) == 0 && !seenSuperInitCall && instanceInitializer) { seenSuperInitCall = true; @@ -184,7 +189,7 @@ method, new ClassInlinerEligibility(receiverUsedAsReturnValue)); } - public void identifyParameterUsages( + private void identifyParameterUsages( DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { List<ParameterUsage> usages = new ArrayList<>(); List<Value> values = code.collectArguments(); @@ -203,7 +208,7 @@ private ParameterUsage collectParameterUsages(int i, Value value) { ParameterUsageBuilder builder = new ParameterUsageBuilder(value, i); - for (Instruction user : value.uniqueUsers()) { + for (Instruction user : value.aliasedUsers()) { if (!builder.note(user)) { return null; } @@ -211,7 +216,7 @@ return builder.build(); } - public void identifyReturnsArgument( + private void identifyReturnsArgument( DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { List<BasicBlock> normalExits = code.computeNormalExitBlocks(); if (normalExits.isEmpty()) { @@ -233,18 +238,19 @@ isNeverNull &= value.getTypeLattice().isReference() && value.isNeverNull(); } if (returnValue != null) { - if (returnValue.isArgument()) { + Value aliasedValue = returnValue.getAliasedValue(); + if (aliasedValue.isArgument()) { // Find the argument number. - int index = code.collectArguments().indexOf(returnValue); - assert index != -1; + int index = aliasedValue.computeArgumentPosition(code); + assert index >= 0; feedback.methodReturnsArgument(method, index); } - if (returnValue.isConstant()) { - if (returnValue.definition.isConstNumber()) { - long value = returnValue.definition.asConstNumber().getRawValue(); + if (aliasedValue.isConstant()) { + if (aliasedValue.definition.isConstNumber()) { + long value = aliasedValue.definition.asConstNumber().getRawValue(); feedback.methodReturnsConstantNumber(method, value); - } else if (returnValue.definition.isConstString()) { - ConstString constStringInstruction = returnValue.definition.asConstString(); + } else if (aliasedValue.definition.isConstString()) { + ConstString constStringInstruction = aliasedValue.definition.asConstString(); if (!constStringInstruction.instructionInstanceCanThrow()) { feedback.methodReturnsConstantString(method, constStringInstruction.getValue()); } @@ -256,9 +262,11 @@ } } - public void identifyTrivialInitializer( + private void identifyTrivialInitializer( DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { - if (!method.isInstanceInitializer() && !method.isClassInitializer()) { + assert !appView.appInfo().isPinned(method.method); + + if (!method.isInitializer()) { return; } @@ -266,8 +274,13 @@ return; } + if (appView.appInfo().mayHaveSideEffects.containsKey(method.method)) { + return; + } + DexClass clazz = appView.appInfo().definitionFor(method.method.holder); if (clazz == null) { + assert false; return; } @@ -303,6 +316,10 @@ continue; } + if (insn.isAssume()) { + continue; + } + if (insn.isNewInstance()) { NewInstance newInstance = insn.asNewInstance(); if (createdSingletonInstance != null @@ -374,6 +391,10 @@ continue; } + if (insn.isAssume()) { + continue; + } + if (insn.isArgument()) { continue; } @@ -440,7 +461,7 @@ return TrivialInstanceInitializer.INSTANCE; } - public void identifyInvokeSemanticsForInlining( + private void identifyInvokeSemanticsForInlining( DexEncodedMethod method, IRCode code, AppView<?> appView, OptimizationFeedback feedback) { if (method.isStatic()) { // Identifies if the method preserves class initialization after inlining. @@ -692,7 +713,7 @@ return true; } - public void computeDynamicReturnType( + private void computeDynamicReturnType( DynamicTypeOptimization dynamicTypeOptimization, OptimizationFeedback feedback, DexEncodedMethod method, @@ -721,7 +742,7 @@ } } - public void computeInitializedClassesOnNormalExit( + private void computeInitializedClassesOnNormalExit( OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) { if (options.enableInitializedClassesAnalysis && appView.appInfo().hasLiveness()) { AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness(); @@ -734,7 +755,7 @@ } } - public void computeMayHaveSideEffects( + private void computeMayHaveSideEffects( OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) { // If the method is native, we don't know what could happen. assert !method.accessFlags.isNative(); @@ -802,7 +823,7 @@ return false; } - public void computeReturnValueOnlyDependsOnArguments( + private void computeReturnValueOnlyDependsOnArguments( OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) { if (!options.enableDeterminismAnalysis) { return; @@ -815,7 +836,7 @@ } // Track usage of parameters and compute their nullability and possibility of NPE. - public void computeNonNullParamOrThrow( + private void computeNonNullParamOrThrow( OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) { if (method.getOptimizationInfo().getNonNullParamOrThrow() != null) { return; @@ -843,7 +864,7 @@ } } - public void computeNonNullParamOnNormalExits(OptimizationFeedback feedback, IRCode code) { + private void computeNonNullParamOnNormalExits(OptimizationFeedback feedback, IRCode code) { Set<BasicBlock> normalExits = Sets.newIdentityHashSet(); normalExits.addAll(code.computeNormalExitBlocks()); DominatorTree dominatorTree = new DominatorTree(code, MAY_HAVE_UNREACHABLE_BLOCKS);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java index 55ce1a4..9d3bb4a 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -7,6 +7,7 @@ import com.android.tools.r8.graph.AppInfoWithSubtyping; import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; import java.util.function.Function; @@ -22,12 +23,16 @@ private int readBits = 0; private boolean cannotBeKept = false; private boolean valueHasBeenPropagated = false; - private TypeLatticeElement dynamicType = null; + private ClassTypeLatticeElement dynamicLowerBoundType = null; + private TypeLatticeElement dynamicUpperBoundType = null; public void fixupClassTypeReferences( Function<DexType, DexType> mapping, AppView<? extends AppInfoWithSubtyping> appView) { - if (dynamicType != null) { - dynamicType = dynamicType.fixupClassTypeReferences(mapping, appView); + if (dynamicLowerBoundType != null) { + dynamicLowerBoundType = dynamicLowerBoundType.fixupClassTypeReferences(mapping, appView); + } + if (dynamicUpperBoundType != null) { + dynamicUpperBoundType = dynamicUpperBoundType.fixupClassTypeReferences(mapping, appView); } } @@ -58,12 +63,21 @@ } @Override - public TypeLatticeElement getDynamicType() { - return dynamicType; + public ClassTypeLatticeElement getDynamicLowerBoundType() { + return dynamicLowerBoundType; } - void setDynamicType(TypeLatticeElement type) { - dynamicType = type; + void setDynamicLowerBoundType(ClassTypeLatticeElement type) { + dynamicLowerBoundType = type; + } + + @Override + public TypeLatticeElement getDynamicUpperBoundType() { + return dynamicUpperBoundType; + } + + void setDynamicUpperBoundType(TypeLatticeElement type) { + dynamicUpperBoundType = type; } @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java index d185a37..a994f5d 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -95,8 +95,14 @@ } @Override - public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) { - getFieldOptimizationInfoForUpdating(field).setDynamicType(type); + public void markFieldHasDynamicLowerBoundType( + DexEncodedField field, ClassTypeLatticeElement type) { + getFieldOptimizationInfoForUpdating(field).setDynamicLowerBoundType(type); + } + + @Override + public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) { + getFieldOptimizationInfoForUpdating(field).setDynamicUpperBoundType(type); } @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java index 79b2e7f..9b5c988 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -36,7 +36,11 @@ public void markFieldAsPropagated(DexEncodedField field) {} @Override - public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) {} + public void markFieldHasDynamicLowerBoundType( + DexEncodedField field, ClassTypeLatticeElement type) {} + + @Override + public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) {} @Override public void markFieldBitsRead(DexEncodedField field, int bitsRead) {}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java index 9119b4b..474a963 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -40,7 +40,13 @@ } @Override - public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) { + public void markFieldHasDynamicLowerBoundType( + DexEncodedField field, ClassTypeLatticeElement type) { + // Ignored. + } + + @Override + public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) { // Ignored. }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java index 0495bba..7e61600 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
@@ -14,6 +14,7 @@ import com.android.tools.r8.ir.code.InvokeMethodWithReceiver; import com.android.tools.r8.ir.code.Return; import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.Pair; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -110,6 +111,10 @@ // Returns false if the instruction is not supported. public boolean note(Instruction instruction) { + if (instruction.isAssume()) { + // Keep examining other users, but the param usage builder should consider aliased users. + return true; + } if (instruction.isIf()) { return note(instruction.asIf()); } @@ -141,7 +146,8 @@ private boolean note(If ifInstruction) { if (ifInstruction.asIf().isZeroTest()) { - assert ifInstruction.inValues().size() == 1 && ifInstruction.inValues().get(0) == arg; + assert ifInstruction.inValues().size() == 1 + && ifInstruction.inValues().get(0).getAliasedValue() == arg; ifZeroTestTypes.add(ifInstruction.asIf().getType()); return true; } @@ -150,7 +156,7 @@ private boolean note(InstanceGet instanceGetInstruction) { assert arg != instanceGetInstruction.outValue(); - if (instanceGetInstruction.object() == arg) { + if (instanceGetInstruction.object().getAliasedValue() == arg) { hasFieldRead = true; return true; } @@ -159,12 +165,12 @@ private boolean note(InstancePut instancePutInstruction) { assert arg != instancePutInstruction.outValue(); - if (instancePutInstruction.object() == arg) { + if (instancePutInstruction.object().getAliasedValue() == arg) { hasFieldAssignment = true; - isAssignedToField |= instancePutInstruction.value() == arg; + isAssignedToField |= instancePutInstruction.value().getAliasedValue() == arg; return true; } - if (instancePutInstruction.value() == arg) { + if (instancePutInstruction.value().getAliasedValue() == arg) { isAssignedToField = true; return true; } @@ -172,7 +178,8 @@ } private boolean note(InvokeMethodWithReceiver invokeInstruction) { - if (invokeInstruction.inValues().lastIndexOf(arg) == 0) { + if (ListUtils.lastIndexMatching( + invokeInstruction.inValues(), v -> v.getAliasedValue() == arg) == 0) { callsOnReceiver.add( new Pair<>( invokeInstruction.asInvokeMethodWithReceiver().getType(), @@ -183,7 +190,8 @@ } private boolean note(Return returnInstruction) { - assert returnInstruction.inValues().size() == 1 && returnInstruction.inValues().get(0) == arg; + assert returnInstruction.inValues().size() == 1 + && returnInstruction.inValues().get(0).getAliasedValue() == arg; isReturned = true; return true; }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java index 06e7332..09a8d13 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
@@ -371,7 +371,12 @@ // We may get more precise type information if the method is reprocessed (e.g., due to // optimization info collected from all call sites), and hence the `returnsObjectOfType` is // allowed to become more precise. - assert returnsObjectOfType == UNKNOWN_TYPE || type.lessThanOrEqual(returnsObjectOfType, appView) + // TODO(b/142559221): non-materializable assume instructions? + // Nullability could be less precise, though. For example, suppose a value is known to be + // non-null after a safe invocation, hence recorded with the non-null variant. If that call is + // inlined and the method is reprocessed, such non-null assumption cannot be made again. + assert returnsObjectOfType == UNKNOWN_TYPE + || type.lessThanOrEqualUpToNullability(returnsObjectOfType, appView) : "return type changed from " + returnsObjectOfType + " to " + type; returnsObjectOfType = type; }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java index 467a5b6..8cbb1f4 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
@@ -119,11 +119,14 @@ public void reportWillExceedInstructionBudget(int numberOfInstructions, int threshold) {} @Override + public void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold) {} + + @Override public void reportWillExceedMonitorEnterValuesBudget( int numberOfMonitorEnterValuesAfterInlining, int threshold) {} @Override - public boolean verifyReasonHasBeenReported() { + public boolean unsetReasonHasBeenReportedFlag() { return true; } }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java index ab80544..1ff91c0 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -113,8 +113,10 @@ public abstract void reportWillExceedInstructionBudget(int numberOfInstructions, int threshold); + public abstract void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold); + public abstract void reportWillExceedMonitorEnterValuesBudget( int numberOfMonitorEnterValuesAfterInlining, int threshold); - public abstract boolean verifyReasonHasBeenReported(); + public abstract boolean unsetReasonHasBeenReportedFlag(); }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java index 7d25661..64026ca 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
@@ -247,6 +247,15 @@ } @Override + public void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold) { + printWithExceededThreshold( + "would exceed the maximum inlining depth", + "current inlining depth", + actualInliningDepth, + threshold); + } + + @Override public void reportWillExceedMonitorEnterValuesBudget( int numberOfMonitorEnterValuesAfterInlining, int threshold) { printWithExceededThreshold( @@ -257,8 +266,9 @@ } @Override - public boolean verifyReasonHasBeenReported() { + public boolean unsetReasonHasBeenReportedFlag() { assert reasonHasBeenReported; + reasonHasBeenReported = false; return true; } }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java index 5c6cb8f..f10c3fe 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
@@ -9,6 +9,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.code.BasicBlock; import com.android.tools.r8.ir.code.CheckCast; @@ -147,12 +148,25 @@ public final ListIterator<BasicBlock> blocks; private InstructionListIterator instructions; + // The inlining context (caller), if any. + private final DexEncodedMethod context; + CodeProcessor( AppView<AppInfoWithLiveness> appView, Function<DexType, Strategy> strategyProvider, LambdaTypeVisitor lambdaChecker, DexEncodedMethod method, IRCode code) { + this(appView, strategyProvider, lambdaChecker, method, code, null); + } + + CodeProcessor( + AppView<AppInfoWithLiveness> appView, + Function<DexType, Strategy> strategyProvider, + LambdaTypeVisitor lambdaChecker, + DexEncodedMethod method, + IRCode code, + DexEncodedMethod context) { this.appView = appView; this.strategyProvider = strategyProvider; this.factory = appView.dexItemFactory(); @@ -161,6 +175,7 @@ this.method = method; this.code = code; this.blocks = code.listIterator(); + this.context = context; } public final InstructionListIterator instructions() { @@ -178,6 +193,19 @@ } } + private boolean shouldRewrite(DexField field) { + return shouldRewrite(field.holder); + } + + private boolean shouldRewrite(DexMethod method) { + return shouldRewrite(method.holder); + } + + private boolean shouldRewrite(DexType type) { + // Rewrite references to lambda classes if we are outside the class. + return type != (context != null ? context : method).method.holder; + } + @Override public Void handleInvoke(Invoke invoke) { if (invoke.isInvokeNewArray()) { @@ -199,7 +227,7 @@ // Invalidate signature, there still should not be lambda references. lambdaChecker.accept(invokeMethod.getInvokedMethod().proto); // Only rewrite references to lambda classes if we are outside the class. - if (invokeMethod.getInvokedMethod().holder != this.method.method.holder) { + if (shouldRewrite(invokeMethod.getInvokedMethod())) { process(strategy, invokeMethod); } return null; @@ -218,7 +246,7 @@ Strategy strategy = strategyProvider.apply(newInstance.clazz); if (strategy.isValidNewInstance(this, newInstance)) { // Only rewrite references to lambda classes if we are outside the class. - if (newInstance.clazz != this.method.method.holder) { + if (shouldRewrite(newInstance.clazz)) { process(strategy, newInstance); } } @@ -260,7 +288,7 @@ DexField field = instanceGet.getField(); Strategy strategy = strategyProvider.apply(field.holder); if (strategy.isValidInstanceFieldRead(this, field)) { - if (field.holder != this.method.method.holder) { + if (shouldRewrite(field)) { // Only rewrite references to lambda classes if we are outside the class. process(strategy, instanceGet); } @@ -279,7 +307,7 @@ DexField field = instancePut.getField(); Strategy strategy = strategyProvider.apply(field.holder); if (strategy.isValidInstanceFieldWrite(this, field)) { - if (field.holder != this.method.method.holder) { + if (shouldRewrite(field)) { // Only rewrite references to lambda classes if we are outside the class. process(strategy, instancePut); } @@ -298,7 +326,7 @@ DexField field = staticGet.getField(); Strategy strategy = strategyProvider.apply(field.holder); if (strategy.isValidStaticFieldRead(this, field)) { - if (field.holder != this.method.method.holder) { + if (shouldRewrite(field)) { // Only rewrite references to lambda classes if we are outside the class. process(strategy, staticGet); } @@ -314,7 +342,7 @@ DexField field = staticPut.getField(); Strategy strategy = strategyProvider.apply(field.holder); if (strategy.isValidStaticFieldWrite(this, field)) { - if (field.holder != this.method.method.holder) { + if (shouldRewrite(field)) { // Only rewrite references to lambda classes if we are outside the class. process(strategy, staticPut); }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java index a90704a..b226521 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -56,7 +56,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -84,6 +83,43 @@ // 5. synthesize group lambda classes. // public final class LambdaMerger { + + private abstract static class Mode { + + void rewriteCode(DexEncodedMethod method, IRCode code, DexEncodedMethod context) {} + + void analyzeCode(DexEncodedMethod method, IRCode code) {} + } + + private class AnalyzeMode extends Mode { + + @Override + void analyzeCode(DexEncodedMethod method, IRCode code) { + new AnalysisStrategy(method, code).processCode(); + } + } + + private class ApplyMode extends Mode { + + private final Set<DexType> lambdaGroupsClasses; + private final LambdaMergerOptimizationInfoFixer optimizationInfoFixer; + + ApplyMode( + Set<DexType> lambdaGroupTypes, LambdaMergerOptimizationInfoFixer optimizationInfoFixer) { + this.lambdaGroupsClasses = lambdaGroupTypes; + this.optimizationInfoFixer = optimizationInfoFixer; + } + + @Override + void rewriteCode(DexEncodedMethod method, IRCode code, DexEncodedMethod context) { + if (lambdaGroupsClasses.contains(method.method.holder)) { + // Don't rewrite the methods that we have synthesized for the lambda group classes. + return; + } + new ApplyStrategy(method, code, context, optimizationInfoFixer).processCode(); + } + } + // Maps lambda into a group, only contains lambdas we decided to merge. // NOTE: needs synchronization. private final Map<DexType, LambdaGroup> lambdas = new IdentityHashMap<>(); @@ -113,7 +149,7 @@ private final Kotlin kotlin; private final DiagnosticsHandler reporter; - private BiFunction<DexEncodedMethod, IRCode, CodeProcessor> strategyFactory = null; + private Mode mode; // Lambda visitor invalidating lambdas it sees. private final LambdaTypeVisitor lambdaInvalidator; @@ -153,8 +189,7 @@ // Collect all group candidates and assign unique lambda ids inside each group. // We do this before methods are being processed to guarantee stable order of // lambdas inside each group. - public final void collectGroupCandidates( - DexApplication app, AppView<AppInfoWithLiveness> appView) { + public final void collectGroupCandidates(DexApplication app) { // Collect lambda groups. app.classes().stream() .filter(cls -> !appView.appInfo().isPinned(cls.type)) @@ -188,20 +223,50 @@ // Remove trivial groups. removeTrivialLambdaGroups(); - assert strategyFactory == null; - strategyFactory = AnalysisStrategy::new; + assert mode == null; + mode = new AnalyzeMode(); } - // Is called by IRConverter::rewriteCode, performs different actions - // depending on phase: - // - in ANALYZE phase just analyzes invalid usages of lambda classes - // inside the method code, invalidated such lambda classes, - // collects methods that need to be patched. - // - in APPLY phase patches the code to use lambda group classes, also - // asserts that there are no more invalid lambda class references. - public final void processMethodCode(DexEncodedMethod method, IRCode code) { - if (strategyFactory != null) { - strategyFactory.apply(method, code).processCode(); + /** + * Is called by IRConverter::rewriteCode. Performs different actions depending on the current + * mode. + * + * <ol> + * <li>in ANALYZE mode analyzes invalid usages of lambda classes inside the method code, + * invalidated such lambda classes, collects methods that need to be patched. + * <li>in APPLY mode does nothing. + * </ol> + */ + public final void analyzeCode(DexEncodedMethod method, IRCode code) { + if (mode != null) { + mode.analyzeCode(method, code); + } + } + + /** + * Is called by IRConverter::rewriteCode. Performs different actions depending on the current + * mode. + * + * <ol> + * <li>in ANALYZE mode does nothing. + * <li>in APPLY mode patches the code to use lambda group classes, also asserts that there are + * no more invalid lambda class references. + * </ol> + */ + public final void rewriteCode(DexEncodedMethod method, IRCode code) { + if (mode != null) { + mode.rewriteCode(method, code, null); + } + } + + /** + * Similar to {@link #rewriteCode(DexEncodedMethod, IRCode)}, but for rewriting code for inlining. + * The {@param context} is the caller that {@param method} is being inlined into. + */ + public final void rewriteCodeForInlining( + DexEncodedMethod method, IRCode code, DexEncodedMethod context) { + if (mode != null) { + mode.rewriteCode(method, code, context); } } @@ -239,7 +304,9 @@ feedback.fixupOptimizationInfos(appView, executorService, optimizationInfoFixer); // Switch to APPLY strategy. - this.strategyFactory = (method, code) -> new ApplyStrategy(method, code, optimizationInfoFixer); + Set<DexType> lambdaGroupTypes = + lambdaGroupsClasses.values().stream().map(clazz -> clazz.type).collect(Collectors.toSet()); + this.mode = new ApplyMode(lambdaGroupTypes, optimizationInfoFixer); // Add synthesized lambda group classes to the builder. for (Entry<LambdaGroup, DexProgramClass> entry : lambdaGroupsClasses.entrySet()) { @@ -274,7 +341,7 @@ // Rewrite lambda class references into lambda group class // references inside methods from the processing queue. rewriteLambdaReferences(converter, executorService, feedback); - this.strategyFactory = null; + this.mode = null; } private void analyzeReferencesInProgramClasses( @@ -459,13 +526,15 @@ private ApplyStrategy( DexEncodedMethod method, IRCode code, + DexEncodedMethod context, LambdaMergerOptimizationInfoFixer optimizationInfoFixer) { super( LambdaMerger.this.appView, LambdaMerger.this::strategyProvider, lambdaChecker, method, - code); + code, + context); this.optimizationInfoFixer = optimizationInfoFixer; }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java index 8456912..26fbcee 100644 --- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java +++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -38,11 +38,13 @@ import com.android.tools.r8.ir.regalloc.RegisterPositions.Type; import com.android.tools.r8.logging.Log; import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.SetUtils; import com.android.tools.r8.utils.StringUtils; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multiset; import com.google.common.collect.Multisets; +import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; @@ -280,7 +282,8 @@ boolean isEntryBlock = true; for (BasicBlock block : blocks) { InstructionListIterator instructionIterator = block.listIterator(code); - Set<Value> liveLocalValues = new HashSet<>(liveAtEntrySets.get(block).liveLocalValues); + Set<Value> liveLocalValues = + SetUtils.newIdentityHashSet(liveAtEntrySets.get(block).liveLocalValues); // Skip past arguments and open argument and phi locals. if (isEntryBlock) { isEntryBlock = false; @@ -2516,9 +2519,9 @@ Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets, List<LiveIntervals> liveIntervals) { for (BasicBlock block : code.topologicallySortedBlocks()) { - Set<Value> live = new HashSet<>(); - Set<Value> phiOperands = new HashSet<>(); - Set<Value> liveAtThrowingInstruction = new HashSet<>(); + Set<Value> live = Sets.newIdentityHashSet(); + Set<Value> phiOperands = Sets.newIdentityHashSet(); + Set<Value> liveAtThrowingInstruction = Sets.newIdentityHashSet(); Set<BasicBlock> exceptionalSuccessors = block.getCatchHandlers().getUniqueTargets(); for (BasicBlock successor : block.getSuccessors()) { // Values live at entry to a block that is an exceptional successor are only live
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java new file mode 100644 index 0000000..356f609 --- /dev/null +++ b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
@@ -0,0 +1,83 @@ +// Copyright (c) 2019, 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.shaking; + +import com.android.tools.r8.graph.DexAnnotation; +import com.android.tools.r8.graph.DexAnnotationElement; +import com.android.tools.r8.graph.DexEncodedAnnotation; +import com.android.tools.r8.graph.DexEncodedField; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexProgramClass; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.graph.DexValue; +import com.android.tools.r8.graph.DexValue.DexValueArray; +import com.android.tools.r8.graph.DexValue.DexValueType; +import com.android.tools.r8.graph.GraphLense; +import com.android.tools.r8.utils.ArrayUtils; + +public class AnnotationFixer { + + private final GraphLense lense; + + public AnnotationFixer(GraphLense lense) { + this.lense = lense; + } + + public void run(Iterable<DexProgramClass> classes) { + for (DexProgramClass clazz : classes) { + clazz.annotations = clazz.annotations.rewrite(this::rewriteAnnotation); + clazz.forEachMethod(this::processMethod); + clazz.forEachField(this::processField); + } + } + + private void processMethod(DexEncodedMethod method) { + method.annotations = method.annotations.rewrite(this::rewriteAnnotation); + method.parameterAnnotationsList = + method.parameterAnnotationsList.rewrite( + dexAnnotationSet -> dexAnnotationSet.rewrite(this::rewriteAnnotation)); + } + + private void processField(DexEncodedField field) { + field.annotations = field.annotations.rewrite(this::rewriteAnnotation); + } + + private DexAnnotation rewriteAnnotation(DexAnnotation original) { + return original.rewrite(this::rewriteEncodedAnnotation); + } + + private DexEncodedAnnotation rewriteEncodedAnnotation(DexEncodedAnnotation original) { + DexEncodedAnnotation rewritten = + original.rewrite(lense::lookupType, this::rewriteAnnotationElement); + assert rewritten != null; + return rewritten; + } + + private DexAnnotationElement rewriteAnnotationElement(DexAnnotationElement original) { + DexValue rewrittenValue = rewriteValue(original.value); + if (rewrittenValue != original.value) { + return new DexAnnotationElement(original.name, rewrittenValue); + } + return original; + } + + private DexValue rewriteValue(DexValue value) { + if (value.isDexValueType()) { + DexType originalType = value.asDexValueType().value; + DexType rewrittenType = lense.lookupType(originalType); + if (rewrittenType != originalType) { + return new DexValueType(rewrittenType); + } + } else if (value.isDexValueArray()) { + DexValue[] originalValues = value.asDexValueArray().getValues(); + DexValue[] rewrittenValues = + ArrayUtils.map(DexValue[].class, originalValues, this::rewriteValue); + if (rewrittenValues != originalValues) { + return new DexValueArray(rewrittenValues); + } + } + return value; + } +}
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java index 693058b..701904c 100644 --- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java +++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -108,11 +108,6 @@ private boolean isAnnotationTypeLive(DexAnnotation annotation) { DexType annotationType = annotation.annotation.type.toBaseType(appView.dexItemFactory()); - DexClass definition = appView.definitionFor(annotationType); - // TODO(b/73102187): How to handle annotations without definition. - if (appView.options().isShrinking() && definition == null) { - return false; - } return appView.appInfo().isNonProgramTypeOrLiveProgramType(annotationType); } @@ -276,7 +271,8 @@ private DexAnnotationElement rewriteAnnotationElement( DexType annotationType, DexAnnotationElement original) { DexClass definition = appView.definitionFor(annotationType); - // TODO(b/73102187): How to handle annotations without definition. + // We cannot strip annotations where we cannot look up the definition, because this will break + // apps that rely on the annotation to exist. See b/134766810 for more information. if (definition == null) { return original; }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java index 0689533..5eebf8b 100644 --- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java +++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -182,6 +182,10 @@ /** Set of live types defined in the library and classpath. Used to avoid duplicate tracing. */ private final Set<DexClass> liveNonProgramTypes = Sets.newIdentityHashSet(); + /** Mapping from each unused interface to the set of live types that implements the interface. */ + private final Map<DexProgramClass, Set<DexProgramClass>> unusedInterfaceTypes = + new IdentityHashMap<>(); + /** * Set of proto extension types that are technically live, but which we have not traced because * they are dead according to the generated extension registry shrinker. @@ -226,7 +230,7 @@ * Set of methods that belong to live classes and can be reached by invokes. These need to be * kept. */ - private final SetWithReason<DexEncodedMethod> liveMethods; + private final LiveMethodsSet liveMethods; /** * Set of fields that belong to live classes and can be reached by invokes. These need to be kept. @@ -309,7 +313,7 @@ liveAnnotations = new SetWithReason<>(graphReporter::registerAnnotation); instantiatedTypes = new SetWithReason<>(graphReporter::registerClass); targetedMethods = new SetWithReason<>(graphReporter::registerMethod); - liveMethods = new SetWithReason<>(graphReporter::registerMethod); + liveMethods = new LiveMethodsSet(graphReporter::registerMethod); liveFields = new SetWithReason<>(graphReporter::registerField); instantiatedInterfaceTypes = new SetWithReason<>(graphReporter::registerInterface); } @@ -483,7 +487,7 @@ private boolean enqueueMarkMethodLiveAction( DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { assert method.method.holder == clazz.type; - if (liveMethods.add(method, reason)) { + if (liveMethods.add(clazz, method, reason)) { workList.enqueueMarkMethodLiveAction(clazz, method, reason); return true; } @@ -1128,7 +1132,7 @@ KeepReason reason = KeepReason.reachableFromLiveType(holder.type); for (DexType iface : holder.interfaces.values) { - markInterfaceTypeAsLiveViaInheritanceClause(iface, reason); + markInterfaceTypeAsLiveViaInheritanceClause(iface, holder); } if (holder.superType != null) { @@ -1139,6 +1143,10 @@ markTypeAsLive(holder.superType, reason); } + // If this is an interface that has just become live, then report previously seen but unreported + // implemented-by edges. + transitionUnusedInterfaceToLive(holder); + // We cannot remove virtual methods defined earlier in the type hierarchy if it is widening // access and is defined in an interface: // @@ -1204,32 +1212,29 @@ } } - private void markInterfaceTypeAsLiveViaInheritanceClause(DexType type, KeepReason reason) { - if (appView.options().enableUnusedInterfaceRemoval && !mode.isTracingMainDex()) { - DexProgramClass clazz = getProgramClassOrNull(type); - if (clazz == null) { - return; - } + private void markInterfaceTypeAsLiveViaInheritanceClause( + DexType type, DexProgramClass implementer) { + DexProgramClass clazz = getProgramClassOrNull(type); + if (clazz == null) { + return; + } - assert clazz.isInterface(); - - if (!clazz.interfaces.isEmpty()) { - markTypeAsLive(type, reason); - return; - } - - for (DexEncodedMethod method : clazz.virtualMethods()) { - if (!method.accessFlags.isAbstract()) { - markTypeAsLive(type, reason); - return; - } - } - - // No need to mark the type as live. If an interface type is only reachable via the - // inheritance clause of another type, and the interface only has abstract methods, it can - // simply be removed from the inheritance clause. + if (!appView.options().enableUnusedInterfaceRemoval || mode.isTracingMainDex()) { + markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer)); } else { - markTypeAsLive(type, reason); + if (liveTypes.contains(clazz)) { + // The interface is already live, so make sure to report this implements-edge. + graphReporter.reportClassReferencedFrom(clazz, implementer); + } else { + // No need to mark the type as live. If an interface type is only reachable via the + // inheritance clause of another type it can simply be removed from the inheritance clause. + // The interface is needed if it has a live default interface method or field, though. + // Therefore, we record that this implemented-by edge has not been reported, such that we + // can report it in the future if one its members becomes live. + unusedInterfaceTypes + .computeIfAbsent(clazz, ignore -> Sets.newIdentityHashSet()) + .add(implementer); + } } } @@ -1394,9 +1399,7 @@ // Already targeted. return; } - markTypeAsLive(method.method.holder, - holder -> graphReporter.reportClassReferencedFrom(holder, method)); - markParameterAndReturnTypesAsLive(method); + markReferencedTypesAsLive(method); processAnnotations(method, method.annotations.annotations); method.parameterAnnotationsList.forEachAnnotation( annotation -> processAnnotation(method, annotation)); @@ -1652,6 +1655,19 @@ && !instantiatedTypes.contains(current.asProgramClass())); } + private void transitionUnusedInterfaceToLive(DexProgramClass clazz) { + if (clazz.isInterface()) { + Set<DexProgramClass> implementedBy = unusedInterfaceTypes.remove(clazz); + if (implementedBy != null) { + for (DexProgramClass implementer : implementedBy) { + markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer)); + } + } + } else { + assert !unusedInterfaceTypes.containsKey(clazz); + } + } + private void markFieldAsTargeted(DexField field, DexEncodedMethod context) { markTypeAsLive(field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); markTypeAsLive(field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context)); @@ -2083,7 +2099,7 @@ } if (resolutionTarget.accessFlags.isPrivate() || resolutionTarget.accessFlags.isStatic()) { - brokenSuperInvokes.add(method); + brokenSuperInvokes.add(resolutionTarget.method); } DexProgramClass resolutionTargetClass = getProgramClassOrNull(resolutionTarget.method.holder); if (resolutionTargetClass != null) { @@ -2103,7 +2119,7 @@ return; } if (target.accessFlags.isPrivate()) { - brokenSuperInvokes.add(method); + brokenSuperInvokes.add(resolutionTarget.method); } if (Log.ENABLED) { Log.verbose(getClass(), "Adding super constraint from `%s` to `%s`", from.method, @@ -2474,11 +2490,9 @@ } } markParameterAndReturnTypesAsLive(method); - if (appView.definitionFor(method.method.holder).isProgramClass()) { - processAnnotations(method, method.annotations.annotations); - method.parameterAnnotationsList.forEachAnnotation( - annotation -> processAnnotation(method, annotation)); - } + processAnnotations(method, method.annotations.annotations); + method.parameterAnnotationsList.forEachAnnotation( + annotation -> processAnnotation(method, annotation)); method.registerCodeReferences(new UseRegistry(options.itemFactory, clazz, method)); // Add all dependent members to the workqueue. @@ -2488,6 +2502,12 @@ analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method)); } + private void markReferencedTypesAsLive(DexEncodedMethod method) { + markTypeAsLive( + method.method.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, method)); + markParameterAndReturnTypesAsLive(method); + } + private void markParameterAndReturnTypesAsLive(DexEncodedMethod method) { for (DexType parameterType : method.method.proto.parameters.values) { markTypeAsLive( @@ -2880,6 +2900,31 @@ } } + private class LiveMethodsSet { + + private final Set<DexEncodedMethod> items = Sets.newIdentityHashSet(); + + private final BiConsumer<DexEncodedMethod, KeepReason> register; + + LiveMethodsSet(BiConsumer<DexEncodedMethod, KeepReason> register) { + this.register = register; + } + + boolean add(DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) { + register.accept(method, reason); + transitionUnusedInterfaceToLive(clazz); + return items.add(method); + } + + boolean contains(DexEncodedMethod method) { + return items.contains(method); + } + + Set<DexEncodedMethod> getItems() { + return Collections.unmodifiableSet(items); + } + } + private static class SetWithReason<T> { private final Set<T> items = Sets.newIdentityHashSet();
diff --git a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java index 5dc405e..521b90c 100644 --- a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java +++ b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
@@ -199,6 +199,16 @@ } public KeepReasonWitness reportClassReferencedFrom( + DexProgramClass clazz, DexProgramClass implementer) { + if (keptGraphConsumer != null) { + ClassGraphNode source = getClassGraphNode(implementer.type); + ClassGraphNode target = getClassGraphNode(clazz.type); + return reportEdge(source, target, EdgeKind.ReferencedFrom); + } + return KeepReasonWitness.INSTANCE; + } + + public KeepReasonWitness reportClassReferencedFrom( DexProgramClass clazz, DexEncodedMethod method) { if (keptGraphConsumer != null) { MethodGraphNode source = getMethodGraphNode(method.method);
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java index 5ab6474..cc9fafa 100644 --- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
@@ -32,7 +32,7 @@ } @VisibleForTesting - static ProguardConfigurationSourceStrings createConfigurationForTesting( + public static ProguardConfigurationSourceStrings createConfigurationForTesting( List<String> config) { return new ProguardConfigurationSourceStrings(config); }
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java index 487afa2..9326e31 100644 --- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java +++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -157,6 +157,8 @@ private final HashMultiset<Wrapper<DexField>> fieldBuckets = HashMultiset.create(); private final HashMultiset<Wrapper<DexMethod>> methodBuckets = HashMultiset.create(); + private boolean hasSynchronizedMethods = false; + public Representative(DexProgramClass clazz) { this.clazz = clazz; include(clazz); @@ -168,10 +170,14 @@ Wrapper<DexField> wrapper = fieldEquivalence.wrap(field.field); fieldBuckets.add(wrapper); } + boolean classHasSynchronizedMethods = false; for (DexEncodedMethod method : clazz.methods()) { + assert !hasSynchronizedMethods || !method.accessFlags.isSynchronized(); + classHasSynchronizedMethods |= method.accessFlags.isSynchronized(); Wrapper<DexMethod> wrapper = methodEquivalence.wrap(method.method); methodBuckets.add(wrapper); } + hasSynchronizedMethods |= classHasSynchronizedMethods; } // Returns true if this representative should no longer be used. The current heuristic is to @@ -322,83 +328,42 @@ assert satisfiesMergeCriteria(clazz) == group; assert group != MergeGroup.DONT_MERGE; - String pkg = clazz.type.getPackageDescriptor(); - return mayMergeAcrossPackageBoundaries(clazz) - ? mergeGlobally(clazz, pkg, group) - : mergeInsidePackage(clazz, pkg, group); + return merge( + clazz, + mayMergeAcrossPackageBoundaries(clazz) + ? group.globalKey() + : group.key(clazz.type.getPackageDescriptor())); } - private boolean mergeGlobally(DexProgramClass clazz, String pkg, MergeGroup group) { - Representative globalRepresentative = representatives.get(group.globalKey()); - if (globalRepresentative == null) { - if (isValidRepresentative(clazz)) { - // Make the current class the global representative. - setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz)); - } else { - clearRepresentative(group.globalKey()); - } - - // Do not attempt to merge this class inside its own package, because that could lead to - // an increase in the global representative, which is not desirable. - return false; - } else { - // Check if we can merge the current class into the current global representative. - globalRepresentative.include(clazz); - - if (globalRepresentative.isFull()) { - if (isValidRepresentative(clazz)) { - // Make the current class the global representative instead. - setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz)); - } else { - clearRepresentative(group.globalKey()); - } - - // Do not attempt to merge this class inside its own package, because that could lead to - // an increase in the global representative, which is not desirable. + private boolean merge(DexProgramClass clazz, MergeGroup.Key key) { + Representative representative = representatives.get(key); + if (representative != null) { + if (representative.hasSynchronizedMethods && clazz.hasStaticSynchronizedMethods()) { + // We are not allowed to merge synchronized classes with synchronized methods. return false; - } else { - // Merge this class into the global representative. - moveMembersFromSourceToTarget(clazz, globalRepresentative.clazz); - return true; } - } - } - - private boolean mergeInsidePackage(DexProgramClass clazz, String pkg, MergeGroup group) { - MergeGroup.Key key = group.key(pkg); - Representative packageRepresentative = representatives.get(key); - if (packageRepresentative != null) { + // Check if current candidate is a better choice depending on visibility. For package private + // or protected, the key is parameterized by the package name already, so we just have to + // check accessibility-flags. For global this is no-op. if (isValidRepresentative(clazz) - && clazz.accessFlags.isMoreVisibleThan( - packageRepresentative.clazz.accessFlags, - clazz.type.getPackageName(), - packageRepresentative.clazz.type.getPackageName())) { - // Use `clazz` as a representative for this package instead. + && !representative.clazz.accessFlags.isAtLeastAsVisibleAs(clazz.accessFlags)) { + assert clazz.type.getPackageDescriptor().equals(key.packageOrGlobal); + assert representative.clazz.type.getPackageDescriptor().equals(key.packageOrGlobal); Representative newRepresentative = getOrCreateRepresentative(key, clazz); - newRepresentative.include(packageRepresentative.clazz); - + newRepresentative.include(representative.clazz); if (!newRepresentative.isFull()) { - setRepresentative(group.key(pkg), newRepresentative); - moveMembersFromSourceToTarget(packageRepresentative.clazz, clazz); + setRepresentative(key, newRepresentative); + moveMembersFromSourceToTarget(representative.clazz, clazz); return true; } - - // We are not allowed to merge members into a class that is less visible. - return false; - } - - // Merge current class into the representative of this package if it has room. - packageRepresentative.include(clazz); - - // If there is room, then merge, otherwise fall-through to update the representative of this - // package. - if (!packageRepresentative.isFull()) { - moveMembersFromSourceToTarget(clazz, packageRepresentative.clazz); - return true; + } else { + representative.include(clazz); + if (!representative.isFull()) { + moveMembersFromSourceToTarget(clazz, representative.clazz); + return true; + } } } - - // We were unable to use the current representative for this package (if any). if (isValidRepresentative(clazz)) { setRepresentative(key, getOrCreateRepresentative(key, clazz)); }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java index cd23ff7..cccf0f4 100644 --- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java +++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -108,32 +109,41 @@ } private void pruneUnusedInterfaces(DexProgramClass clazz) { - int numberOfReachableInterfaces = 0; + boolean implementsUnusedInterfaces = false; for (DexType type : clazz.interfaces.values) { // TODO(christofferqa): Extend unused interface removal to library classes. - if (isTypeLive(type)) { - numberOfReachableInterfaces++; + if (!isTypeLive(type)) { + implementsUnusedInterfaces = true; + break; } } - if (numberOfReachableInterfaces == clazz.interfaces.size()) { + if (!implementsUnusedInterfaces) { return; } - if (numberOfReachableInterfaces == 0) { - clazz.interfaces = DexTypeList.empty(); - return; - } - - DexType[] reachableInterfaces = new DexType[numberOfReachableInterfaces]; - int i = 0; + Set<DexType> reachableInterfaces = new LinkedHashSet<>(); for (DexType type : clazz.interfaces.values) { - if (isTypeLive(type)) { - reachableInterfaces[i] = type; - i++; + retainReachableInterfacesFrom(type, reachableInterfaces); + } + if (reachableInterfaces.isEmpty()) { + clazz.interfaces = DexTypeList.empty(); + } else { + clazz.interfaces = new DexTypeList(reachableInterfaces.toArray(DexType.EMPTY_ARRAY)); + } + } + + private void retainReachableInterfacesFrom(DexType type, Set<DexType> reachableInterfaces) { + if (isTypeLive(type)) { + reachableInterfaces.add(type); + } else { + DexProgramClass unusedInterface = appView.definitionForProgramType(type); + assert unusedInterface != null; + assert unusedInterface.isInterface(); + for (DexType interfaceType : unusedInterface.interfaces.values) { + retainReachableInterfacesFrom(interfaceType, reachableInterfaces); } } - clazz.interfaces = new DexTypeList(reachableInterfaces); } private void pruneMembersAndAttributes(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java index b8b8482..4dc15ab 100644 --- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java +++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -138,6 +138,7 @@ RESOLUTION_FOR_METHODS_MAY_CHANGE, SERVICE_LOADER, STATIC_INITIALIZERS, + STATIC_SYNCHRONIZED_METHODS, UNHANDLED_INVOKE_DIRECT, UNHANDLED_INVOKE_SUPER, UNSAFE_INLINING, @@ -198,6 +199,9 @@ case UNSUPPORTED_ATTRIBUTES: message = "since inner-class attributes are not supported"; break; + case STATIC_SYNCHRONIZED_METHODS: + message = "since it has static synchronized methods which can lead to unwanted behavior"; + break; default: assert false; } @@ -806,6 +810,14 @@ assert isStillMergeCandidate(clazz); } + // Check for static synchronized methods on source + if (clazz.hasStaticSynchronizedMethods()) { + if (Log.ENABLED) { + AbortReason.STATIC_SYNCHRONIZED_METHODS.printLogMessageForClass(clazz); + } + return; + } + // Guard against the case where we have two methods that may get the same signature // if we replace types. This is rare, so we approximate and err on the safe side here. if (new CollisionDetector(clazz.type, targetClass.type).mayCollide()) { @@ -1460,7 +1472,9 @@ for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) { synthesizedBridge.updateMethodSignatures(this::fixupMethod); } - return lensBuilder.build(appView, mergedClasses); + GraphLense graphLense = lensBuilder.build(appView, mergedClasses); + new AnnotationFixer(graphLense).run(appView.appInfo().classes()); + return graphLense; } private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) {
diff --git a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java index 7de79dc..6b36180 100644 --- a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java +++ b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
@@ -66,7 +66,7 @@ private List<ProgramResource> readArchive() throws IOException { List<ProgramResource> classResources = null; - try (ZipFile zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8)) { + try (ZipFile zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8)) { final Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java index e612e60..5a50f1b 100644 --- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java +++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -53,7 +53,8 @@ private List<ProgramResource> readArchive() throws IOException { List<ProgramResource> dexResources = new ArrayList<>(); List<ProgramResource> classResources = new ArrayList<>(); - try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) { + try (ZipFile zipFile = + FileUtils.createZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) { final Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); @@ -109,7 +110,8 @@ @Override public void accept(Visitor resourceBrowser) throws ResourceException { - try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) { + try (ZipFile zipFile = + FileUtils.createZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) { final Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java index 416b389..474f5d0 100644 --- a/src/main/java/com/android/tools/r8/utils/ClassMap.java +++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -42,7 +42,7 @@ * We also allow the transition from Supplier of a null value to the actual value null and vice * versa. */ - private final ConcurrentHashMap<DexType, Supplier<T>> classes; + private final Map<DexType, Supplier<T>> classes; /** * Class provider if available. @@ -55,7 +55,7 @@ */ private final AtomicReference<ClassProvider<T>> classProvider = new AtomicReference<>(); - ClassMap(ConcurrentHashMap<DexType, Supplier<T>> classes, ClassProvider<T> classProvider) { + ClassMap(Map<DexType, Supplier<T>> classes, ClassProvider<T> classProvider) { assert classProvider == null || classProvider.getClassKind() == getClassKind(); this.classes = classes == null ? new ConcurrentHashMap<>() : classes; this.classProvider.set(classProvider);
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java index 87b8506..7185445 100644 --- a/src/main/java/com/android/tools/r8/utils/FileUtils.java +++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -9,12 +9,14 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.List; +import java.util.zip.ZipFile; public class FileUtils { @@ -28,6 +30,9 @@ public static final String JAVA_EXTENSION = ".java"; public static final String MODULE_INFO_CLASS = "module-info.class"; + public static final boolean isAndroid = + System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik"); + public static boolean isDexFile(Path path) { String name = path.getFileName().toString().toLowerCase(); return name.endsWith(DEX_EXTENSION); @@ -94,7 +99,7 @@ public static Path validateOutputFile(Path path, Reporter reporter) { if (path != null) { boolean isJarOrZip = isZipFile(path) || isJarFile(path); - if (!isJarOrZip && !(Files.exists(path) && Files.isDirectory(path))) { + if (!isJarOrZip && !(Files.exists(path) && Files.isDirectory(path))) { reporter.error(new StringDiagnostic( "Invalid output: " + path @@ -184,4 +189,21 @@ return path.replace('/', '\\'); } } + + public static ZipFile createZipFile(File file, Charset charset) throws IOException { + if (!isAndroid) { + return new ZipFile(file, charset); + } + // On Android pre-26 we cannot use the constructor ZipFile(file, charset). + // By default Android use UTF_8 as the charset, so we can use the default constructor. + if (Charset.defaultCharset() == StandardCharsets.UTF_8) { + return new ZipFile(file); + } + // If the Android runtime is started with a different default charset, the default constructor + // won't work. It is possible to support this case if we read/write the ZipFile not with it's + // own Input/OutputStream, but an external one which one can define on a different Charset than + // default. We do not support this at the moment since R8 on dex is used only in tests, and + // UTF_8 is the default charset used in tests. + throw new RuntimeException("R8 can run on dex only with UTF_8 as the default charset."); + } }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java index cf30d7a..e5557d9 100644 --- a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java +++ b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
@@ -100,7 +100,7 @@ private ZipFile getOpenZipFile() throws IOException { if (openedZipFile == null) { try { - openedZipFile = new ZipFile(path.toFile(), StandardCharsets.UTF_8); + openedZipFile = FileUtils.createZipFile(path.toFile(), StandardCharsets.UTF_8); } catch (IOException e) { if (!Files.exists(path)) { throw new NoSuchFileException(path.toString());
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 89aeb2d..687e7da 100644 --- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java +++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -198,6 +198,9 @@ public boolean enableNonNullTracking = true; public boolean enableInlining = !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null; + // TODO(b/141451716): Evaluate the effect of allowing inlining in the inlinee. + public boolean applyInliningToInlinee = false; + public int applyInliningToInlineeMaxDepth = 0; public boolean enableInliningOfInvokesWithNullableReceivers = true; public boolean disableInliningOfLibraryMethodOverrides = true; public boolean enableClassInlining = true; @@ -970,6 +973,8 @@ public boolean dontReportFailingCheckDiscarded = false; public boolean deterministicSortingBasedOnDexType = true; public PrintStream whyAreYouNotInliningConsumer = System.out; + public boolean trackDesugaredAPIConversions = + System.getProperty("com.android.tools.r8.trackDesugaredAPIConversions") != null; // Flag to turn on/off JDK11+ nest-access control even when not required (Cf backend) public boolean enableForceNestBasedAccessDesugaringForTest = false;
diff --git a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java index 8e3fe1a..7ffce35 100644 --- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java +++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -5,6 +5,7 @@ package com.android.tools.r8.utils; import com.android.tools.r8.errors.Unimplemented; +import com.android.tools.r8.errors.Unreachable; import com.android.tools.r8.ir.code.Instruction; import com.android.tools.r8.ir.code.InstructionIterator; import com.android.tools.r8.ir.code.InstructionListIterator; @@ -65,6 +66,16 @@ return null; } + public static <T> T previousUntil(ListIterator<T> iterator, Predicate<T> predicate) { + while (iterator.hasPrevious()) { + T previous = iterator.previous(); + if (predicate.test(previous)) { + return previous; + } + } + throw new Unreachable(); + } + public static <T> void removeIf(Iterator<T> iterator, Predicate<T> predicate) { while (iterator.hasNext()) { T item = iterator.next();
diff --git a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java index 223ff37..7de7a1f 100644 --- a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java +++ b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
@@ -12,6 +12,17 @@ B b = new InterfaceImpl(); b.foo(); + + // Ensure that the instantiations are not dead code eliminated. + escape(a); + escape(b); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } public interface A {
diff --git a/src/test/examples/classmerging/MethodCollisionTest.java b/src/test/examples/classmerging/MethodCollisionTest.java index a9010a4..4456498 100644 --- a/src/test/examples/classmerging/MethodCollisionTest.java +++ b/src/test/examples/classmerging/MethodCollisionTest.java
@@ -7,8 +7,22 @@ public class MethodCollisionTest { public static void main(String[] args) { - new B().m(); - new D().m(); + B b = new B(); + b.m(); + + D d = new D(); + d.m(); + + // Ensure that the instantiations are not dead code eliminated. + escape(b); + escape(d); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } public static class A {
diff --git a/src/test/examples/classmerging/NeverInline.java b/src/test/examples/classmerging/NeverInline.java new file mode 100644 index 0000000..9438902 --- /dev/null +++ b/src/test/examples/classmerging/NeverInline.java
@@ -0,0 +1,10 @@ +// Copyright (c) 2019, 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 classmerging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +public @interface NeverInline {}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java index ce7bf14..6fde90e 100644 --- a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java +++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -18,6 +18,17 @@ // package references OtherSimpleInterface. OtherSimpleInterface y = new OtherSimpleInterfaceImpl(); y.bar(); + + // Ensure that the instantiations are not dead code eliminated. + escape(x); + escape(y); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
diff --git a/src/test/examples/classmerging/SuperCallRewritingTest.java b/src/test/examples/classmerging/SuperCallRewritingTest.java index 7a3d45c..6f59419 100644 --- a/src/test/examples/classmerging/SuperCallRewritingTest.java +++ b/src/test/examples/classmerging/SuperCallRewritingTest.java
@@ -7,6 +7,17 @@ public class SuperCallRewritingTest { public static void main(String[] args) { System.out.println("Calling referencedMethod on SubClassThatReferencesSuperMethod"); - System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod()); + SubClassThatReferencesSuperMethod obj = new SubClassThatReferencesSuperMethod(); + System.out.println(obj.referencedMethod()); + + // Ensure that the instantiations are not dead code eliminated. + escape(obj); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } }
diff --git a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java index a17d30a..c263e98 100644 --- a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java +++ b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
@@ -15,6 +15,17 @@ BSub b = new BSub(); a.m(b); b.m(a); + + // Ensure that the instantiations are not dead code eliminated. + escape(a); + escape(b); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } private static class A {
diff --git a/src/test/examples/classmerging/TemplateMethodTest.java b/src/test/examples/classmerging/TemplateMethodTest.java index ac9de15..fe7de76 100644 --- a/src/test/examples/classmerging/TemplateMethodTest.java +++ b/src/test/examples/classmerging/TemplateMethodTest.java
@@ -9,6 +9,16 @@ public static void main(String[] args) { AbstractClass obj = new AbstractClassImpl(); obj.foo(); + + // Ensure that the instantiations are not dead code eliminated. + escape(obj); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } private abstract static class AbstractClass {
diff --git a/src/test/examples/classmerging/Test.java b/src/test/examples/classmerging/Test.java index 6d5c51f..d3b2762 100644 --- a/src/test/examples/classmerging/Test.java +++ b/src/test/examples/classmerging/Test.java
@@ -13,8 +13,17 @@ ConflictingInterfaceImpl impl = new ConflictingInterfaceImpl(); callMethodOnIface(impl); System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod()); - System.out.println(new Outer().getInstance().method()); + Outer outer = new Outer(); + Outer.SubClass inner = outer.getInstance(); + System.out.println(outer.getInstance().method()); System.out.println(new SubClass(42)); + + // Ensure that the instantiations are not dead code eliminated. + escape(clazz); + escape(iface); + escape(impl); + escape(inner); + escape(outer); } private static void callMethodOnIface(GenericInterface iface) { @@ -31,4 +40,11 @@ System.out.println(ClassWithConflictingMethod.conflict(null)); System.out.println(OtherClassWithConflictingMethod.conflict(null)); } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } + } }
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt index c75058d..11a66c6 100644 --- a/src/test/examples/classmerging/keep-rules.txt +++ b/src/test/examples/classmerging/keep-rules.txt
@@ -68,4 +68,8 @@ public static void main(...); } +-neverinline class * { + @classmerging.NeverInline <methods>; +} + -printmapping
diff --git a/src/test/examples/hello/keep-rules.txt b/src/test/examples/hello/keep-rules.txt new file mode 100644 index 0000000..5a49e41 --- /dev/null +++ b/src/test/examples/hello/keep-rules.txt
@@ -0,0 +1 @@ +-keep class hello.Hello { public static void main(...);}
diff --git a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java index 10f995d..5158228 100644 --- a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java +++ b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
@@ -11,6 +11,16 @@ // invoke-interface instruction and not invoke-virtual instruction. A obj = new B(); obj.f(); + + // Ensure that the instantiations are not dead code eliminated. + escape(obj); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java index 7a0e325..f237c2e 100644 --- a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java +++ b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
@@ -7,7 +7,18 @@ public class NestedDefaultInterfaceMethodsTest { public static void main(String[] args) { - new C().m(); + C obj = new C(); + obj.m(); + + // Ensure that the instantiations are not dead code eliminated. + escape(obj); + } + + @NeverInline + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } } public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NeverInline.java b/src/test/examplesAndroidO/classmerging/NeverInline.java new file mode 100644 index 0000000..9438902 --- /dev/null +++ b/src/test/examplesAndroidO/classmerging/NeverInline.java
@@ -0,0 +1,10 @@ +// Copyright (c) 2019, 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 classmerging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +public @interface NeverInline {}
diff --git a/src/test/examplesAndroidO/classmerging/keep-rules.txt b/src/test/examplesAndroidO/classmerging/keep-rules.txt index 4df182e..18a133d 100644 --- a/src/test/examplesAndroidO/classmerging/keep-rules.txt +++ b/src/test/examplesAndroidO/classmerging/keep-rules.txt
@@ -14,5 +14,9 @@ public static void main(...); } +-neverinline class * { + @classmerging.NeverInline <methods>; +} + # TODO(herhut): Consider supporting merging of inner-class attributes. # -keepattributes * \ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/AlwaysInline.java b/src/test/java/com/android/tools/r8/AlwaysInline.java new file mode 100644 index 0000000..97a799a --- /dev/null +++ b/src/test/java/com/android/tools/r8/AlwaysInline.java
@@ -0,0 +1,10 @@ +// Copyright (c) 2019, 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +public @interface AlwaysInline {}
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java index 6695f46..e53fdce 100644 --- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java +++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -17,6 +17,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.Consumer; @@ -193,6 +194,28 @@ } @Override + public ProguardTestBuilder addLibraryClasses(Class<?>... classes) { + addLibraryClasses(Arrays.asList(classes)); + return self(); + } + + @Override + public ProguardTestBuilder addLibraryClasses(Collection<Class<?>> classes) { + List<Path> pathsForClasses = new ArrayList<>(classes.size()); + for (Class<?> clazz : classes) { + pathsForClasses.add(ToolHelper.getClassFileForTestClass(clazz)); + } + try { + Path out = getState().getNewTempFolder().resolve("out.jar"); + TestBase.writeClassFilesToJar(out, pathsForClasses); + libraryjars.add(out); + } catch (IOException e) { + throw new RuntimeException(e); + } + return self(); + } + + @Override public ProguardTestBuilder addClasspathClasses(Collection<Class<?>> classes) { throw new Unimplemented("No support for adding classpath data directly"); }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java index b834aac..8a7d989 100644 --- a/src/test/java/com/android/tools/r8/R8TestBuilder.java +++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -196,6 +196,19 @@ options -> options.testing.allowUnusedProguardConfigurationRules = true); } + public T enableAlwaysInliningAnnotations() { + return enableAlwaysInliningAnnotations(AlwaysInline.class.getPackage().getName()); + } + + public T enableAlwaysInliningAnnotations(String annotationPackageName) { + if (!enableInliningAnnotations) { + enableInliningAnnotations = true; + addInternalKeepRules( + "-alwaysinline class * { @" + annotationPackageName + ".AlwaysInline *; }"); + } + return self(); + } + public T enableInliningAnnotations() { return enableInliningAnnotations(NeverInline.class.getPackage().getName()); }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java index 9b8abe6..797d185 100644 --- a/src/test/java/com/android/tools/r8/TestBase.java +++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -147,6 +147,17 @@ return testForJvm(temp); } + public TestBuilder<?, ?> testForRuntime(TestRuntime runtime, AndroidApiLevel apiLevel) { + if (runtime.isCf()) { + return testForJvm(); + } else { + assert runtime.isDex(); + D8TestBuilder d8TestBuilder = testForD8(); + d8TestBuilder.setMinApi(apiLevel); + return d8TestBuilder; + } + } + public ProguardTestBuilder testForProguard() { return testForProguard(temp); }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java index 0464ccb..84e8cc9 100644 --- a/src/test/java/com/android/tools/r8/ToolHelper.java +++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -137,7 +137,7 @@ private static final String PROGUARD6_0_1 = "third_party/proguard/proguard6.0.1/bin/proguard"; private static final String PROGUARD = PROGUARD5_2_1; public static final String JACOCO_AGENT = "third_party/jacoco/org.jacoco.agent-0.8.2-runtime.jar"; - public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = "third_party/proguardsettings"; + public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = "third_party/proguardsettings/"; private static final String RETRACE6_0_1 = "third_party/proguard/proguard6.0.1/bin/retrace"; private static final String RETRACE = RETRACE6_0_1; @@ -1147,6 +1147,10 @@ return compatSink.build(); } + public static void runL8(L8Command command) throws CompilationFailedException { + runL8(command, options -> {}); + } + public static void runL8(L8Command command, Consumer<InternalOptions> optionsModifier) throws CompilationFailedException { InternalOptions internalOptions = command.getInternalOptions();
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java new file mode 100644 index 0000000..7007821 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -0,0 +1,82 @@ +// Copyright (c) 2019, 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.classmerging; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class HorizontalClassMergerShouldMergeSynchronizedMethodTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public HorizontalClassMergerShouldMergeSynchronizedMethodTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testNoMergingOfClassUsedInMonitor() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addInnerClasses(HorizontalClassMergerShouldMergeSynchronizedMethodTest.class) + .addKeepMainRule(Main.class) + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("1", "2", "3") + .inspect( + inspector -> { + assertFalse( + inspector.clazz(LockOne.class).isPresent() + && inspector.clazz(LockTwo.class).isPresent()); + assertTrue( + inspector.clazz(LockOne.class).isPresent() + || inspector.clazz(LockTwo.class).isPresent()); + }); + } + + // Will be merged with LockTwo. + static class LockOne { + + static synchronized void print1() { + System.out.println("1"); + } + + static synchronized void print2() { + System.out.println("2"); + } + } + + public static class LockTwo { + + static void print3() { + System.out.println("3"); + } + } + + public static class Main { + + public static void main(String[] args) { + LockOne.print1(); + LockOne.print2(); + LockTwo.print3(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java new file mode 100644 index 0000000..a811257 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java
@@ -0,0 +1,150 @@ +// Copyright (c) 2019, 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.classmerging; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.NeverMerge; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.ToolHelper.DexVm.Version; +import java.io.IOException; +import java.lang.Thread.State; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class HorizontalClassMergerSynchronizedMethodTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED. + return getTestParameters() + .withDexRuntimesStartingFromExcluding(Version.V4_0_4) + .withAllApiLevels() + .build(); + } + + public HorizontalClassMergerSynchronizedMethodTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException { + testForRuntime(parameters.getRuntime(), parameters.getApiLevel()) + .addInnerClasses(HorizontalClassMergerSynchronizedMethodTest.class) + .run(parameters.getRuntime(), HorizontalClassMergerSynchronizedMethodTest.Main.class) + .assertSuccessWithOutput("Hello World!"); + } + + @Test + public void testNoMergingOfClassUsedInMonitor() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addInnerClasses(HorizontalClassMergerSynchronizedMethodTest.class) + .addKeepMainRule(Main.class) + .enableMergeAnnotations() + .setMinApi(parameters.getApiLevel()) + .compile() + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutput("Hello World!") + .inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent())); + } + + private interface I { + + void action(); + } + + // Will be merged with LockTwo. + static class LockOne { + + static synchronized void acquire(I c) { + c.action(); + } + } + + public static class LockTwo { + + static synchronized void acquire(I c) { + Main.inTwoCritical = true; + while (!Main.inThreeCritical) {} + c.action(); + } + } + + @NeverMerge + public static class LockThree { + + static synchronized void acquire(I c) { + Main.inThreeCritical = true; + while (!Main.inTwoCritical) {} + c.action(); + } + } + + public static class AcquireOne implements I { + + @Override + public void action() { + LockOne.acquire(() -> System.out.print("Hello ")); + } + } + + public static class AcquireThree implements I { + + @Override + public void action() { + LockThree.acquire(() -> System.out.print("World!")); + } + } + + public static class Main { + + static volatile boolean inTwoCritical = false; + static volatile boolean inThreeCritical = false; + static volatile boolean arnoldWillNotBeBack = false; + + private static volatile Thread t1 = new Thread(Main::lockThreeThenOne); + private static volatile Thread t2 = new Thread(Main::lockTwoThenThree); + private static volatile Thread terminator = new Thread(Main::arnold); + + public static void main(String[] args) { + t1.start(); + t2.start(); + // This thread is started to ensure termination in case we are rewriting incorrectly. + terminator.start(); + + while (!arnoldWillNotBeBack) {} + } + + static void lockThreeThenOne() { + LockThree.acquire(new AcquireOne()); + } + + static void lockTwoThenThree() { + LockTwo.acquire(new AcquireThree()); + } + + static void arnold() { + while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) { + if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) { + System.err.println("DEADLOCKED!"); + System.exit(1); + break; + } + } + arnoldWillNotBeBack = true; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java index cd0f8b6..3fa47cb 100644 --- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java +++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
@@ -7,15 +7,17 @@ import static org.junit.Assert.assertEquals; import com.android.tools.r8.AssumeMayHaveSideEffects; +import com.android.tools.r8.CompilationFailedException; import com.android.tools.r8.NeverInline; import com.android.tools.r8.TestBase; import com.android.tools.r8.TestParameters; import com.android.tools.r8.TestParametersCollection; -import com.android.tools.r8.ToolHelper; import com.android.tools.r8.utils.StringUtils; import com.android.tools.r8.utils.codeinspector.CodeInspector; import com.android.tools.r8.utils.codeinspector.FoundClassSubject; +import java.io.IOException; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,6 +53,9 @@ } } + private final String EXPECTED = + StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()"); + private final TestParameters parameters; public StaticClassMergerTest(TestParameters parameters) { @@ -59,16 +64,19 @@ @Parameters(name = "{0}") public static TestParametersCollection data() { - return getTestParameters().withAllRuntimes().build(); + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + @Test + public void testRuntime() throws IOException, CompilationFailedException, ExecutionException { + testForRuntime(parameters.getRuntime(), parameters.getApiLevel()) + .addInnerClasses(StaticClassMergerTest.class) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutput(EXPECTED); } @Test public void testStaticClassIsRemoved() throws Exception { - String expected = - StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()"); - - testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected); - CodeInspector inspector = testForR8(parameters.getBackend()) .addInnerClasses(StaticClassMergerTest.class) @@ -76,8 +84,9 @@ .noMinification() .enableInliningAnnotations() .enableSideEffectAnnotations() + .setMinApi(parameters.getApiLevel()) .run(parameters.getRuntime(), TestClass.class) - .assertSuccessWithOutput(expected) + .assertSuccessWithOutput(EXPECTED) .inspector(); // Check that one of the two static merge candidates has been removed
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java new file mode 100644 index 0000000..46ccfd2 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java
@@ -0,0 +1,112 @@ +// Copyright (c) 2019, 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.classmerging; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class StaticClassMergerVisibilityTest extends TestBase { + + private final TestParameters parameters; + + public StaticClassMergerVisibilityTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + @Test + public void testStaticClassIsRemoved() throws Exception { + CodeInspector inspector = + testForR8(parameters.getBackend()) + .addInnerClasses(StaticClassMergerVisibilityTest.class) + .addKeepMainRule(Main.class) + .enableInliningAnnotations() + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("A.print()", "B.print()", "C.print()", "D.print()") + .inspector(); + + // The global group will have one representative, which is C. + ClassSubject clazzC = inspector.clazz(C.class); + assertThat(clazzC, isPresent()); + assertEquals(1, clazzC.allMethods().size()); + + // The package group will be merged into one of A, B or C. + assertExactlyOneIsPresent( + inspector.clazz(A.class), inspector.clazz(B.class), inspector.clazz(D.class)); + } + + private void assertExactlyOneIsPresent(ClassSubject... subjects) { + boolean seenPresent = false; + for (ClassSubject subject : subjects) { + if (subject.isPresent()) { + assertFalse(seenPresent); + seenPresent = true; + } + } + assertTrue(seenPresent); + } + + // Will be merged into the package group. + private static class A { + @NeverInline + private static void print() { + System.out.println("A.print()"); + } + } + + // Will be merged into the package group. + static class B { + @NeverInline + static void print() { + System.out.println("B.print()"); + } + } + + // Will be merged into global group + public static class C { + @NeverInline + public static void print() { + System.out.println("C.print()"); + } + } + + // Will be merged into the package group. + protected static class D { + @NeverInline + protected static void print() { + System.out.println("D.print()"); + } + } + + public static class Main { + + public static void main(String[] args) { + A.print(); + B.print(); + C.print(); + D.print(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java new file mode 100644 index 0000000..5f6e255 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -0,0 +1,85 @@ +// Copyright (c) 2019, 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.classmerging; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class VerticalClassMergerShouldMergeSynchronizedMethodTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public VerticalClassMergerShouldMergeSynchronizedMethodTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testNoMergingOfClassUsedInMonitor() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addInnerClasses(VerticalClassMergerShouldMergeSynchronizedMethodTest.class) + .addKeepMainRule(Main.class) + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("1", "2", "3", "4") + .inspect( + inspector -> { + assertThat(inspector.clazz(LockOne.class), not(isPresent())); + assertThat(inspector.clazz(LockTwo.class), isPresent()); + }); + } + + // Will be merged with LockTwo. + static class LockOne { + + synchronized void print1() { + System.out.println("1"); + print2(); + } + + private synchronized void print2() { + System.out.println("2"); + } + } + + public static class LockTwo extends LockOne { + + synchronized void print3() { + System.out.println("3"); + } + + static void print4() { + System.out.println("4"); + } + } + + public static class Main { + + public static void main(String[] args) { + LockTwo lockTwo = new LockTwo(); + lockTwo.print1(); + lockTwo.print3(); + LockTwo.print4(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java new file mode 100644 index 0000000..9b3c384 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java
@@ -0,0 +1,128 @@ +// Copyright (c) 2019, 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.classmerging; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringContains.containsString; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.R8TestRunResult; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.ToolHelper.DexVm.Version; +import java.io.IOException; +import java.lang.Thread.State; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class VerticalClassMergerSynchronizedBlockTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED. + return getTestParameters() + .withDexRuntimesStartingFromExcluding(Version.V4_0_4) + .withAllApiLevels() + .build(); + } + + public VerticalClassMergerSynchronizedBlockTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException { + testForRuntime(parameters.getRuntime(), parameters.getApiLevel()) + .addInnerClasses(VerticalClassMergerSynchronizedBlockTest.class) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutput("Hello World!"); + } + + @Test + public void testNoMergingOfClassUsedInMonitor() + throws IOException, CompilationFailedException, ExecutionException { + // TODO(b/142438687): Fix expectation when fixed. + R8TestRunResult result = + testForR8(parameters.getBackend()) + .addInnerClasses(VerticalClassMergerSynchronizedBlockTest.class) + .addKeepMainRule(Main.class) + .setMinApi(parameters.getApiLevel()) + .compile() + .run(parameters.getRuntime(), Main.class) + .assertFailureWithErrorThatMatches(containsString("DEADLOCKED!")); + if (result.getExitCode() == 0) { + result.inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent())); + } + } + + abstract static class LockOne {} + + static class LockTwo extends LockOne {} + + static class LockThree {} + + public static class Main { + + private static volatile boolean inLockThreeCritical = false; + private static volatile boolean inLockTwoCritical = false; + private static volatile boolean arnoldWillNotBeBack = false; + + private static volatile Thread t1 = new Thread(Main::lockThreeThenOne); + private static volatile Thread t2 = new Thread(Main::lockTwoThenThree); + private static volatile Thread t3 = new Thread(Main::arnold); + + static void synchronizedAccessThroughLocks(String arg) { + System.out.print(arg); + } + + public static void main(String[] args) { + t1.start(); + t2.start(); + // This thread is started to ensure termination in case we are rewriting incorrectly. + t3.start(); + + while (!arnoldWillNotBeBack) {} + } + + static void lockThreeThenOne() { + synchronized (LockThree.class) { + inLockThreeCritical = true; + while (!inLockTwoCritical) {} + synchronized (LockOne.class) { + synchronizedAccessThroughLocks("Hello "); + } + } + } + + static void lockTwoThenThree() { + synchronized (LockTwo.class) { + inLockTwoCritical = true; + while (!inLockThreeCritical) {} + synchronized (LockThree.class) { + synchronizedAccessThroughLocks("World!"); + } + } + } + + static void arnold() { + while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) { + if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) { + System.err.println("DEADLOCKED!"); + System.exit(1); + break; + } + } + arnoldWillNotBeBack = true; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java new file mode 100644 index 0000000..e77ee05 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java
@@ -0,0 +1,150 @@ +// Copyright (c) 2019, 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.classmerging; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.NeverMerge; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.ToolHelper.DexVm.Version; +import java.io.IOException; +import java.lang.Thread.State; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class VerticalClassMergerSynchronizedMethodTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED. + return getTestParameters() + .withDexRuntimesStartingFromExcluding(Version.V4_0_4) + .withAllApiLevels() + .build(); + } + + public VerticalClassMergerSynchronizedMethodTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException { + testForRuntime(parameters.getRuntime(), parameters.getApiLevel()) + .addInnerClasses(VerticalClassMergerSynchronizedMethodTest.class) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutput("Hello World!"); + } + + @Test + public void testNoMergingOfClassUsedInMonitor() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addInnerClasses(VerticalClassMergerSynchronizedMethodTest.class) + .addKeepMainRule(Main.class) + .enableMergeAnnotations() + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutput("Hello World!") + .inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent())); + } + + private interface I { + + void action(); + } + + // Will be merged into LockTwo. + abstract static class LockOne { + + static synchronized void acquire(I c) { + c.action(); + } + } + + @NeverMerge + public static class LockTwo extends LockOne { + + static synchronized void acquire(I c) { + Main.inTwoCritical = true; + while (!Main.inThreeCritical) {} + c.action(); + } + } + + @NeverMerge + public static class LockThree { + + static synchronized void acquire(I c) { + Main.inThreeCritical = true; + while (!Main.inTwoCritical) {} + c.action(); + } + } + + public static class AcquireOne implements I { + + @Override + public void action() { + LockOne.acquire(() -> System.out.print("Hello ")); + } + } + + public static class AcquireThree implements I { + + @Override + public void action() { + LockThree.acquire(() -> System.out.print("World!")); + } + } + + public static class Main { + + static volatile boolean inTwoCritical = false; + static volatile boolean inThreeCritical = false; + static volatile boolean arnoldWillNotBeBack = false; + + private static volatile Thread t1 = new Thread(Main::lockThreeThenOne); + private static volatile Thread t2 = new Thread(Main::lockTwoThenThree); + private static volatile Thread terminator = new Thread(Main::arnold); + + public static void main(String[] args) { + t1.start(); + t2.start(); + // This thread is started to ensure termination in case we are rewriting incorrectly. + terminator.start(); + + while (!arnoldWillNotBeBack) {} + } + + static void lockThreeThenOne() { + LockThree.acquire(new AcquireOne()); + } + + static void lockTwoThenThree() { + LockTwo.acquire(new AcquireThree()); + } + + static void arnold() { + while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) { + if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) { + System.err.println("DEADLOCKED!"); + System.exit(1); + break; + } + } + arnoldWillNotBeBack = true; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java index 6b4f17d..82385a2 100644 --- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java +++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
@@ -10,8 +10,6 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import com.android.tools.r8.CompilationFailedException; import com.android.tools.r8.OutputMode; @@ -78,7 +76,6 @@ .resolve("classmerging").resolve("keep-rules-dontoptimize.txt"); private void configure(InternalOptions options) { - options.enableClassInlining = false; options.enableSideEffectAnalysis = false; options.enableUnusedInterfaceRemoval = false; options.testing.nondeterministicCycleElimination = true; @@ -90,6 +87,7 @@ testForR8(Backend.DEX) .addProgramFiles(EXAMPLE_JAR) .addKeepRuleFiles(proguardConfig) + .enableProguardTestOptions() .noMinification() .addOptionsModification(optionsConsumer) .compile() @@ -110,18 +108,18 @@ runR8(EXAMPLE_KEEP, this::configure); // GenericInterface should be merged into GenericInterfaceImpl. for (String candidate : CAN_BE_MERGED) { - assertFalse(inspector.clazz(candidate).isPresent()); + assertThat(inspector.clazz(candidate), not(isPresent())); } - assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent()); - assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent()); - assertTrue(inspector.clazz("classmerging.SubClass").isPresent()); + assertThat(inspector.clazz("classmerging.GenericInterfaceImpl"), isPresent()); + assertThat(inspector.clazz("classmerging.Outer$SubClass"), isPresent()); + assertThat(inspector.clazz("classmerging.SubClass"), isPresent()); } @Test public void testClassesHaveNotBeenMerged() throws Throwable { runR8(DONT_OPTIMIZE, null); for (String candidate : CAN_BE_MERGED) { - assertTrue(inspector.clazz(candidate).isPresent()); + assertThat(inspector.clazz(candidate), isPresent()); } } @@ -304,8 +302,8 @@ @Test public void testConflictWasDetected() throws Throwable { runR8(EXAMPLE_KEEP, this::configure); - assertTrue(inspector.clazz("classmerging.ConflictingInterface").isPresent()); - assertTrue(inspector.clazz("classmerging.ConflictingInterfaceImpl").isPresent()); + assertThat(inspector.clazz("classmerging.ConflictingInterface"), isPresent()); + assertThat(inspector.clazz("classmerging.ConflictingInterfaceImpl"), isPresent()); } @Test
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java b/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java new file mode 100644 index 0000000..998682c --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java
@@ -0,0 +1,101 @@ +// Copyright (c) 2019, 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.corelib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.ByteDataView; +import com.android.tools.r8.DexIndexedConsumer; +import com.android.tools.r8.DiagnosticsHandler; +import com.android.tools.r8.L8Command; +import com.android.tools.r8.OutputMode; +import com.android.tools.r8.StringResource; +import com.android.tools.r8.ToolHelper; +import com.android.tools.r8.utils.AndroidApiLevel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.zip.ZipFile; +import org.junit.Test; + +public class EmptyDesugaredLibrary extends CoreLibDesugarTestBase { + + private L8Command.Builder prepareL8Builder(AndroidApiLevel minApiLevel) { + return L8Command.builder() + .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P)) + .addProgramFiles(ToolHelper.getDesugarJDKLibs()) + .addDesugaredLibraryConfiguration( + StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING)) + .setMinApiLevel(minApiLevel.getLevel()); + } + + static class CountingProgramConsumer extends DexIndexedConsumer.ForwardingConsumer { + + public int count = 0; + + public CountingProgramConsumer() { + super(null); + } + + @Override + public void accept( + int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) { + count++; + } + } + + @Test + public void testEmptyDesugaredLibrary() throws Exception { + for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) { + if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) { + // No need to test all API levels. + continue; + } + CountingProgramConsumer programConsumer = new CountingProgramConsumer(); + ToolHelper.runL8(prepareL8Builder(apiLevel).setProgramConsumer(programConsumer).build()); + assertEquals( + apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1, programConsumer.count); + } + } + + @Test + public void testEmptyDesugaredLibraryDexZip() throws Exception { + for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) { + if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) { + // No need to test all API levels. + continue; + } + Path desugaredLibraryZip = temp.newFolder().toPath().resolve("desugar_jdk_libs_dex.zip"); + ToolHelper.runL8( + prepareL8Builder(apiLevel).setOutput(desugaredLibraryZip, OutputMode.DexIndexed).build()); + assertTrue(Files.exists(desugaredLibraryZip)); + assertEquals( + apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1, + new ZipFile(desugaredLibraryZip.toFile(), StandardCharsets.UTF_8).size()); + } + } + + @Test + public void testEmptyDesugaredLibraryDexDirectory() throws Exception { + for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) { + if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) { + // No need to test all API levels. + continue; + } + Path desugaredLibraryDirectory = temp.newFolder().toPath(); + ToolHelper.runL8( + prepareL8Builder(apiLevel) + .setOutput(desugaredLibraryDirectory, OutputMode.DexIndexed) + .build()); + assertEquals( + apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1, + Files.walk(desugaredLibraryDirectory) + .filter(path -> path.toString().endsWith(".dex")) + .count()); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java new file mode 100644 index 0000000..044f075 --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java
@@ -0,0 +1,135 @@ +// Copyright (c) 2019, 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.corelib; + +import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION; +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assume.assumeTrue; + +import com.android.tools.r8.D8; +import com.android.tools.r8.D8TestBuilder; +import com.android.tools.r8.D8TestCompileResult; +import com.android.tools.r8.R8; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.TestRuntime.CfVm; +import com.android.tools.r8.ToolHelper; +import com.android.tools.r8.ToolHelper.DexVm.Version; +import com.android.tools.r8.ToolHelper.ProcessResult; +import com.android.tools.r8.utils.AndroidApiLevel; +import com.android.tools.r8.utils.StringUtils; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class HelloWorldCompiledOnArtTest extends CoreLibDesugarTestBase { + + // TODO(b/142621961): Create an abstraction to easily run tests on External DexR8. + // Manage pathMock in the abstraction. + private static Path pathMock; + + @BeforeClass + public static void compilePathBackport() throws Exception { + assumeTrue("JDK8 is not checked-in on Windows", !ToolHelper.isWindows()); + pathMock = getStaticTemp().newFolder("PathMock").toPath(); + ProcessResult processResult = + ToolHelper.runJavac( + CfVm.JDK8, + Collections.emptyList(), + pathMock, + getAllFilesWithSuffixInDirectory(Paths.get("src/test/r8OnArtBackport"), "java")); + assertEquals(0, processResult.exitCode); + } + + public static Path[] getPathBackport() throws Exception { + return getAllFilesWithSuffixInDirectory(pathMock, "class"); + } + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters() + .withDexRuntimesStartingFromIncluding(Version.V7_0_0) + .withApiLevelsStartingAtIncluding(AndroidApiLevel.L) + .build(); + } + + public HelloWorldCompiledOnArtTest(TestParameters parameters) { + this.parameters = parameters; + } + + private static String commandLinePathFor(String string) { + return Paths.get(string).toAbsolutePath().toString(); + } + + private static final String HELLO_KEEP = + commandLinePathFor("src/test/examples/hello/keep-rules.txt"); + private static final String HELLO_PATH = + commandLinePathFor(ToolHelper.EXAMPLES_BUILD_DIR + "hello" + JAR_EXTENSION); + + @Test + public void testHelloCompiledWithR8Dex() throws Exception { + Path helloOutput = temp.newFolder("helloOutput").toPath().resolve("out.zip").toAbsolutePath(); + compileR8ToDexWithD8() + .run( + parameters.getRuntime(), + R8.class, + "--release", + "--output", + helloOutput.toString(), + "--lib", + commandLinePathFor(ToolHelper.JAVA_8_RUNTIME), + "--pg-conf", + HELLO_KEEP, + HELLO_PATH) + .assertSuccess(); + verifyResult(helloOutput); + } + + @Test + public void testHelloCompiledWithD8Dex() throws Exception { + Path helloOutput = temp.newFolder("helloOutput").toPath().resolve("out.zip").toAbsolutePath(); + compileR8ToDexWithD8() + .run( + parameters.getRuntime(), + D8.class, + "--release", + "--output", + helloOutput.toString(), + "--lib", + commandLinePathFor(ToolHelper.JAVA_8_RUNTIME), + HELLO_PATH) + .assertSuccess(); + verifyResult(helloOutput); + } + + private void verifyResult(Path helloOutput) throws IOException { + ProcessResult processResult = ToolHelper.runArtRaw(helloOutput.toString(), "hello.Hello"); + assertEquals(StringUtils.lines("Hello, world"), processResult.stdout); + } + + private D8TestCompileResult compileR8ToDexWithD8() throws Exception { + D8TestBuilder d8TestBuilder = + testForD8().addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR); + if (parameters.getApiLevel().getLevel() < AndroidApiLevel.O.getLevel()) { + d8TestBuilder.addProgramFiles(getPathBackport()); + } + return d8TestBuilder + .setMinApi(parameters.getApiLevel()) + .enableCoreLibraryDesugaring(parameters.getApiLevel()) + .compile() + .addDesugaredCoreLibraryRunClassPath(this::buildDesugaredLibrary, parameters.getApiLevel()) + .withArt6Plus64BitsLib() + .withArtFrameworks(); + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java new file mode 100644 index 0000000..178af61 --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java
@@ -0,0 +1,120 @@ +// Copyright (c) 2019, 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.corelib; + +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.util.LinkedHashSet; +import java.util.Spliterator; +import java.util.Spliterators; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class LinkedHashSetTest extends CoreLibDesugarTestBase { + + private final TestParameters parameters; + + @Parameterized.Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimes().withAllApiLevels().build(); + } + + public LinkedHashSetTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testLinkedHashSetOverrides() throws Exception { + String stdOut = + testForD8() + .addInnerClasses(LinkedHashSetTest.class) + .setMinApi(parameters.getApiLevel()) + .enableCoreLibraryDesugaring(parameters.getApiLevel()) + .compile() + .addDesugaredCoreLibraryRunClassPath( + this::buildDesugaredLibrary, parameters.getApiLevel()) + .run(parameters.getRuntime(), Executor.class) + .assertSuccess() + .getStdOut(); + assertLines2By2Correct(stdOut); + } + + static class Executor { + @SuppressWarnings("RedundantOperationOnEmptyContainer") + public static void main(String[] args) { + Spliterator<String> spliterator; + + // Spliterator of Set is only distinct. + // Spliterator of LinkedHashSet is distinct and ordered. + // Spliterator of CustomLinkedHashSetOverride is distinct, ordered and immutable. + // If an incorrect method is found, characteristics are incorrect. + + spliterator = new LinkedHashSet<String>().spliterator(); + System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + System.out.println("false"); + + spliterator = new CustomLinkedHashSetOverride<String>().spliterator(); + System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + System.out.println("true"); + + spliterator = new CustomLinkedHashSetNoOverride<String>().spliterator(); + System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + System.out.println("false"); + + spliterator = new CustomLinkedHashSetOverride<String>().superSpliterator(); + System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + System.out.println("false"); + + spliterator = new SubclassOverride<String>().superSpliterator(); + System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED)); + System.out.println("true"); + System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + System.out.println("true"); + } + } + + static class CustomLinkedHashSetOverride<E> extends LinkedHashSet<E> { + + @Override + public Spliterator<E> spliterator() { + return Spliterators.spliterator( + this, Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE); + } + + public Spliterator<E> superSpliterator() { + return super.spliterator(); + } + } + + static class SubclassOverride<E> extends CustomLinkedHashSetOverride<E> { + @Override + public Spliterator<E> superSpliterator() { + return super.spliterator(); + } + } + + @SuppressWarnings("WeakerAccess") + static class CustomLinkedHashSetNoOverride<E> extends LinkedHashSet<E> {} +}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java new file mode 100644 index 0000000..df4e4d8 --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java
@@ -0,0 +1,156 @@ +// Copyright (c) 2019, 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.corelib; + +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class RetargetOverrideTest extends CoreLibDesugarTestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimes().withAllApiLevels().build(); + } + + public RetargetOverrideTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testRetargetOverrideD8() throws Exception { + String stdout = + testForD8() + .addInnerClasses(RetargetOverrideTest.class) + .enableCoreLibraryDesugaring(parameters.getApiLevel()) + .setMinApi(parameters.getApiLevel()) + .compile() + .addDesugaredCoreLibraryRunClassPath( + this::buildDesugaredLibrary, parameters.getApiLevel()) + .run(parameters.getRuntime(), Executor.class) + .assertSuccess() + .getStdOut(); + assertLines2By2Correct(stdout); + } + + @Test + public void testRetargetOverrideR8() throws Exception { + String stdout = + testForR8(Backend.DEX) + .addKeepMainRule(Executor.class) + .addInnerClasses(RetargetOverrideTest.class) + .enableCoreLibraryDesugaring(parameters.getApiLevel()) + .setMinApi(parameters.getApiLevel()) + .compile() + .addDesugaredCoreLibraryRunClassPath( + this::buildDesugaredLibrary, parameters.getApiLevel()) + .run(parameters.getRuntime(), Executor.class) + .assertSuccess() + .getStdOut(); + assertLines2By2Correct(stdout); + } + + static class Executor { + + public static void main(String[] args) { + java.sql.Date date = new java.sql.Date(123456789); + System.out.println(date.toInstant()); + System.out.println("1970-01-02T10:17:36.789Z"); + + GregorianCalendar gregCal = new GregorianCalendar(1990, 2, 22); + System.out.println(gregCal.toInstant()); + System.out.println("1990-03-22T00:00:00Z"); + + // TODO(b/142846107): Enable overrides of retarget core members. + // MyCalendarOverride myCal = new MyCalendarOverride(1990, 2, 22); + // System.out.println(myCal.toZonedDateTime()); + // System.out.println("1990-11-22T00:00Z[GMT]"); + // System.out.println(myCal.toInstant()); + // System.out.println("1990-03-22T00:00:00Z"); + + MyCalendarNoOverride myCalN = new MyCalendarNoOverride(1990, 2, 22); + System.out.println(myCalN.toZonedDateTime()); + System.out.println("1990-03-22T00:00Z[GMT]"); + System.out.println(myCalN.toInstant()); + System.out.println("1990-03-22T00:00:00Z"); + + // TODO(b/142846107): Enable overrides of retarget core members. + // MyDateOverride myDate = new MyDateOverride(123456789); + // System.out.println(myDate.toInstant()); + // System.out.println("1970-01-02T10:17:45.789Z"); + + MyDateNoOverride myDateN = new MyDateNoOverride(123456789); + System.out.println(myDateN.toInstant()); + System.out.println("1970-01-02T10:17:36.789Z"); + + MyAtomicInteger myAtomicInteger = new MyAtomicInteger(42); + System.out.println(myAtomicInteger.getAndUpdate(x -> x + 1)); + System.out.println("42"); + System.out.println(myAtomicInteger.getAndUpdate(x -> x + 5)); + System.out.println("43"); + System.out.println(myAtomicInteger.updateAndGet(x -> x + 100)); + System.out.println("148"); + System.out.println(myAtomicInteger.updateAndGet(x -> x + 500)); + System.out.println("648"); + } + } + + // static class MyCalendarOverride extends GregorianCalendar { + // + // public MyCalendarOverride(int year, int month, int dayOfMonth) { + // super(year, month, dayOfMonth); + // } + // + // // Cannot override toInstant (final). + // + // @Override + // public ZonedDateTime toZonedDateTime() { + // return super.toZonedDateTime().withMonth(11); + // } + // } + + static class MyCalendarNoOverride extends GregorianCalendar { + public MyCalendarNoOverride(int year, int month, int dayOfMonth) { + super(year, month, dayOfMonth); + } + } + + // static class MyDateOverride extends Date { + // + // public MyDateOverride(long date) { + // super(date); + // } + // + // @Override + // public Instant toInstant() { + // return super.toInstant().plusSeconds(9); + // } + // } + + static class MyDateNoOverride extends Date { + + public MyDateNoOverride(long date) { + super(date); + } + } + + static class MyAtomicInteger extends AtomicInteger { + // No overrides, all final methods. + public MyAtomicInteger(int initialValue) { + super(initialValue); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java index ed5ca26..3775eba 100644 --- a/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java +++ b/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java
@@ -4,6 +4,11 @@ package com.android.tools.r8.desugar.corelib.conversionTests; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +import com.android.tools.r8.D8TestCompileResult; +import com.android.tools.r8.TestDiagnosticMessages; import com.android.tools.r8.TestRuntime.DexRuntime; import com.android.tools.r8.ToolHelper.DexVm; import com.android.tools.r8.utils.AndroidApiLevel; @@ -22,12 +27,15 @@ @Test public void testRewrittenAPICalls() throws Exception { Path customLib = testForD8().addProgramClasses(CustomLibClass.class).compile().writeToZip(); - testForD8() - .setMinApi(AndroidApiLevel.B) - .addProgramClasses(Executor.class) - .addLibraryClasses(CustomLibClass.class) - .enableCoreLibraryDesugaring(AndroidApiLevel.B) - .compile() + D8TestCompileResult compileResult = + testForD8() + .setMinApi(AndroidApiLevel.B) + .addProgramClasses(Executor.class) + .addLibraryClasses(CustomLibClass.class) + .addOptionsModification(options -> options.testing.trackDesugaredAPIConversions = true) + .enableCoreLibraryDesugaring(AndroidApiLevel.B) + .compile(); + compileResult .addDesugaredCoreLibraryRunClassPath( this::buildDesugaredLibraryWithConversionExtension, AndroidApiLevel.B) .addRunClasspathFiles(customLib) @@ -41,8 +49,29 @@ "-1000000000-01-01T00:00:00.999999999Z", "GMT", "GMT")); + assertTrackedAPIS(compileResult.getDiagnosticMessages()); } + private void assertTrackedAPIS(TestDiagnosticMessages diagnosticMessages) { + assertTrue( + diagnosticMessages + .getWarnings() + .get(0) + .getDiagnosticMessage() + .startsWith("Tracked desugared API conversions:")); + assertEquals( + 9, diagnosticMessages.getWarnings().get(0).getDiagnosticMessage().split("\n").length); + assertTrue( + diagnosticMessages + .getWarnings() + .get(1) + .getDiagnosticMessage() + .startsWith("Tracked callback desugared API conversions:")); + assertEquals( + 1, diagnosticMessages.getWarnings().get(1).getDiagnosticMessage().split("\n").length); + } + + static class Executor { private static final String ZONE_ID = "GMT";
diff --git a/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java new file mode 100644 index 0000000..82e7e93 --- /dev/null +++ b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
@@ -0,0 +1,49 @@ +// Copyright (c) 2019, 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.internal; + +import static com.android.tools.r8.ToolHelper.isLocalDevelopment; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import com.android.tools.r8.R8TestCompileResult; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.nio.file.Paths; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class Gmail17060416TreeShakeJarVerificationTest extends GmailCompilationBase { + private static final int MAX_SIZE = 20000000; + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimes().build(); + } + + public Gmail17060416TreeShakeJarVerificationTest(TestParameters parameters) { + super(170604, 16); + this.parameters = parameters; + } + + @Test + public void buildAndTreeShakeFromDeployJar() throws Exception { + assumeTrue(isLocalDevelopment()); + + R8TestCompileResult compileResult = + testForR8(parameters.getBackend()) + .addKeepRuleFiles(Paths.get(base).resolve(BASE_PG_CONF)) + .allowUnusedProguardConfigurationRules() + .compile(); + + int appSize = applicationSize(compileResult.app); + assertTrue("Expected max size of " + MAX_SIZE+ ", got " + appSize, appSize < MAX_SIZE); + } + +}
diff --git a/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java new file mode 100644 index 0000000..0f6a563 --- /dev/null +++ b/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java
@@ -0,0 +1,51 @@ +// Copyright (c) 2019, 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.internal; + +import static com.android.tools.r8.ToolHelper.isLocalDevelopment; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import com.android.tools.r8.R8TestCompileResult; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.ToolHelper; +import java.nio.file.Paths; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class Gmail18082615TreeShakeJarVerificationTest extends GmailCompilationBase { + private static final int MAX_SIZE = 20000000; + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimes().build(); + } + + public Gmail18082615TreeShakeJarVerificationTest(TestParameters parameters) { + super(180826, 15); + this.parameters = parameters; + } + + @Test + public void buildAndTreeShakeFromDeployJar() throws Exception { + assumeTrue(isLocalDevelopment()); + + R8TestCompileResult compileResult = + testForR8(parameters.getBackend()) + .addKeepRuleFiles( + Paths.get(base).resolve(BASE_PG_CONF), + Paths.get(ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS, PG_CONF)) + .allowUnusedProguardConfigurationRules() + .compile(); + + int appSize = applicationSize(compileResult.app); + assertTrue("Expected max size of " + MAX_SIZE+ ", got " + appSize, appSize < MAX_SIZE); + } +}
diff --git a/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java b/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java new file mode 100644 index 0000000..7332dd8 --- /dev/null +++ b/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java
@@ -0,0 +1,19 @@ +// Copyright (c) 2019, 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.internal; + +abstract class GmailCompilationBase extends CompilationTestBase { + static final String APK = "Gmail_release_unsigned.apk"; + static final String DEPLOY_JAR = "Gmail_release_unstripped_deploy.jar"; + static final String PG_JAR = "Gmail_release_unstripped_proguard.jar"; + static final String PG_MAP = "Gmail_release_unstripped_proguard.map"; + static final String BASE_PG_CONF = "Gmail_release_unstripped_proguard.config"; + static final String PG_CONF = "Gmail_proguard.config"; + + final String base; + + GmailCompilationBase(int majorVersion, int minorVersion) { + this.base = "third_party/gmail/gmail_android_" + majorVersion + "." + minorVersion + "/"; + } +}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java index 0168988..e8417f1 100644 --- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java +++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
@@ -22,7 +22,7 @@ public void buildAndTreeShakeFromDeployJar() throws Exception { List<String> additionalProguardConfiguration = ImmutableList.of( - ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "/GmsCore_proguard.config"); + ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "GmsCore_proguard.config"); AndroidApp app1 = buildAndTreeShakeFromDeployJar( CompilationMode.RELEASE,
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java index 4ffbe2b..92b7c68 100644 --- a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java +++ b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
@@ -28,13 +28,13 @@ this.base = "third_party/youtube/youtube.android_" + majorVersion + "." + minorVersion + "/"; } - public List<Path> getKeepRuleFiles() { + protected List<Path> getKeepRuleFiles() { return ImmutableList.of( Paths.get(base).resolve(PG_CONF), Paths.get(ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS).resolve(PG_CONF)); } - public List<Path> getProgramFiles() throws IOException { + protected List<Path> getProgramFiles() throws IOException { List<Path> result = new ArrayList<>(); for (Path keepRuleFile : getKeepRuleFiles()) { for (String line : FileUtils.readAllLines(keepRuleFile)) { @@ -47,12 +47,12 @@ return result; } - public Path getReleaseApk() { + Path getReleaseApk() { return Paths.get(base).resolve("YouTubeRelease.apk"); } - public Path getReleaseProguardMap() { - return Paths.get(base).resolve("YouTubeRelease_proguard.map"); + Path getReleaseProguardMap() { + return Paths.get(base).resolve(PG_MAP); } void runR8AndCheckVerification(CompilationMode mode, String input) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java new file mode 100644 index 0000000..7813a2d --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java
@@ -0,0 +1,86 @@ +// Copyright (c) 2019, 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.analysis.sideeffect; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class SingletonClassInitializerPatternCanBePostponedTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public SingletonClassInitializerPatternCanBePostponedTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(SingletonClassInitializerPatternCanBePostponedTest.class) + .addKeepMainRule(TestClass.class) + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("Hello world!"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + // A.inlineable() should be inlined because we should be able to determine that A.<clinit>() can + // safely be postponed. + assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent())); + } + + static class TestClass { + + public static void main(String[] args) { + A.inlineable(); + System.out.println(A.getInstance().getMessage()); + } + } + + static class A { + + static A INSTANCE = new A(" world!"); + + final String message; + + A(String message) { + this.message = message; + } + + static void inlineable() { + System.out.print("Hello"); + } + + static A getInstance() { + return INSTANCE; + } + + String getMessage() { + return message; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java new file mode 100644 index 0000000..d29753b --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
@@ -0,0 +1,92 @@ +// Copyright (c) 2019, 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.analysis.sideeffect; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class SingletonClassInitializerPatternCannotBePostponedTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public SingletonClassInitializerPatternCannotBePostponedTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(SingletonClassInitializerPatternCannotBePostponedTest.class) + .addKeepMainRule(TestClass.class) + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("Hello world!"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + // A.inlineable() cannot be inlined because it should trigger the class initialization of A, + // which should trigger the class initialization of B, which will print "Hello". + assertThat(classSubject.uniqueMethodWithName("inlineable"), isPresent()); + } + + static class TestClass { + + public static void main(String[] args) { + A.inlineable(); + System.out.println(A.getInstance().getMessage()); + } + } + + static class A { + + static B INSTANCE = new B("world!"); + + static void inlineable() { + System.out.print(" "); + } + + static B getInstance() { + return INSTANCE; + } + } + + static class B { + + static { + System.out.print("Hello"); + } + + final String message; + + B(String message) { + this.message = message; + } + + String getMessage() { + return message; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java new file mode 100644 index 0000000..b567014 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
@@ -0,0 +1,31 @@ +// Copyright (c) 2019, 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.conversion; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexMethod; +import com.android.tools.r8.graph.ParameterAnnotationsList; +import com.android.tools.r8.ir.conversion.CallGraph.Node; + +class CallGraphTestBase extends TestBase { + private DexItemFactory dexItemFactory = new DexItemFactory(); + + Node createNode(String methodName) { + DexMethod signature = + dexItemFactory.createMethod( + dexItemFactory.objectType, + dexItemFactory.createProto(dexItemFactory.voidType), + methodName); + return new Node( + new DexEncodedMethod(signature, null, null, ParameterAnnotationsList.empty(), null)); + } + + Node createForceInlinedNode(String methodName) { + Node node = createNode(methodName); + node.method.getMutableOptimizationInfo().markForceInline(); + return node; + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java similarity index 87% rename from src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java rename to src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java index f767bf2..f38629f 100644 --- a/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java +++ b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
@@ -2,7 +2,7 @@ // 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.callgraph; +package com.android.tools.r8.ir.conversion; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -11,14 +11,9 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.android.tools.r8.TestBase; import com.android.tools.r8.errors.CompilationError; -import com.android.tools.r8.graph.DexEncodedMethod; -import com.android.tools.r8.graph.DexItemFactory; -import com.android.tools.r8.graph.DexMethod; -import com.android.tools.r8.graph.ParameterAnnotationsList; import com.android.tools.r8.ir.conversion.CallGraph.Node; -import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator; +import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator; import com.android.tools.r8.utils.InternalOptions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -29,7 +24,7 @@ import java.util.function.BooleanSupplier; import org.junit.Test; -public class CycleEliminationTest extends TestBase { +public class CycleEliminationTest extends CallGraphTestBase { private static class Configuration { @@ -44,8 +39,6 @@ } } - private DexItemFactory dexItemFactory = new DexItemFactory(); - @Test public void testSimpleCycle() { Node method = createNode("n1"); @@ -197,20 +190,4 @@ } } } - - private Node createNode(String methodName) { - DexMethod signature = - dexItemFactory.createMethod( - dexItemFactory.objectType, - dexItemFactory.createProto(dexItemFactory.voidType), - methodName); - return new Node( - new DexEncodedMethod(signature, null, null, ParameterAnnotationsList.empty(), null)); - } - - private Node createForceInlinedNode(String methodName) { - Node node = createNode(methodName); - node.method.getMutableOptimizationInfo().markForceInline(); - return node; - } }
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java new file mode 100644 index 0000000..2cde812 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
@@ -0,0 +1,223 @@ +// Copyright (c) 2019, 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.conversion; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.ir.conversion.CallGraph.Node; +import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator; +import com.android.tools.r8.utils.InternalOptions; +import com.google.common.collect.Sets; +import java.util.Set; +import java.util.TreeSet; +import org.junit.Test; + +public class NodeExtractionTest extends CallGraphTestBase { + + private InternalOptions options = new InternalOptions(); + + // Note that building a test graph is intentionally repeated to avoid race conditions and/or + // non-deterministic test results due to cycle elimination. + + @Test + public void testExtractLeaves_withoutCycle() { + Node n1, n2, n3, n4, n5, n6; + Set<Node> nodes; + + n1 = createNode("n1"); + n2 = createNode("n2"); + n3 = createNode("n3"); + n4 = createNode("n4"); + n5 = createNode("n5"); + n6 = createNode("n6"); + + n2.addCallerConcurrently(n1); + n3.addCallerConcurrently(n2); + n4.addCallerConcurrently(n2); + n4.addCallerConcurrently(n5); + n6.addCallerConcurrently(n5); + + nodes = new TreeSet<>(); + nodes.add(n1); + nodes.add(n2); + nodes.add(n3); + nodes.add(n4); + nodes.add(n5); + nodes.add(n6); + + Set<Node> wave = Sets.newIdentityHashSet(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(3, wave.size()); + assertThat(wave, hasItem(n3)); + assertThat(wave, hasItem(n4)); + assertThat(wave, hasItem(n6)); + wave.clear(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n2)); + assertThat(wave, hasItem(n5)); + wave.clear(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(1, wave.size()); + assertThat(wave, hasItem(n1)); + assertTrue(nodes.isEmpty()); + } + + @Test + public void testExtractLeaves_withCycle() { + Node n1, n2, n3, n4, n5, n6; + Set<Node> nodes; + + n1 = createNode("n1"); + n2 = createNode("n2"); + n3 = createNode("n3"); + n4 = createNode("n4"); + n5 = createNode("n5"); + n6 = createNode("n6"); + + n2.addCallerConcurrently(n1); + n3.addCallerConcurrently(n2); + n4.addCallerConcurrently(n2); + n4.addCallerConcurrently(n5); + n6.addCallerConcurrently(n5); + + nodes = new TreeSet<>(); + nodes.add(n1); + nodes.add(n2); + nodes.add(n3); + nodes.add(n4); + nodes.add(n5); + nodes.add(n6); + + n1.addCallerConcurrently(n3); + n3.method.getMutableOptimizationInfo().markForceInline(); + CycleEliminator cycleEliminator = new CycleEliminator(nodes, options); + assertEquals(1, cycleEliminator.breakCycles().numberOfRemovedEdges()); + + Set<Node> wave = Sets.newIdentityHashSet(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(3, wave.size()); + assertThat(wave, hasItem(n3)); + assertThat(wave, hasItem(n4)); + assertThat(wave, hasItem(n6)); + wave.clear(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n2)); + assertThat(wave, hasItem(n5)); + wave.clear(); + + MethodProcessor.extractLeaves(nodes, wave::add); + assertEquals(1, wave.size()); + assertThat(wave, hasItem(n1)); + assertTrue(nodes.isEmpty()); + } + + @Test + public void testExtractRoots_withoutCycle() { + Node n1, n2, n3, n4, n5, n6; + Set<Node> nodes; + + n1 = createNode("n1"); + n2 = createNode("n2"); + n3 = createNode("n3"); + n4 = createNode("n4"); + n5 = createNode("n5"); + n6 = createNode("n6"); + + n2.addCallerConcurrently(n1); + n3.addCallerConcurrently(n2); + n4.addCallerConcurrently(n2); + n4.addCallerConcurrently(n5); + n6.addCallerConcurrently(n5); + + nodes = new TreeSet<>(); + nodes.add(n1); + nodes.add(n2); + nodes.add(n3); + nodes.add(n4); + nodes.add(n5); + nodes.add(n6); + + Set<Node> wave = Sets.newIdentityHashSet(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n1)); + assertThat(wave, hasItem(n5)); + wave.clear(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n2)); + assertThat(wave, hasItem(n6)); + wave.clear(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n3)); + assertThat(wave, hasItem(n4)); + assertTrue(nodes.isEmpty()); + } + + @Test + public void testExtractRoots_withCycle() { + Node n1, n2, n3, n4, n5, n6; + Set<Node> nodes; + + n1 = createNode("n1"); + n2 = createNode("n2"); + n3 = createNode("n3"); + n4 = createNode("n4"); + n5 = createNode("n5"); + n6 = createNode("n6"); + + n2.addCallerConcurrently(n1); + n3.addCallerConcurrently(n2); + n4.addCallerConcurrently(n2); + n4.addCallerConcurrently(n5); + n6.addCallerConcurrently(n5); + + nodes = new TreeSet<>(); + nodes.add(n1); + nodes.add(n2); + nodes.add(n3); + nodes.add(n4); + nodes.add(n5); + nodes.add(n6); + + n1.addCallerConcurrently(n3); + n3.method.getMutableOptimizationInfo().markForceInline(); + CycleEliminator cycleEliminator = new CycleEliminator(nodes, options); + assertEquals(1, cycleEliminator.breakCycles().numberOfRemovedEdges()); + + Set<Node> wave = Sets.newIdentityHashSet(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n1)); + assertThat(wave, hasItem(n5)); + wave.clear(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n2)); + assertThat(wave, hasItem(n6)); + wave.clear(); + + PostMethodProcessor.extractRoots(nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(n3)); + assertThat(wave, hasItem(n4)); + assertTrue(nodes.isEmpty()); + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java new file mode 100644 index 0000000..769515c --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
@@ -0,0 +1,198 @@ +// Copyright (c) 2019, 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.conversion; + +import static com.android.tools.r8.shaking.ProguardConfigurationSourceStrings.createConfigurationForTesting; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.dex.ApplicationReader; +import com.android.tools.r8.graph.AppInfoWithSubtyping; +import com.android.tools.r8.graph.AppServices; +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexApplication; +import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.ir.conversion.CallGraph.Node; +import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.shaking.Enqueuer; +import com.android.tools.r8.shaking.EnqueuerFactory; +import com.android.tools.r8.shaking.ProguardConfigurationParser; +import com.android.tools.r8.shaking.RootSetBuilder; +import com.android.tools.r8.utils.AndroidApp; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.ThreadUtils; +import com.android.tools.r8.utils.Timing; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import org.junit.Test; + +public class PartialCallGraphTest extends CallGraphTestBase { + private final AppView<AppInfoWithLiveness> appView; + private final InternalOptions options = new InternalOptions(); + private final ExecutorService executorService = ThreadUtils.getExecutorService(options); + + public PartialCallGraphTest() throws Exception { + Timing timing = new Timing("PartialCallGraphTest.setup"); + AndroidApp app = testForD8().addProgramClasses(TestClass.class).compile().app; + DexApplication application = new ApplicationReader(app, options, timing).read().toDirect(); + AppView<AppInfoWithSubtyping> appView = + AppView.createForR8(new AppInfoWithSubtyping(application), options); + appView.setAppServices(AppServices.builder(appView).build()); + ProguardConfigurationParser parser = + new ProguardConfigurationParser(appView.dexItemFactory(), options.reporter); + parser.parse( + createConfigurationForTesting( + ImmutableList.of("-keep class ** { void m1(); void m5(); }"))); + appView.setRootSet( + new RootSetBuilder( + appView, application, parser.getConfig().getRules()).run(executorService)); + Enqueuer enqueuer = EnqueuerFactory.createForInitialTreeShaking(appView); + this.appView = + appView.setAppInfo( + enqueuer.traceApplication( + appView.rootSet(), + parser.getConfig().getDontWarnPatterns(), + executorService, + timing)); + } + + @Test + public void testFullGraph() throws Exception { + CallGraph cg = + new CallGraphBuilder(appView).build(executorService, new Timing("testFullGraph")); + Node m1 = findNode(cg.nodes, "m1"); + Node m2 = findNode(cg.nodes, "m2"); + Node m3 = findNode(cg.nodes, "m3"); + Node m4 = findNode(cg.nodes, "m4"); + Node m5 = findNode(cg.nodes, "m5"); + Node m6 = findNode(cg.nodes, "m6"); + assertNotNull(m1); + assertNotNull(m2); + assertNotNull(m3); + assertNotNull(m4); + assertNotNull(m5); + assertNotNull(m6); + + Set<Node> wave = Sets.newIdentityHashSet(); + + MethodProcessor.extractLeaves(cg.nodes, wave::add); + assertEquals(4, wave.size()); // including <init> + assertThat(wave, hasItem(m3)); + assertThat(wave, hasItem(m4)); + assertThat(wave, hasItem(m6)); + wave.clear(); + + MethodProcessor.extractLeaves(cg.nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(m2)); + assertThat(wave, hasItem(m5)); + wave.clear(); + + MethodProcessor.extractLeaves(cg.nodes, wave::add); + assertEquals(1, wave.size()); + assertThat(wave, hasItem(m1)); + assertTrue(cg.nodes.isEmpty()); + } + + @Test + public void testPartialGraph() throws Exception { + DexEncodedMethod em1 = findMethod("m1"); + DexEncodedMethod em2 = findMethod("m2"); + DexEncodedMethod em4 = findMethod("m4"); + DexEncodedMethod em5 = findMethod("m5"); + assertNotNull(em1); + assertNotNull(em2); + assertNotNull(em4); + assertNotNull(em5); + + CallGraph pg = + new PartialCallGraphBuilder(appView, ImmutableSet.of(em1, em2, em4, em5)) + .build(executorService, new Timing("tetPartialGraph")); + + Node m1 = findNode(pg.nodes, "m1"); + Node m2 = findNode(pg.nodes, "m2"); + Node m4 = findNode(pg.nodes, "m4"); + Node m5 = findNode(pg.nodes, "m5"); + assertNotNull(m1); + assertNotNull(m2); + assertNotNull(m4); + assertNotNull(m5); + + Set<Node> wave = Sets.newIdentityHashSet(); + + PostMethodProcessor.extractRoots(pg.nodes, wave::add); + assertEquals(2, wave.size()); + assertThat(wave, hasItem(m1)); + assertThat(wave, hasItem(m5)); + wave.clear(); + + PostMethodProcessor.extractRoots(pg.nodes, wave::add); + assertEquals(1, wave.size()); + assertThat(wave, hasItem(m2)); + wave.clear(); + + PostMethodProcessor.extractRoots(pg.nodes, wave::add); + assertEquals(1, wave.size()); + assertThat(wave, hasItem(m4)); + assertTrue(pg.nodes.isEmpty()); + } + + private Node findNode(Iterable<Node> nodes, String name) { + for (Node n : nodes) { + if (n.method.method.name.toString().equals(name)) { + return n; + } + } + return null; + } + + private DexEncodedMethod findMethod(String name) { + for (DexClass clazz : appView.appInfo().classes()) { + for (DexEncodedMethod method : clazz.methods()) { + if (method.method.name.toString().equals(name)) { + return method; + } + } + } + return null; + } + + static class TestClass { + void m1() { + System.out.println("m1"); + m2(); + } + + void m2() { + System.out.println("m2"); + m3(); + m4(); + } + + void m3() { + System.out.println("m3"); + } + + void m4() { + System.out.println("m4"); + } + + void m5() { + System.out.println("m5"); + m6(); + m4(); + } + + void m6() { + System.out.println("m6"); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java index 86245a3..5e0e77f 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -121,6 +121,9 @@ o.enableInlining = inlining; o.enableInliningOfInvokesWithNullableReceivers = false; o.inliningInstructionLimit = 6; + // Tests depend on nullability of receiver and argument in general. Learning very accurate + // nullability from actual usage in tests bothers what we want to test. + o.enableCallSiteOptimizationInfoPropagation = false; }); }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java new file mode 100644 index 0000000..a1577a3 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java
@@ -0,0 +1,103 @@ +// Copyright (c) 2019, 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.optimize.checkcast; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import com.android.tools.r8.utils.codeinspector.InstructionSubject; +import com.android.tools.r8.utils.codeinspector.MethodSubject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TrivialTypeTestsAfterBranchPruningTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public TrivialTypeTestsAfterBranchPruningTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(TrivialTypeTestsAfterBranchPruningTest.class) + .addKeepMainRule(TestClass.class) + .enableInliningAnnotations() + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("Hello world!"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject classSubject = inspector.clazz(TestClass.class); + assertThat(classSubject, isPresent()); + assertThat(classSubject.uniqueMethodWithName("dead"), not(isPresent())); + + MethodSubject trivialCastMethodSubject = + classSubject.uniqueMethodWithName("trivialCastAfterBranchPruningTest"); + assertThat(trivialCastMethodSubject, isPresent()); + assertTrue( + trivialCastMethodSubject.streamInstructions().noneMatch(InstructionSubject::isCheckCast)); + + MethodSubject branchPruningMethodSubject = + classSubject.uniqueMethodWithName("branchPruningAfterInstanceOfOptimization"); + assertThat(branchPruningMethodSubject, isPresent()); + assertTrue( + branchPruningMethodSubject + .streamInstructions() + .noneMatch(InstructionSubject::isInstanceOf)); + } + + static class TestClass { + + static boolean alwaysTrue = true; + + public static void main(String[] args) { + branchPruningAfterInstanceOfOptimization(trivialCastAfterBranchPruningTest()); + } + + @NeverInline + static A trivialCastAfterBranchPruningTest() { + return (A) (alwaysTrue ? new A() : new B()); + } + + @NeverInline + static void branchPruningAfterInstanceOfOptimization(A obj) { + if (obj instanceof A) { + System.out.println("Hello world!"); + } else { + dead(); + } + } + + @NeverInline + static void dead() { + throw new RuntimeException(); + } + } + + static class A {} + + static class B {} +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java index fbcc6a1..afdfabb 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -64,7 +64,8 @@ @RunWith(Parameterized.class) public class ClassInlinerTest extends TestBase { - private Backend backend; + + private final Backend backend; @Parameterized.Parameters(name = "Backend: {0}") public static Backend[] data() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java index 4dd3551..bf5e92a 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
@@ -74,7 +74,11 @@ } @NeverInline - public static void escape(Object o) {} + static void escape(Object o) { + if (System.currentTimeMillis() < 0) { + System.out.println(o); + } + } } // Simple builder that should be class inlined.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java new file mode 100644 index 0000000..94d775b --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java
@@ -0,0 +1,78 @@ +// Copyright (c) 2019, 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.optimize.ifs; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import com.android.tools.r8.utils.codeinspector.InstructionSubject; +import com.android.tools.r8.utils.codeinspector.MethodSubject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class EnumComparisonTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public EnumComparisonTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(EnumComparisonTest.class) + .addKeepMainRule(TestClass.class) + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("MyEnum.A == MyEnum.A", "MyEnum.A != MyEnum.B"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject classSubject = inspector.clazz(TestClass.class); + assertThat(classSubject, isPresent()); + + MethodSubject mainMethodSubject = classSubject.mainMethod(); + assertThat(mainMethodSubject, isPresent()); + assertTrue(mainMethodSubject.streamInstructions().noneMatch(InstructionSubject::isThrow)); + } + + static class TestClass { + + public static void main(String[] args) { + if (MyEnum.A == MyEnum.A) { + System.out.println("MyEnum.A == MyEnum.A"); + } else { + throw new RuntimeException(); + } + if (MyEnum.A == MyEnum.B) { + throw new RuntimeException(); + } else { + System.out.println("MyEnum.A != MyEnum.B"); + } + } + } + + enum MyEnum { + A, + B + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java index e8308f7..5f1b7a5 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -4,19 +4,19 @@ package com.android.tools.r8.ir.optimize.inliner; -import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod; import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import com.android.tools.r8.AlwaysInline; import com.android.tools.r8.AssumeMayHaveSideEffects; import com.android.tools.r8.NeverClassInline; import com.android.tools.r8.TestBase; import com.android.tools.r8.TestParameters; -import com.android.tools.r8.TestParametersCollection; import com.android.tools.r8.utils.codeinspector.ClassSubject; import com.android.tools.r8.utils.codeinspector.CodeInspector; -import com.android.tools.r8.utils.codeinspector.MethodSubject; +import com.google.common.collect.ImmutableList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -25,14 +25,17 @@ @RunWith(Parameterized.class) public class SingleTargetAfterInliningTest extends TestBase { + private final int maxInliningDepth; private final TestParameters parameters; - @Parameters(name = "{0}") - public static TestParametersCollection data() { - return getTestParameters().withAllRuntimes().build(); + @Parameters(name = "{1}, max inlining depth: {0}") + public static List<Object[]> data() { + return buildParameters( + ImmutableList.of(0, 1), getTestParameters().withAllRuntimesAndApiLevels().build()); } - public SingleTargetAfterInliningTest(TestParameters parameters) { + public SingleTargetAfterInliningTest(int maxInliningDepth, TestParameters parameters) { + this.maxInliningDepth = maxInliningDepth; this.parameters = parameters; } @@ -41,31 +44,41 @@ testForR8(parameters.getBackend()) .addInnerClasses(SingleTargetAfterInliningTest.class) .addKeepMainRule(TestClass.class) + .addOptionsModification( + options -> { + options.applyInliningToInlinee = true; + options.applyInliningToInlineeMaxDepth = maxInliningDepth; + }) + .enableAlwaysInliningAnnotations() .enableClassInliningAnnotations() .enableSideEffectAnnotations() - .setMinApi(parameters.getRuntime()) - .noMinification() + .setMinApi(parameters.getApiLevel()) .compile() .inspect(this::inspect) .run(parameters.getRuntime(), TestClass.class) - .assertSuccessWithOutputLines("B"); + .assertSuccessWithOutputLines("B.foo()", "B.bar()"); } private void inspect(CodeInspector inspector) { - ClassSubject aClassSubject = inspector.clazz(A.class); - assertThat(aClassSubject, isPresent()); - ClassSubject testClassSubject = inspector.clazz(TestClass.class); assertThat(testClassSubject, isPresent()); // The indirection() method should be inlined. assertThat(testClassSubject.uniqueMethodWithName("indirection"), not(isPresent())); - // The main() method invokes A.method(). - // TODO(b/141451716): A.method() should be inlined into main(). - MethodSubject mainMethodSubject = testClassSubject.mainMethod(); - assertThat(mainMethodSubject, isPresent()); - assertThat(mainMethodSubject, invokesMethod(aClassSubject.uniqueMethodWithName("method"))); + // A.foo() should be absent if the max inlining depth is 1, because indirection() has been + // inlined into main(), which makes A.foo() eligible for inlining into main(). + ClassSubject aClassSubject = inspector.clazz(A.class); + assertThat(aClassSubject, isPresent()); + if (maxInliningDepth == 0) { + assertThat(aClassSubject.uniqueMethodWithName("foo"), isPresent()); + } else { + assert maxInliningDepth == 1; + assertThat(aClassSubject.uniqueMethodWithName("foo"), not(isPresent())); + } + + // A.bar() should always be inlined because it is marked as @AlwaysInline. + assertThat(aClassSubject.uniqueMethodWithName("bar"), not(isPresent())); } static class TestClass { @@ -78,13 +91,16 @@ } private static void indirection(A obj) { - obj.method(); + obj.foo(); + obj.bar(); } } abstract static class A { - abstract void method(); + abstract void foo(); + + abstract void bar(); } @NeverClassInline @@ -94,8 +110,14 @@ B() {} @Override - void method() { - System.out.println("B"); + void foo() { + System.out.println("B.foo()"); + } + + @AlwaysInline + @Override + void bar() { + System.out.println("B.bar()"); } } @@ -106,8 +128,13 @@ C() {} @Override - void method() { - System.out.println("C"); + void foo() { + System.out.println("C.foo()"); + } + + @Override + void bar() { + System.out.println("C.bar()"); } } }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java index b0828fe..f0c6a78 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -167,6 +167,7 @@ testForR8(parameters.getBackend()) .addProgramClasses(classes) .enableInliningAnnotations() + .enableSideEffectAnnotations() .addKeepMainRule(main) .allowAccessModification() .noMinification()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java index f725d21..eb8e6a4 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.ir.optimize.staticizer.movetohost; +import com.android.tools.r8.AssumeMayHaveSideEffects; import com.android.tools.r8.NeverInline; public class MoveToHostFieldOnlyTestClass { @@ -12,6 +13,7 @@ test.testOk_fieldOnly(); } + @AssumeMayHaveSideEffects @NeverInline private void testOk_fieldOnly() { // Any instance method call whose target holder is not the candidate will invalidate candidacy,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java new file mode 100644 index 0000000..d2fa3f8 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
@@ -0,0 +1,119 @@ +// Copyright (c) 2019, 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.optimize.unusedinterfaces; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +import com.android.tools.r8.NeverMerge; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class UnusedInterfaceRemovalTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public UnusedInterfaceRemovalTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(UnusedInterfaceRemovalTest.class) + .addKeepMainRule(TestClass.class) + .enableMergeAnnotations() + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("A.foo()", "A.bar()"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject iClassSubject = inspector.clazz(I.class); + assertThat(iClassSubject, isPresent()); + + ClassSubject jClassSubject = inspector.clazz(J.class); + assertThat(jClassSubject, isPresent()); + + ClassSubject kClassSubject = inspector.clazz(K.class); + assertThat(kClassSubject, not(isPresent())); + + ClassSubject aClassSubject = inspector.clazz(A.class); + assertThat(aClassSubject, isPresent()); + assertEquals(2, aClassSubject.getDexClass().interfaces.size()); + assertEquals( + aClassSubject.getDexClass().interfaces.values[0], iClassSubject.getDexClass().type); + assertEquals( + aClassSubject.getDexClass().interfaces.values[1], jClassSubject.getDexClass().type); + } + + static class TestClass { + + public static void main(String[] args) { + I obj = System.currentTimeMillis() >= 0 ? new A() : new B(); + obj.foo(); + ((J) obj).bar(); + } + } + + @NeverMerge + interface I { + + void foo(); + } + + @NeverMerge + interface J { + + void bar(); + } + + interface K extends I, J {} + + static class A implements K { + + @Override + public void foo() { + System.out.println("A.foo()"); + } + + @Override + public void bar() { + System.out.println("A.bar()"); + } + } + + // To prevent that we detect a single target and start inlining or rewriting the signature in the + // invoke. + static class B implements K { + + @Override + public void foo() { + throw new RuntimeException(); + } + + @Override + public void bar() { + throw new RuntimeException(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java new file mode 100644 index 0000000..90761db --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
@@ -0,0 +1,97 @@ +// Copyright (c) 2019, 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.optimize.unusedinterfaces; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.NeverMerge; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class UnusedInterfaceWithDefaultMethodTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public UnusedInterfaceWithDefaultMethodTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void test() throws Exception { + testForR8(parameters.getBackend()) + .addInnerClasses(UnusedInterfaceWithDefaultMethodTest.class) + .addKeepMainRule(TestClass.class) + .enableInliningAnnotations() + .enableMergeAnnotations() + .setMinApi(parameters.getApiLevel()) + .compile() + .inspect(this::inspect) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutputLines("J.m()"); + } + + private void inspect(CodeInspector inspector) { + ClassSubject iClassSubject = inspector.clazz(I.class); + assertThat(iClassSubject, isPresent()); + + ClassSubject jClassSubject = inspector.clazz(J.class); + assertThat(jClassSubject, isPresent()); + + ClassSubject aClassSubject = inspector.clazz(A.class); + assertThat(aClassSubject, isPresent()); + + // Verify that J is not considered an unused interface, since it provides an implementation of + // m() that happens to be used. + assertEquals(1, aClassSubject.getDexClass().interfaces.size()); + assertEquals( + jClassSubject.getDexClass().type, aClassSubject.getDexClass().interfaces.values[0]); + } + + static class TestClass { + + public static void main(String[] args) { + indirection(new A()); + } + + @NeverInline + private static void indirection(I obj) { + obj.m(); + } + } + + @NeverMerge + interface I { + + void m(); + } + + @NeverMerge + interface J extends I { + + @NeverInline + @Override + default void m() { + System.out.println("J.m()"); + } + } + + static class A implements J {} +}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java index f0a8a84..cfeb611 100644 --- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java +++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -291,30 +291,18 @@ @Test public void testAccessor() throws Exception { TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS; - String mainClass = addMainToClasspath("accessors.AccessorKt", - "accessor_accessPropertyFromCompanionClass"); + String mainClass = + addMainToClasspath("accessors.AccessorKt", "accessor_accessPropertyFromCompanionClass"); runTest( "accessors", mainClass, disableClassStaticizer, - (app) -> { + app -> { + // The classes are removed entirely as a result of member value propagation, inlining, and + // the fact that the classes do not have observable side effects. CodeInspector codeInspector = new CodeInspector(app); - ClassSubject outerClass = - checkClassIsKept(codeInspector, testedClass.getOuterClassName()); - ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName()); - - // Property field has been removed due to member value propagation. - String propertyName = "property"; - checkFieldIsRemoved(outerClass, JAVA_LANG_STRING, propertyName); - - // The getter is always inlined since it just calls into the accessor. - MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName); - checkMethodIsAbsent(companionClass, getter); - - // The accessor is also inlined. - MemberNaming.MethodSignature getterAccessor = - testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION); - checkMethodIsRemoved(outerClass, getterAccessor); + checkClassIsRemoved(codeInspector, testedClass.getOuterClassName()); + checkClassIsRemoved(codeInspector, testedClass.getClassName()); }); }
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java new file mode 100644 index 0000000..cb4bed0 --- /dev/null +++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java
@@ -0,0 +1,104 @@ +// Copyright (c) 2019, 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.naming.applymapping; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.NeverClassInline; +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.StringUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ApplyMappingInterfaceClInitTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public ApplyMappingInterfaceClInitTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + @Ignore("b/142909857") + public void testNotRenamingClInitIfNotInMap() + throws ExecutionException, CompilationFailedException, IOException { + testForR8(parameters.getBackend()) + .addInnerClasses(ApplyMappingInterfaceClInitTest.class) + .addKeepMainRule(Main.class) + .addKeepClassAndMembersRules(TestInterface.class) + .addApplyMapping("") + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .inspect(this::verifyNoRenamingOfClInit); + } + + @Test + @Ignore("b/142909857") + public void testNotRenamingClInitIfInMap() + throws ExecutionException, CompilationFailedException, IOException { + String interfaceName = TestInterface.class.getTypeName(); + testForR8(parameters.getBackend()) + .addInnerClasses(ApplyMappingInterfaceClInitTest.class) + .addKeepMainRule(Main.class) + .addKeepClassAndMembersRules(TestInterface.class) + .addApplyMapping( + StringUtils.lines( + interfaceName + " -> " + interfaceName + ":", " void <clinit>() -> a")) + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .inspect(this::verifyNoRenamingOfClInit); + } + + private void verifyNoRenamingOfClInit(CodeInspector inspector) { + ClassSubject interfaceSubject = inspector.clazz(TestInterface.class); + assertThat(interfaceSubject, isPresent()); + interfaceSubject.allMethods().stream() + .allMatch( + method -> { + boolean classInitNotRenamed = !method.isClassInitializer() || !method.isRenamed(); + assertTrue(classInitNotRenamed); + return classInitNotRenamed; + }); + } + + public interface TestInterface { + Throwable t = new Throwable(); + + void foo(); + } + + @NeverClassInline + public static class Main implements TestInterface { + + public static void main(String[] args) { + new Main().foo(); + } + + @Override + @NeverInline + public void foo() { + System.out.println("Hello World!"); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java new file mode 100644 index 0000000..484fb10 --- /dev/null +++ b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java
@@ -0,0 +1,159 @@ +// Copyright (c) 2018, 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.shaking.annotations; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.NeverClassInline; +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AnnotationShakingBehaviorTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public AnnotationShakingBehaviorTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testKeepUnusedTypeInAnnotation() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addProgramClasses(Factory.class, Main.class, C.class) + .addKeepMainRule(Main.class) + .addKeepClassAndMembersRules(Factory.class) + .addKeepAttributes("*Annotation*") + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("Hello World!") + .inspect(inspector -> assertThat(inspector.clazz(C.class), isPresent())); + } + + @Test + public void testWillKeepAnnotatedProgramClass() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addProgramClasses(Factory.class, MainWithMethodAnnotation.class, C.class) + .addKeepMainRule(MainWithMethodAnnotation.class) + .addKeepClassAndMembersRules(Factory.class) + .addKeepAttributes("*Annotation*") + .enableInliningAnnotations() + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), MainWithMethodAnnotation.class) + .assertSuccessWithOutputLines("Hello World!") + .inspect(inspector -> assertThat(inspector.clazz(C.class), isPresent())); + } + + @Test + public void testGenericSignatureDoNotKeepType() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addProgramClasses(MainWithGenericC.class, C.class) + .addKeepMainRule(MainWithGenericC.class) + .addKeepAttributes("Signature", "InnerClasses", "EnclosingMethod") + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), MainWithGenericC.class) + .assertSuccessWithOutputLines("Hello World!") + .inspect( + inspector -> { + assertThat(inspector.clazz(C.class), not(isPresent())); + }); + } + + @Test + public void testDisappearsWhenMerging() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addProgramClasses(Factory.class, MainWithNewB.class, A.class, B.class, C.class) + .addKeepMainRule(MainWithNewB.class) + .addKeepClassAndMembersRules(Factory.class) + .addKeepAttributes("*Annotation*") + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), MainWithNewB.class) + .assertSuccessWithOutputLines("Hello World!") + .inspect( + inspector -> { + assertThat(inspector.clazz(C.class), not(isPresent())); + }); + } + + @Retention(value = RetentionPolicy.RUNTIME) + public @interface Factory { + Class<?> ref() default Object.class; + } + + public static class C {} + + @Factory(ref = C.class) // <-- we are explicitly keeping Main. + public static class Main { + + public static void main(String[] args) { + System.out.println("Hello World!"); + } + } + + public static class MainWithMethodAnnotation { + + public static void main(String[] args) { + test(); + } + + @Factory(ref = C.class) // <-- We are not explicitly saying that test() should be kept. + @NeverInline + public static void test() { + System.out.println("Hello World!"); + } + } + + public static class MainWithGenericC { + + static List<C> cs = new ArrayList<>(); + + public static void main(String[] args) { + if (cs.size() == args.length) { + System.out.println("Hello World!"); + } + } + } + + @Factory(ref = C.class) + public static class A {} + + @NeverClassInline + public static class B extends A { + @NeverInline + public void world() { + System.out.println("Hello World!"); + } + } + + public static class MainWithNewB { + + public static void main(String[] args) { + new B().world(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java new file mode 100644 index 0000000..4e6adad --- /dev/null +++ b/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java
@@ -0,0 +1,181 @@ +// Copyright (c) 2019, 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.shaking.annotations; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeTrue; + +import com.android.tools.r8.BaseCompilerCommand; +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestCompileResult; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestRunResult; +import com.android.tools.r8.TestShrinkerBuilder; +import com.android.tools.r8.utils.BooleanUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.MethodSubject; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class LibraryAndMissingAnnotationsTest extends TestBase { + + private final TestParameters parameters; + private final boolean includeOnLibraryPath; + + @Parameters(name = "{0}, includeOnLibraryPath: {1}") + public static List<Object[]> data() { + return buildParameters( + getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values()); + } + + private static Function<TestParameters, Path> compilationResults = + memoizeFunction(LibraryAndMissingAnnotationsTest::compileLibraryAnnotationToRuntime); + + private static Path compileLibraryAnnotationToRuntime(TestParameters parameters) + throws CompilationFailedException, IOException { + return testForR8(getStaticTemp(), parameters.getBackend()) + .addProgramClasses(LibraryAnnotation.class) + .addKeepClassAndMembersRulesWithAllowObfuscation(LibraryAnnotation.class) + .setMinApi(parameters.getApiLevel()) + .compile() + .writeToZip(); + } + + public LibraryAndMissingAnnotationsTest(TestParameters parameters, boolean includeOnLibraryPath) { + this.parameters = parameters; + this.includeOnLibraryPath = includeOnLibraryPath; + } + + @Test + public void testMainWithUseR8() throws Exception { + runTest(testForR8(parameters.getBackend()), MainWithUse.class); + } + + @Test + public void testMainWithUseR8Compat() throws Exception { + runTest(testForR8Compat(parameters.getBackend()), MainWithUse.class); + } + + @Test + public void testMainWithUseProguard() throws Exception { + assumeTrue(parameters.isCfRuntime()); + runTest(testForProguard(), MainWithUse.class); + } + + @Test + public void testMainWithoutUseR8() throws Exception { + runTest(testForR8(parameters.getBackend()), MainWithoutUse.class); + } + + @Test + public void testMainWithoutUseR8Compat() throws Exception { + runTest(testForR8Compat(parameters.getBackend()), MainWithoutUse.class); + } + + @Test + public void testMainWithoutUseProguard() throws Exception { + assumeTrue(parameters.isCfRuntime()); + runTest(testForProguard(), MainWithoutUse.class); + } + + private < + C extends BaseCompilerCommand, + B extends BaseCompilerCommand.Builder<C, B>, + CR extends TestCompileResult<CR, RR>, + RR extends TestRunResult<RR>, + T extends TestShrinkerBuilder<C, B, CR, RR, T>> + void runTest(TestShrinkerBuilder<C, B, CR, RR, T> builder, Class<?> mainClass) + throws Exception { + T t = + builder + .addProgramClasses(Foo.class, mainClass) + .addKeepAttributes("*Annotation*") + .addLibraryFiles(runtimeJar(parameters)) + .addKeepClassAndMembersRules(Foo.class) + .addKeepRules("-dontwarn " + LibraryAndMissingAnnotationsTest.class.getTypeName()) + .addKeepMainRule(mainClass) + .setMinApi(parameters.getApiLevel()); + if (includeOnLibraryPath) { + t.addLibraryClasses(LibraryAnnotation.class); + } else { + t.addKeepRules("-dontwarn " + LibraryAnnotation.class.getTypeName()); + } + t.compile() + .addRunClasspathFiles(compilationResults.apply(parameters)) + .run(parameters.getRuntime(), mainClass) + .inspect( + inspector -> { + ClassSubject clazz = inspector.clazz(Foo.class); + assertThat(clazz, isPresent()); + assertThat(clazz.annotation(LibraryAnnotation.class.getTypeName()), isPresent()); + MethodSubject foo = clazz.uniqueMethodWithName("foo"); + assertThat(foo, isPresent()); + assertThat(foo.annotation(LibraryAnnotation.class.getTypeName()), isPresent()); + assertFalse(foo.getMethod().parameterAnnotationsList.isEmpty()); + assertEquals( + LibraryAnnotation.class.getTypeName(), + foo.getMethod() + .parameterAnnotationsList + .get(0) + .annotations[0] + .getAnnotationType() + .toSourceString()); + assertThat( + clazz + .uniqueFieldWithName("bar") + .annotation(LibraryAnnotation.class.getTypeName()), + isPresent()); + }) + .assertSuccessWithOutputLines("Hello World!"); + } + + @Test + public void testMainWithoutUse() {} + + @Retention(RetentionPolicy.RUNTIME) + @interface LibraryAnnotation {} + + @LibraryAnnotation + public static class Foo { + + @LibraryAnnotation public String bar = "Hello World!"; + + @LibraryAnnotation + public void foo(@LibraryAnnotation String arg) { + System.out.println(arg); + } + } + + public static class MainWithoutUse { + + public static void main(String[] args) { + Foo foo = new Foo(); + foo.foo(foo.bar); + } + } + + public static class MainWithUse { + public static void main(String[] args) { + if (args.length > 0 && args[0].equals(LibraryAnnotation.class.getTypeName())) { + System.out.print("This will never be printed"); + } + Foo foo = new Foo(); + foo.foo(foo.bar); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java new file mode 100644 index 0000000..45b29f7 --- /dev/null +++ b/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java
@@ -0,0 +1,119 @@ +// Copyright (c) 2018, 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.shaking.annotations; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.CompilationFailedException; +import com.android.tools.r8.NeverClassInline; +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.graph.DexAnnotationSet; +import com.android.tools.r8.graph.DexEncodedAnnotation; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.graph.DexValue; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class PrunedOrMergedAnnotationTest extends TestBase { + + private final TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + public PrunedOrMergedAnnotationTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void testRewritingInFactory() + throws IOException, CompilationFailedException, ExecutionException { + testForR8(parameters.getBackend()) + .addInnerClasses(PrunedOrMergedAnnotationTest.class) + .addKeepMainRule(Main.class) + .addKeepAttributes("*Annotation*") + .addKeepClassAndMembersRules(Factory.class) + .enableInliningAnnotations() + .enableClassInliningAnnotations() + .setMinApi(parameters.getApiLevel()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("Hello", "World!") + .inspect( + inspector -> { + assertThat(inspector.clazz(A.class), not(isPresent())); + DexType mergedType = inspector.clazz(B.class).getDexClass().type; + ClassSubject classC = inspector.clazz(C.class); + assertThat(classC, isPresent()); + DexEncodedAnnotation annotation = + classC.annotation(Factory.class.getTypeName()).getAnnotation(); + assertTrue(valueIsDexType(mergedType, annotation.elements[0].value)); + assertTrue( + Arrays.stream(annotation.elements[1].value.asDexValueArray().getValues()) + .allMatch(value -> valueIsDexType(mergedType, value))); + // Check that method parameter annotations are rewritten as well. + DexEncodedMethod method = inspector.clazz(Main.class).mainMethod().getMethod(); + DexAnnotationSet annotationSet = method.parameterAnnotationsList.get(0); + DexEncodedAnnotation parameterAnnotation = annotationSet.annotations[0].annotation; + assertTrue(valueIsDexType(mergedType, parameterAnnotation.elements[0].value)); + }); + } + + private boolean valueIsDexType(DexType type, DexValue value) { + assertTrue(value.isDexValueType()); + assertEquals(type, value.asDexValueType().value); + return true; + } + + public @interface Factory { + + Class<?> extending() default Object.class; + + Class<?>[] other() default Object[].class; + } + + public static class A {} + + @NeverClassInline + public static class B extends A { + @NeverInline + public void world() { + System.out.println("World!"); + } + } + + @Factory( + extending = A.class, + other = {A.class, B.class}) + public static class C { + @NeverInline + public static void hello() { + System.out.println("Hello"); + } + } + + public static class Main { + + public static void main(@Factory(extending = A.class) String[] args) { + C.hello(); + new B().world(); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java index ca354f9..a5fbc59 100644 --- a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java +++ b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
@@ -8,7 +8,6 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import com.android.tools.r8.AssumeMayHaveSideEffects; import com.android.tools.r8.NeverClassInline; import com.android.tools.r8.NeverMerge; import com.android.tools.r8.TestBase; @@ -29,7 +28,7 @@ @Parameterized.Parameters(name = "{0}") public static TestParametersCollection data() { - return getTestParameters().withAllRuntimes().build(); + return getTestParameters().withAllRuntimesAndApiLevels().build(); } public LibraryMethodOverrideTest(TestParameters parameters) { @@ -44,8 +43,7 @@ .addOptionsModification(options -> options.enableTreeShakingOfLibraryMethodOverrides = true) .enableClassInliningAnnotations() .enableMergeAnnotations() - .enableSideEffectAnnotations() - .setMinApi(parameters.getRuntime()) + .setMinApi(parameters.getApiLevel()) .compile() .inspect(this::verifyOutput) .run(parameters.getRuntime(), TestClass.class) @@ -68,7 +66,16 @@ for (Class<?> nonEscapingClass : nonEscapingClasses) { ClassSubject classSubject = inspector.clazz(nonEscapingClass); assertThat(classSubject, isPresent()); - assertThat(classSubject.uniqueMethodWithName("toString"), not(isPresent())); + + // TODO(b/142772856): None of the non-escaping classes should have a toString() method. It is + // a requirement that the instance initializers are considered trivial for this to work, + // though, even when they have a side effect (as long as the receiver does not escape via the + // side effecting instruction). + if (nonEscapingClass == DoesNotEscapeWithSubThatDoesNotOverrideSub.class) { + assertThat(classSubject.uniqueMethodWithName("toString"), not(isPresent())); + } else { + assertThat(classSubject.uniqueMethodWithName("toString"), isPresent()); + } } } @@ -128,8 +135,12 @@ @NeverClassInline static class DoesNotEscape { - @AssumeMayHaveSideEffects - DoesNotEscape() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscape() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() { @@ -140,8 +151,12 @@ @NeverClassInline static class DoesNotEscapeWithSubThatDoesNotOverride { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatDoesNotOverride() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatDoesNotOverride() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() { @@ -153,15 +168,23 @@ static class DoesNotEscapeWithSubThatDoesNotOverrideSub extends DoesNotEscapeWithSubThatDoesNotOverride { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatDoesNotOverrideSub() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatDoesNotOverrideSub() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } } @NeverClassInline static class DoesNotEscapeWithSubThatOverrides { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatOverrides() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatOverrides() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() { @@ -172,8 +195,12 @@ @NeverClassInline static class DoesNotEscapeWithSubThatOverridesSub extends DoesNotEscapeWithSubThatOverrides { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatOverridesSub() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatOverridesSub() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() { @@ -188,8 +215,12 @@ @NeverClassInline static class DoesNotEscapeWithSubThatOverridesAndEscapes { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatOverridesAndEscapes() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatOverridesAndEscapes() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() { @@ -201,8 +232,12 @@ static class DoesNotEscapeWithSubThatOverridesAndEscapesSub extends DoesNotEscapeWithSubThatOverridesAndEscapes { - @AssumeMayHaveSideEffects - DoesNotEscapeWithSubThatOverridesAndEscapesSub() {} + // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a + // side effect. + DoesNotEscapeWithSubThatOverridesAndEscapesSub() { + // Side effect to ensure that the constructor is not removed from main(). + System.out.print(""); + } @Override public String toString() {
diff --git a/src/test/r8OnArtBackport/FileAlreadyExistsException.java b/src/test/r8OnArtBackport/FileAlreadyExistsException.java new file mode 100644 index 0000000..a725932 --- /dev/null +++ b/src/test/r8OnArtBackport/FileAlreadyExistsException.java
@@ -0,0 +1,13 @@ +// Copyright (c) 2019, 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 java.nio.file; + +import java.io.IOException; + +public class FileAlreadyExistsException extends IOException { + public FileAlreadyExistsException(String file) { + super(file); + } +}
diff --git a/src/test/r8OnArtBackport/Files.java b/src/test/r8OnArtBackport/Files.java new file mode 100644 index 0000000..99d1956 --- /dev/null +++ b/src/test/r8OnArtBackport/Files.java
@@ -0,0 +1,50 @@ +// Copyright (c) 2019, 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 java.nio.file; + +import com.android.tools.r8.MockedPath; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +public final class Files { + private Files() {} + + public static boolean exists(Path path, LinkOption... options) { + if (options.length != 0) { + throw new RuntimeException("Unsupported in the Files mock."); + } + return new File(path.toString()).exists(); + } + + public static boolean isDirectory(Path path, LinkOption... options) { + if (options.length != 0) { + throw new RuntimeException("Unsupported in the Files mock."); + } + return new File(path.toString()).isDirectory(); + } + + public static byte[] readAllBytes(Path path) throws IOException { + if (!(path instanceof MockedPath)) { + throw new RuntimeException("Unsupported in the Files mock."); + } + MockedPath mockedPath = (MockedPath) path; + return mockedPath.getAllBytes(); + } + + public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + boolean append = false; + for (OpenOption option : options) { + if (option == StandardOpenOption.APPEND) { + append = true; + } + } + if (!(path instanceof MockedPath)) { + throw new RuntimeException("Unsupported in the Files mock."); + } + MockedPath mockedPath = (MockedPath) path; + return mockedPath.newOutputStream(append); + } +}
diff --git a/src/test/r8OnArtBackport/LinkOption.java b/src/test/r8OnArtBackport/LinkOption.java new file mode 100644 index 0000000..3b4b510 --- /dev/null +++ b/src/test/r8OnArtBackport/LinkOption.java
@@ -0,0 +1,7 @@ +// Copyright (c) 2019, 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 java.nio.file; + +public interface LinkOption {}
diff --git a/src/test/r8OnArtBackport/MockedPath.java b/src/test/r8OnArtBackport/MockedPath.java new file mode 100644 index 0000000..c5147df --- /dev/null +++ b/src/test/r8OnArtBackport/MockedPath.java
@@ -0,0 +1,151 @@ +// Copyright (c) 2019, 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; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.Path; + +// Mock Path since Path is not present before Dex-8. +// File is present and it contains most of the utilities. +public class MockedPath implements Path { + + private MockedPath(File file) { + this.wrappedFile = file; + } + + public static Path of(File file, String... more) { + // Delegate to File separator management. + File current = file; + for (String s : more) { + current = new File(current.getPath(), s); + } + return new MockedPath(current); + } + + public static Path of(String first, String... more) { + return of(new File(first), more); + } + + // MockedPath wraps the path in a file. + private File wrappedFile; + + @Override + public FileSystem getFileSystem() { + throw new RuntimeException("Mocked Path does not implement getFileSystem"); + } + + @Override + public boolean isAbsolute() { + throw new RuntimeException("Mocked Path does not implement isAbsolute"); + } + + @Override + public Path getRoot() { + throw new RuntimeException("Mocked Path does not implement getRoot"); + } + + @Override + public Path getFileName() { + return of(wrappedFile.getName()); + } + + @Override + public Path getParent() { + return of(wrappedFile.getParent()); + } + + @Override + public int getNameCount() { + throw new RuntimeException("Mocked Path does not implement getNameCount"); + } + + @Override + public Path getName(int index) { + throw new RuntimeException("Mocked Path does not implement getName"); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + throw new RuntimeException("Mocked Path does not implement subpath"); + } + + @Override + public boolean startsWith(Path other) { + throw new RuntimeException("Mocked Path does not implement startsWith"); + } + + @Override + public boolean endsWith(Path other) { + throw new RuntimeException("Mocked Path does not implement endswith"); + } + + @Override + public Path normalize() { + throw new RuntimeException("Mocked Path does not implement normalize"); + } + + @Override + public Path resolve(Path other) { + return new MockedPath(new File(wrappedFile.getPath(), other.toString())); + } + + @Override + public Path relativize(Path other) { + throw new RuntimeException("Mocked Path does not implement relativize"); + } + + @Override + public URI toUri() { + throw new RuntimeException("Mocked Path does not implement toUri"); + } + + @Override + public Path toAbsolutePath() { + throw new RuntimeException("Mocked Path does not implement toAbsolutePath"); + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + throw new RuntimeException("Mocked Path does not implement toRealPath"); + } + + @Override + public int compareTo(Path other) { + throw new RuntimeException("Mocked Path does not implement compareTo"); + } + + @Override + public String toString() { + return wrappedFile.toString(); + } + + @Override + public File toFile() { + return wrappedFile; + } + + // Compatibility with Files. + + public byte[] getAllBytes() throws IOException { + FileInputStream fileInputStream = new FileInputStream(wrappedFile); + // In android the result of file.length() is long + // byte count of the file-content + long byteLength = wrappedFile.length(); + byte[] filecontent = new byte[(int) byteLength]; + fileInputStream.read(filecontent, 0, (int) byteLength); + return filecontent; + } + + public OutputStream newOutputStream(boolean append) throws IOException { + return new FileOutputStream(wrappedFile, append); + } +}
diff --git a/src/test/r8OnArtBackport/NoSuchFileException.java b/src/test/r8OnArtBackport/NoSuchFileException.java new file mode 100644 index 0000000..9c8e9cf --- /dev/null +++ b/src/test/r8OnArtBackport/NoSuchFileException.java
@@ -0,0 +1,13 @@ +// Copyright (c) 2019, 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 java.nio.file; + +import java.io.IOException; + +public class NoSuchFileException extends IOException { + public NoSuchFileException(String file) { + super(file); + } +}
diff --git a/src/test/r8OnArtBackport/OpenOption.java b/src/test/r8OnArtBackport/OpenOption.java new file mode 100644 index 0000000..21b5fb6 --- /dev/null +++ b/src/test/r8OnArtBackport/OpenOption.java
@@ -0,0 +1,7 @@ +// Copyright (c) 2019, 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 java.nio.file; + +public interface OpenOption {}
diff --git a/src/test/r8OnArtBackport/Path.java b/src/test/r8OnArtBackport/Path.java new file mode 100644 index 0000000..474725f --- /dev/null +++ b/src/test/r8OnArtBackport/Path.java
@@ -0,0 +1,91 @@ +// Copyright (c) 2019, 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 java.nio.file; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; + +// Removed Watchable. +public interface Path extends Comparable<Path>, Iterable<Path> { + public static Path of(String first, String... more) { + throw new RuntimeException("Path does not implement of, use MockedPath.of"); + } + + public static Path of(URI uri) { + throw new RuntimeException("Path does not implement of, use MockedPath.of"); + } + + FileSystem getFileSystem(); + + boolean isAbsolute(); + + Path getRoot(); + + Path getFileName(); + + Path getParent(); + + int getNameCount(); + + Path getName(int index); + + Path subpath(int beginIndex, int endIndex); + + boolean startsWith(Path other); + + default boolean startsWith(String other) { + throw new RuntimeException("Path does not implement startsWith"); + } + + boolean endsWith(Path other); + + default boolean endsWith(String other) { + throw new RuntimeException("Path does not implement endsWith"); + } + + Path normalize(); + + Path resolve(Path other); + + default Path resolve(String other) { + throw new RuntimeException("Path does not implement resolve"); + } + + default Path resolveSibling(Path other) { + throw new RuntimeException("Path does not implement resolveSibling"); + } + + default Path resolveSibling(String other) { + throw new RuntimeException("Path does not implement resolveSibling"); + } + + Path relativize(Path other); + + URI toUri(); + + Path toAbsolutePath(); + + Path toRealPath(LinkOption... options) throws IOException; + + default File toFile() { + throw new RuntimeException("Path does not implement toFile"); + } + + @Override + default Iterator<Path> iterator() { + throw new RuntimeException("Path does not implement iterator"); + } + + @Override + int compareTo(Path other); + + boolean equals(Object other); + + int hashCode(); + + String toString(); +}
diff --git a/src/test/r8OnArtBackport/Paths.java b/src/test/r8OnArtBackport/Paths.java new file mode 100644 index 0000000..06d044f --- /dev/null +++ b/src/test/r8OnArtBackport/Paths.java
@@ -0,0 +1,16 @@ +// Copyright (c) 2019, 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 java.nio.file; + +import com.android.tools.r8.MockedPath; + +public final class Paths { + + private Paths() {} + + public static Path get(String first, String... more) { + return MockedPath.of(first, more); + } +}
diff --git a/src/test/r8OnArtBackport/StandardOpenOption.java b/src/test/r8OnArtBackport/StandardOpenOption.java new file mode 100644 index 0000000..0e4d208 --- /dev/null +++ b/src/test/r8OnArtBackport/StandardOpenOption.java
@@ -0,0 +1,18 @@ +// Copyright (c) 2019, 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 java.nio.file; + +public enum StandardOpenOption implements OpenOption { + READ, + WRITE, + APPEND, + TRUNCATE_EXISTING, + CREATE, + CREATE_NEW, + DELETE_ON_CLOSE, + SPARSE, + SYNC, + DSYNC; +}
diff --git a/tools/gradle.py b/tools/gradle.py index 842816e..0e926e6 100755 --- a/tools/gradle.py +++ b/tools/gradle.py
@@ -31,6 +31,11 @@ default=False, action='store_true') parser.add_argument('--java-home', '--java_home', help='Use a custom java version to run gradle.') + parser.add_argument('--worktree', + help='Gradle is running in a worktree and may lock up ' + 'the gradle caches.', + action='store_true', + default=False) return parser.parse_known_args() def GetJavaEnv(env): @@ -101,6 +106,8 @@ args.append('-Dorg.gradle.java.home=' + options.java_home) if options.no_internal: args.append('-Pno_internal') + if options.worktree: + args.append('-g=' + os.path.join(utils.REPO_ROOT, ".gradle_user_home")) return RunGradle(args) if __name__ == '__main__':
diff --git a/tools/internal_test.py b/tools/internal_test.py index 9654b4b..c85942b 100755 --- a/tools/internal_test.py +++ b/tools/internal_test.py
@@ -77,7 +77,7 @@ 'find-xmx-min': 800, 'find-xmx-max': 1200, 'find-xmx-range': 32, - 'oom-threshold': 1000, + 'oom-threshold': 1037, }, { 'app': 'iosched', @@ -90,6 +90,8 @@ ] def find_min_xmx_command(record): + assert record['find-xmx-min'] < record['find-xmx-max'] + assert record['find-xmx-range'] < record['find-xmx-max'] - record['find-xmx-min'] return [ 'tools/run_on_app.py', '--compiler=r8',
diff --git a/tools/test.py b/tools/test.py index 2d45d1c..5091f61 100755 --- a/tools/test.py +++ b/tools/test.py
@@ -128,6 +128,9 @@ result.add_option('--fail-fast', '--fail_fast', default=False, action='store_true', help='Stop on first failure. Passes --fail-fast to gradle test runner.') + result.add_option('--worktree', + default=False, action='store_true', + help='Tests are run in worktree and should not use gradle user home.') return result.parse_args() def archive_failures(): @@ -215,6 +218,8 @@ gradle_args.append('R8LibNoDeps') if options.r8lib_no_deps: gradle_args.append('-Pr8lib_no_deps') + if options.worktree: + gradle_args.append('-g=' + os.path.join(utils.REPO_ROOT, ".gradle_user_home")) # Build an R8 with dependencies for bootstrapping tests before adding test sources. gradle_args.append('r8WithRelocatedDeps')