Support for $closeResource synthesized by java 9 compiler

Pilot implementation of rewriting $closeResource(...) methods
synthesized by java 9 compiler for some cases of try-with-resources.

The problem with current implementation is that on pre-19 devices
even though java.lang.AutoCloseable is available in platform (not in
android.jar), is not fully supported in a sense that not all classes
implement it properly.

We have to replace it with our version of $closeResource(...) which
takes the resource as java.lang.Object and checks if it can cast
it to AutoCloseable.

To avoid duplicating the methods we also use one single method defined
in an utility class '$r8$twr$utility' created when needed. This class
needs to be deduped for incremental dexing scenarios.

Bug: 70292540
Change-Id: I4da89a8482ab6895267e58c3debc7ad4e12a3dfe
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 3f02009..94f914f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -138,6 +138,7 @@
   public final DexString objectDescriptor = createString("Ljava/lang/Object;");
   public final DexString objectArrayDescriptor = createString("[Ljava/lang/Object;");
   public final DexString classDescriptor = createString("Ljava/lang/Class;");
+  public final DexString autoCloseableDescriptor = createString("Ljava/lang/AutoCloseable;");
   public final DexString classArrayDescriptor = createString("[Ljava/lang/Class;");
   public final DexString fieldDescriptor = createString("Ljava/lang/reflect/Field;");
   public final DexString methodDescriptor = createString("Ljava/lang/reflect/Method;");
@@ -196,6 +197,7 @@
   public final DexType annotationType = createType(annotationDescriptor);
   public final DexType throwableType = createType(throwableDescriptor);
   public final DexType classType = createType(classDescriptor);
+  public final DexType autoCloseableType = createType(autoCloseableDescriptor);
 
   public final DexType stringBuilderType = createType(stringBuilderDescriptor);
   public final DexType stringBufferType = createType(stringBufferDescriptor);
@@ -218,6 +220,10 @@
   public final Kotlin kotlin;
   public final PolymorphicMethods polymorphicMethods = new PolymorphicMethods();
 
+  public final DexString twrCloseResourceMethodName = createString("$closeResource");
+  public final DexProto twrCloseResourceMethodProto =
+      createProto(voidType, throwableType, autoCloseableType);
+
   // Dex system annotations.
   // See https://source.android.com/devices/tech/dalvik/dex-format.html#system-annotation
   public final DexType annotationDefault = createType("Ldalvik/annotation/AnnotationDefault;");
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 629017b..c1bf4f2 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -21,6 +21,7 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Iterator;
+import java.util.function.BiFunction;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.MethodVisitor;
@@ -44,9 +45,9 @@
   private final DexMethod method;
   private final Origin origin;
   private MethodNode node;
-  private ReparseContext context;
+  protected ReparseContext context;
 
-  private final JarApplicationReader application;
+  protected final JarApplicationReader application;
 
   public JarCode(
       DexMethod method, Origin origin, ReparseContext context, JarApplicationReader application) {
@@ -239,10 +240,14 @@
   }
 
   private void parseCode(ReparseContext context, boolean useJsrInliner) {
-    SecondVisitor classVisitor = new SecondVisitor(context, application, useJsrInliner);
+    SecondVisitor classVisitor = new SecondVisitor(createCodeLocator(context), useJsrInliner);
     new ClassReader(context.classCache).accept(classVisitor, ClassReader.SKIP_FRAMES);
   }
 
+  protected BiFunction<String, String, JarCode> createCodeLocator(ReparseContext context) {
+    return new DefaultCodeLocator(context, application);
+  }
+
   private boolean hasJsr(ReparseContext context) {
     for (Code code : context.codeList) {
       if (hasJsr(code.asJarCode().node)) {
@@ -263,21 +268,34 @@
     return false;
   }
 
+  private static class DefaultCodeLocator implements BiFunction<String, String, JarCode> {
+    private final ReparseContext context;
+    private final JarApplicationReader application;
+    private int methodIndex = 0;
+
+    private DefaultCodeLocator(ReparseContext context, JarApplicationReader application) {
+      this.context = context;
+      this.application = application;
+    }
+
+    @Override
+    public JarCode apply(String name, String desc) {
+      JarCode code = context.codeList.get(methodIndex++).asJarCode();
+      assert code.method == application.getMethod(context.owner.type, name, desc);
+      return code;
+    }
+  }
+
   /**
    * Fills the MethodNodes of all the methods in the class and removes the ReparseContext.
    */
   private static class SecondVisitor extends ClassVisitor {
-
-    private final ReparseContext context;
-    private final JarApplicationReader application;
+    private final BiFunction<String, String, JarCode> codeLocator;
     private final boolean useJsrInliner;
-    private int methodIndex = 0;
 
-    public SecondVisitor(
-        ReparseContext context, JarApplicationReader application, boolean useJsrInliner) {
+    public SecondVisitor(BiFunction<String, String, JarCode> codeLocator, boolean useJsrInliner) {
       super(Opcodes.ASM6);
-      this.context = context;
-      this.application = application;
+      this.codeLocator = codeLocator;
       this.useJsrInliner = useJsrInliner;
     }
 
@@ -291,8 +309,7 @@
       JarCode code = null;
       MethodAccessFlags flags = JarClassFileReader.createMethodAccessFlags(name, access);
       if (!flags.isAbstract() && !flags.isNative()) {
-        code = context.codeList.get(methodIndex++).asJarCode();
-        assert code.method == application.getMethod(context.owner.type, name, desc);
+        code = codeLocator.apply(name, desc);
       }
       if (code != null) {
         code.context = null;
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 15627e0..f26a3b4 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
@@ -37,6 +37,7 @@
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
 import com.android.tools.r8.ir.desugar.StringConcatRewriter;
+import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
@@ -93,6 +94,7 @@
   private final StringConcatRewriter stringConcatRewriter;
   private final LambdaRewriter lambdaRewriter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
+  private final TwrCloseResourceRewriter twrCloseResourceRewriter;
   private final LambdaMerger lambdaMerger;
   private final ClassInliner classInliner;
   private final ClassStaticizer classStaticizer;
@@ -136,6 +138,9 @@
     this.interfaceMethodRewriter =
         (options.enableDesugaring && enableInterfaceMethodDesugaring())
             ? new InterfaceMethodRewriter(this, options) : null;
+    this.twrCloseResourceRewriter =
+        (options.enableDesugaring && enableTwrCloseResourceDesugaring())
+            ? new TwrCloseResourceRewriter(this) : null;
     this.lambdaMerger = options.enableLambdaMerging
         ? new LambdaMerger(appInfo.dexItemFactory, options.reporter) : null;
     this.covariantReturnTypeAnnotationTransformer =
@@ -239,6 +244,10 @@
     throw new Unreachable();
   }
 
+  private boolean enableTwrCloseResourceDesugaring() {
+    return enableTryWithResourcesDesugaring() && !options.canUseTwrCloseResourceMethod();
+  }
+
   private boolean enableTryWithResourcesDesugaring() {
     switch (options.tryWithResourcesDesugaring) {
       case Off:
@@ -289,6 +298,12 @@
     }
   }
 
+  private void synthesizeTwrCloseResourceUtilityClass(Builder<?> builder) {
+    if (twrCloseResourceRewriter != null) {
+      twrCloseResourceRewriter.synthesizeUtilityClass(builder, options);
+    }
+  }
+
   private void processCovariantReturnTypeAnnotations(Builder<?> builder) {
     if (covariantReturnTypeAnnotationTransformer != null) {
       covariantReturnTypeAnnotationTransformer.process(builder);
@@ -308,6 +323,7 @@
 
     synthesizeLambdaClasses(builder);
     desugarInterfaceMethods(builder, ExcludeDexResources);
+    synthesizeTwrCloseResourceUtilityClass(builder);
     processCovariantReturnTypeAnnotations(builder);
 
     handleSynthesizedClassMapping(builder);
@@ -475,6 +491,7 @@
 
     synthesizeLambdaClasses(builder);
     desugarInterfaceMethods(builder, IncludeAllResources);
+    synthesizeTwrCloseResourceUtilityClass(builder);
 
     handleSynthesizedClassMapping(builder);
     finalizeLambdaMerging(application, directFeedback, builder, executorService);
@@ -679,7 +696,7 @@
     printC1VisualizerHeader(method);
     printMethod(code, "Initial IR (SSA)");
 
-    DexClass holder = appInfo.definitionFor(method.method.holder);
+    DexClass holder = codeRewriter.definitionFor(method.method.holder);
     if (method.getCode() != null && method.getCode().isJarCode()
         && holder.hasKotlinInfo()) {
       computeKotlinNotNullParamHints(feedback, holder.getKotlinInfo(), method, code);
@@ -805,6 +822,10 @@
       assert code.isConsistentSSA();
     }
 
+    if (twrCloseResourceRewriter != null) {
+      twrCloseResourceRewriter.rewriteMethodCode(code);
+    }
+
     if (lambdaMerger != null) {
       lambdaMerger.processMethodCode(method, code);
       assert code.isConsistentSSA();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
new file mode 100644
index 0000000..15f1535
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
@@ -0,0 +1,205 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexApplication.Builder;
+import com.android.tools.r8.graph.DexEncodedField;
+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.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.synthetic.TemplateMethodCode;
+import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Set;
+
+// Try with resources outlining processor. Handles $closeResource methods
+// synthesized by java 9 compiler.
+//
+// Works in two phases:
+//   a. during method code processing finds all references to $closeResource(...) synthesized
+//      by java compiler, and replaces them with references to a special utility class method.
+//   b. after all methods are processed and if there was at least one method referencing
+//      $closeResource(...), it synthesizes utility class with appropriate methods.
+//
+// Note that we don't remove $closeResource(...) synthesized by java compiler, relying on
+// tree shaking to remove them since now they should not be referenced.
+//
+public final class TwrCloseResourceRewriter {
+  public static final String UTILITY_CLASS_DESCRIPTOR = "L$r8$twr$utility;";
+
+  private final IRConverter converter;
+  private final DexItemFactory factory;
+
+  private final DexMethod twrCloseResourceMethod;
+
+  private final Set<DexProgramClass> referencingClasses = Sets.newConcurrentHashSet();
+
+  public TwrCloseResourceRewriter(IRConverter converter) {
+    this.converter = converter;
+    this.factory = converter.appInfo.dexItemFactory;
+
+    DexType twrUtilityClass = factory.createType(UTILITY_CLASS_DESCRIPTOR);
+    DexProto twrCloseResourceProto = factory.createProto(
+        factory.voidType, factory.throwableType, factory.objectType);
+    this.twrCloseResourceMethod = factory.createMethod(
+        twrUtilityClass, twrCloseResourceProto, factory.twrCloseResourceMethodName);
+  }
+
+  // Rewrites calls to $closeResource() method. Can me invoked concurrently.
+  public void rewriteMethodCode(IRCode code) {
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (!instruction.isInvokeStatic()) {
+        continue;
+      }
+      InvokeStatic invoke = instruction.asInvokeStatic();
+      if (!isSynthesizedCloseResourceMethod(invoke.getInvokedMethod(), converter)) {
+        continue;
+      }
+
+      // Replace with a call to a synthetic utility with the only
+      // implementation of the method $closeResource.
+      assert invoke.outValue() == null;
+      assert invoke.inValues().size() == 2;
+      iterator.replaceCurrentInstruction(
+          new InvokeStatic(twrCloseResourceMethod, null, invoke.inValues()));
+
+      // Mark as a class referencing utility class.
+      referencingClasses.add(
+          converter.appInfo.definitionFor(code.method.method.holder).asProgramClass());
+    }
+  }
+
+  public static boolean isSynthesizedCloseResourceMethod(DexMethod method, IRConverter converter) {
+    DexMethod original = converter.getGraphLense().getOriginalMethodSignature(method);
+    assert original != null;
+    // We consider all methods of *any* class with expected name and signature
+    // to be synthesized by java 9 compiler for try-with-resources, reasoning:
+    //
+    //  * we need to look to all potential classes because the calls might be
+    //    moved by inlining.
+    //  * theoretically we could check appropriate encoded method for having
+    //    right attributes, but it still does not guarantee much since we also
+    //    need to look into code and doing this seems an overkill
+    //
+    return original.name == converter.appInfo.dexItemFactory.twrCloseResourceMethodName
+        && original.proto == converter.appInfo.dexItemFactory.twrCloseResourceMethodProto;
+  }
+
+  public void synthesizeUtilityClass(Builder<?> builder, InternalOptions options) {
+    if (referencingClasses.isEmpty()) {
+      return;
+    }
+
+    // The only encoded method.
+    TemplateMethodCode code = new CloseResourceMethodCode(options, twrCloseResourceMethod);
+    MethodAccessFlags flags = MethodAccessFlags.fromSharedAccessFlags(
+        Constants.ACC_PUBLIC | Constants.ACC_STATIC | Constants.ACC_SYNTHETIC, false);
+    DexEncodedMethod method = new DexEncodedMethod(twrCloseResourceMethod,
+        flags, DexAnnotationSet.empty(), ParameterAnnotationsList.empty(), code);
+
+    // Create utility class.
+    DexProgramClass utilityClass =
+        new DexProgramClass(
+            twrCloseResourceMethod.holder,
+            null,
+            new SynthesizedOrigin("twr utility class", getClass()),
+            ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC),
+            factory.objectType,
+            DexTypeList.empty(),
+            null,
+            null,
+            Collections.emptyList(),
+            DexAnnotationSet.empty(),
+            DexEncodedField.EMPTY_ARRAY,
+            DexEncodedField.EMPTY_ARRAY,
+            new DexEncodedMethod[]{method},
+            DexEncodedMethod.EMPTY_ARRAY,
+            factory.getSkipNameValidationForTesting(),
+            referencingClasses);
+
+    code.setUpContext(utilityClass);
+
+    // Process created class and method.
+    boolean addToMainDexList = referencingClasses.stream()
+        .anyMatch(clazz -> converter.appInfo.isInMainDexList(clazz.type));
+    converter.optimizeSynthesizedClass(utilityClass);
+    builder.addSynthesizedClass(utilityClass, addToMainDexList);
+  }
+
+  private static final class CloseResourceMethodCode extends TemplateMethodCode {
+    private static final String TEMPLATE_METHOD_NAME = "closeResourceImpl";
+    private static final String TEMPLATE_METHOD_DESC = "(Ljava/lang/Throwable;Ljava/lang/Object;)V";
+
+    CloseResourceMethodCode(InternalOptions options, DexMethod method) {
+      super(options, method, TEMPLATE_METHOD_NAME, TEMPLATE_METHOD_DESC);
+    }
+
+    // The following method defines the code of
+    //
+    //     public static void $closeResource(Throwable throwable, Object resource)
+    //
+    // method to be inserted into utility class, and will be used instead
+    // of the following implementation added by java 9 compiler:
+    //
+    //     private static void $closeResource(Throwable, AutoCloseable);
+    //        0: aload_0
+    //        1: ifnull        22
+    //        4: aload_1
+    //        5: invokeinterface #1,  1  // java/lang/AutoCloseable.close:()V
+    //       10: goto          28
+    //       13: astore_2
+    //       14: aload_0
+    //       15: aload_2
+    //       16: invokevirtual #3 // java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
+    //       19: goto          28
+    //       22: aload_1
+    //       23: invokeinterface #1,  1  // java/lang/AutoCloseable.close:()V
+    //       28: return
+    //
+    public static void closeResourceImpl(Throwable throwable, Object resource) throws Throwable {
+      try {
+        if (resource instanceof AutoCloseable) {
+          ((AutoCloseable) resource).close();
+        } else {
+          try {
+            Method method = resource.getClass().getMethod("close");
+            method.invoke(resource);
+          } catch (NoSuchMethodException | SecurityException e) {
+            throw new AssertionError(resource.getClass() + " does not have a close() method.", e);
+          } catch (IllegalAccessException
+              | IllegalArgumentException | ExceptionInInitializerError e) {
+            throw new AssertionError("Fail to call close() on " + resource.getClass(), e);
+          } catch (InvocationTargetException e) {
+            throw e.getCause();
+          }
+        }
+      } catch (Throwable e) {
+        // NOTE: we don't call addSuppressed(...) since the call will be removed
+        // by try-with-resource desugar anyways.
+        throw throwable != null ? throwable : e;
+      }
+    }
+  }
+}
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 b856cc5..32cef41 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
@@ -1561,7 +1561,7 @@
     }
   }
 
-  private DexClass definitionFor(DexType type) {
+  public DexClass definitionFor(DexType type) {
     if (cachedClass != null && cachedClass.type == type) {
       return cachedClass;
     }
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 491269e..67b39c3 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
@@ -27,6 +27,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -68,7 +69,9 @@
   }
 
   public boolean isBlackListed(DexEncodedMethod method) {
-    return blackList.contains(method.method) || appInfo.neverInline.contains(method.method);
+    return blackList.contains(method.method)
+        || appInfo.neverInline.contains(method.method)
+        || TwrCloseResourceRewriter.isSynthesizedCloseResourceMethod(method.method, converter);
   }
 
   private ConstraintWithTarget instructionAllowedForInlining(
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/TemplateMethodCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/TemplateMethodCode.java
new file mode 100644
index 0000000..fdca28c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/TemplateMethodCode.java
@@ -0,0 +1,60 @@
+// 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.ir.synthetic;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader.ReparseContext;
+import com.android.tools.r8.graph.JarCode;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.util.function.BiFunction;
+
+// Source code representing code of a method generated based on a template.
+public abstract class TemplateMethodCode extends JarCode {
+  private final String templateMethodName;
+  private final String templateMethodDesc;
+
+  protected TemplateMethodCode(
+      InternalOptions options, DexMethod method, String name, String desc) {
+    super(method, Origin.unknown(), new ReparseContext(), new JarApplicationReader(options));
+    this.templateMethodName = name;
+    this.templateMethodDesc = desc;
+  }
+
+  public void setUpContext(DexProgramClass owner) {
+    assert context != null;
+    context.owner = owner;
+    context.classCache = getClassAsBytes();
+  }
+
+  @Override
+  protected BiFunction<String, String, JarCode> createCodeLocator(ReparseContext context) {
+    return this::getCodeOrNull;
+  }
+
+  private JarCode getCodeOrNull(String name, String desc) {
+    return name.equals(templateMethodName) && desc.equals(templateMethodDesc) ? this : null;
+  }
+
+  private byte[] getClassAsBytes() {
+    Class<? extends TemplateMethodCode> clazz = this.getClass();
+    String s = clazz.getSimpleName() + ".class";
+    Class outer = clazz.getEnclosingClass();
+    while (outer != null) {
+      s = outer.getSimpleName() + '$' + s;
+      outer = outer.getEnclosingClass();
+    }
+    try {
+      return ByteStreams.toByteArray(clazz.getResourceAsStream(s));
+    } catch (IOException e) {
+      throw new Unreachable();
+    }
+  }
+}
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 1fb2551..4c05311 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -486,6 +486,10 @@
     return hasMinApi(AndroidApiLevel.L);
   }
 
+  public boolean canUseTwrCloseResourceMethod() {
+    return hasMinApi(AndroidApiLevel.K);
+  }
+
   public boolean canUsePrivateInterfaceMethods() {
     return hasMinApi(AndroidApiLevel.N);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
index ee510dd..f136486 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
@@ -53,24 +54,21 @@
   }
 
   public static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
-    // Currently only allow collapsing synthetic lambda or dispatch classes.
+    assert a.type == b.type;
+    // Currently only allow collapsing synthetic lambda, dispatch and twr-utility classes.
     if (a.originatesFromDexResource()
         && b.originatesFromDexResource()
         && a.accessFlags.isSynthetic()
         && b.accessFlags.isSynthetic()
-        && (bothAreLambdaClasses(a.type, b.type) || bothAreDispatchClasses(a.type, b.type))) {
+        && assumeClassesAreEqual(a)) {
       return a;
     }
     throw new CompilationError("Program type already present: " + a.type.toSourceString());
   }
 
-  private static boolean bothAreLambdaClasses(DexType a, DexType b) {
-    return LambdaRewriter.hasLambdaClassPrefix(a) &&
-        LambdaRewriter.hasLambdaClassPrefix(b);
-  }
-
-  private static boolean bothAreDispatchClasses(DexType a, DexType b) {
-    return InterfaceMethodRewriter.hasDispatchClassSuffix(a) &&
-        InterfaceMethodRewriter.hasDispatchClassSuffix(b);
+  private static boolean assumeClassesAreEqual(DexProgramClass a) {
+    return LambdaRewriter.hasLambdaClassPrefix(a.type)
+        || InterfaceMethodRewriter.hasDispatchClassSuffix(a.type)
+        || a.type.toString().equals(TwrCloseResourceRewriter.UTILITY_CLASS_DESCRIPTOR);
   }
 }
diff --git a/src/test/examplesJava9/twrcloseresource/Iface.class b/src/test/examplesJava9/twrcloseresource/Iface.class
new file mode 100644
index 0000000..c27b3ee
--- /dev/null
+++ b/src/test/examplesJava9/twrcloseresource/Iface.class
Binary files differ
diff --git a/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.class b/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.class
new file mode 100644
index 0000000..8d6979d
--- /dev/null
+++ b/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.class
Binary files differ
diff --git a/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.java b/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.java
new file mode 100644
index 0000000..d4dbf5b
--- /dev/null
+++ b/src/test/examplesJava9/twrcloseresource/TwrCloseResourceTest.java
@@ -0,0 +1,139 @@
+// 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 twrcloseresource;
+
+import java.util.jar.JarFile;
+
+public class TwrCloseResourceTest implements Iface {
+  public static void main(String[] args) {
+    TwrCloseResourceTest o = new TwrCloseResourceTest();
+    o.foo(args[0]);
+    o.iFoo(args[0]);
+    bar(args[0]);
+    Iface.iBar(args[0]);
+  }
+
+  synchronized void foo(String arg) {
+    try {
+      try (JarFile a = new JarFile(arg)) {
+        System.out.println("A");
+      } catch (Exception e) {
+        System.out.println("B");
+        try (JarFile a = new JarFile(arg)) {
+          System.out.println("C");
+        }
+        System.out.println("D");
+        throw new RuntimeException();
+      }
+      try (JarFile a = new JarFile(arg)) {
+        System.out.println("E");
+      }
+    } catch (Exception e) {
+      System.out.println("F");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("G");
+      try (JarFile b = new JarFile(arg)) {
+        System.out.println("H");
+      } finally {
+        System.out.println("I");
+        throw new RuntimeException();
+      }
+    } catch (Exception e) {
+      System.out.println("J");
+    }
+    System.out.println("K");
+  }
+
+  static synchronized void bar(String arg) {
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("1");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("2");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("3");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("4");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("5");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("6");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("7");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("8");
+    }
+    System.out.println("99");
+  }
+}
+
+interface Iface {
+  default void iFoo(String arg) {
+    try {
+      try (JarFile a = new JarFile(arg)) {
+        System.out.println("iA");
+      } catch (Exception e) {
+        System.out.println("iB");
+        try (JarFile a = new JarFile(arg)) {
+          System.out.println("iC");
+        }
+        System.out.println("iD");
+        throw new RuntimeException();
+      }
+      try (JarFile a = new JarFile(arg)) {
+        System.out.println("iE");
+      }
+    } catch (Exception e) {
+      System.out.println("iF");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("iG");
+      try (JarFile b = new JarFile(arg)) {
+        System.out.println("iH");
+      } finally {
+        System.out.println("iI");
+        throw new RuntimeException();
+      }
+    } catch (Exception e) {
+      System.out.println("iJ");
+    }
+    System.out.println("iK");
+  }
+
+  static void iBar(String arg) {
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("i1");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("i2");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("i3");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("i4");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("i5");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("i6");
+    }
+    try (JarFile a = new JarFile(arg)) {
+      System.out.println("i7");
+      throw new RuntimeException();
+    } catch (Exception e) {
+      System.out.println("i8");
+    }
+    System.out.println("i99");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index f149cba..48a14f3 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -50,6 +50,7 @@
     final String testName;
     final String packageName;
     final String mainClass;
+    final List<String> args = new ArrayList<>();
 
     Integer androidJarVersion = null;
 
@@ -78,6 +79,11 @@
       return withClassCheck(clazz -> clazz.forAllMethods(check));
     }
 
+    C withArg(String arg) {
+      args.add(arg);
+      return self();
+    }
+
     <T extends InstructionSubject> C withInstructionCheck(
         Predicate<InstructionSubject> filter, Consumer<T> check) {
       return withMethodCheck(method -> {
@@ -153,7 +159,7 @@
         }
       }
 
-      execute(testName, qualifiedMainClass, new Path[]{inputFile}, new Path[]{out});
+      execute(testName, qualifiedMainClass, new Path[]{inputFile}, new Path[]{out}, args);
     }
 
     abstract C withMinApiLevel(int minApiLevel);
@@ -205,6 +211,11 @@
   // Defines methods failing on JVM, specifies the output to be used for comparison.
   private static Map<String, String> expectedJvmResult =
       ImmutableMap.of(
+          "twr-close-resource",
+          "A\nE\nG\nH\nI\nJ\nK\n"
+              + "iA\niE\niG\niH\niI\niJ\niK\n"
+              + "1\n2\n3\n4\n5\n6\n7\n8\n99\n"
+              + "i1\ni2\ni3\ni4\ni5\ni6\ni7\ni8\ni99\n",
           "native-private-interface-methods",
           "0: s>i>a\n"
               + "1: d>i>s>i>a\n"
@@ -284,13 +295,21 @@
         .run();
   }
 
+  @Test
+  public void testTwrCloseResourceMethod() throws Throwable {
+    TestRunner<?> test = test("twr-close-resource", "twrcloseresource", "TwrCloseResourceTest");
+    test
+        .withMinApiLevel(AndroidApiLevel.I.getLevel())
+        .withAndroidJar(AndroidApiLevel.K.getLevel())
+        .withArg(test.getInputJar().toAbsolutePath().toString())
+        .run();
+  }
+
   abstract RunExamplesJava9Test<B>.TestRunner<?> test(String testName, String packageName,
       String mainClass);
 
-  void execute(
-      String testName,
-      String qualifiedMainClass, Path[] jars, Path[] dexes)
-      throws IOException {
+  void execute(String testName,
+      String qualifiedMainClass, Path[] jars, Path[] dexes, List<String> args) throws IOException {
 
     boolean expectedToFail = expectedToFail(testName);
     if (expectedToFail) {
@@ -299,7 +318,11 @@
     String output = ToolHelper.runArtNoVerificationErrors(
         Arrays.stream(dexes).map(Path::toString).collect(Collectors.toList()),
         qualifiedMainClass,
-        null);
+        builder -> {
+          for (String arg : args) {
+            builder.appendProgramArgument(arg);
+          }
+        });
     String jvmResult = null;
     if (expectedJvmResult.containsKey(testName)) {
       jvmResult = expectedJvmResult.get(testName);
@@ -320,4 +343,4 @@
     }
   }
 
-}
\ No newline at end of file
+}