Model possibly missing interfaces.

This CL amends the interface collection in the type lattice to denote
possibly missing interfaces. The known issue of ZipFile not
implementing Closeable is modeled as such a potentially missing
interface on pre-19 APIs.

Bug: 177532008
Change-Id: I110e8bba9dfd2214f787277bc456bf7bab34586b
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 56b1a53..8ff4d1e 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -15,12 +15,15 @@
 import com.android.tools.r8.graph.ResolutionResult.IncompatibleClassResult;
 import com.android.tools.r8.graph.ResolutionResult.NoSuchMethodResult;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection.Builder;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.shaking.MainDexClasses;
 import com.android.tools.r8.shaking.MissingClasses;
 import com.android.tools.r8.synthesis.CommittedItems;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.TriConsumer;
@@ -287,31 +290,78 @@
   }
 
   /** Collect all interfaces that this type directly or indirectly implements. */
-  public Set<DexType> implementedInterfaces(DexType type) {
+  public InterfaceCollection implementedInterfaces(DexType type) {
     assert type.isClassType();
     DexClass clazz = definitionFor(type);
     if (clazz == null) {
-      return Collections.emptySet();
+      return InterfaceCollection.empty();
     }
 
     // Fast path for a type below object with no interfaces.
     if (clazz.superType == dexItemFactory().objectType && clazz.interfaces.isEmpty()) {
-      return clazz.isInterface() ? Collections.singleton(type) : Collections.emptySet();
+      return clazz.isInterface()
+          ? InterfaceCollection.singleton(type)
+          : InterfaceCollection.empty();
     }
 
     // Slow path traverses the full super type hierarchy.
-    Set<DexType> interfaces = Sets.newIdentityHashSet();
+    Builder builder = InterfaceCollection.builder();
     if (clazz.isInterface()) {
-      interfaces.add(type);
+      builder.addInterface(type, true);
     }
-    forEachSuperType(
-        clazz,
-        (superType, subclass, isInterface) -> {
-          if (isInterface) {
-            interfaces.add(superType);
+    // First find all interface leafs from the class super-type chain.
+    Set<DexType> seenAndKnown = Sets.newIdentityHashSet();
+    Deque<Pair<DexClass, Boolean>> worklist = new ArrayDeque<>();
+    {
+      DexClass implementor = clazz;
+      while (implementor != null) {
+        for (DexType iface : implementor.interfaces) {
+          if (seenAndKnown.contains(iface)) {
+            continue;
           }
-        });
-    return interfaces;
+          boolean isKnown =
+              InterfaceCollection.isKnownToImplement(iface, implementor.getType(), options());
+          builder.addInterface(iface, isKnown);
+          if (isKnown) {
+            seenAndKnown.add(iface);
+          }
+          DexClass definition = definitionFor(iface);
+          if (definition != null && !definition.interfaces.isEmpty()) {
+            worklist.add(new Pair<>(definition, isKnown));
+          }
+        }
+        if (implementor.superType == null
+            || implementor.superType == options().dexItemFactory().objectType) {
+          break;
+        }
+        implementor = definitionFor(implementor.superType);
+      }
+    }
+    // Second complete the worklist of interfaces. All paths must be visited as an interface may
+    // be unknown on one but not on another.
+    while (!worklist.isEmpty()) {
+      Pair<DexClass, Boolean> item = worklist.poll();
+      DexClass implementor = item.getFirst();
+      assert !implementor.interfaces.isEmpty();
+      for (DexType itf : implementor.interfaces) {
+        if (seenAndKnown.contains(itf)) {
+          continue;
+        }
+        // A derived interface is known only if the full chain leading to it is known.
+        boolean isKnown =
+            item.getSecond()
+                && InterfaceCollection.isKnownToImplement(itf, implementor.getType(), options());
+        builder.addInterface(itf, isKnown);
+        if (isKnown) {
+          seenAndKnown.add(itf);
+        }
+        DexClass definition = definitionFor(itf);
+        if (definition != null && !definition.interfaces.isEmpty()) {
+          worklist.add(new Pair<>(definition, isKnown));
+        }
+      }
+    }
+    return builder.build();
   }
 
   public boolean isExternalizable(DexType type) {
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 e2845c4..b510c4f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.ReferenceTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -44,7 +45,6 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -90,9 +90,9 @@
   // ReferenceTypeElement canonicalization.
   private final ConcurrentHashMap<DexType, ReferenceTypeElement> referenceTypes =
       new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexType, Set<DexType>> classTypeInterfaces =
+  private final ConcurrentHashMap<DexType, InterfaceCollection> classTypeInterfaces =
       new ConcurrentHashMap<>();
-  public final LRUCacheTable<Set<DexType>, Set<DexType>, Set<DexType>>
+  public final LRUCacheTable<InterfaceCollection, InterfaceCollection, InterfaceCollection>
       leastUpperBoundOfInterfacesTable = LRUCacheTable.create(8, 8);
 
   boolean sorted = false;
@@ -231,6 +231,8 @@
   public final DexString iterableDescriptor = createString("Ljava/lang/Iterable;");
   public final DexString mathDescriptor = createString("Ljava/lang/Math;");
   public final DexString strictMathDescriptor = createString("Ljava/lang/StrictMath;");
+  public final DexString closeableDescriptor = createString("Ljava/io/Closeable;");
+  public final DexString zipFileDescriptor = createString("Ljava/util/zip/ZipFile;");
 
   public final DexString stringBuilderDescriptor = createString("Ljava/lang/StringBuilder;");
   public final DexString stringBufferDescriptor = createString("Ljava/lang/StringBuffer;");
@@ -361,6 +363,9 @@
   public final DexType methodType = createStaticallyKnownType(methodDescriptor);
   public final DexType autoCloseableType = createStaticallyKnownType(autoCloseableDescriptor);
 
+  public final DexType closeableType = createStaticallyKnownType(closeableDescriptor);
+  public final DexType zipFileType = createStaticallyKnownType(zipFileDescriptor);
+
   public final DexType stringBuilderType = createStaticallyKnownType(stringBuilderDescriptor);
   public final DexType stringBufferType = createStaticallyKnownType(stringBufferDescriptor);
 
@@ -2421,12 +2426,12 @@
               if (type.isClassType()) {
                 if (!appView.enableWholeProgramOptimizations()) {
                   // Don't reason at the level of interfaces in D8.
-                  return ClassTypeElement.create(type, nullability, Collections.emptySet());
+                  return ClassTypeElement.create(type, nullability, InterfaceCollection.empty());
                 }
                 assert appView.appInfo().hasClassHierarchy();
                 if (appView.isInterface(type).isTrue()) {
                   return ClassTypeElement.create(
-                      objectType, nullability, Collections.singleton(type));
+                      objectType, nullability, InterfaceCollection.singleton(type));
                 }
                 // In theory, `interfaces` is the least upper bound of implemented interfaces.
                 // It is expensive to walk through type hierarchy; collect implemented interfaces;
@@ -2441,12 +2446,12 @@
         .getOrCreateVariant(nullability);
   }
 
-  public Set<DexType> getOrComputeLeastUpperBoundOfImplementedInterfaces(
+  public InterfaceCollection getOrComputeLeastUpperBoundOfImplementedInterfaces(
       DexType type, AppView<? extends AppInfoWithClassHierarchy> appView) {
     return classTypeInterfaces.computeIfAbsent(
         type,
         t -> {
-          Set<DexType> itfs = appView.appInfo().implementedInterfaces(t);
+          InterfaceCollection itfs = appView.appInfo().implementedInterfaces(t);
           return computeLeastUpperBoundOfInterfaces(appView, itfs, itfs);
         });
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
index eeeea70..144f465 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
@@ -8,19 +8,21 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection.Builder;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.SetUtils;
-import com.google.common.collect.ImmutableSet;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Queue;
-import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -28,14 +30,14 @@
 
   // Least upper bound of interfaces that this class type is implementing.
   // Lazily computed on demand via DexItemFactory, where the canonicalized set will be maintained.
-  private Set<DexType> lazyInterfaces;
+  private InterfaceCollection lazyInterfaces;
   private AppView<? extends AppInfoWithClassHierarchy> appView;
   // On-demand link between other nullability-variants.
   private final NullabilityVariants<ClassTypeElement> variants;
   private final DexType type;
 
   public static ClassTypeElement create(
-      DexType classType, Nullability nullability, Set<DexType> interfaces) {
+      DexType classType, Nullability nullability, InterfaceCollection interfaces) {
     assert interfaces != null;
     return NullabilityVariants.create(
         nullability,
@@ -55,7 +57,7 @@
   private ClassTypeElement(
       DexType classType,
       Nullability nullability,
-      Set<DexType> interfaces,
+      InterfaceCollection interfaces,
       NullabilityVariants<ClassTypeElement> variants,
       AppView<? extends AppInfoWithClassHierarchy> appView) {
     super(nullability);
@@ -71,7 +73,7 @@
     return type;
   }
 
-  public Set<DexType> getInterfaces() {
+  public InterfaceCollection getInterfaces() {
     if (lazyInterfaces == null) {
       assert appView != null;
       lazyInterfaces =
@@ -105,8 +107,8 @@
   @Override
   public boolean isBasedOnMissingClass(AppView<? extends AppInfoWithClassHierarchy> appView) {
     return appView.appInfo().isMissingOrHasMissingSuperType(getClassType())
-        || getInterfaces().stream()
-            .anyMatch(type -> appView.appInfo().isMissingOrHasMissingSuperType(type));
+        || getInterfaces()
+            .anyMatch((iface, isKnown) -> appView.appInfo().isMissingOrHasMissingSuperType(iface));
   }
 
   @Override
@@ -131,13 +133,17 @@
     builder.append(" ");
     builder.append(type);
     builder.append(" {");
-    Set<DexType> interfaces = getInterfaces();
-    if (interfaces != null) {
-      List<DexType> sortedInterfaces = new ArrayList<>(interfaces);
-      sortedInterfaces.sort(DexType::compareTo);
-      builder.append(
-          sortedInterfaces.stream().map(DexType::toString).collect(Collectors.joining(", ")));
-    }
+    InterfaceCollection interfaces = getInterfaces();
+    List<Pair<DexType, Boolean>> sortedInterfaces = interfaces.getInterfaceList();
+    sortedInterfaces.sort(Comparator.comparing(Pair::getFirst));
+    builder.append(
+        sortedInterfaces.stream()
+            .map(
+                pair ->
+                    pair.getSecond()
+                        ? pair.getFirst().toString()
+                        : ("maybe(" + pair.getFirst() + ")"))
+            .collect(Collectors.joining(", ")));
     builder.append("}");
     return builder.toString();
   }
@@ -165,36 +171,42 @@
 
     // For most types there will not have been a change thus we iterate without allocating a new
     // set for holding modified interfaces.
-    boolean hasChangedInterfaces = false;
-    DexClass interfaceToClassChange = null;
-    for (DexType iface : getInterfaces()) {
-      DexType substitutedType = mapping.apply(iface);
-      if (iface != substitutedType) {
-        hasChangedInterfaces = true;
-        DexClass mappedClass = appView.definitionFor(substitutedType);
-        if (!mappedClass.isInterface()) {
-          if (interfaceToClassChange != null && mappedClass != interfaceToClassChange) {
-            throw new CompilationError(
-                "More than one interface has changed to a class: "
-                    + interfaceToClassChange
-                    + " and "
-                    + mappedClass);
-          }
-          interfaceToClassChange = mappedClass;
-        }
-      }
-    }
-    if (hasChangedInterfaces) {
-      if (interfaceToClassChange != null) {
-        assert !interfaceToClassChange.isInterface();
+    BooleanBox hasChangedInterfaces = new BooleanBox();
+    Box<DexClass> interfaceToClassChange = new Box<>();
+    getInterfaces()
+        .forEach(
+            (iface, isKnown) -> {
+              DexType substitutedType = mapping.apply(iface);
+              if (iface != substitutedType) {
+                hasChangedInterfaces.set();
+                DexClass mappedClass = appView.definitionFor(substitutedType);
+                if (!mappedClass.isInterface()) {
+                  if (interfaceToClassChange.isSet()
+                      && mappedClass != interfaceToClassChange.get()) {
+                    throw new CompilationError(
+                        "More than one interface has changed to a class: "
+                            + interfaceToClassChange.get()
+                            + " and "
+                            + mappedClass);
+                  }
+                  interfaceToClassChange.set(mappedClass);
+                }
+              }
+            });
+    if (hasChangedInterfaces.get()) {
+      if (interfaceToClassChange.isSet()) {
+        assert !interfaceToClassChange.get().isInterface();
         assert type == appView.dexItemFactory().objectType;
-        return create(interfaceToClassChange.type, nullability, appView);
+        return create(interfaceToClassChange.get().type, nullability, appView);
       } else {
-        Set<DexType> newInterfaces = new HashSet<>();
-        for (DexType iface : lazyInterfaces) {
-          newInterfaces.add(mapping.apply(iface));
-        }
-        return create(mappedType, nullability, newInterfaces);
+        Builder builder = InterfaceCollection.builder();
+        lazyInterfaces.forEach(
+            (iface, isKnown) -> {
+              DexType rewritten = mapping.apply(iface);
+              assert iface == rewritten || isKnown : "Rewritten implies program types thus known.";
+              builder.addInterface(rewritten, isKnown);
+            });
+        return create(mappedType, nullability, builder.build());
       }
     }
     return this;
@@ -210,15 +222,15 @@
               ? getClassType()
               : appView.dexItemFactory().objectType,
           nullability,
-          Collections.emptySet());
+          InterfaceCollection.empty());
     }
     DexType lubType =
         computeLeastUpperBoundOfClasses(
             appView.appInfo().withClassHierarchy(), getClassType(), other.getClassType());
-    Set<DexType> c1lubItfs = getInterfaces();
-    Set<DexType> c2lubItfs = other.getInterfaces();
-    Set<DexType> lubItfs = null;
-    if (c1lubItfs.size() == c2lubItfs.size() && c1lubItfs.containsAll(c2lubItfs)) {
+    InterfaceCollection c1lubItfs = getInterfaces();
+    InterfaceCollection c2lubItfs = other.getInterfaces();
+    InterfaceCollection lubItfs = null;
+    if (c1lubItfs.equals(c2lubItfs)) {
       lubItfs = c1lubItfs;
     }
     if (lubItfs == null) {
@@ -228,9 +240,85 @@
     return ClassTypeElement.create(lubType, nullability, lubItfs);
   }
 
-  private enum InterfaceMarker {
-    LEFT,
-    RIGHT
+  /**
+   * Internal marker for finding the LUB between sets of interfaces.
+   *
+   * <p>The marker is used both as the identification of which side the traversal is on and if that
+   * item is known to always be present. That use denotes a immutable use fo the marker and reuses
+   * the static constants defined below. When traversing the interface super chains each point is
+   * mapped to a mutable marking that keeps track of what paths have reached it. The mutable use is
+   * allocated with 'createEmpty' and updated with 'merge'.
+   */
+  private static class InterfaceMarker {
+
+    // Each side is tracked with a three-valued marking.
+    // Note that the value FALSE is not part of the possible three values, only:
+    //   FALSE: not marked / not present.
+    //   TRUE: marked and known to be present.
+    //   UNKNOWN: marked and unknown if actually present.
+    private OptionalBool left;
+    private OptionalBool right;
+
+    static final InterfaceMarker LEFT_KNOWN =
+        new InterfaceMarker(OptionalBool.TRUE, OptionalBool.FALSE);
+    static final InterfaceMarker LEFT_UNKNOWN =
+        new InterfaceMarker(OptionalBool.UNKNOWN, OptionalBool.FALSE);
+    static final InterfaceMarker RIGHT_KNOWN =
+        new InterfaceMarker(OptionalBool.FALSE, OptionalBool.TRUE);
+    static final InterfaceMarker RIGHT_UNKNOWN =
+        new InterfaceMarker(OptionalBool.FALSE, OptionalBool.UNKNOWN);
+
+    static InterfaceMarker forLeft(boolean isKnown) {
+      return isKnown ? LEFT_KNOWN : LEFT_UNKNOWN;
+    }
+
+    static InterfaceMarker forRight(boolean isKnown) {
+      return isKnown ? RIGHT_KNOWN : RIGHT_UNKNOWN;
+    }
+
+    static InterfaceMarker createUnmarked() {
+      return new InterfaceMarker(OptionalBool.FALSE, OptionalBool.FALSE);
+    }
+
+    public InterfaceMarker(OptionalBool left, OptionalBool right) {
+      this.left = left;
+      this.right = right;
+      assert !isMarkedOnBothSides();
+    }
+
+    boolean isMarked() {
+      return left.isPossiblyTrue() || right.isPossiblyTrue();
+    }
+
+    boolean isMarkedOnBothSides() {
+      return left.isPossiblyTrue() && right.isPossiblyTrue();
+    }
+
+    static OptionalBool knownIfAnyIsKnown(OptionalBool v1, OptionalBool v2) {
+      assert v1.isPossiblyTrue() || v2.isPossiblyTrue();
+      return v1.isTrue() || v2.isTrue() ? OptionalBool.TRUE : OptionalBool.UNKNOWN;
+    }
+
+    boolean knownIfBothAreKnown() {
+      assert isMarkedOnBothSides();
+      return left.isTrue() && right.isTrue();
+    }
+
+    boolean merge(InterfaceMarker marker) {
+      assert marker.isMarked();
+      assert !marker.isMarkedOnBothSides();
+      if (marker.left.isPossiblyTrue()) {
+        OptionalBool oldLeft = left;
+        left = knownIfAnyIsKnown(left, marker.left);
+        // Only continue if the other side is absent and this side changed.
+        return right.isFalse() && left != oldLeft;
+      } else {
+        OptionalBool oldRight = right;
+        right = knownIfAnyIsKnown(right, marker.right);
+        // Only continue if the other side is absent and this side changed.
+        return left.isFalse() && right != oldRight;
+      }
+    }
   }
 
   private static class InterfaceWithMarker {
@@ -287,12 +375,15 @@
     return objectType;
   }
 
-  public static Set<DexType> computeLeastUpperBoundOfInterfaces(
-      AppView<? extends AppInfoWithClassHierarchy> appView, Set<DexType> s1, Set<DexType> s2) {
+  public static InterfaceCollection computeLeastUpperBoundOfInterfaces(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      InterfaceCollection s1,
+      InterfaceCollection s2) {
     if (s1.isEmpty() || s2.isEmpty()) {
-      return Collections.emptySet();
+      return InterfaceCollection.empty();
     }
-    Set<DexType> cached = appView.dexItemFactory().leastUpperBoundOfInterfacesTable.get(s1, s2);
+    InterfaceCollection cached =
+        appView.dexItemFactory().leastUpperBoundOfInterfacesTable.get(s1, s2);
     if (cached != null) {
       return cached;
     }
@@ -300,58 +391,46 @@
     if (cached != null) {
       return cached;
     }
-    Map<DexType, Set<InterfaceMarker>> seen = new IdentityHashMap<>();
+    Map<DexType, InterfaceMarker> seen = new IdentityHashMap<>();
     Queue<InterfaceWithMarker> worklist = new ArrayDeque<>();
-    for (DexType itf1 : s1) {
-      worklist.add(new InterfaceWithMarker(itf1, InterfaceMarker.LEFT));
-    }
-    for (DexType itf2 : s2) {
-      worklist.add(new InterfaceWithMarker(itf2, InterfaceMarker.RIGHT));
-    }
+    s1.forEach(
+        (itf1, isKnown) ->
+            worklist.add(new InterfaceWithMarker(itf1, InterfaceMarker.forLeft(isKnown))));
+    s2.forEach(
+        (itf2, isKnown) ->
+            worklist.add(new InterfaceWithMarker(itf2, InterfaceMarker.forRight(isKnown))));
+
     while (!worklist.isEmpty()) {
       InterfaceWithMarker item = worklist.poll();
       DexType itf = item.itf;
       InterfaceMarker marker = item.marker;
-      Set<InterfaceMarker> markers = seen.computeIfAbsent(itf, k -> new HashSet<>());
-      // If this interface is a lower one in this set, skip.
-      if (markers.contains(marker)) {
-        continue;
-      }
-      // If this interface is already visited by the other set, add marker for this set and skip.
-      if (markers.size() == 1) {
-        markers.add(marker);
-        continue;
-      }
-      // Otherwise, this type is freshly visited.
-      markers.add(marker);
-      // Put super interfaces into the worklist.
-      DexClass itfClass = appView.definitionFor(itf);
-      if (itfClass != null) {
-        for (DexType superItf : itfClass.interfaces.values) {
-          markers = seen.computeIfAbsent(superItf, k -> new HashSet<>());
-          if (!markers.contains(marker)) {
+      InterfaceMarker marking = seen.computeIfAbsent(itf, k -> InterfaceMarker.createUnmarked());
+      if (marking.merge(marker)) {
+        // Put super interfaces into the worklist.
+        DexClass itfClass = appView.definitionFor(itf);
+        if (itfClass != null) {
+          for (DexType superItf : itfClass.interfaces.values) {
             worklist.add(new InterfaceWithMarker(superItf, marker));
           }
         }
       }
     }
 
-    ImmutableSet.Builder<DexType> commonBuilder = ImmutableSet.builder();
-    for (Map.Entry<DexType, Set<InterfaceMarker>> entry : seen.entrySet()) {
-      // Keep commonly visited interfaces only
-      if (entry.getValue().size() < 2) {
-        continue;
-      }
-      commonBuilder.add(entry.getKey());
-    }
-    Set<DexType> commonlyVisited = commonBuilder.build();
+    List<Pair<DexType, Boolean>> commonlyVisited = new ArrayList<>(seen.size());
+    seen.forEach(
+        (itf, marking) -> {
+          // Keep commonly visited interfaces only
+          if (marking.isMarkedOnBothSides()) {
+            commonlyVisited.add(new Pair<>(itf, marking.knownIfBothAreKnown()));
+          }
+        });
 
-    ImmutableSet.Builder<DexType> lubBuilder = ImmutableSet.builder();
-    for (DexType itf : commonlyVisited) {
+    Builder lubBuilder = InterfaceCollection.builder();
+    for (Pair<DexType, Boolean> entry : commonlyVisited) {
       // If there is a strict sub interface of this interface, it is not the least element.
       boolean notTheLeast = false;
-      for (DexType other : commonlyVisited) {
-        if (appView.appInfo().isStrictSubtypeOf(other, itf)) {
+      for (Pair<DexType, Boolean> other : commonlyVisited) {
+        if (appView.appInfo().isStrictSubtypeOf(other.getFirst(), entry.getFirst())) {
           notTheLeast = true;
           break;
         }
@@ -359,11 +438,11 @@
       if (notTheLeast) {
         continue;
       }
-      lubBuilder.add(itf);
+      lubBuilder.addInterface(entry.getFirst(), entry.getSecond());
     }
-    Set<DexType> lub = lubBuilder.build();
+    InterfaceCollection lub = lubBuilder.build();
     // Cache the computation result only if the given two sets of interfaces are different.
-    if (s1.size() != s2.size() || !s1.containsAll(s2)) {
+    if (!s1.equals(s2)) {
       synchronized (appView.dexItemFactory().leastUpperBoundOfInterfacesTable) {
         appView.dexItemFactory().leastUpperBoundOfInterfacesTable.put(s1, s2, lub);
       }
@@ -386,14 +465,6 @@
     if (!type.equals(other.type)) {
       return false;
     }
-    Set<DexType> thisInterfaces = getInterfaces();
-    Set<DexType> otherInterfaces = other.getInterfaces();
-    if (thisInterfaces == otherInterfaces) {
-      return true;
-    }
-    if (thisInterfaces.size() != otherInterfaces.size()) {
-      return false;
-    }
-    return thisInterfaces.containsAll(otherInterfaces);
+    return getInterfaces().equals(other.getInterfaces());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/InterfaceCollection.java b/src/main/java/com/android/tools/r8/ir/analysis/type/InterfaceCollection.java
new file mode 100644
index 0000000..7b80c82
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/InterfaceCollection.java
@@ -0,0 +1,163 @@
+// Copyright (c) 2021, 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.type;
+
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Pair;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMap;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMap.Entry;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMaps;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
+
+public class InterfaceCollection {
+
+  public static boolean isKnownToImplement(
+      DexType iface, DexType implementor, InternalOptions options) {
+    if (options.canHaveZipFileWithMissingCloseableBug()
+        && implementor == options.dexItemFactory().zipFileType
+        && iface == options.dexItemFactory().closeableType) {
+      return false;
+    }
+    return true;
+  }
+
+  public static class Builder {
+    private Reference2BooleanMap<DexType> interfaces = new Reference2BooleanOpenHashMap<>();
+
+    private Builder() {}
+
+    public Builder addInterface(DexType iface, DexClass implementor, InternalOptions options) {
+      return addInterface(
+          iface,
+          !implementor.isLibraryClass()
+              || isKnownToImplement(iface, implementor.getType(), options));
+    }
+
+    public Builder addInterface(DexType iface, DexType implementor, InternalOptions options) {
+      return addInterface(iface, isKnownToImplement(iface, implementor, options));
+    }
+
+    public Builder addInterface(DexType type, boolean isKnown) {
+      interfaces.compute(
+          type,
+          (existingType, existingIsKnown) ->
+              // If the entry is new 'existingIsKnown == null', so we join with (null or true).
+              (existingIsKnown == null || existingIsKnown) && isKnown);
+      return this;
+    }
+
+    public InterfaceCollection build() {
+      if (interfaces.isEmpty()) {
+        return InterfaceCollection.empty();
+      }
+      return new InterfaceCollection(interfaces);
+    }
+  }
+
+  private static final InterfaceCollection EMPTY =
+      new InterfaceCollection(Reference2BooleanMaps.emptyMap());
+
+  public static InterfaceCollection empty() {
+    return EMPTY;
+  }
+
+  public static InterfaceCollection singleton(DexType type) {
+    return new InterfaceCollection(Reference2BooleanMaps.singleton(type, true));
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Set of interfaces mapping to an optional presence.
+   *
+   * <ul>
+   *   <li>An unmapped type is known to not be present.
+   *   <li>A type mapped to true is known to always be present.
+   *   <li>A type mapped to false is not always known to be present.
+   */
+  private final Reference2BooleanMap<DexType> interfaces;
+
+  private InterfaceCollection(Reference2BooleanMap<DexType> interfaces) {
+    assert interfaces != null;
+    this.interfaces = interfaces;
+  }
+
+  public boolean isEmpty() {
+    return interfaces.isEmpty();
+  }
+
+  public int size() {
+    return interfaces.size();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof InterfaceCollection)) {
+      return false;
+    }
+    InterfaceCollection that = (InterfaceCollection) o;
+    return interfaces.equals(that.interfaces);
+  }
+
+  @Override
+  public int hashCode() {
+    return interfaces.hashCode();
+  }
+
+  public void forEach(BiConsumer<DexType, Boolean> fn) {
+    interfaces.forEach(fn::accept);
+  }
+
+  public boolean anyMatch(BiPredicate<DexType, Boolean> fn) {
+    for (Entry<DexType> entry : interfaces.reference2BooleanEntrySet()) {
+      if (fn.test(entry.getKey(), entry.getBooleanValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public List<Pair<DexType, Boolean>> getInterfaceList() {
+    List<Pair<DexType, Boolean>> list = new ArrayList<>(interfaces.size());
+    interfaces.forEach((iface, isKnown) -> list.add(new Pair<>(iface, isKnown)));
+    return list;
+  }
+
+  public boolean hasSingleKnownInterface() {
+    DexType singleKnownInterface = getSingleKnownInterface();
+    return singleKnownInterface != null;
+  }
+
+  public DexType getSingleKnownInterface() {
+    if (interfaces.size() != 1) {
+      return null;
+    }
+    DexType type = interfaces.keySet().iterator().next();
+    return interfaces.getBoolean(type) ? type : null;
+  }
+
+  public OptionalBool contains(DexType type) {
+    Boolean value = interfaces.get(type);
+    if (value == null) {
+      return OptionalBool.FALSE;
+    }
+    return value ? OptionalBool.TRUE : OptionalBool.unknown();
+  }
+
+  public boolean containsKnownInterface(DexType type) {
+    return contains(type).isTrue();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
index 94b9489..d06fd44 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
@@ -18,7 +18,6 @@
 import java.util.Comparator;
 import java.util.Deque;
 import java.util.List;
-import java.util.Set;
 
 public class TypeAnalysis {
 
@@ -181,9 +180,9 @@
       ClassTypeElement classType = receiverUpperBoundType.asClassType();
       DexType refinedType = classType.getClassType();
       if (refinedType == appView.dexItemFactory().objectType) {
-        Set<DexType> interfaces = classType.getInterfaces();
-        if (interfaces.size() == 1) {
-          refinedType = interfaces.iterator().next();
+        DexType singleKnownInterface = classType.getInterfaces().getSingleKnownInterface();
+        if (singleKnownInterface != null) {
+          refinedType = singleKnownInterface;
         }
       }
       if (appView.appInfo().isSubtype(refinedType, staticReceiverType)) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
index a2c8d57..9736b13 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
@@ -147,11 +147,7 @@
    * @return {@code true} if {@param this} is strictly less than {@param other}.
    */
   public boolean strictlyLessThan(TypeElement other, AppView<?> appView) {
-    if (equals(other)) {
-      return false;
-    }
-    TypeElement lub = join(other, appView);
-    return !equals(lub) && other.equals(lub);
+    return !equals(other) && internalLessThan(other, appView);
   }
 
   /**
@@ -163,7 +159,13 @@
    * @return {@code true} if {@param this} is less than or equal to {@param other}.
    */
   public boolean lessThanOrEqual(TypeElement other, AppView<?> appView) {
-    return equals(other) || strictlyLessThan(other, appView);
+    return equals(other) || internalLessThan(other, appView);
+  }
+
+  private boolean internalLessThan(TypeElement other, AppView<?> appView) {
+    // The equals check has already been done by callers, so only the join is computed.
+    TypeElement lub = join(other, appView);
+    return !equals(lub) && other.equals(lub);
   }
 
   /**
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 01cf2ba..efc52d1 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
@@ -596,9 +596,13 @@
       if (instruction.outValue != null && instruction.outValue.getType().isClassType()) {
         ClassTypeElement classTypeLattice = instruction.outValue.getType().asClassType();
         assert !verticallyMergedClasses.hasBeenMergedIntoSubtype(classTypeLattice.getClassType());
-        for (DexType itf : classTypeLattice.getInterfaces()) {
-          assert !verticallyMergedClasses.hasBeenMergedIntoSubtype(itf);
-        }
+        assert !classTypeLattice
+            .getInterfaces()
+            .anyMatch(
+                (itf, isKnown) -> {
+                  assert !verticallyMergedClasses.hasBeenMergedIntoSubtype(itf);
+                  return false;
+                });
       }
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
index 29b8a3b..1ef9cf5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection.Builder;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
@@ -19,9 +21,7 @@
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
-import com.google.common.collect.ImmutableSet;
 import java.util.List;
-import java.util.Set;
 
 public final class InvokeCustom extends Invoke {
 
@@ -44,19 +44,18 @@
   }
 
   private static boolean verifyLambdaInterfaces(
-      TypeElement returnType, Set<DexType> lambdaInterfaceSet, DexType objectType) {
-    Set<DexType> primaryInterfaces = returnType.asClassType().getInterfaces();
+      TypeElement returnType, InterfaceCollection lambdaInterfaceSet, DexType objectType) {
+    InterfaceCollection primaryInterfaces = returnType.asClassType().getInterfaces();
     if (returnType.asClassType().getClassType() == objectType) {
-      assert primaryInterfaces.size() == 1;
       // The interfaces returned by the LambdaDescriptor assumed to already contain the primary
       // interface. If they're both singleton lists they must be identical and we can return the
       // primary return type.
-      assert lambdaInterfaceSet.contains(primaryInterfaces.iterator().next());
+      assert lambdaInterfaceSet.containsKnownInterface(primaryInterfaces.getSingleKnownInterface());
     } else {
       // We arrive here if the primary interface is a missing class. In that case the
       // returnType will be the missing type as the class type.
       assert primaryInterfaces.isEmpty();
-      assert lambdaInterfaceSet.contains(returnType.asClassType().getClassType());
+      assert lambdaInterfaceSet.containsKnownInterface(returnType.asClassType().getClassType());
     }
     return true;
   }
@@ -76,20 +75,21 @@
     // The primary return type is either an interface or a missing type.
     assert returnType instanceof ClassTypeElement;
 
-    Set<DexType> primaryInterfaces = returnType.asClassType().getInterfaces();
+    InterfaceCollection primaryInterfaces = returnType.asClassType().getInterfaces();
     DexType objectType = appView.dexItemFactory().objectType;
 
     if (returnType.asClassType().getClassType() == objectType) {
-      assert primaryInterfaces.size() == 1;
+      assert primaryInterfaces.hasSingleKnownInterface();
       // Shortcut for the common case: single interface. Save creating a new lattice type.
       if (lambdaInterfaces.size() == 1) {
-        assert lambdaInterfaces.get(0) == primaryInterfaces.iterator().next();
+        assert lambdaInterfaces.get(0) == primaryInterfaces.getSingleKnownInterface();
         return returnType;
       }
     }
 
-    Set<DexType> lambdaInterfaceSet =
-        ImmutableSet.<DexType>builder().addAll(lambdaInterfaces).build();
+    Builder builder = InterfaceCollection.builder();
+    lambdaInterfaces.forEach(iface -> builder.addInterface(iface, true));
+    InterfaceCollection lambdaInterfaceSet = builder.build();
 
     assert verifyLambdaInterfaces(returnType, lambdaInterfaceSet, objectType);
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index f673395..11b4ddf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -955,7 +955,7 @@
         return true;
       }
       if (arrayBaseType.isClassType()) {
-        return arrayBaseType.asClassType().getInterfaces().size() == 0;
+        return arrayBaseType.asClassType().getInterfaces().isEmpty();
       }
       return false;
     }
@@ -972,8 +972,8 @@
         // have a common super interface nor are they implemented by a common superclass so the
         // argument type of the outline will be java.lang.Object.
         if (valueClassType.getClassType() == objectType
-            && valueClassType.getInterfaces().size() == 1) {
-          return valueClassType.getInterfaces().iterator().next();
+            && valueClassType.getInterfaces().hasSingleKnownInterface()) {
+          return valueClassType.getInterfaces().getSingleKnownInterface();
         } else {
           return valueClassType.getClassType();
         }
diff --git a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
index a8bd96c..8e19257 100644
--- a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
@@ -643,19 +643,26 @@
             clazz -> {
               // TODO(b/133091438): Extend the if check to test for !clazz.isLibrary().
               if (!clazz.isInterface()) {
-                for (DexType directlyImplemented :
-                    appView.appInfo().implementedInterfaces(clazz.type)) {
-                  InterfaceReservationState iState = interfaceStateMap.get(directlyImplemented);
-                  if (iState != null) {
-                    DexType frontierType = minifierState.getFrontier(clazz.type);
-                    iState.addReservationType(frontierType);
-                    // The reservation state should already be added, but if a class is extending
-                    // an interface, we will not visit the class during the sub-type traversel
-                    if (minifierState.getReservationState(clazz.type) == null) {
-                      minifierState.allocateReservationStateAndReserve(clazz.type, frontierType);
-                    }
-                  }
-                }
+                appView
+                    .appInfo()
+                    .implementedInterfaces(clazz.type)
+                    .forEach(
+                        (directlyImplemented, ignoreIsKnownAndReserveInAllCases) -> {
+                          InterfaceReservationState iState =
+                              interfaceStateMap.get(directlyImplemented);
+                          if (iState != null) {
+                            DexType frontierType = minifierState.getFrontier(clazz.type);
+                            iState.addReservationType(frontierType);
+                            // The reservation state should already be added, but if a class is
+                            // extending
+                            // an interface, we will not visit the class during the sub-type
+                            // traversel
+                            if (minifierState.getReservationState(clazz.type) == null) {
+                              minifierState.allocateReservationStateAndReserve(
+                                  clazz.type, frontierType);
+                            }
+                          }
+                        });
               }
             });
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 7443af8..8e6b599 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -1352,12 +1352,7 @@
         // The type java.lang.Object could be any instantiated type. Assume a finalizer exists.
         return true;
       }
-      for (DexType iface : type.getInterfaces()) {
-        if (mayHaveFinalizer(iface)) {
-          return true;
-        }
-      }
-      return false;
+      return type.getInterfaces().anyMatch((iface, isKnown) -> mayHaveFinalizer(iface));
     }
     return mayHaveFinalizer(type.getClassType());
   }
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 84b76fd..429de15 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2016,4 +2016,11 @@
   public boolean canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug() {
     return isGeneratingClassFiles() || minApiLevel < AndroidApiLevel.L.getLevel();
   }
+
+  // The standard library prior to API 19 did not contain a ZipFile that implemented Closable.
+  //
+  // See b/177532008.
+  public boolean canHaveZipFileWithMissingCloseableBug() {
+    return isGeneratingClassFiles() || minApiLevel < AndroidApiLevel.K.getLevel();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
index 82ee9e8..83164c5 100644
--- a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
+++ b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
@@ -9,9 +9,11 @@
 
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
+import java.util.HashSet;
 import java.util.Set;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -53,7 +55,7 @@
 
     // class ArrayList implements List
     DexType arrayList = factory.createType("Ljava/util/ArrayList;");
-    Set<DexType> interfaces = appInfo.implementedInterfaces(arrayList);
+    Set<DexType> interfaces = setOfAll(appInfo.implementedInterfaces(arrayList));
     assertThat(interfaces, hasItems(serializable));
     assertThat(interfaces, hasItems(iterable));
     assertThat(interfaces, hasItems(collection));
@@ -62,7 +64,7 @@
 
     // class LinkedList implements List, Deque
     DexType linkedList = factory.createType("Ljava/util/LinkedList;");
-    interfaces = appInfo.implementedInterfaces(linkedList);
+    interfaces = setOfAll(appInfo.implementedInterfaces(linkedList));
     assertThat(interfaces, hasItems(serializable));
     assertThat(interfaces, hasItems(iterable));
     assertThat(interfaces, hasItems(collection));
@@ -71,6 +73,12 @@
     assertThat(interfaces, hasItems(queue));
   }
 
+  private static Set<DexType> setOfAll(InterfaceCollection interfaces) {
+    HashSet<DexType> set = new HashSet<>(interfaces.size());
+    interfaces.forEach((iface, ignoredIsKnown) -> set.add(iface));
+    return set;
+  }
+
   @Test
   public void implementedInterfaces_collections_kotlin() {
     DexType iterable = factory.createType("Ljava/lang/Iterable;");
@@ -84,13 +92,13 @@
     DexType ktAbsList = factory.createType("Lkotlin/collections/AbstractList;");
     DexType ktAbsSet = factory.createType("Lkotlin/collections/AbstractSet;");
 
-    Set<DexType> interfaces = appInfo.implementedInterfaces(ktAbsList);
+    Set<DexType> interfaces = setOfAll(appInfo.implementedInterfaces(ktAbsList));
     assertThat(interfaces, hasItems(iterable));
     assertThat(interfaces, hasItems(collection));
     assertThat(interfaces, hasItems(list));
     assertThat(interfaces, not(hasItems(set)));
 
-    interfaces = appInfo.implementedInterfaces(ktAbsSet);
+    interfaces = setOfAll(appInfo.implementedInterfaces(ktAbsSet));
     assertThat(interfaces, hasItems(iterable));
     assertThat(interfaces, hasItems(collection));
     assertThat(interfaces, hasItems(set));
@@ -107,7 +115,7 @@
     DexType pType = factory.createType("Ljava/lang/reflect/ParameterizedType;");
     DexType klass = factory.createType("Ljava/lang/Class;");
 
-    Set<DexType> interfaces = appInfo.implementedInterfaces(klass);
+    Set<DexType> interfaces = setOfAll(appInfo.implementedInterfaces(klass));
     assertThat(interfaces, hasItems(serializable));
     assertThat(interfaces, hasItems(annotatedElement));
     assertThat(interfaces, hasItems(genericDeclaration));
@@ -130,7 +138,7 @@
     DexType mutableReference0 =
         factory.createType("Lkotlin/jvm/internal/MutablePropertyReference0;");
 
-    Set<DexType> interfaces = appInfo.implementedInterfaces(mutableReference0);
+    Set<DexType> interfaces = setOfAll(appInfo.implementedInterfaces(mutableReference0));
     assertThat(interfaces, hasItems(kCallable));
     assertThat(interfaces, hasItems(kProperty));
     assertThat(interfaces, hasItems(kMutableProperty));
@@ -147,15 +155,14 @@
     // interface Function0 : Function
     DexType function0 = factory.createType("Lkotlin/jvm/functions/Function0;");
 
-    Set<DexType> interfaces = appInfo.implementedInterfaces(lambda);
+    Set<DexType> interfaces = setOfAll(appInfo.implementedInterfaces(lambda));
     assertThat(interfaces, not(hasItems(lambda)));
     assertThat(interfaces, hasItems(function));
     assertThat(interfaces, hasItems(functionBase));
 
-    interfaces = appInfo.implementedInterfaces(function0);
+    interfaces = setOfAll(appInfo.implementedInterfaces(function0));
     assertThat(interfaces, hasItems(function0));
     assertThat(interfaces, hasItems(function));
     assertThat(interfaces, not(hasItems(functionBase)));
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeElementWidthTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeElementWidthTest.java
index 304d344..c07c431 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeElementWidthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeElementWidthTest.java
@@ -10,7 +10,6 @@
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
 
 public class TypeElementWidthTest extends TestBase {
@@ -78,7 +77,7 @@
     DexItemFactory dexItemFactory = new DexItemFactory();
     ClassTypeElement referenceType =
         ClassTypeElement.create(
-            dexItemFactory.objectType, Nullability.maybeNull(), ImmutableSet.of());
+            dexItemFactory.objectType, Nullability.maybeNull(), InterfaceCollection.empty());
     assertFalse(referenceType.isSinglePrimitive());
     assertFalse(referenceType.isWidePrimitive());
     assertEquals(1, referenceType.requiredRegisters());
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
index 5cdd0ca..4f15b2d 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection.Builder;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -554,6 +555,14 @@
         Nullability.bottom(), ReferenceTypeElement.getNull().asMeetWithNotNull().nullability());
   }
 
+  private static InterfaceCollection itfs(DexType... types) {
+    Builder builder = InterfaceCollection.builder();
+    for (DexType type : types) {
+      builder.addInterface(type, true);
+    }
+    return builder.build();
+  }
+
   @Test
   public void testLeastUpperBoundOfInterfaces() {
     DexType collection = factory.createType("Ljava/util/Collection;");
@@ -561,37 +570,32 @@
     DexType list = factory.createType("Ljava/util/List;");
     DexType serializable = factory.serializableType;
 
-    Set<DexType> lub =
-        computeLeastUpperBoundOfInterfaces(appView, ImmutableSet.of(set), ImmutableSet.of(list));
+    InterfaceCollection lub = computeLeastUpperBoundOfInterfaces(appView, itfs(set), itfs(list));
     assertEquals(1, lub.size());
-    assertTrue(lub.contains(collection));
-    verifyViaPairwiseJoin(lub,
-        ImmutableSet.of(set), ImmutableSet.of(list));
+    assertTrue(lub.containsKnownInterface(collection));
+    assertEquals(collection, lub.getSingleKnownInterface());
+    verifyViaPairwiseJoin(lub, ImmutableSet.of(set), ImmutableSet.of(list));
 
     lub =
         computeLeastUpperBoundOfInterfaces(
-            appView, ImmutableSet.of(set, serializable), ImmutableSet.of(list, serializable));
+            appView, itfs(set, serializable), itfs(list, serializable));
     assertEquals(2, lub.size());
-    assertTrue(lub.contains(collection));
-    assertTrue(lub.contains(serializable));
+    assertTrue(lub.containsKnownInterface(collection));
+    assertTrue(lub.containsKnownInterface(serializable));
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(set, serializable), ImmutableSet.of(list, serializable));
 
-    lub =
-        computeLeastUpperBoundOfInterfaces(
-            appView, ImmutableSet.of(set), ImmutableSet.of(list, serializable));
+    lub = computeLeastUpperBoundOfInterfaces(appView, itfs(set), itfs(list, serializable));
     assertEquals(1, lub.size());
-    assertTrue(lub.contains(collection));
-    assertFalse(lub.contains(serializable));
+    assertTrue(lub.containsKnownInterface(collection));
+    assertFalse(lub.containsKnownInterface(serializable));
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(set), ImmutableSet.of(list, serializable));
 
-    lub =
-        computeLeastUpperBoundOfInterfaces(
-            appView, ImmutableSet.of(set, serializable), ImmutableSet.of(list));
+    lub = computeLeastUpperBoundOfInterfaces(appView, itfs(set, serializable), itfs(list));
     assertEquals(1, lub.size());
-    assertTrue(lub.contains(collection));
-    assertFalse(lub.contains(serializable));
+    assertTrue(lub.containsKnownInterface(collection));
+    assertFalse(lub.containsKnownInterface(serializable));
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(set, serializable), ImmutableSet.of(list));
 
@@ -599,16 +603,14 @@
     DexType wType = factory.createType("Ljava/lang/reflect/WildcardType;");
     DexType pType = factory.createType("Ljava/lang/reflect/ParameterizedType;");
 
-    lub =
-        computeLeastUpperBoundOfInterfaces(appView, ImmutableSet.of(wType), ImmutableSet.of(pType));
+    lub = computeLeastUpperBoundOfInterfaces(appView, itfs(wType), itfs(pType));
     assertEquals(1, lub.size());
-    assertTrue(lub.contains(type));
+    assertTrue(lub.containsKnownInterface(type));
+    assertEquals(type, lub.getSingleKnownInterface());
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(wType), ImmutableSet.of(pType));
 
-    lub =
-        computeLeastUpperBoundOfInterfaces(
-            appView, ImmutableSet.of(list, serializable), ImmutableSet.of(pType));
+    lub = computeLeastUpperBoundOfInterfaces(appView, itfs(list, serializable), itfs(pType));
     assertEquals(0, lub.size());
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(list, serializable), ImmutableSet.of(pType));
@@ -621,41 +623,36 @@
         DescriptorUtils.javaTypeToDescriptor(I3.class.getCanonicalName()));
     DexType i4 = factory.createType(
         DescriptorUtils.javaTypeToDescriptor(I4.class.getCanonicalName()));
-    lub = computeLeastUpperBoundOfInterfaces(appView, ImmutableSet.of(i3), ImmutableSet.of(i4));
+    lub = computeLeastUpperBoundOfInterfaces(appView, itfs(i3), itfs(i4));
     assertEquals(2, lub.size());
-    assertTrue(lub.contains(i1));
-    assertTrue(lub.contains(i2));
+    assertTrue(lub.containsKnownInterface(i1));
+    assertTrue(lub.containsKnownInterface(i2));
     verifyViaPairwiseJoin(lub,
         ImmutableSet.of(i3), ImmutableSet.of(i4));
   }
 
-  private void verifyViaPairwiseJoin(Set<DexType> lub, Set<DexType> s1, Set<DexType>s2) {
-    ImmutableSet.Builder<DexType> builder = ImmutableSet.builder();
+  private void verifyViaPairwiseJoin(InterfaceCollection lub, Set<DexType> s1, Set<DexType> s2) {
+    InterfaceCollection.Builder builder = InterfaceCollection.builder();
     for (DexType i1 : s1) {
       for (DexType i2 : s2) {
-        Set<DexType> lubPerPair =
-            computeLeastUpperBoundOfInterfaces(appView, ImmutableSet.of(i1), ImmutableSet.of(i2));
-        for (DexType lubInterface : lubPerPair) {
-          builder.add(lubInterface);
-        }
+        InterfaceCollection lubPerPair =
+            computeLeastUpperBoundOfInterfaces(
+                appView, InterfaceCollection.singleton(i1), InterfaceCollection.singleton(i2));
+        lubPerPair.forEach(builder::addInterface);
       }
     }
-    Set<DexType> pairwiseJoin = builder.build();
-    ImmutableSet.Builder<DexType> lubBuilder = ImmutableSet.builder();
-    for (DexType itf : pairwiseJoin) {
-      // If there is a strict sub interface of this interface, it is not the least element.
-      if (pairwiseJoin.stream()
-          .anyMatch(other -> appView.appInfo().isStrictSubtypeOf(other, itf))) {
-        continue;
-      }
-      lubBuilder.add(itf);
-    }
-    Set<DexType> pairwiseLub = lubBuilder.build();
-
-    assertEquals(pairwiseLub.size(), lub.size());
-    for (DexType i : pairwiseLub) {
-      assertTrue(lub.contains(i));
-    }
+    InterfaceCollection pairwiseJoin = builder.build();
+    InterfaceCollection.Builder lubBuilder = InterfaceCollection.builder();
+    pairwiseJoin.forEach(
+        (itf, isKnown) -> {
+          // If there is a strict sub interface of this interface, it is not the least element.
+          if (!pairwiseJoin.anyMatch(
+              (other, ignoredOtherIsKnown) -> appView.appInfo().isStrictSubtypeOf(other, itf))) {
+            lubBuilder.addInterface(itf, isKnown);
+          }
+        });
+    InterfaceCollection pairwiseLub = lubBuilder.build();
+    assertEquals(pairwiseLub, lub);
   }
 
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
index b975ca0..b15cca9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
@@ -3,16 +3,31 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.instanceofremoval;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 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.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import java.io.IOException;
+import java.nio.channels.Channel;
 import java.nio.file.Path;
 import java.util.jar.JarFile;
 import org.junit.Test;
@@ -80,15 +95,117 @@
         .setMinApi(parameters.getApiLevel())
         .addLibraryFiles(getAndroidJar())
         .run(parameters.getRuntime(), TestClass.class, getZipFile())
-        .apply(
-            result -> {
-              if (!runtimeZipFileIsCloseable()) {
-                // TODO(b/177532008): This should succeed with the usual expected output.
-                result.assertFailure();
-              } else {
-                result.assertSuccessWithOutput(expectedOutput());
-              }
-            });
+        .assertSuccessWithOutput(expectedOutput());
+  }
+
+  @Test
+  public void testTypeStructure() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    // Set the min API and create the raw app.
+    InternalOptions options = new InternalOptions();
+    options.minApiLevel = parameters.getApiLevel().getLevel();
+    DirectMappedDexApplication application =
+        new ApplicationReader(
+                AndroidApp.builder()
+                    .addProgramFiles(ToolHelper.getClassFileForTestClass(MyJarFileScanner.class))
+                    .addLibraryFiles(getAndroidJar())
+                    .build(),
+                options,
+                Timing.empty())
+            .read()
+            .toDirect();
+    AppView<AppInfoWithClassHierarchy> appView = AppView.createForR8(application);
+
+    // Type references.
+    DexType zipFileType = appView.dexItemFactory().createType("Ljava/util/zip/ZipFile;");
+    DexType closeableType = appView.dexItemFactory().createType("Ljava/io/Closeable;");
+    DexType autoCloseableType = appView.dexItemFactory().createType("Ljava/lang/AutoCloseable;");
+    DexType scannerType = appView.dexItemFactory().createType("Ljava/util/Scanner;");
+    DexType myJarFileChannel = buildType(MyJarFileScanner.class, appView.dexItemFactory());
+
+    // Read the computed interface types. Both the "type element" which is used in IR and the full
+    // computed dependencies.
+    ClassTypeElement zipFileTypeElement = makeTypeElement(appView, zipFileType);
+    {
+      InterfaceCollection directInterfaces = zipFileTypeElement.getInterfaces();
+      InterfaceCollection allInterfaces = appView.appInfo().implementedInterfaces(zipFileType);
+      if (zipFileHasCloseable()) {
+        // After API 19 / K the types are known and present.
+        assertEquals(closeableType, directInterfaces.getSingleKnownInterface());
+        assertTrue(allInterfaces.containsKnownInterface(closeableType));
+        assertTrue(allInterfaces.containsKnownInterface(autoCloseableType));
+        assertEquals(2, allInterfaces.size());
+      } else {
+        // The interfaces are still present due to the android jar for K, but they are marked
+        // unknown.
+        assertTrue(directInterfaces.contains(closeableType).isUnknown());
+        assertFalse(directInterfaces.containsKnownInterface(closeableType));
+        assertEquals(1, directInterfaces.size());
+        // Same for the collection of all interfaces. Since the
+        assertTrue(allInterfaces.contains(closeableType).isUnknown());
+        assertTrue(allInterfaces.contains(autoCloseableType).isUnknown());
+        assertFalse(allInterfaces.containsKnownInterface(closeableType));
+        assertFalse(allInterfaces.containsKnownInterface(autoCloseableType));
+        assertEquals(2, allInterfaces.size());
+      }
+    }
+
+    ClassTypeElement scannerTypeElement = makeTypeElement(appView, scannerType);
+    {
+      // Scanner implements Closable and Iterator on all APIs.
+      InterfaceCollection directInterfaces = scannerTypeElement.getInterfaces();
+      assertTrue(directInterfaces.containsKnownInterface(closeableType));
+      assertEquals(2, directInterfaces.size());
+
+      // Joining a type of known with a type of unknown should still be unknown.
+      ClassTypeElement joinLeft1 =
+          scannerTypeElement.join(zipFileTypeElement, appView).asClassType();
+      ClassTypeElement joinRight1 =
+          zipFileTypeElement.join(scannerTypeElement, appView).asClassType();
+      if (zipFileHasCloseable()) {
+        assertTrue(joinLeft1.getInterfaces().contains(closeableType).isTrue());
+        assertTrue(joinRight1.getInterfaces().contains(closeableType).isTrue());
+      } else {
+        assertTrue(joinLeft1.getInterfaces().contains(closeableType).isUnknown());
+        assertTrue(joinRight1.getInterfaces().contains(closeableType).isUnknown());
+      }
+    }
+
+    // Custom class derived from JarFile and Channel, thus it must implement closable.
+    ClassTypeElement myJarFileScannerTypeElement = makeTypeElement(appView, myJarFileChannel);
+
+    // Joining with Scanner will retain it as always Closeable.
+    ClassTypeElement joinWithScanner =
+        myJarFileScannerTypeElement.join(scannerTypeElement, appView).asClassType();
+    assertTrue(joinWithScanner.getInterfaces().contains(closeableType).isTrue());
+
+    // Joining with ZipFile will loose the assurance that it is Closeable.
+    ClassTypeElement joinWithZipFile =
+        myJarFileScannerTypeElement.join(zipFileTypeElement, appView).asClassType();
+    assertTrue(joinWithZipFile.getInterfaces().contains(closeableType).isPossiblyTrue());
+    assertEquals(
+        zipFileHasCloseable(), joinWithZipFile.getInterfaces().contains(closeableType).isTrue());
+  }
+
+  private boolean zipFileHasCloseable() {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
+  }
+
+  private static ClassTypeElement makeTypeElement(
+      AppView<AppInfoWithClassHierarchy> appView, DexType type) {
+    return ClassTypeElement.create(type, Nullability.maybeNull(), appView);
+  }
+
+  static class MyJarFileScanner extends JarFile implements Channel {
+
+    public MyJarFileScanner(String name) throws IOException {
+      super(name);
+    }
+
+    @Override
+    public boolean isOpen() {
+      return false;
+    }
   }
 
   static class TestClass {