blob: cd7bf166c827e8587be03e113c3fbe52ae41188b [file] [log] [blame]
// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DexAnnotation;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.utils.ObjectUtils;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public abstract class KeepAnnotationCollectionInfo {
public static class RetentionInfo {
private static final RetentionInfo RETAIN_NONE = new RetentionInfo("none");
private static final RetentionInfo RETAIN_VISIBLE = new RetentionInfo("visible");
private static final RetentionInfo RETAIN_INVISIBLE = new RetentionInfo("invisible");
private static final RetentionInfo RETAIN_ALL = new RetentionInfo("all");
public static RetentionInfo getRetainNone() {
return RETAIN_NONE;
}
public static RetentionInfo getRetainAll() {
return RETAIN_ALL;
}
public static RetentionInfo getRetainVisible() {
return RETAIN_VISIBLE;
}
public static RetentionInfo getRetainInvisible() {
return RETAIN_INVISIBLE;
}
private final String name;
private RetentionInfo(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public String toString() {
return name;
}
public boolean isLessThanOrEqualTo(RetentionInfo other) {
return equals(other) || isNone() || other.isAll();
}
public RetentionInfo join(RetentionInfo other) {
if (other.isLessThanOrEqualTo(this)) {
return this;
}
if (isLessThanOrEqualTo(other)) {
return other;
}
return RETAIN_ALL;
}
public boolean isNone() {
return equals(RETAIN_NONE);
}
public boolean isAll() {
return equals(RETAIN_ALL);
}
public boolean isVisible() {
return equals(RETAIN_VISIBLE);
}
public boolean isInvisible() {
return equals(RETAIN_INVISIBLE);
}
public boolean matches(DexAnnotation annotation) {
if (isNone()) {
return false;
}
if (isAll()) {
return true;
}
int visibility = annotation.getVisibility();
return (isVisible() && visibility == DexAnnotation.VISIBILITY_RUNTIME)
|| (isInvisible() && visibility == DexAnnotation.VISIBILITY_BUILD);
}
}
public static class KeepAnnotationInfo {
private static KeepAnnotationInfo BOTTOM =
new KeepAnnotationInfo(null, RetentionInfo.RETAIN_NONE);
private static KeepAnnotationInfo TOP = new KeepAnnotationInfo(null, RetentionInfo.RETAIN_ALL);
private static KeepAnnotationInfo VISIBLE =
new KeepAnnotationInfo(null, RetentionInfo.RETAIN_VISIBLE);
private static KeepAnnotationInfo INVISIBLE =
new KeepAnnotationInfo(null, RetentionInfo.RETAIN_INVISIBLE);
public static KeepAnnotationInfo getTop() {
return TOP;
}
public static KeepAnnotationInfo getBottom() {
return BOTTOM;
}
public static KeepAnnotationInfo create(DexType typeOrNullForAny, RetentionInfo retention) {
return typeOrNullForAny == null
? createForAnyType(retention)
: new KeepAnnotationInfo(typeOrNullForAny, retention);
}
public static KeepAnnotationInfo createForAnyType(RetentionInfo retention) {
if (retention.isAll()) {
return getTop();
}
if (retention.isVisible()) {
return VISIBLE;
}
if (retention.isInvisible()) {
return INVISIBLE;
}
assert retention.isNone();
return getBottom();
}
// A concrete annotation type or null if applicable to any type.
private final DexType type;
// The retention set for which this info is applicable. A RETAIN_NONE value implies bottom.
private final RetentionInfo retention;
private KeepAnnotationInfo(DexType type, RetentionInfo retention) {
this.type = type;
this.retention = retention;
}
public boolean isAnyType() {
return type == null;
}
public boolean isTop() {
return isAnyType() && retention.isAll();
}
public boolean isBottom() {
// The bottom is independent of the type.
return retention.isNone();
}
public boolean isLessThanOrEqualTo(KeepAnnotationInfo other) {
if (isBottom()) {
return true;
}
return (other.isAnyType() || type.isIdenticalTo(other.type))
&& retention.isLessThanOrEqualTo(other.retention);
}
public KeepAnnotationInfo joinForSameType(KeepAnnotationInfo other) {
// For now, we only want to support joining the info on the same types.
assert ObjectUtils.identical(type, other.type);
if (other.retention.isLessThanOrEqualTo(retention)) {
return this;
}
if (retention.isLessThanOrEqualTo(other.retention)) {
return other;
}
return create(type, retention.join(other.retention));
}
public boolean matches(DexAnnotation annotation) {
if (type != null && type.isNotIdenticalTo(annotation.getAnnotationType())) {
return false;
}
return retention.matches(annotation);
}
@Override
public String toString() {
return "KeepAnnotationInfo{" + "type=" + type + ", retention=" + retention + '}';
}
}
// Singleton representing all possible collections of annotations.
private static final class TopKeepAnnotationCollectionInfo extends KeepAnnotationCollectionInfo {
private static final KeepAnnotationCollectionInfo INSTANCE =
new TopKeepAnnotationCollectionInfo();
public static KeepAnnotationCollectionInfo getInstance() {
return INSTANCE;
}
@Override
public boolean isTop() {
return true;
}
@Override
public boolean isRemovalAllowed(DexAnnotation annotation) {
return false;
}
@Override
public String toString() {
return "top";
}
}
// Singleton class representing no collections of annotations.
private static final class BottomKeepAnnotationCollectionInfo
extends KeepAnnotationCollectionInfo {
private static final KeepAnnotationCollectionInfo INSTANCE =
new BottomKeepAnnotationCollectionInfo();
public static KeepAnnotationCollectionInfo getInstance() {
return INSTANCE;
}
@Override
public boolean isBottom() {
return true;
}
@Override
public boolean isRemovalAllowed(DexAnnotation annotation) {
return true;
}
@Override
public String toString() {
return "bottom";
}
}
private static final class IntermediateKeepAnnotationCollectionInfo
extends KeepAnnotationCollectionInfo {
private final KeepAnnotationInfo anyTypeInfo;
private final Map<DexType, KeepAnnotationInfo> specificTypeInfo;
private IntermediateKeepAnnotationCollectionInfo(
KeepAnnotationInfo anyTypeInfo, Map<DexType, KeepAnnotationInfo> specificTypeInfo) {
assert anyTypeInfo != null;
assert anyTypeInfo.isAnyType();
assert !anyTypeInfo.isTop() || specificTypeInfo == null;
assert specificTypeInfo == null || !specificTypeInfo.isEmpty();
this.anyTypeInfo = anyTypeInfo;
this.specificTypeInfo = specificTypeInfo;
}
@Override
IntermediateKeepAnnotationCollectionInfo asIntermediate() {
return this;
}
@Override
public boolean isRemovalAllowed(DexAnnotation annotation) {
if (anyTypeInfo.matches(annotation)) {
return false;
}
if (specificTypeInfo != null) {
KeepAnnotationInfo info = specificTypeInfo.get(annotation.getAnnotationType());
if (info != null) {
assert info.type.isIdenticalTo(annotation.getAnnotationType());
return !info.retention.matches(annotation);
}
}
return true;
}
public boolean internalIsLessThanOrEqualTo(IntermediateKeepAnnotationCollectionInfo other) {
if (!anyTypeInfo.isLessThanOrEqualTo(other.anyTypeInfo)) {
return false;
}
if (specificTypeInfo == null) {
// Our specific types are "bottom" so this is less than.
return true;
}
if (other.specificTypeInfo == null) {
// Other specific types are "bottom" and this is not bottom, so it is not less than.
return false;
}
// Check that each specific type is less than the content of the type in other.
for (DexType type : specificTypeInfo.keySet()) {
KeepAnnotationInfo otherInfo = other.specificTypeInfo.get(type);
if (otherInfo == null || !specificTypeInfo.get(type).isLessThanOrEqualTo(otherInfo)) {
return false;
}
}
return true;
}
}
public static Builder builder() {
return Builder.createBottom();
}
public Builder toBuilder() {
return Builder.createFrom(this);
}
public boolean isTop() {
return false;
}
public boolean isBottom() {
return false;
}
IntermediateKeepAnnotationCollectionInfo asIntermediate() {
throw new Unreachable();
}
public abstract boolean isRemovalAllowed(DexAnnotation annotation);
public boolean isLessThanOrEqualTo(KeepAnnotationCollectionInfo other) {
if (this == other) {
return true;
}
if (isBottom() || other.isTop()) {
return true;
}
if (isTop() || other.isBottom()) {
return false;
}
return asIntermediate().internalIsLessThanOrEqualTo(other.asIntermediate());
}
public static class Builder {
public static Builder createTop() {
return new Builder(KeepAnnotationInfo.getTop());
}
public static Builder createBottom() {
return new Builder(KeepAnnotationInfo.getBottom());
}
public void destructiveMakeTop() {
anyTypeInfo = KeepAnnotationInfo.getTop();
specificTypeInfo = null;
assert isTop();
}
// Info applicable to any type.
private KeepAnnotationInfo anyTypeInfo;
// Info applicable to only specific types. Null if no type specific info is present.
private Map<DexType, KeepAnnotationInfo> specificTypeInfo = null;
private Builder(KeepAnnotationInfo anyTypeInfo) {
assert anyTypeInfo != null;
assert anyTypeInfo.isAnyType();
this.anyTypeInfo = anyTypeInfo;
}
private static Builder createFrom(KeepAnnotationCollectionInfo original) {
if (original.isTop()) {
return createTop();
}
if (original.isBottom()) {
return createBottom();
}
IntermediateKeepAnnotationCollectionInfo intermediate = original.asIntermediate();
Builder builder = builder();
builder.anyTypeInfo = intermediate.anyTypeInfo;
if (intermediate.specificTypeInfo != null) {
builder.specificTypeInfo = new IdentityHashMap<>(intermediate.specificTypeInfo);
}
return builder;
}
public boolean isTop() {
return anyTypeInfo.isTop();
}
public boolean isBottom() {
return specificTypeInfo == null && anyTypeInfo.isBottom();
}
public boolean isEqualTo(KeepAnnotationCollectionInfo other) {
if (other.isBottom()) {
return isBottom();
}
if (other.isTop()) {
return isTop();
}
IntermediateKeepAnnotationCollectionInfo intermediate = other.asIntermediate();
return anyTypeInfo.equals(intermediate.anyTypeInfo)
&& Objects.equals(specificTypeInfo, intermediate.specificTypeInfo);
}
public void destructiveJoin(Builder other) {
// The empty collection is bottom which joins as identity.
if (other.isBottom()) {
return;
}
if (other.isTop()) {
destructiveMakeTop();
return;
}
// Always join the any-type info. If the any-type becomes top, then it applies to all
// specific types too, and we can simply update the info to top.
KeepAnnotationInfo oldAnyTypeInfo = anyTypeInfo;
anyTypeInfo = anyTypeInfo.joinForSameType(other.anyTypeInfo);
if (anyTypeInfo.isTop()) {
destructiveMakeTop();
return;
}
if (other.specificTypeInfo != null) {
if (specificTypeInfo == null) {
specificTypeInfo = new IdentityHashMap<>(other.specificTypeInfo);
} else {
other.specificTypeInfo.forEach(
(type, info) ->
specificTypeInfo.compute(
type,
(t, existing) -> existing == null ? info : existing.joinForSameType(info)));
}
pruneSubsumedSpecificTypeInfos();
} else if (anyTypeInfo != oldAnyTypeInfo) {
pruneSubsumedSpecificTypeInfos();
}
}
private void pruneSubsumedSpecificTypeInfos() {
if (specificTypeInfo == null) {
return;
}
assert !specificTypeInfo.isEmpty();
// If the any-type does not retain anything then nothing needs to be pruned.
if (anyTypeInfo.isBottom()) {
return;
}
List<DexType> typesToPrune = null;
for (DexType type : specificTypeInfo.keySet()) {
KeepAnnotationInfo info = specificTypeInfo.get(type);
if (info.retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
if (typesToPrune == null) {
typesToPrune = new ArrayList<>(specificTypeInfo.size());
}
typesToPrune.add(type);
}
}
if (typesToPrune != null) {
if (typesToPrune.size() == specificTypeInfo.size()) {
specificTypeInfo = null;
} else {
typesToPrune.forEach(specificTypeInfo::remove);
}
}
}
public void destructiveJoinAnyTypeInfo(RetentionInfo retention) {
if (retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
return;
}
anyTypeInfo = KeepAnnotationInfo.createForAnyType(anyTypeInfo.retention.join(retention));
pruneSubsumedSpecificTypeInfos();
}
public void destructiveJoinTypeInfo(DexType type, RetentionInfo retention) {
assert type != null;
if (retention.isLessThanOrEqualTo(anyTypeInfo.retention)) {
return;
}
if (specificTypeInfo == null) {
// We expect the number of specific annotations to keep to be small, so we use a small
// initial capacity.
specificTypeInfo = new IdentityHashMap<>(3);
}
specificTypeInfo.compute(
type,
(t, prev) -> {
KeepAnnotationInfo info = new KeepAnnotationInfo(t, retention);
return prev == null ? info : info.joinForSameType(prev);
});
}
public KeepAnnotationCollectionInfo build() {
if (isTop()) {
assert specificTypeInfo == null;
return TopKeepAnnotationCollectionInfo.getInstance();
}
if (isBottom()) {
return BottomKeepAnnotationCollectionInfo.getInstance();
}
return new IntermediateKeepAnnotationCollectionInfo(anyTypeInfo, specificTypeInfo);
}
}
}