Merge commit 'f13d18f7f2778efd7aea9fbd270558ddde38df74' into dev-release
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
new file mode 100644
index 0000000..cd00e00
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, 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.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a class, field or method as being accessed from native code via JNI.
+ *
+ * <p>When a class is annotated, member patterns can be used to define which members are to be kept.
+ * When no member patterns are specified the default pattern is to match just the class.
+ *
+ * <p>When a member is annotated, the member patterns cannot be used as the annotated member itself
+ * fully defines the item to be kept (i.e., itself).
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface UsedByNative {
+  String description() default "";
+
+  /**
+   * Conditions that should be satisfied for the annotation to be in effect.
+   *
+   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   */
+  KeepCondition[] preconditions() default {};
+
+  /** Additional targets to be kept in addition to the annotated class/members. */
+  KeepTarget[] additionalTargets() default {};
+
+  /**
+   * The target kind to be kept.
+   *
+   * <p>When annotating a class without member patterns, the default kind is {@link
+   * KeepItemKind#ONLY_CLASS}.
+   *
+   * <p>When annotating a class with member patterns, the default kind is {@link
+   * KeepItemKind#CLASS_AND_MEMBERS}.
+   *
+   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   *
+   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   */
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
+  // Member patterns. See KeepTarget for documentation.
+  MemberAccessFlags[] memberAccess() default {};
+
+  MethodAccessFlags[] methodAccess() default {};
+
+  String methodName() default "";
+
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {""};
+
+  FieldAccessFlags[] fieldAccess() default {};
+
+  String fieldName() default "";
+
+  String fieldType() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
new file mode 100644
index 0000000..3b065ca
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, 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.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark a class, field or method as being reflectively accessed.
+ *
+ * <p>Note: Before using this annotation, consider if instead you can annotate the code that is
+ * doing reflection with {@link UsesReflection}. Annotating the reflecting code is generally more
+ * clear and maintainable, and it also naturally gives rise to edges that describe just the
+ * reflected aspects of the program. The {@link UsedByReflection} annotation is suitable for cases
+ * where the reflecting code is not under user control, or in migrating away from rules.
+ *
+ * <p>When a class is annotated, member patterns can be used to define which members are to be kept.
+ * When no member patterns are specified the default pattern is to match just the class.
+ *
+ * <p>When a member is annotated, the member patterns cannot be used as the annotated member itself
+ * fully defines the item to be kept (i.e., itself).
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface UsedByReflection {
+  String description() default "";
+
+  /**
+   * Conditions that should be satisfied for the annotation to be in effect.
+   *
+   * <p>Defaults to no conditions, thus trivially/unconditionally satisfied.
+   */
+  KeepCondition[] preconditions() default {};
+
+  /** Additional targets to be kept in addition to the annotated class/members. */
+  KeepTarget[] additionalTargets() default {};
+
+  /**
+   * The target kind to be kept.
+   *
+   * <p>When annotating a class without member patterns, the default kind is {@link
+   * KeepItemKind#ONLY_CLASS}.
+   *
+   * <p>When annotating a class with member patterns, the default kind is {@link
+   * KeepItemKind#CLASS_AND_MEMBERS}.
+   *
+   * <p>When annotating a member, the default kind is {@link KeepItemKind#ONLY_MEMBERS}.
+   *
+   * <p>It is not possible to use ONLY_CLASS if annotating a member.
+   */
+  KeepItemKind kind() default KeepItemKind.DEFAULT;
+
+  // Member patterns. See KeepTarget for documentation.
+  MemberAccessFlags[] memberAccess() default {};
+
+  MethodAccessFlags[] methodAccess() default {};
+
+  String methodName() default "";
+
+  String methodReturnType() default "";
+
+  String[] methodParameters() default {""};
+
+  FieldAccessFlags[] fieldAccess() default {};
+
+  String fieldName() default "";
+
+  String fieldType() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index c8894fc..7c24ff0 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Option;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflection;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
 import com.android.tools.r8.keepanno.ast.KeepClassReference;
@@ -53,6 +54,7 @@
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
@@ -116,6 +118,10 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiClassVisitor(parent, this::setContext, className);
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionClassVisitor(descriptor, parent, this::setContext, className);
+      }
       return null;
     }
 
@@ -189,6 +195,11 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiMemberVisitor(parent, this::setContext, createItemContext());
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionMemberVisitor(
+            descriptor, parent, this::setContext, createItemContext());
+      }
       return null;
     }
 
@@ -246,6 +257,11 @@
       if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
         return new ForApiMemberVisitor(parent, this::setContext, createItemContext());
       }
+      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+          || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
+        return new UsedByReflectionMemberVisitor(
+            descriptor, parent, this::setContext, createItemContext());
+      }
       return null;
     }
   }
@@ -483,6 +499,196 @@
     }
   }
 
+  /**
+   * Parsing of @UsedByReflection or @UsedByNative on a class context.
+   *
+   * <p>When used on a class context the annotation allows the member related content of a normal
+   * item. This parser extends the base item visitor and throws an error if any class specific
+   * properties are encountered.
+   */
+  private static class UsedByReflectionClassVisitor extends KeepItemVisitorBase {
+    private final String annotationDescriptor;
+    private final String className;
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepConsequences.Builder consequences = KeepConsequences.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+
+    UsedByReflectionClassVisitor(
+        String annotationDescriptor,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        String className) {
+      this.annotationDescriptor = annotationDescriptor;
+      this.className = className;
+      this.parent = parent;
+      addContext.accept(metaInfoBuilder);
+      // The class context/holder is the annotated class.
+      visit(Item.className, className);
+    }
+
+    @Override
+    public String getAnnotationName() {
+      int sep = annotationDescriptor.lastIndexOf('/');
+      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Edge.description) && value instanceof String) {
+        metaInfoBuilder.setDescription((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.preconditions)) {
+        return new KeepPreconditionsVisitor(getAnnotationName(), builder::setPreconditions);
+      }
+      if (name.equals(UsedByReflection.additionalTargets)) {
+        return new KeepConsequencesVisitor(
+            getAnnotationName(),
+            additionalConsequences -> {
+              additionalConsequences.forEachTarget(consequences::addTarget);
+            });
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (getKind() == null && !isDefaultMemberDeclaration()) {
+        // If no explict kind is set and member declarations have been made, keep the class too.
+        visitEnum(null, Kind.DESCRIPTOR, Kind.CLASS_AND_MEMBERS);
+      }
+      super.visitEnd();
+      KeepItemReference item = getItemReference();
+      if (item.isBindingReference()) {
+        // TODO(b/248408342): The edge can have preconditions so it should support bindings!
+        throw new KeepEdgeException("@" + getAnnotationName() + " cannot reference bindings");
+      }
+      KeepItemPattern itemPattern = item.asItemPattern();
+      String descriptor = AnnotationConstants.getDescriptorFromClassTypeName(className);
+      String itemDescriptor =
+          itemPattern.getClassReference().asClassNamePattern().getExactDescriptor();
+      if (!descriptor.equals(itemDescriptor)) {
+        throw new KeepEdgeException(
+            "@" + getAnnotationName() + " must reference its class context " + className);
+      }
+      if (itemPattern.getKind().equals(KeepItemKind.ONLY_MEMBERS)) {
+        throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its class");
+      }
+      if (!itemPattern.getExtendsPattern().isAny()) {
+        throw new KeepEdgeException(
+            "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
+      }
+      consequences.addTarget(KeepTarget.builder().setItemPattern(itemPattern).build());
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setConsequences(consequences.build())
+              .build());
+    }
+  }
+
+  /**
+   * Parsing of @UsedByReflection or @UsedByNative on a member context.
+   *
+   * <p>When used on a member context the annotation does not allow member related patterns.
+   */
+  private static class UsedByReflectionMemberVisitor extends AnnotationVisitorBase {
+    private final String annotationDescriptor;
+    private final Parent<KeepEdge> parent;
+    private final KeepItemPattern context;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+
+    private final KeepConsequences.Builder consequences = KeepConsequences.builder();
+    private KeepItemKind kind = KeepItemKind.ONLY_MEMBERS;
+
+    UsedByReflectionMemberVisitor(
+        String annotationDescriptor,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        KeepItemPattern context) {
+      this.annotationDescriptor = annotationDescriptor;
+      this.parent = parent;
+      this.context = context;
+      addContext.accept(metaInfoBuilder);
+    }
+
+    @Override
+    public String getAnnotationName() {
+      int sep = annotationDescriptor.lastIndexOf('/');
+      return annotationDescriptor.substring(sep + 1, annotationDescriptor.length() - 1);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Edge.description) && value instanceof String) {
+        metaInfoBuilder.setDescription((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnum(String name, String descriptor, String value) {
+      if (!descriptor.equals(AnnotationConstants.Kind.DESCRIPTOR)) {
+        super.visitEnum(name, descriptor, value);
+      }
+      switch (value) {
+        case Kind.DEFAULT:
+          // The default value is obtained by not assigning a kind (e.g., null in the builder).
+          break;
+        case Kind.ONLY_CLASS:
+          kind = KeepItemKind.ONLY_CLASS;
+          break;
+        case Kind.ONLY_MEMBERS:
+          kind = KeepItemKind.ONLY_MEMBERS;
+          break;
+        case Kind.CLASS_AND_MEMBERS:
+          kind = KeepItemKind.CLASS_AND_MEMBERS;
+          break;
+        default:
+          super.visitEnum(name, descriptor, value);
+      }
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.preconditions)) {
+        return new KeepPreconditionsVisitor(getAnnotationName(), builder::setPreconditions);
+      }
+      if (name.equals(UsedByReflection.additionalTargets)) {
+        return new KeepConsequencesVisitor(
+            getAnnotationName(),
+            additionalConsequences -> {
+              additionalConsequences.forEachTarget(consequences::addTarget);
+            });
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (kind.equals(KeepItemKind.ONLY_CLASS)) {
+        throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its member");
+      }
+      consequences.addTarget(
+          KeepTarget.builder()
+              .setItemPattern(KeepItemPattern.builder().copyFrom(context).setKind(kind).build())
+              .build());
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setConsequences(consequences.build())
+              .build());
+    }
+  }
+
   private static class UsesReflectionVisitor extends AnnotationVisitorBase {
     private final Parent<KeepEdge> parent;
     private final KeepEdge.Builder builder = KeepEdge.builder();
@@ -766,11 +972,11 @@
   }
 
   private static class MethodDeclaration extends Declaration<KeepMethodPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepMethodAccessPattern.Builder accessBuilder = null;
     private KeepMethodPattern.Builder builder = null;
 
-    private MethodDeclaration(String annotationName) {
+    private MethodDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
     }
 
@@ -845,11 +1051,11 @@
   }
 
   private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepFieldAccessPattern.Builder accessBuilder = null;
     private KeepFieldPattern.Builder builder = null;
 
-    public FieldDeclaration(String annotationName) {
+    public FieldDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
     }
 
@@ -911,12 +1117,12 @@
   }
 
   private static class MemberDeclaration extends Declaration<KeepMemberPattern> {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private KeepMemberAccessPattern.Builder accessBuilder = null;
     private final MethodDeclaration methodDeclaration;
     private final FieldDeclaration fieldDeclaration;
 
-    MemberDeclaration(String annotationName) {
+    MemberDeclaration(Supplier<String> annotationName) {
       this.annotationName = annotationName;
       methodDeclaration = new MethodDeclaration(annotationName);
       fieldDeclaration = new FieldDeclaration(annotationName);
@@ -985,7 +1191,7 @@
     private KeepItemReference itemReference = null;
 
     KeepItemVisitorBase() {
-      memberDeclaration = new MemberDeclaration(getAnnotationName());
+      memberDeclaration = new MemberDeclaration(this::getAnnotationName);
     }
 
     public KeepItemReference getItemReference() {
@@ -1124,18 +1330,18 @@
   }
 
   private static class StringArrayVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     private final Consumer<List<String>> fn;
     private final List<String> strings = new ArrayList<>();
 
-    public StringArrayVisitor(String annotationName, Consumer<List<String>> fn) {
+    public StringArrayVisitor(Supplier<String> annotationName, Consumer<List<String>> fn) {
       this.annotationName = annotationName;
       this.fn = fn;
     }
 
     @Override
     public String getAnnotationName() {
-      return annotationName;
+      return annotationName.get();
     }
 
     @Override
@@ -1302,18 +1508,18 @@
   }
 
   private static class MemberAccessVisitor extends AnnotationVisitorBase {
-    private final String annotationName;
+    private final Supplier<String> annotationName;
     KeepMemberAccessPattern.BuilderBase<?, ?> builder;
 
     public MemberAccessVisitor(
-        String annotationName, KeepMemberAccessPattern.BuilderBase<?, ?> builder) {
+        Supplier<String> annotationName, KeepMemberAccessPattern.BuilderBase<?, ?> builder) {
       this.annotationName = annotationName;
       this.builder = builder;
     }
 
     @Override
     public String getAnnotationName() {
-      return annotationName;
+      return annotationName.get();
     }
 
     static boolean withNormalizedAccessFlag(String flag, BiPredicate<String, Boolean> fn) {
@@ -1376,7 +1582,8 @@
 
     KeepMethodAccessPattern.Builder builder;
 
-    public MethodAccessVisitor(String annotationName, KeepMethodAccessPattern.Builder builder) {
+    public MethodAccessVisitor(
+        Supplier<String> annotationName, KeepMethodAccessPattern.Builder builder) {
       super(annotationName, builder);
       this.builder = builder;
     }
@@ -1421,7 +1628,8 @@
 
     KeepFieldAccessPattern.Builder builder;
 
-    public FieldAccessVisitor(String annotationName, KeepFieldAccessPattern.Builder builder) {
+    public FieldAccessVisitor(
+        Supplier<String> annotationName, KeepFieldAccessPattern.Builder builder) {
       super(annotationName, builder);
       this.builder = builder;
     }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 0daeda1..876d04d 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -70,6 +70,23 @@
     public static final String additionalPreconditions = "additionalPreconditions";
   }
 
+  public static final class UsedByReflection {
+    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByReflection> CLASS =
+        com.android.tools.r8.keepanno.annotations.UsedByReflection.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String description = "description";
+    public static final String preconditions = "preconditions";
+    public static final String additionalTargets = "additionalTargets";
+    public static final String memberAccess = "memberAccess";
+  }
+
+  public static final class UsedByNative {
+    public static final Class<com.android.tools.r8.keepanno.annotations.UsedByNative> CLASS =
+        com.android.tools.r8.keepanno.annotations.UsedByNative.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    // Content is the same as UsedByReflection.
+  }
+
   // Implicit hidden item which is "super type" of Condition and Target.
   public static final class Item {
     public static final String classFromBinding = "classFromBinding";
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index fa30c4f..8bf79ed 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -102,7 +102,7 @@
   private static final String ZIP_EXTENSION = ".zip";
 
   private static boolean isArchive(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(APK_EXTENSION)
         || name.endsWith(JAR_EXTENSION)
         || name.endsWith(ZIP_EXTENSION);
diff --git a/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java b/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
index ac084da..831530f 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
@@ -55,6 +55,9 @@
     /**
      * Add program files to extract marker information from.
      *
+     * <p>Each file added here will result in exactly one callback to {@link
+     * MarkerInfoConsumer#acceptMarkerInfo}.
+     *
      * <p>All program files supported by the input and output of D8/R8 can be passed here.
      */
     public Builder addProgramFiles(Collection<Path> programFiles) {
@@ -62,13 +65,23 @@
       return this;
     }
 
-    /** Add dex encoded bytes to extract marker information from. */
+    /**
+     * Add dex encoded bytes to extract marker information from.
+     *
+     * <p>Each data & origin pair added here will result in exactly one callback to {@link
+     * MarkerInfoConsumer#acceptMarkerInfo}.
+     */
     public Builder addDexProgramData(byte[] data, Origin origin) {
       dexData.add(new Pair<>(origin, data));
       return this;
     }
 
-    /** Add classfile encoded bytes to extract marker information from. */
+    /**
+     * Add classfile encoded bytes to extract marker information from.
+     *
+     * <p>Each data & origin pair added here will result in exactly one callback to {@link
+     * MarkerInfoConsumer#acceptMarkerInfo}.
+     */
     public Builder addClassProgramData(byte[] data, Origin origin) {
       cfData.add(new Pair<>(origin, data));
       return this;
diff --git a/src/main/java/com/android/tools/r8/MarkerInfoConsumer.java b/src/main/java/com/android/tools/r8/MarkerInfoConsumer.java
index 48e4a30..87342e4 100644
--- a/src/main/java/com/android/tools/r8/MarkerInfoConsumer.java
+++ b/src/main/java/com/android/tools/r8/MarkerInfoConsumer.java
@@ -7,7 +7,19 @@
 @Keep
 public interface MarkerInfoConsumer {
 
+  /**
+   * Callback that provides the marker information of a resource.
+   *
+   * <p>This callback is called exactly once for each resource in the {@link ExtractMarkerCommand},
+   * also when no marker information is present in that resource.
+   */
   void acceptMarkerInfo(MarkerInfoConsumerData data);
 
+  /**
+   * Callback to inform the extraction of marker information is complete.
+   *
+   * <p>After the callback is invoked no further calls to {@link
+   * MarkerInfoConsumer#acceptMarkerInfo} will occur.
+   */
   void finished();
 }
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index e03dc38..8ad4f29 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -77,6 +77,7 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
+import com.android.tools.r8.utils.StringUtils;
 import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
@@ -597,7 +598,7 @@
   }
 
   private String ifPostfix(IfType kind) {
-    return kind.toString().toLowerCase();
+    return StringUtils.toLowerCase(kind.toString());
   }
 
   public void print(CfIf conditional) {
@@ -797,7 +798,7 @@
   }
 
   private String opcodeName(int opcode) {
-    return Printer.OPCODES[opcode].toLowerCase();
+    return StringUtils.toLowerCase(Printer.OPCODES[opcode]);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
index d56f09a..110e7ca 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
@@ -104,6 +104,7 @@
 
     // Linear scan over instructions.
     CfFrameState state = initialState.asContinue().getValue();
+    int actualInstructionIndexForReporting = 0;
     for (int i = 0; i < code.getInstructions().size(); i++) {
       CfInstruction instruction = code.getInstruction(i);
       assert !state.isError();
@@ -120,7 +121,11 @@
           if (state.isError()) {
             return fail(
                 CfCodeStackMapValidatingException.invalidStackMapForInstruction(
-                    method, i, instruction, state.asError().getMessage(), appView));
+                    method,
+                    actualInstructionIndexForReporting,
+                    instruction,
+                    state.asError().getMessage(),
+                    appView));
           }
         }
       }
@@ -143,7 +148,8 @@
         state = traversalContinuation.asContinue().getValue();
       }
       TraversalContinuation<CfCodeDiagnostics, CfFrameState> traversalContinuation =
-          computeStateForNextInstruction(instruction, i, state, labelToFrameMap);
+          computeStateForNextInstruction(
+              instruction, i, actualInstructionIndexForReporting, state, labelToFrameMap);
       if (traversalContinuation.isContinue()) {
         state = traversalContinuation.asContinue().getValue();
       } else {
@@ -152,12 +158,23 @@
       if (state.isError()) {
         return fail(
             CfCodeStackMapValidatingException.invalidStackMapForInstruction(
-                method, i, instruction, state.asError().getMessage(), appView));
+                method,
+                actualInstructionIndexForReporting,
+                instruction,
+                state.asError().getMessage(),
+                appView));
+      }
+      if (isActualCfInstruction(instruction)) {
+        ++actualInstructionIndexForReporting;
       }
     }
     return StackMapStatus.VALID;
   }
 
+  private static boolean isActualCfInstruction(CfInstruction instruction) {
+    return !instruction.isLabel() && !instruction.isFrame() && !instruction.isPosition();
+  }
+
   private TraversalContinuation<CfCodeDiagnostics, Map<CfLabel, CfFrame>> buildLabelToFrameMap() {
     Map<CfLabel, CfFrame> labelToFrameMap = new IdentityHashMap<>();
     List<CfLabel> labels = new ArrayList<>();
@@ -319,6 +336,7 @@
   private TraversalContinuation<CfCodeDiagnostics, CfFrameState> computeStateForNextInstruction(
       CfInstruction instruction,
       int instructionIndex,
+      int actualInstructionIndexForReporting,
       CfFrameState state,
       Map<CfLabel, CfFrame> labelToFrameMap) {
     if (!instruction.isJump()) {
@@ -334,8 +352,7 @@
     if (instruction.asJump().hasFallthrough()) {
       return TraversalContinuation.doContinue(state);
     }
-    int nextInstructionIndex = instructionIndex + 1;
-    CfInstruction nextInstruction = code.getInstruction(nextInstructionIndex);
+    CfInstruction nextInstruction = code.getInstruction(instructionIndex + 1);
     CfFrame nextFrame = null;
     if (nextInstruction.isFrame()) {
       nextFrame = nextInstruction.asFrame();
@@ -352,7 +369,11 @@
     }
     return TraversalContinuation.doBreak(
         CfCodeStackMapValidatingException.invalidStackMapForInstruction(
-            method, nextInstructionIndex, nextInstruction, "Expected frame instruction", appView));
+            method,
+            actualInstructionIndexForReporting + 1,
+            nextInstruction,
+            "Expected frame instruction",
+            appView));
   }
 
   private boolean isFinalAndExitInstruction(CfInstruction instruction) {
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 2055d69..bc293d3 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -121,7 +121,6 @@
   public void close() {
     // This close behavior is needed to reduce peak memory usage of D8/R8.
     indexedItems = null;
-    codes = null;
     offsetMap = null;
     dexReader = null;
     stringIDs = null;
@@ -130,9 +129,6 @@
   // Mapping from indexes to indexable dex items.
   private OffsetToObjectMapping indexedItems = new OffsetToObjectMapping();
 
-  // Mapping from offset to code item;
-  private Int2ReferenceMap<DexCode> codes = new Int2ReferenceOpenHashMap<>();
-
   // Mapping from offset to dex item;
   private Int2ReferenceMap<Object> offsetMap = new Int2ReferenceOpenHashMap<>();
 
@@ -172,32 +168,28 @@
     this.options = options;
   }
 
-  private void ensureCodesInited(int offset) {
+  // We explicitly reread the code objects even if they are deduplicated in the input (i.e., two
+  // methods point to the same code object) to allow us to change code objects in our pipeline.
+  private DexCode readCodeObject(int offset) {
     if (offset == 0) {
-      return;
-    }
-
-    if (codes == null) {
-      codes = new Int2ReferenceOpenHashMap<>();
+      return null;
     }
 
     if (classKind == ClassKind.LIBRARY) {
       // Ignore contents of library files.
-      return;
+      return null;
     }
     DexSection dexSection = lookupSection(Constants.TYPE_CODE_ITEM);
     if (dexSection.length == 0) {
-      return;
+      return null;
     }
 
-    if (!codes.containsKey(offset)) {
-      int currentPos = dexReader.position();
-      dexReader.position(offset);
-      dexReader.align(4);
-      DexCode code = parseCodeItem();
-      codes.put(offset, code); // Update the file local offset to code mapping.
-      dexReader.position(currentPos);
-    }
+    int currentPos = dexReader.position();
+    dexReader.position(offset);
+    dexReader.align(4);
+    DexCode code = parseCodeItem();
+    dexReader.position(currentPos);
+    return code;
   }
 
   private DexTypeList parseTypeList() {
@@ -758,9 +750,7 @@
       int codeOff = dexReader.getUleb128();
       DexCode code = null;
       if (!skipCodes) {
-        ensureCodesInited(codeOff);
-        assert codeOff == 0 || codes.get(codeOff) != null;
-        code = codes.get(codeOff);
+        code = readCodeObject(codeOff);
       }
       DexMethod method = indexedItems.getMethod(methodIndex);
       accessFlags.setConstructor(method, dexItemFactory);
diff --git a/src/main/java/com/android/tools/r8/dex/Marker.java b/src/main/java/com/android/tools/r8/dex/Marker.java
index 4470baf..0fa52a1 100644
--- a/src/main/java/com/android/tools/r8/dex/Marker.java
+++ b/src/main/java/com/android/tools/r8/dex/Marker.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
@@ -156,7 +157,7 @@
 
   public Marker setCompilationMode(CompilationMode mode) {
     assert !jsonObject.has(COMPILATION_MODE);
-    jsonObject.addProperty(COMPILATION_MODE, mode.toString().toLowerCase());
+    jsonObject.addProperty(COMPILATION_MODE, StringUtils.toLowerCase(mode.toString()));
     return this;
   }
 
@@ -167,22 +168,24 @@
   public String getBackend() {
     if (!hasBackend()) {
       // Before adding backend we would always compile to dex if min-api was specified.
-      return hasMinApi() ? Backend.DEX.name().toLowerCase() : Backend.CF.name().toLowerCase();
+      return hasMinApi()
+          ? StringUtils.toLowerCase(Backend.DEX.name())
+          : StringUtils.toLowerCase(Backend.CF.name());
     }
     return jsonObject.get(BACKEND).getAsString();
   }
 
   public boolean isCfBackend() {
-    return getBackend().equals(Backend.CF.name().toLowerCase());
+    return getBackend().equals(StringUtils.toLowerCase(Backend.CF.name()));
   }
 
   public boolean isDexBackend() {
-    return getBackend().equals(Backend.DEX.name().toLowerCase());
+    return getBackend().equals(StringUtils.toLowerCase(Backend.DEX.name()));
   }
 
   public Marker setBackend(Backend backend) {
     assert !hasBackend();
-    jsonObject.addProperty(BACKEND, backend.name().toLowerCase());
+    jsonObject.addProperty(BACKEND, StringUtils.toLowerCase(backend.name()));
     return this;
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
index 58f2b54..cd99771 100644
--- a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
+++ b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.io.ByteStreams;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntStack;
@@ -95,7 +96,7 @@
     }
     ProguardPathFilter filter = getFilter.apply(proguardConfiguration);
     return filter.isEnabled()
-        && !file.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION)
+        && !StringUtils.toLowerCase(file.getName()).endsWith(FileUtils.CLASS_EXTENSION)
         && filter.matches(file.getName());
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 63975c0..875a4fe 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -165,14 +166,14 @@
   public static String deriveCommonPrefixAndSanityCheck(List<String> fileNames) {
     Iterator<String> nameIterator = fileNames.iterator();
     String first = nameIterator.next();
-    if (!first.toLowerCase().endsWith(FileUtils.DEX_EXTENSION)) {
+    if (!StringUtils.toLowerCase(first).endsWith(FileUtils.DEX_EXTENSION)) {
       throw new RuntimeException("Illegal suffix for dex file: `" + first + "`.");
     }
     String prefix = first.substring(0, first.length() - FileUtils.DEX_EXTENSION.length());
     int index = 2;
     while (nameIterator.hasNext()) {
       String next = nameIterator.next();
-      if (!next.toLowerCase().endsWith(FileUtils.DEX_EXTENSION)) {
+      if (!StringUtils.toLowerCase(next).endsWith(FileUtils.DEX_EXTENSION)) {
         throw new RuntimeException("Illegal suffix for dex file: `" + first + "`.");
       }
       if (!next.startsWith(prefix)) {
diff --git a/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java b/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
index cc3f47c..59788bc 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCodeStackMapValidatingException.java
@@ -70,7 +70,7 @@
       String detailMessage,
       AppView<?> appView) {
     StringBuilder sb =
-        new StringBuilder("Invalid stack map table at instruction ")
+        new StringBuilder("Invalid stack map table at instruction index ")
             .append(instructionIndex)
             .append(": ")
             .append(instruction)
diff --git a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeMetadataProvider.java b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeMetadataProvider.java
index 02ba8d4..eab9e7b 100644
--- a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeMetadataProvider.java
+++ b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeMetadataProvider.java
@@ -43,6 +43,13 @@
     return backing.get(instruction);
   }
 
+  public void remap(Instruction oldKey, Instruction newKey) {
+    BytecodeInstructionMetadata value = backing.remove(oldKey);
+    if (value != null) {
+      backing.put(newKey, value);
+    }
+  }
+
   public static class Builder {
 
     private final Map<Instruction, BytecodeInstructionMetadata.Builder> builders =
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
index 7f3a0b9..a4ed229 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingDefinition.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class AlwaysMaterializingDefinition extends ConstInstruction {
 
@@ -49,6 +50,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java
index 0e09c04..cd4d6a4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingNop.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class AlwaysMaterializingNop extends Instruction {
 
@@ -46,6 +47,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("Unexpected use of materializing NOP prior to CF/DEX finalization.");
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return other instanceof AlwaysMaterializingNop;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
index 21b02ba..a5cf0e4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class AlwaysMaterializingUser extends Instruction {
 
@@ -46,6 +47,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Assume.java b/src/main/java/com/android/tools/r8/ir/code/Assume.java
index e0a6238..174b913 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Assume.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Assume.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 import java.util.Objects;
 import java.util.Set;
 
@@ -173,6 +174,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable(ERROR_MESSAGE);
+  }
+
+  @Override
   public int maxInValueRegister() {
     throw new Unreachable(ERROR_MESSAGE);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java b/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
index 15afa68..58928b0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstMethodHandle.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class ConstMethodHandle extends ConstInstruction {
 
@@ -72,6 +73,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addConstMethodHandle(methodHandle);
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return other.isConstMethodHandle() && other.asConstMethodHandle().methodHandle == methodHandle;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java b/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
index 0b586ca..87031d2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstMethodType.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class ConstMethodType extends ConstInstruction {
 
@@ -70,6 +71,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addConstMethodType(methodType);
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     return other.isConstMethodType() && other.asConstMethodType().methodType == methodType;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
index 1ff7509..089ee34 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 import com.android.tools.r8.utils.StringUtils;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
@@ -136,6 +137,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Dup.java b/src/main/java/com/android/tools/r8/ir/code/Dup.java
index 2c9c3be..41dea29 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Dup.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Dup.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class Dup extends Instruction {
 
@@ -67,6 +68,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used before finalizing to CF.");
+  }
+
+  @Override
   public void buildCf(CfBuilder builder) {
     if (this.inValues.get(0).getType().isWidePrimitive()) {
       builder.add(new CfStackInstruction(Opcode.Dup2), this);
diff --git a/src/main/java/com/android/tools/r8/ir/code/Dup2.java b/src/main/java/com/android/tools/r8/ir/code/Dup2.java
index 7eff1c4..eebb405 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Dup2.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Dup2.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 import com.google.common.collect.ImmutableList;
 
 public class Dup2 extends Instruction {
@@ -85,6 +86,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used before finalizing to CF.");
+  }
+
+  @Override
   public void buildCf(CfBuilder builder) {
     builder.add(new CfStackInstruction(Opcode.Dup2), this);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Inc.java b/src/main/java/com/android/tools/r8/ir/code/Inc.java
index de99253..72b2bf2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Inc.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Inc.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.analysis.constant.LatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.lightir.LirBuilder;
 import java.util.function.Function;
 
 public class Inc extends Unop {
@@ -82,6 +83,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public void buildCf(CfBuilder builder) {
     // Check that this instruction does not have any metadata attached, as it might not materialize
     // as an iinc in CfCode.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 511c157..9321203 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -225,10 +225,7 @@
 
   public abstract void buildCf(CfBuilder builder);
 
-  // TODO(b/225838009): Make this abstract.
-  public void buildLir(LirBuilder<Value, ?> builder) {
-    throw new Unimplemented("Missing impl for " + getClass().getSimpleName());
-  }
+  public abstract void buildLir(LirBuilder<Value, ?> builder);
 
   public void replaceValue(Value oldValue, Value newValue) {
     for (int i = 0; i < inValues.size(); i++) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Load.java b/src/main/java/com/android/tools/r8/ir/code/Load.java
index fe565bc..ac3865d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Load.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Load.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class Load extends Instruction {
 
@@ -69,6 +70,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used in LIR.");
+  }
+
+  @Override
   public void buildDex(DexBuilder builder) {
     throw new Unreachable("This classfile-specific IR should not be inserted in the Dex backend.");
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 58e1507..5dc04da 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class Move extends Instruction {
   private static final String ERROR_MESSAGE =
@@ -58,6 +59,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable(ERROR_MESSAGE);
+  }
+
+  @Override
   public int maxInValueRegister() {
     return Constants.U16BIT_MAX;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Pop.java b/src/main/java/com/android/tools/r8/ir/code/Pop.java
index 01bdc96..5839b47 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Pop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Pop.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class Pop extends Instruction {
 
@@ -85,6 +86,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used in LIR.");
+  }
+
+  @Override
   public void buildDex(DexBuilder builder) {
     throw new Unreachable("This classfile-specific IR should not be inserted in the Dex backend.");
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/RecordFieldValues.java b/src/main/java/com/android/tools/r8/ir/code/RecordFieldValues.java
index db1cfaf..7ab1c92 100644
--- a/src/main/java/com/android/tools/r8/ir/code/RecordFieldValues.java
+++ b/src/main/java/com/android/tools/r8/ir/code/RecordFieldValues.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 import java.util.Arrays;
 import java.util.List;
 
@@ -26,6 +27,7 @@
 
   public RecordFieldValues(DexField[] fields, Value outValue, List<Value> fieldValues) {
     super(outValue, fieldValues);
+    assert fields.length == fieldValues.size();
     this.fields = fields;
   }
 
@@ -77,6 +79,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addRecordFieldValues(getFields(), inValues());
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     if (!other.isRecordFieldValues()) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/SafeCheckCast.java b/src/main/java/com/android/tools/r8/ir/code/SafeCheckCast.java
index 1aee996..bfb48e6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/SafeCheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/SafeCheckCast.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class SafeCheckCast extends CheckCast {
 
@@ -28,6 +29,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addSafeCheckCast(getType(), object());
+  }
+
+  @Override
   DexCheckCast createCheckCast(int register) {
     return new DexSafeCheckCast(register, getType());
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Store.java b/src/main/java/com/android/tools/r8/ir/code/Store.java
index 6714e0d..0ace979 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Store.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Store.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 public class Store extends Instruction {
 
@@ -71,6 +72,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used in LIR.");
+  }
+
+  @Override
   public void buildDex(DexBuilder builder) {
     throw new Unreachable("This classfile-specific IR should not be inserted in the Dex backend.");
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
index e25685c..5d079559 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.lightir.LirBuilder;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
 
@@ -95,6 +96,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public void buildCf(CfBuilder builder) {
     throw new Unreachable();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Swap.java b/src/main/java/com/android/tools/r8/ir/code/Swap.java
index 87ed900..3e826c8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Swap.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Swap.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 import com.google.common.collect.ImmutableList;
 
 public class Swap extends Instruction {
@@ -61,6 +62,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable("This classfile-specific IR should not be used in LIR.");
+  }
+
+  @Override
   public void buildDex(DexBuilder builder) {
     throw new Unreachable("This classfile-specific IR should not be inserted in the Dex backend.");
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/UninitializedThisLocalRead.java b/src/main/java/com/android/tools/r8/ir/code/UninitializedThisLocalRead.java
index 3d8419d..3561840 100644
--- a/src/main/java/com/android/tools/r8/ir/code/UninitializedThisLocalRead.java
+++ b/src/main/java/com/android/tools/r8/ir/code/UninitializedThisLocalRead.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 /**
  * To preserve stack-map table information regarding uninitializedThis flags the
@@ -50,6 +51,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable(ERROR_MESSAGE);
+  }
+
+  @Override
   public boolean identicalNonValueNonPositionParts(Instruction other) {
     throw new Unreachable(ERROR_MESSAGE);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/UnusedArgument.java b/src/main/java/com/android/tools/r8/ir/code/UnusedArgument.java
index 29fc073..85c3490 100644
--- a/src/main/java/com/android/tools/r8/ir/code/UnusedArgument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/UnusedArgument.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LirBuilder;
 
 /**
  * A special instruction to load the value of an argument that has been removed as a result of code
@@ -42,6 +43,11 @@
   }
 
   @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    throw new Unreachable();
+  }
+
+  @Override
   public DeadInstructionResult canBeDeadCode(AppView<?> appview, IRCode code) {
     return DeadInstructionResult.deadIfOutValueIsDead();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 588ce91..f297e8d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -1492,7 +1492,8 @@
   }
 
   public void addRecordFieldValues(DexField[] fields, IntList registers, int outValue) {
-    List<Value> arguments = new ArrayList<>();
+    assert fields.length == registers.size();
+    List<Value> arguments = new ArrayList<>(registers.size());
     for (int register : registers) {
       arguments.add(readRegister(register, ValueTypeConstraint.OBJECT));
     }
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 431a610..8f9378e 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
@@ -25,8 +25,12 @@
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.passes.BinopRewriter;
+import com.android.tools.r8.ir.conversion.passes.CommonSubexpressionElimination;
 import com.android.tools.r8.ir.conversion.passes.ParentConstructorHoistingCodeRewriter;
+import com.android.tools.r8.ir.conversion.passes.SplitBranchOnKnownBoolean;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer;
 import com.android.tools.r8.ir.optimize.AssertionErrorTwoArgsConstructorRewriter;
@@ -110,6 +114,8 @@
   private final ClassInliner classInliner;
   protected final InternalOptions options;
   public final CodeRewriter codeRewriter;
+  public final CommonSubexpressionElimination commonSubexpressionElimination;
+  private final SplitBranchOnKnownBoolean splitBranchOnKnownBoolean;
   public final AssertionErrorTwoArgsConstructorRewriter assertionErrorTwoArgsConstructorRewriter;
   private final NaturalIntLoopRemover naturalIntLoopRemover = new NaturalIntLoopRemover();
   public final MemberValuePropagation<?> memberValuePropagation;
@@ -122,6 +128,7 @@
   private final TypeChecker typeChecker;
   protected final ServiceLoaderRewriter serviceLoaderRewriter;
   private final EnumValueOptimizer enumValueOptimizer;
+  private final BinopRewriter binopRewriter;
   protected final EnumUnboxer enumUnboxer;
   protected final InstanceInitializerOutliner instanceInitializerOutliner;
   protected final RemoveVerificationErrorForUnknownReturnedValues
@@ -159,6 +166,8 @@
     this.appView = appView;
     this.options = appView.options();
     this.codeRewriter = new CodeRewriter(appView);
+    this.commonSubexpressionElimination = new CommonSubexpressionElimination(appView);
+    this.splitBranchOnKnownBoolean = new SplitBranchOnKnownBoolean(appView);
     this.assertionErrorTwoArgsConstructorRewriter =
         appView.options().desugarState.isOn()
             ? new AssertionErrorTwoArgsConstructorRewriter(appView)
@@ -209,6 +218,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumValueOptimizer = null;
+      this.binopRewriter = null;
       this.enumUnboxer = EnumUnboxer.empty();
       this.assumeInserter = null;
       this.instanceInitializerOutliner = null;
@@ -271,6 +281,10 @@
               : null;
       this.enumValueOptimizer =
           options.enableEnumValueOptimization ? new EnumValueOptimizer(appViewWithLiveness) : null;
+      this.binopRewriter =
+          options.testing.enableBinopOptimization && !options.debug
+              ? new BinopRewriter(appView)
+              : null;
     } else {
       AppView<AppInfo> appViewWithoutClassHierarchy = appView.withoutClassHierarchy();
       this.assumeInserter = null;
@@ -291,6 +305,7 @@
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
       this.enumValueOptimizer = null;
+      this.binopRewriter = null;
       this.enumUnboxer = EnumUnboxer.empty();
     }
     this.stringSwitchRemover =
@@ -733,9 +748,7 @@
           code, methodProcessor, methodProcessingContext);
       timing.end();
     }
-    timing.begin("Run CSE");
-    codeRewriter.commonSubexpressionElimination(code);
-    timing.end();
+    commonSubexpressionElimination.run(context, code, timing);
     timing.begin("Simplify arrays");
     codeRewriter.simplifyArrayConstruction(code);
     timing.end();
@@ -764,6 +777,7 @@
       timing.end();
     }
     timing.end();
+    splitBranchOnKnownBoolean.run(code.context(), code, timing);
     if (options.enableRedundantConstNumberOptimization) {
       timing.begin("Remove const numbers");
       codeRewriter.redundantConstNumberRemoval(code);
@@ -774,6 +788,9 @@
       new RedundantFieldLoadAndStoreElimination(appView, code).run();
       timing.end();
     }
+    if (binopRewriter != null) {
+      binopRewriter.run(context, code, timing);
+    }
 
     if (options.testing.invertConditionals) {
       invertConditionalsForTesting(code);
@@ -1083,9 +1100,20 @@
         doRoundtripWithStrategy(code, new ExternalPhisStrategy(), "indirect phis", timing);
     IRCode round2 =
         doRoundtripWithStrategy(round1, new PhiInInstructionsStrategy(), "inline phis", timing);
+    remapBytecodeMetadataProvider(code, round2, bytecodeMetadataProvider);
     return round2;
   }
 
+  private static void remapBytecodeMetadataProvider(
+      IRCode oldCode, IRCode newCode, BytecodeMetadataProvider bytecodeMetadataProvider) {
+    InstructionIterator it1 = oldCode.instructionIterator();
+    InstructionIterator it2 = newCode.instructionIterator();
+    while (it1.hasNext() && it2.hasNext()) {
+      bytecodeMetadataProvider.remap(it1.next(), it2.next());
+    }
+    assert !it1.hasNext() && !it2.hasNext();
+  }
+
   private <EV, S extends LirStrategy<Value, EV>> IRCode doRoundtripWithStrategy(
       IRCode code, S strategy, String name, Timing timing) {
     timing.begin("IR->LIR (" + name + ")");
@@ -1175,8 +1203,11 @@
   }
 
   public String printMethod(IRCode code, String title, String previous) {
-    if (options.extensiveLoggingFilter.size() > 0
-        && options.extensiveLoggingFilter.contains(code.method().getReference().toSourceString())) {
+    if (options.extensiveLoggingFilter.isEmpty()) {
+      return previous;
+    }
+    String methodString = code.method().getReference().toSourceString();
+    if (options.extensiveLoggingFilter.contains(methodString)) {
       String current = code.toString();
       System.out.println();
       System.out.println("-----------------------------------------------------------------------");
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
new file mode 100644
index 0000000..93ad3ff
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BinopRewriter.java
@@ -0,0 +1,212 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.conversion.passes;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.Add;
+import com.android.tools.r8.ir.code.And;
+import com.android.tools.r8.ir.code.Binop;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.Div;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Mul;
+import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.Or;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Rem;
+import com.android.tools.r8.ir.code.Shl;
+import com.android.tools.r8.ir.code.Shr;
+import com.android.tools.r8.ir.code.Sub;
+import com.android.tools.r8.ir.code.Ushr;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.Xor;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+public class BinopRewriter extends CodeRewriterPass<AppInfo> {
+
+  private static final int ALL_BITS_SET = -1;
+
+  public BinopRewriter(AppView<?> appView) {
+    super(appView);
+  }
+
+  private final Map<Class<?>, BinopDescriptor> descriptors = createBinopDescriptors();
+
+  private Map<Class<?>, BinopDescriptor> createBinopDescriptors() {
+    ImmutableMap.Builder<Class<?>, BinopDescriptor> builder = ImmutableMap.builder();
+    builder.put(Add.class, new BinopDescriptor(0, 0, null, null));
+    builder.put(Sub.class, new BinopDescriptor(null, 0, null, null));
+    builder.put(Mul.class, new BinopDescriptor(1, 1, 0, 0));
+    // The following two can be improved if we handle ZeroDivide.
+    builder.put(Div.class, new BinopDescriptor(null, 1, null, null));
+    builder.put(Rem.class, new BinopDescriptor(null, null, null, null));
+    builder.put(And.class, new BinopDescriptor(ALL_BITS_SET, ALL_BITS_SET, 0, 0));
+    builder.put(Or.class, new BinopDescriptor(0, 0, ALL_BITS_SET, ALL_BITS_SET));
+    builder.put(Xor.class, new BinopDescriptor(0, 0, null, null));
+    builder.put(Shl.class, new BinopDescriptor(null, 0, 0, null));
+    builder.put(Shr.class, new BinopDescriptor(null, 0, 0, null));
+    builder.put(Ushr.class, new BinopDescriptor(null, 0, 0, null));
+    return builder.build();
+  }
+
+  /**
+   * A Binop descriptor describes left and right identity and absorbing element of binop. <code>
+   * In a space K, for a binop *:
+   * - i is left identity if for each x in K, i * x = x.
+   * - i is right identity if for each x in K, x * i = x.
+   * - a is left absorbing if for each x in K, a * x = a.
+   * - a is right absorbing if for each x in K, x * a = a.
+   * </code>
+   */
+  private static class BinopDescriptor {
+
+    final Integer leftIdentity;
+    final Integer rightIdentity;
+    final Integer leftAbsorbing;
+    final Integer rightAbsorbing;
+
+    private BinopDescriptor(
+        Integer leftIdentity,
+        Integer rightIdentity,
+        Integer leftAbsorbing,
+        Integer rightAbsorbing) {
+      this.leftIdentity = leftIdentity;
+      this.rightIdentity = rightIdentity;
+      this.leftAbsorbing = leftAbsorbing;
+      this.rightAbsorbing = rightAbsorbing;
+    }
+  }
+
+  @Override
+  String getTimingId() {
+    return "BinopRewriter";
+  }
+
+  @Override
+  boolean shouldRewriteCode(ProgramMethod method, IRCode code) {
+    return true;
+  }
+
+  @Override
+  public void rewriteCode(ProgramMethod method, IRCode code) {
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction next = iterator.next();
+      if (next.isBinop() && !next.isCmp()) {
+        Binop binop = next.asBinop();
+        if (binop.getNumericType() == NumericType.INT
+            || binop.getNumericType() == NumericType.LONG) {
+          BinopDescriptor binopDescriptor = descriptors.get(binop.getClass());
+          assert binopDescriptor != null;
+          ConstNumber constNumber = getConstNumber(binop.leftValue());
+          if (constNumber != null) {
+            if (simplify(
+                binop,
+                iterator,
+                constNumber,
+                binopDescriptor.leftIdentity,
+                binop.rightValue(),
+                binopDescriptor.leftAbsorbing,
+                binop.leftValue())) {
+              continue;
+            }
+          }
+          constNumber = getConstNumber(binop.rightValue());
+          if (constNumber != null) {
+            simplify(
+                binop,
+                iterator,
+                constNumber,
+                binopDescriptor.rightIdentity,
+                binop.leftValue(),
+                binopDescriptor.rightAbsorbing,
+                binop.rightValue());
+          }
+        }
+      }
+    }
+    code.removeAllDeadAndTrivialPhis();
+    assert code.isConsistentSSA(appView);
+  }
+
+  private ConstNumber getConstNumber(Value val) {
+    ConstNumber constNumber = getConstNumberIfConstant(val);
+    if (constNumber != null) {
+      return constNumber;
+    }
+    // phi(v1(0), v2(0)) is equivalent to ConstNumber(0) for the simplification.
+    if (val.isPhi() && getConstNumberIfConstant(val.asPhi().getOperands().get(0)) != null) {
+      ConstNumber phiConstNumber = null;
+      WorkList<Phi> phiWorkList = WorkList.newIdentityWorkList(val.asPhi());
+      while (phiWorkList.hasNext()) {
+        Phi next = phiWorkList.next();
+        for (Value operand : next.getOperands()) {
+          ConstNumber operandConstNumber = getConstNumberIfConstant(operand);
+          if (operandConstNumber != null) {
+            if (phiConstNumber == null) {
+              phiConstNumber = operandConstNumber;
+            } else if (operandConstNumber.getRawValue() == phiConstNumber.getRawValue()) {
+              assert operandConstNumber.getOutType() == phiConstNumber.getOutType();
+            } else {
+              // Different const numbers, cannot conclude a value from the phi.
+              return null;
+            }
+          } else if (operand.isPhi()) {
+            phiWorkList.addIfNotSeen(operand.asPhi());
+          } else {
+            return null;
+          }
+        }
+      }
+      return phiConstNumber;
+    }
+    return null;
+  }
+
+  private static ConstNumber getConstNumberIfConstant(Value val) {
+    if (val.isConstant() && val.getConstInstruction().isConstNumber()) {
+      return val.getConstInstruction().asConstNumber();
+    }
+    return null;
+  }
+
+  private boolean simplify(
+      Binop binop,
+      InstructionListIterator iterator,
+      ConstNumber constNumber,
+      Integer identityElement,
+      Value identityReplacement,
+      Integer absorbingElement,
+      Value absorbingReplacement) {
+    int intValue;
+    if (constNumber.outValue().getType().isInt()) {
+      intValue = constNumber.getIntValue();
+    } else {
+      assert constNumber.outValue().getType().isLong();
+      long longValue = constNumber.getLongValue();
+      intValue = (int) longValue;
+      if ((long) intValue != longValue) {
+        return false;
+      }
+    }
+    if (identityElement != null && identityElement == intValue) {
+      binop.outValue().replaceUsers(identityReplacement);
+      iterator.remove();
+      return true;
+    }
+    if (absorbingElement != null && absorbingElement == intValue) {
+      binop.outValue().replaceUsers(absorbingReplacement);
+      iterator.remove();
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
new file mode 100644
index 0000000..69c4e20
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CommonSubexpressionElimination.java
@@ -0,0 +1,200 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.conversion.passes;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.Binop;
+import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import java.util.List;
+
+public class CommonSubexpressionElimination extends CodeRewriterPass<AppInfo> {
+
+  public CommonSubexpressionElimination(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getTimingId() {
+    return "CommonSubexpressionElimination";
+  }
+
+  @Override
+  boolean shouldRewriteCode(ProgramMethod method, IRCode code) {
+    return true;
+  }
+
+  @Override
+  void rewriteCode(ProgramMethod method, IRCode code) {
+    int noCandidate = code.reserveMarkingColor();
+    if (hasCSECandidate(code, noCandidate)) {
+      final ListMultimap<Wrapper<Instruction>, Value> instructionToValue =
+          ArrayListMultimap.create();
+      final CSEExpressionEquivalence equivalence = new CSEExpressionEquivalence(options);
+      final DominatorTree dominatorTree = new DominatorTree(code);
+      for (int i = 0; i < dominatorTree.getSortedBlocks().length; i++) {
+        BasicBlock block = dominatorTree.getSortedBlocks()[i];
+        if (block.isMarked(noCandidate)) {
+          continue;
+        }
+        InstructionListIterator iterator = block.listIterator(code);
+        while (iterator.hasNext()) {
+          Instruction instruction = iterator.next();
+          if (isCSEInstructionCandidate(instruction)) {
+            List<Value> candidates = instructionToValue.get(equivalence.wrap(instruction));
+            boolean eliminated = false;
+            if (candidates.size() > 0) {
+              for (Value candidate : candidates) {
+                if (dominatorTree.dominatedBy(block, candidate.definition.getBlock())
+                    && shareCatchHandlers(instruction, candidate.definition)) {
+                  instruction.outValue().replaceUsers(candidate);
+                  candidate.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+                  eliminated = true;
+                  iterator.removeOrReplaceByDebugLocalRead();
+                  break; // Don't try any more candidates.
+                }
+              }
+            }
+            if (!eliminated) {
+              instructionToValue.put(equivalence.wrap(instruction), instruction.outValue());
+            }
+          }
+        }
+      }
+    }
+    code.returnMarkingColor(noCandidate);
+    assert code.isConsistentSSA(appView);
+  }
+
+  private static class CSEExpressionEquivalence extends Equivalence<Instruction> {
+
+    private final InternalOptions options;
+
+    private CSEExpressionEquivalence(InternalOptions options) {
+      this.options = options;
+    }
+
+    @Override
+    protected boolean doEquivalent(Instruction a, Instruction b) {
+      // Some Dalvik VMs incorrectly handle Cmp instructions which leads to a requirement
+      // that we do not perform common subexpression elimination for them. See comment on
+      // canHaveCmpLongBug for details.
+      if (a.isCmp() && options.canHaveCmpLongBug()) {
+        return false;
+      }
+      // Note that we don't consider positions because CSE can at most remove an instruction.
+      if (!a.identicalNonValueNonPositionParts(b)) {
+        return false;
+      }
+      // For commutative binary operations any order of in-values are equal.
+      if (a.isBinop() && a.asBinop().isCommutative()) {
+        Value a0 = a.inValues().get(0);
+        Value a1 = a.inValues().get(1);
+        Value b0 = b.inValues().get(0);
+        Value b1 = b.inValues().get(1);
+        return (identicalValue(a0, b0) && identicalValue(a1, b1))
+            || (identicalValue(a0, b1) && identicalValue(a1, b0));
+      } else {
+        // Compare all in-values.
+        assert a.inValues().size() == b.inValues().size();
+        for (int i = 0; i < a.inValues().size(); i++) {
+          if (!identicalValue(a.inValues().get(i), b.inValues().get(i))) {
+            return false;
+          }
+        }
+        return true;
+      }
+    }
+
+    @Override
+    protected int doHash(Instruction instruction) {
+      final int prime = 29;
+      int hash = instruction.getClass().hashCode();
+      if (instruction.isBinop()) {
+        Binop binop = instruction.asBinop();
+        Value in0 = instruction.inValues().get(0);
+        Value in1 = instruction.inValues().get(1);
+        if (binop.isCommutative()) {
+          hash += hash * prime + getHashCode(in0) * getHashCode(in1);
+        } else {
+          hash += hash * prime + getHashCode(in0);
+          hash += hash * prime + getHashCode(in1);
+        }
+        return hash;
+      } else {
+        for (Value value : instruction.inValues()) {
+          hash += hash * prime + getHashCode(value);
+        }
+      }
+      return hash;
+    }
+
+    private static boolean identicalValue(Value a, Value b) {
+      if (a.equals(b)) {
+        return true;
+      }
+      if (a.isConstNumber() && b.isConstNumber()) {
+        // Do not take assumption that constants are canonicalized.
+        return a.definition.identicalNonValueNonPositionParts(b.definition);
+      }
+      return false;
+    }
+
+    private static int getHashCode(Value a) {
+      if (a.isConstNumber()) {
+        // Do not take assumption that constants are canonicalized.
+        return Long.hashCode(a.definition.asConstNumber().getRawValue());
+      }
+      return a.hashCode();
+    }
+  }
+
+  private boolean shareCatchHandlers(Instruction i0, Instruction i1) {
+    if (!i0.instructionTypeCanThrow()) {
+      assert !i1.instructionTypeCanThrow();
+      return true;
+    }
+    assert i1.instructionTypeCanThrow();
+    // TODO(sgjesse): This could be even better by checking for the exceptions thrown, e.g. div
+    // and rem only ever throw ArithmeticException.
+    CatchHandlers<BasicBlock> ch0 = i0.getBlock().getCatchHandlers();
+    CatchHandlers<BasicBlock> ch1 = i1.getBlock().getCatchHandlers();
+    return ch0.equals(ch1);
+  }
+
+  private boolean isCSEInstructionCandidate(Instruction instruction) {
+    return (instruction.isBinop()
+            || instruction.isUnop()
+            || instruction.isInstanceOf()
+            || instruction.isCheckCast())
+        && instruction.getLocalInfo() == null
+        && !instruction.hasInValueWithLocalInfo();
+  }
+
+  private boolean hasCSECandidate(IRCode code, int noCandidate) {
+    for (BasicBlock block : code.blocks) {
+      for (Instruction instruction : block.getInstructions()) {
+        if (isCSEInstructionCandidate(instruction)) {
+          return true;
+        }
+      }
+      block.mark(noCandidate);
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranchOnKnownBoolean.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranchOnKnownBoolean.java
new file mode 100644
index 0000000..134d2bc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranchOnKnownBoolean.java
@@ -0,0 +1,180 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.conversion.passes;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.Goto;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SplitBranchOnKnownBoolean extends CodeRewriterPass<AppInfo> {
+
+  private static final boolean ALLOW_PARTIAL_REWRITE = true;
+
+  public SplitBranchOnKnownBoolean(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  String getTimingId() {
+    return "SplitBranchOnKnownBoolean";
+  }
+
+  @Override
+  boolean shouldRewriteCode(ProgramMethod method, IRCode code) {
+    return true;
+  }
+
+  /**
+   * Simplify Boolean branches for example: <code>
+   * boolean b = i == j; if (b) { ... } else { ... }
+   * </code> ends up first creating a branch for the boolean b, then a second branch on b. D8/R8
+   * rewrites to: <code>
+   * if (i == j) { ... } else { ... }
+   * </code> More complex control flow are also supported to some extent, including cases where the
+   * input of the second branch comes from a set of dependent phis, and a subset of the inputs are
+   * known boolean values.
+   */
+  @Override
+  void rewriteCode(ProgramMethod method, IRCode code) {
+    List<BasicBlock> candidates = computeCandidates(code);
+    if (candidates.isEmpty()) {
+      return;
+    }
+    Map<Goto, BasicBlock> newTargets = findGotosToRetarget(candidates);
+    if (newTargets.isEmpty()) {
+      return;
+    }
+    retargetGotos(newTargets);
+    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    affectedValues.addAll(code.removeUnreachableBlocks());
+    code.removeAllDeadAndTrivialPhis(affectedValues);
+    if (!affectedValues.isEmpty()) {
+      new TypeAnalysis(appView).narrowing(affectedValues);
+    }
+    if (ALLOW_PARTIAL_REWRITE) {
+      code.splitCriticalEdges();
+    }
+    assert code.isConsistentSSA(appView);
+  }
+
+  private void retargetGotos(Map<Goto, BasicBlock> newTargets) {
+    newTargets.forEach(
+        (goTo, newTarget) -> {
+          BasicBlock initialTarget = goTo.getTarget();
+          for (Phi phi : initialTarget.getPhis()) {
+            int index = initialTarget.getPredecessors().indexOf(goTo.getBlock());
+            phi.removeOperand(index);
+          }
+          goTo.setTarget(newTarget);
+        });
+  }
+
+  private Map<Goto, BasicBlock> findGotosToRetarget(List<BasicBlock> candidates) {
+    Map<Goto, BasicBlock> newTargets = new LinkedHashMap<>();
+    for (BasicBlock block : candidates) {
+      // We need to verify any instruction in between the if and the chain of phis is empty (we
+      // could duplicate instruction, but the common case is empty).
+      // Then we can redirect any known value. This can lead to dead code.
+      If theIf = block.exit().asIf();
+      Set<Phi> allowedPhis = getAllowedPhis(theIf.lhs().asPhi());
+      Set<Phi> foundPhis = Sets.newIdentityHashSet();
+      WorkList.newIdentityWorkList(block)
+          .process(
+              (current, workList) -> {
+                if (current.getInstructions().size() > 1) {
+                  return;
+                }
+                if (current != block && !current.exit().isGoto()) {
+                  return;
+                }
+                if (allowedPhis.containsAll(current.getPhis())) {
+                  foundPhis.addAll(current.getPhis());
+                } else {
+                  return;
+                }
+                workList.addIfNotSeen(current.getPredecessors());
+              });
+      if (!ALLOW_PARTIAL_REWRITE) {
+        for (Phi phi : foundPhis) {
+          for (Value value : phi.getOperands()) {
+            if (!value.isConstant() && !(value.isPhi() && foundPhis.contains(value.asPhi()))) {
+              return newTargets;
+            }
+          }
+        }
+      }
+      List<Phi> sortedFoundPhis = new ArrayList<>(foundPhis);
+      sortedFoundPhis.sort(Phi::compareTo);
+      for (Phi phi : sortedFoundPhis) {
+        BasicBlock phiBlock = phi.getBlock();
+        for (int i = 0; i < phi.getOperands().size(); i++) {
+          Value value = phi.getOperand(i);
+          if (value.isConstant()) {
+            recordNewTargetForGoto(value, phiBlock.getPredecessors().get(i), theIf, newTargets);
+          }
+        }
+      }
+    }
+    return newTargets;
+  }
+
+  private List<BasicBlock> computeCandidates(IRCode code) {
+    List<BasicBlock> candidates = new ArrayList<>();
+    for (BasicBlock block : ListUtils.filter(code.blocks, block -> block.entry().isIf())) {
+      If theIf = block.exit().asIf();
+      if (theIf.isZeroTest()
+          && theIf.lhs().getType().isInt()
+          && theIf.lhs().isPhi()
+          && theIf.lhs().hasSingleUniqueUser()
+          && !theIf.lhs().hasPhiUsers()) {
+        candidates.add(block);
+      }
+    }
+    return candidates;
+  }
+
+  private void recordNewTargetForGoto(
+      Value value, BasicBlock basicBlock, If theIf, Map<Goto, BasicBlock> newTargets) {
+    // The GoTo at the end of basicBlock should target the phiBlock, and should target instead
+    // the correct if destination.
+    assert basicBlock.exit().isGoto();
+    assert value.isConstant();
+    assert value.getType().isInt();
+    assert theIf.isZeroTest();
+    BasicBlock newTarget = theIf.targetFromCondition(value.getConstInstruction().asConstNumber());
+    Goto aGoto = basicBlock.exit().asGoto();
+    newTargets.put(aGoto, newTarget);
+  }
+
+  private Set<Phi> getAllowedPhis(Phi initialPhi) {
+    WorkList<Phi> workList = WorkList.newIdentityWorkList(initialPhi);
+    while (workList.hasNext()) {
+      Phi phi = workList.next();
+      for (Value operand : phi.getOperands()) {
+        if (operand.isPhi()
+            && (operand.uniqueUsers().isEmpty() || phi == initialPhi)
+            && workList.getSeenSet().containsAll(operand.uniquePhiUsers())) {
+          workList.addIfNotSeen(operand.asPhi());
+        }
+      }
+    }
+    return workList.getSeenSet();
+  }
+}
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 692eeee..c0d3e6b 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
@@ -113,13 +113,9 @@
 import com.android.tools.r8.utils.LongInterval;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
@@ -499,6 +495,7 @@
 
   // TODO(sgjesse); Move this somewhere else, and reuse it for some of the other switch rewritings.
   public abstract static class InstructionBuilder<T> {
+
     protected int blockNumber;
     protected final Position position;
 
@@ -515,6 +512,7 @@
   }
 
   public static class SwitchBuilder extends InstructionBuilder<SwitchBuilder> {
+
     private Value value;
     private final Int2ReferenceSortedMap<BasicBlock> keyToTarget = new Int2ReferenceAVLTreeMap<>();
     private BasicBlock fallthrough;
@@ -530,7 +528,7 @@
 
     public SwitchBuilder setValue(Value value) {
       this.value = value;
-      return  this;
+      return this;
     }
 
     public SwitchBuilder addKeyAndTarget(int key, BasicBlock target) {
@@ -575,6 +573,7 @@
   }
 
   public static class IfBuilder extends InstructionBuilder<IfBuilder> {
+
     private final IRCode code;
     private Value left;
     private int right;
@@ -593,12 +592,12 @@
 
     public IfBuilder setLeft(Value left) {
       this.left = left;
-      return  this;
+      return this;
     }
 
     public IfBuilder setRight(int right) {
       this.right = right;
-      return  this;
+      return this;
     }
 
     public IfBuilder setTarget(BasicBlock target) {
@@ -2242,6 +2241,7 @@
   }
 
   private static class FilledArrayCandidate {
+
     final NewArrayEmpty newArrayEmpty;
     final int size;
     final boolean encodeAsFilledNewArray;
@@ -2548,165 +2548,8 @@
     }
   }
 
-  private static class CSEExpressionEquivalence extends Equivalence<Instruction> {
-
-    private final InternalOptions options;
-
-    private CSEExpressionEquivalence(InternalOptions options) {
-      this.options = options;
-    }
-
-    @Override
-    protected boolean doEquivalent(Instruction a, Instruction b) {
-      // Some Dalvik VMs incorrectly handle Cmp instructions which leads to a requirement
-      // that we do not perform common subexpression elimination for them. See comment on
-      // canHaveCmpLongBug for details.
-      if (a.isCmp() && options.canHaveCmpLongBug()) {
-        return false;
-      }
-      // Note that we don't consider positions because CSE can at most remove an instruction.
-      if (!a.identicalNonValueNonPositionParts(b)) {
-        return false;
-      }
-      // For commutative binary operations any order of in-values are equal.
-      if (a.isBinop() && a.asBinop().isCommutative()) {
-        Value a0 = a.inValues().get(0);
-        Value a1 = a.inValues().get(1);
-        Value b0 = b.inValues().get(0);
-        Value b1 = b.inValues().get(1);
-        return (identicalValue(a0, b0) && identicalValue(a1, b1))
-            || (identicalValue(a0, b1) && identicalValue(a1, b0));
-      } else {
-        // Compare all in-values.
-        assert a.inValues().size() == b.inValues().size();
-        for (int i = 0; i < a.inValues().size(); i++) {
-          if (!identicalValue(a.inValues().get(i), b.inValues().get(i))) {
-            return false;
-          }
-        }
-        return true;
-      }
-    }
-
-    @Override
-    protected int doHash(Instruction instruction) {
-      final int prime = 29;
-      int hash = instruction.getClass().hashCode();
-      if (instruction.isBinop()) {
-        Binop binop = instruction.asBinop();
-        Value in0 = instruction.inValues().get(0);
-        Value in1 = instruction.inValues().get(1);
-        if (binop.isCommutative()) {
-          hash += hash * prime + getHashCode(in0) * getHashCode(in1);
-        } else {
-          hash += hash * prime + getHashCode(in0);
-          hash += hash * prime + getHashCode(in1);
-        }
-        return hash;
-      } else {
-        for (Value value : instruction.inValues()) {
-          hash += hash * prime + getHashCode(value);
-        }
-      }
-      return hash;
-    }
-
-    private static boolean identicalValue(Value a, Value b) {
-      if (a.equals(b)) {
-        return true;
-      }
-      if (a.isConstNumber() && b.isConstNumber()) {
-        // Do not take assumption that constants are canonicalized.
-        return a.definition.identicalNonValueNonPositionParts(b.definition);
-      }
-      return false;
-    }
-
-    private static int getHashCode(Value a) {
-      if (a.isConstNumber()) {
-        // Do not take assumption that constants are canonicalized.
-        return Long.hashCode(a.definition.asConstNumber().getRawValue());
-      }
-      return a.hashCode();
-    }
-  }
-
-  private boolean shareCatchHandlers(Instruction i0, Instruction i1) {
-    if (!i0.instructionTypeCanThrow()) {
-      assert !i1.instructionTypeCanThrow();
-      return true;
-    }
-    assert i1.instructionTypeCanThrow();
-    // TODO(sgjesse): This could be even better by checking for the exceptions thrown, e.g. div
-    // and rem only ever throw ArithmeticException.
-    CatchHandlers<BasicBlock> ch0 = i0.getBlock().getCatchHandlers();
-    CatchHandlers<BasicBlock> ch1 = i1.getBlock().getCatchHandlers();
-    return ch0.equals(ch1);
-  }
-
-  private boolean isCSEInstructionCandidate(Instruction instruction) {
-    return (instruction.isBinop()
-        || instruction.isUnop()
-        || instruction.isInstanceOf()
-        || instruction.isCheckCast())
-        && instruction.getLocalInfo() == null
-        && !instruction.hasInValueWithLocalInfo();
-  }
-
-  private boolean hasCSECandidate(IRCode code, int noCandidate) {
-    for (BasicBlock block : code.blocks) {
-      for (Instruction instruction : block.getInstructions()) {
-        if (isCSEInstructionCandidate(instruction)) {
-          return true;
-        }
-      }
-      block.mark(noCandidate);
-    }
-    return false;
-  }
-
-  public void commonSubexpressionElimination(IRCode code) {
-    int noCandidate = code.reserveMarkingColor();
-    if (hasCSECandidate(code, noCandidate)) {
-      final ListMultimap<Wrapper<Instruction>, Value> instructionToValue =
-          ArrayListMultimap.create();
-      final CSEExpressionEquivalence equivalence = new CSEExpressionEquivalence(options);
-      final DominatorTree dominatorTree = new DominatorTree(code);
-      for (int i = 0; i < dominatorTree.getSortedBlocks().length; i++) {
-        BasicBlock block = dominatorTree.getSortedBlocks()[i];
-        if (block.isMarked(noCandidate)) {
-          continue;
-        }
-        InstructionListIterator iterator = block.listIterator(code);
-        while (iterator.hasNext()) {
-          Instruction instruction = iterator.next();
-          if (isCSEInstructionCandidate(instruction)) {
-            List<Value> candidates = instructionToValue.get(equivalence.wrap(instruction));
-            boolean eliminated = false;
-            if (candidates.size() > 0) {
-              for (Value candidate : candidates) {
-                if (dominatorTree.dominatedBy(block, candidate.definition.getBlock())
-                    && shareCatchHandlers(instruction, candidate.definition)) {
-                  instruction.outValue().replaceUsers(candidate);
-                  candidate.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
-                  eliminated = true;
-                  iterator.removeOrReplaceByDebugLocalRead();
-                  break;  // Don't try any more candidates.
-                }
-              }
-            }
-            if (!eliminated) {
-              instructionToValue.put(equivalence.wrap(instruction), instruction.outValue());
-            }
-          }
-        }
-      }
-    }
-    code.returnMarkingColor(noCandidate);
-    assert code.isConsistentSSA(appView);
-  }
-
   static class ControlFlowSimplificationResult {
+
     private boolean anyAffectedValues;
     private boolean anySimplifications;
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
index 5ca2e28..d56f416 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder.SyntheticCodeGenerator;
+import com.android.tools.r8.utils.StringUtils;
 
 public class LocalEnumUnboxingUtilityClass extends EnumUnboxingUtilityClass {
 
@@ -71,7 +72,7 @@
     String fieldName = field.getName().toString();
     if (field.getHolderType() == getSynthesizingContext().getType()) {
       return factory.createString(
-          "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
+          "get" + StringUtils.toUpperCase(fieldName.substring(0, 1)) + fieldName.substring(1));
     }
     assert field == factory.enumMembers.nameField || field == factory.enumMembers.ordinalField;
     return field.getName();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
index b0b64ee..a4e5008 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.StringUtils;
 import java.util.Set;
 
 public class BooleanMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -79,7 +80,7 @@
       if (definition.isConstString()) {
         ConstString constString = definition.asConstString();
         if (!constString.instructionInstanceCanThrow()) {
-          String value = constString.getValue().toString().toLowerCase();
+          String value = StringUtils.toLowerCase(constString.getValue().toString());
           if (value.equals("true")) {
             instructionIterator.replaceCurrentInstructionWithConstInt(code, 1);
           } else if (value.equals("false")) {
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 421be9b..ea4f7b1 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
@@ -29,6 +30,8 @@
 import com.android.tools.r8.ir.code.Cmp;
 import com.android.tools.r8.ir.code.Cmp.Bias;
 import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.ConstMethodHandle;
+import com.android.tools.r8.ir.code.ConstMethodType;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.DebugLocalRead;
@@ -73,8 +76,10 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.RecordFieldValues;
 import com.android.tools.r8.ir.code.Rem;
 import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.SafeCheckCast;
 import com.android.tools.r8.ir.code.Shl;
 import com.android.tools.r8.ir.code.Shr;
 import com.android.tools.r8.ir.code.StaticGet;
@@ -88,6 +93,7 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
 import com.android.tools.r8.lightir.LirCode.PositionEntry;
+import com.android.tools.r8.lightir.LirCode.TryCatchTable;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
@@ -167,12 +173,14 @@
       // instruction denotes a new block.
       if (currentBlock == null) {
         currentBlock = getBasicBlock(nextInstructionIndex);
-        CatchHandlers<Integer> handlers =
-            code.getTryCatchTable().getHandlersForBlock(nextInstructionIndex);
-        if (handlers != null) {
-          List<BasicBlock> targets = ListUtils.map(handlers.getAllTargets(), this::getBasicBlock);
-          targets.forEach(currentBlock::link);
-          currentBlock.linkCatchSuccessors(handlers.getGuards(), targets);
+        TryCatchTable tryCatchTable = code.getTryCatchTable();
+        if (tryCatchTable != null) {
+          CatchHandlers<Integer> handlers = tryCatchTable.getHandlersForBlock(nextInstructionIndex);
+          if (handlers != null) {
+            List<BasicBlock> targets = ListUtils.map(handlers.getAllTargets(), this::getBasicBlock);
+            targets.forEach(currentBlock::link);
+            currentBlock.linkCatchSuccessors(handlers.getGuards(), targets);
+          }
         }
       } else {
         assert !blocks.containsKey(nextInstructionIndex);
@@ -509,6 +517,24 @@
     }
 
     @Override
+    public void onConstMethodHandle(DexMethodHandle methodHandle) {
+      TypeElement handleType =
+          TypeElement.fromDexType(
+              appView.dexItemFactory().methodHandleType, Nullability.definitelyNotNull(), appView);
+      Value dest = getOutValueForNextInstruction(handleType);
+      addInstruction(new ConstMethodHandle(dest, methodHandle));
+    }
+
+    @Override
+    public void onConstMethodType(DexProto methodType) {
+      TypeElement typeElement =
+          TypeElement.fromDexType(
+              appView.dexItemFactory().methodTypeType, Nullability.definitelyNotNull(), appView);
+      Value dest = getOutValueForNextInstruction(typeElement);
+      addInstruction(new ConstMethodType(dest, methodType));
+    }
+
+    @Override
     public void onNumberConversion(NumericType from, NumericType to, EV value) {
       Value dest =
           getOutValueForNextInstruction(
@@ -634,7 +660,7 @@
 
     @Override
     public void onInvokePolymorphic(DexMethod target, DexProto proto, List<EV> arguments) {
-      Value dest = getInvokeInstructionOutputValue(target);
+      Value dest = getInvokeInstructionOutputValue(proto);
       List<Value> ssaArgumentValues = getValues(arguments);
       InvokePolymorphic instruction = new InvokePolymorphic(target, proto, dest, ssaArgumentValues);
       addInstruction(instruction);
@@ -719,6 +745,12 @@
     }
 
     @Override
+    public void onSafeCheckCast(DexType type, EV value) {
+      Value dest = getOutValueForNextInstruction(type.toTypeElement(appView));
+      addInstruction(new SafeCheckCast(dest, getValue(value), type));
+    }
+
+    @Override
     public void onInstanceOf(DexType type, EV value) {
       Value dest = getOutValueForNextInstruction(TypeElement.getInt());
       addInstruction(new InstanceOf(dest, getValue(value), type));
@@ -859,5 +891,14 @@
       Value dest = getOutValueForNextInstruction(TypeElement.getInt());
       addInstruction(new InitClass(dest, clazz));
     }
+
+    @Override
+    public void onRecordFieldValues(DexField[] fields, List<EV> values) {
+      TypeElement typeElement =
+          TypeElement.fromDexType(
+              appView.dexItemFactory().objectArrayType, Nullability.definitelyNotNull(), appView);
+      Value dest = getOutValueForNextInstruction(typeElement);
+      addInstruction(new RecordFieldValues(fields, dest, getValues(values)));
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 60df1eb..b88f1f7 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.cf.code.CfLogicalBinop;
 import com.android.tools.r8.cf.code.CfNumberConversion;
 import com.android.tools.r8.dex.MixedSectionCollection;
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexCallSite;
@@ -16,6 +15,7 @@
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
@@ -82,11 +82,6 @@
   // Mapping from instruction to the end usage of SSA values with debug local info.
   private final Int2ReferenceMap<int[]> debugLocalEnds = new Int2ReferenceOpenHashMap<>();
 
-  // TODO(b/225838009): Reconsider this fixed space as the operand count for phis is much larger.
-  // Pre-allocated space for caching value indexes when writing instructions.
-  private static final int MAX_VALUE_COUNT = 256;
-  private int[] valueIndexBuffer = new int[MAX_VALUE_COUNT];
-
   /**
    * Internal "DexItem" for the instruction payloads such that they can be put in the pool.
    *
@@ -137,6 +132,14 @@
     }
   }
 
+  public static class RecordFieldValuesPayload extends InstructionPayload {
+    public final DexField[] fields;
+
+    public RecordFieldValuesPayload(DexField[] fields) {
+      this.fields = fields;
+    }
+  }
+
   public LirBuilder(DexMethod method, LirEncodingStrategy<V, EV> strategy, DexItemFactory factory) {
     this.factory = factory;
     constants = new Reference2IntOpenHashMap<>();
@@ -178,6 +181,7 @@
   private void setPositionIndex(int instructionIndex, Position position) {
     assert positionTable.isEmpty()
         || ListUtils.last(positionTable).fromInstructionIndex < instructionIndex;
+    assert positionTable.isEmpty() || !ListUtils.last(positionTable).position.equals(position);
     positionTable.add(new PositionEntry(instructionIndex, position));
   }
 
@@ -287,7 +291,6 @@
 
   private LirBuilder<V, EV> addInstructionTemplate(
       int opcode, List<DexItem> items, List<V> values) {
-    assert values.size() < MAX_VALUE_COUNT;
     int instructionIndex = advanceInstructionState();
     int operandSize = 0;
     for (DexItem item : items) {
@@ -297,14 +300,16 @@
       EV value = getEncodedValue(values.get(i));
       int encodedValueIndex = getEncodedValueIndex(value, instructionIndex);
       operandSize += encodedValueIndexSize(encodedValueIndex);
-      valueIndexBuffer[i] = encodedValueIndex;
     }
     writer.writeInstruction(opcode, operandSize);
     for (DexItem item : items) {
       writeConstantIndex(item);
     }
     for (int i = 0; i < values.size(); i++) {
-      writeEncodedValueIndex(valueIndexBuffer[i]);
+      // TODO(b/225838009): Consider backpatching operand size to avoid recomputing value indexes.
+      EV value = getEncodedValue(values.get(i));
+      int encodedValueIndex = getEncodedValueIndex(value, instructionIndex);
+      writeEncodedValueIndex(encodedValueIndex);
     }
     return this;
   }
@@ -379,7 +384,7 @@
       case DOUBLE:
         return addConstDouble(value);
       default:
-        throw new Unimplemented();
+        throw new Unreachable();
     }
   }
 
@@ -391,6 +396,14 @@
     return addOneItemInstruction(LirOpcodes.LDC, type);
   }
 
+  public LirBuilder<V, EV> addConstMethodHandle(DexMethodHandle methodHandle) {
+    return addOneItemInstruction(LirOpcodes.LDC, methodHandle);
+  }
+
+  public LirBuilder<V, EV> addConstMethodType(DexProto methodType) {
+    return addOneItemInstruction(LirOpcodes.LDC, methodType);
+  }
+
   public LirBuilder<V, EV> addNeg(NumericType type, V value) {
     int opcode;
     switch (type) {
@@ -475,6 +488,13 @@
         LirOpcodes.CHECKCAST, Collections.singletonList(type), Collections.singletonList(value));
   }
 
+  public LirBuilder<V, EV> addSafeCheckCast(DexType type, V value) {
+    return addInstructionTemplate(
+        LirOpcodes.CHECKCAST_SAFE,
+        Collections.singletonList(type),
+        Collections.singletonList(value));
+  }
+
   public LirBuilder<V, EV> addInstanceOf(DexType type, V value) {
     return addInstructionTemplate(
         LirOpcodes.INSTANCEOF, Collections.singletonList(type), Collections.singletonList(value));
@@ -688,6 +708,8 @@
     constants.forEach((item, index) -> constantTable[index] = item);
     DebugLocalInfoTable<EV> debugTable =
         debugLocals.isEmpty() ? null : new DebugLocalInfoTable<>(debugLocals, debugLocalEnds);
+    TryCatchTable tryCatchTable =
+        tryCatchRanges.isEmpty() ? null : new TryCatchTable(tryCatchRanges);
     return new LirCode<>(
         metadata,
         constantTable,
@@ -695,7 +717,7 @@
         argumentCount,
         byteWriter.toByteArray(),
         instructionCount,
-        new TryCatchTable(tryCatchRanges),
+        tryCatchTable,
         debugTable,
         strategy.getStrategyInfo());
   }
@@ -860,4 +882,10 @@
   public LirBuilder<V, EV> addInitClass(DexType clazz) {
     return addOneItemInstruction(LirOpcodes.INITCLASS, clazz);
   }
+
+  public LirBuilder<V, EV> addRecordFieldValues(DexField[] fields, List<V> values) {
+    RecordFieldValuesPayload payload = new RecordFieldValuesPayload(fields);
+    return addInstructionTemplate(
+        LirOpcodes.RECORDFIELDVALUES, Collections.singletonList(payload), values);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index 3f84cdf..f1c04ec 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -10,7 +10,9 @@
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.Position;
+import com.google.common.collect.ImmutableMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.Map;
 import java.util.function.BiConsumer;
 
@@ -30,7 +32,9 @@
     final Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers;
 
     public TryCatchTable(Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers) {
-      this.tryCatchHandlers = tryCatchHandlers;
+      assert !tryCatchHandlers.isEmpty();
+      // Copy the map to ensure it has not over-allocated the backing store.
+      this.tryCatchHandlers = new Int2ReferenceOpenHashMap<>(tryCatchHandlers);
     }
 
     public CatchHandlers<Integer> getHandlersForBlock(int blockIndex) {
@@ -45,9 +49,20 @@
     public DebugLocalInfoTable(
         Map<EV, DebugLocalInfo> valueToLocalMap, Int2ReferenceMap<int[]> instructionToEndUseMap) {
       assert !valueToLocalMap.isEmpty();
-      assert !instructionToEndUseMap.isEmpty();
-      this.valueToLocalMap = valueToLocalMap;
-      this.instructionToEndUseMap = instructionToEndUseMap;
+      // TODO(b/283049198): Debug ends may not be maintained so we can't assume they are non-empty.
+      // Copy the maps to ensure they have not over-allocated the backing store.
+      this.valueToLocalMap = ImmutableMap.copyOf(valueToLocalMap);
+      this.instructionToEndUseMap =
+          instructionToEndUseMap.isEmpty()
+              ? null
+              : new Int2ReferenceOpenHashMap<>(instructionToEndUseMap);
+    }
+
+    public int[] getEnds(int index) {
+      if (instructionToEndUseMap == null) {
+        return null;
+      }
+      return instructionToEndUseMap.get(index);
     }
 
     public void forEachLocalDefinition(BiConsumer<EV, DebugLocalInfo> fn) {
@@ -73,7 +88,7 @@
   /** Cached value for the number of logical instructions (excludes arguments, includes phis). */
   private final int instructionCount;
 
-  /** Table of try-catch handlers for each basic block. */
+  /** Table of try-catch handlers for each basic block (if present). */
   private final TryCatchTable tryCatchTable;
 
   /** Table of debug local information for each SSA value (if present). */
@@ -153,9 +168,7 @@
   }
 
   public int[] getDebugLocalEnds(int instructionValueIndex) {
-    return debugLocalInfoTable == null
-        ? null
-        : debugLocalInfoTable.instructionToEndUseMap.get(instructionValueIndex);
+    return debugLocalInfoTable == null ? null : debugLocalInfoTable.getEnds(instructionValueIndex);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 0f0811d..8863892 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -207,6 +207,8 @@
   int DEBUGLOCALREAD = 220;
   int INITCLASS = 221;
   int INVOKEPOLYMORPHIC = 222;
+  int RECORDFIELDVALUES = 223;
+  int CHECKCAST_SAFE = 224;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -537,6 +539,10 @@
         return "INITCLASS";
       case INVOKEPOLYMORPHIC:
         return "INVOKEPOLYMORPHIC";
+      case RECORDFIELDVALUES:
+        return "RECORDFIELDVALUES";
+      case CHECKCAST_SAFE:
+        return "CHECKCAST_SAFE";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index 781db23..73ad12b 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
@@ -20,6 +21,7 @@
 import com.android.tools.r8.lightir.LirBuilder.FillArrayPayload;
 import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
 import com.android.tools.r8.lightir.LirBuilder.NameComputationPayload;
+import com.android.tools.r8.lightir.LirBuilder.RecordFieldValuesPayload;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import java.util.ArrayList;
 import java.util.List;
@@ -101,6 +103,14 @@
     onInstruction();
   }
 
+  public void onConstMethodHandle(DexMethodHandle methodHandle) {
+    onInstruction();
+  }
+
+  public void onConstMethodType(DexProto methodType) {
+    onInstruction();
+  }
+
   private void onArrayGetInternal(MemberType type, LirInstructionView view) {
     if (type.isObject()) {
       DexType destType = (DexType) getConstantItem(view.getNextConstantOperand());
@@ -462,6 +472,10 @@
     onInstruction();
   }
 
+  public void onSafeCheckCast(DexType type, EV value) {
+    onInstruction();
+  }
+
   public void onInstanceOf(DexType type, EV value) {
     onInstruction();
   }
@@ -491,6 +505,10 @@
     onInstruction();
   }
 
+  public void onRecordFieldValues(DexField[] fields, List<EV> values) {
+    onInstruction();
+  }
+
   private DexItem getConstantItem(int index) {
     return code.getConstantItem(index);
   }
@@ -515,6 +533,14 @@
             onConstClass((DexType) item);
             return;
           }
+          if (item instanceof DexMethodHandle) {
+            onConstMethodHandle((DexMethodHandle) item);
+            return;
+          }
+          if (item instanceof DexProto) {
+            onConstMethodType((DexProto) item);
+            return;
+          }
           throw new Unimplemented();
         }
       case LirOpcodes.ICONST_M1:
@@ -1087,6 +1113,13 @@
           onCheckCast(type, value);
           return;
         }
+      case LirOpcodes.CHECKCAST_SAFE:
+        {
+          DexType type = getNextDexTypeOperand(view);
+          EV value = getNextValueOperand(view);
+          onSafeCheckCast(type, value);
+          return;
+        }
       case LirOpcodes.INSTANCEOF:
         {
           DexType type = getNextDexTypeOperand(view);
@@ -1204,6 +1237,14 @@
           onInitClass(clazz);
           return;
         }
+      case LirOpcodes.RECORDFIELDVALUES:
+        {
+          RecordFieldValuesPayload payload =
+              (RecordFieldValuesPayload) getConstantItem(view.getNextConstantOperand());
+          List<EV> values = getInvokeInstructionArguments(view);
+          onRecordFieldValues(payload.fields, values);
+          return;
+        }
       default:
         throw new Unimplemented("No dispatch for opcode " + LirOpcodes.toString(opcode));
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index b20e303..5092894 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
@@ -166,6 +167,16 @@
   }
 
   @Override
+  public void onConstMethodHandle(DexMethodHandle methodHandle) {
+    appendOutValue().append("methodHandle(").append(methodHandle).append(")");
+  }
+
+  @Override
+  public void onConstMethodType(DexProto methodType) {
+    appendOutValue().append("methodType(").append(methodType).append(")");
+  }
+
+  @Override
   public void onBinop(NumericType type, EV left, EV right) {
     appendOutValue();
     appendValueArguments(left, right);
@@ -339,6 +350,11 @@
   }
 
   @Override
+  public void onSafeCheckCast(DexType type, EV value) {
+    onCheckCast(type, value);
+  }
+
+  @Override
   public void onInstanceOf(DexType type, EV value) {
     appendOutValue();
     appendValueArguments(value);
@@ -402,4 +418,9 @@
   public void onInitClass(DexType clazz) {
     builder.append(clazz);
   }
+
+  @Override
+  public void onRecordFieldValues(DexField[] fields, List<EV> values) {
+    appendValueArguments(values);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java b/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
index 0b0d537..0428126 100644
--- a/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
+++ b/src/main/java/com/android/tools/r8/lightir/PhiInInstructionsStrategy.java
@@ -72,12 +72,21 @@
 
     @Override
     public LirStrategyInfo<Integer> getStrategyInfo() {
-      return new LirStrategyInfo<Integer>() {
-        @Override
-        public LirSsaValueStrategy<Integer> getReferenceStrategy() {
-          return referenceStrategy;
-        }
-      };
+      return new StrategyInfo(referenceStrategy);
+    }
+  }
+
+  private static class StrategyInfo extends LirStrategyInfo<Integer> {
+
+    private final LirSsaValueStrategy<Integer> referenceStrategy;
+
+    public StrategyInfo(LirSsaValueStrategy<Integer> referenceStrategy) {
+      this.referenceStrategy = referenceStrategy;
+    }
+
+    @Override
+    public LirSsaValueStrategy<Integer> getReferenceStrategy() {
+      return referenceStrategy;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 9a801b9..cca3952 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
@@ -62,7 +63,7 @@
 
     if (options.getProguardConfiguration().hasDontUseMixedCaseClassnames()) {
       allowMixedCaseNaming = false;
-      isUsed = candidate -> usedTypeNames.contains(candidate.toLowerCase());
+      isUsed = candidate -> usedTypeNames.contains(StringUtils.toLowerCase(candidate));
     } else {
       allowMixedCaseNaming = true;
       isUsed = usedTypeNames::contains;
@@ -70,7 +71,7 @@
   }
 
   private void setUsedTypeName(String typeName) {
-    usedTypeNames.add(allowMixedCaseNaming ? typeName : typeName.toLowerCase());
+    usedTypeNames.add(allowMixedCaseNaming ? typeName : StringUtils.toLowerCase(typeName));
   }
 
   static class ClassRenaming {
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 05681d8..50f9d4d 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -358,6 +358,7 @@
       if (isCommentLineWithJsonBrace()) {
         final String currentRenamedNameFinal = previousRenamedName;
         final MappedRange currentRange = activeMappedRange;
+        final MemberNaming lastAddedNamingFinal = lastAddedNaming;
         // Reading global info should cause member mapping to return since we are now reading
         // headers pertaining to what could be a concatinated file.
         BooleanBox readGlobalInfo = new BooleanBox(false);
@@ -414,8 +415,10 @@
                       currentResidualSignature.clear();
                       return;
                     }
-                    currentRange.setResidualSignatureInternal(
-                        residualSignature.asMethodSignature());
+                    if (!isMappedRangeLastAddedNaming(lastAddedNamingFinal, currentRange)) {
+                      currentRange.setResidualSignatureInternal(
+                          residualSignature.asMethodSignature());
+                    }
                   }
                 }
               }
@@ -477,10 +480,7 @@
         if (activeMappedRange != null) {
           if (residualSignature != null) {
             activeMappedRange.setResidualSignatureInternal(residualSignature);
-          } else if (lastAddedNaming != null
-              && lastAddedNaming
-                  .getOriginalSignature()
-                  .equals(activeMappedRange.getOriginalSignature())) {
+          } else if (isMappedRangeLastAddedNaming(lastAddedNaming, activeMappedRange)) {
             // If we already parsed a residual signature for the newly read mapped range and have
             // lost the information about the residual signature we re-create it again.
             activeMappedRange.setResidualSignatureInternal(
@@ -527,6 +527,12 @@
     }
   }
 
+  private boolean isMappedRangeLastAddedNaming(
+      MemberNaming lastAddedNaming, MappedRange activeMappedRange) {
+    return lastAddedNaming != null
+        && lastAddedNaming.getOriginalSignature().equals(activeMappedRange.getOriginalSignature());
+  }
+
   private MemberNaming addMemberEntryOrCopyInformation(
       MemberNaming lastAddedNaming,
       Signature originalSignature,
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
index c4612a7..e1bf867 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
@@ -18,6 +18,7 @@
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -63,7 +64,7 @@
       Arrays.asList("--art-profile", "--feature-jar");
 
   private static boolean FileUtils_isArchive(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
     return name.endsWith(".apk")
         || name.endsWith(".jar")
         || name.endsWith(".zip")
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index a7caf57..347e254 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -38,17 +38,17 @@
       System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik");
 
   public static boolean isDexFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(DEX_EXTENSION);
   }
 
   public static boolean isVDexFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(VDEX_EXTENSION);
   }
 
   public static boolean isClassFile(String path) {
-    String name = path.toLowerCase();
+    String name = StringUtils.toLowerCase(path);
     // Android does not support Java 9 module, thus skip module-info.
     if (name.equals(MODULE_INFO_CLASS)) {
       return false;
@@ -61,32 +61,32 @@
   }
 
   public static boolean isJarFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(JAR_EXTENSION);
   }
 
   public static boolean isZipFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(ZIP_EXTENSION);
   }
 
   public static boolean isApkFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(APK_EXTENSION);
   }
 
   public static boolean isAarFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(AAR_EXTENSION);
   }
 
   public static boolean isJavaFile(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(JAVA_EXTENSION);
   }
 
   public static boolean isArchive(Path path) {
-    String name = path.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(path.getFileName().toString());
     return name.endsWith(APK_EXTENSION)
         || name.endsWith(JAR_EXTENSION)
         || name.endsWith(ZIP_EXTENSION)
@@ -109,12 +109,14 @@
     return Files.readAllLines(file);
   }
 
-  public static void writeTextFile(Path file, List<String> lines) throws IOException {
+  public static Path writeTextFile(Path file, List<String> lines) throws IOException {
     Files.write(file, lines);
+    return file;
   }
 
-  public static void writeTextFile(Path file, String... lines) throws IOException {
+  public static Path writeTextFile(Path file, String... lines) throws IOException {
     Files.write(file, Arrays.asList(lines));
+    return file;
   }
 
   public static Path validateOutputFile(Path path, Reporter reporter) {
@@ -157,7 +159,7 @@
   }
 
   public static boolean isClassesDexFile(Path file) {
-    String name = file.getFileName().toString().toLowerCase();
+    String name = StringUtils.toLowerCase(file.getFileName().toString());
     if (!name.startsWith("classes") || !name.endsWith(DEX_EXTENSION)) {
       return false;
     }
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 8a733a5..2f66e6f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2082,6 +2082,8 @@
     public boolean calculateItemUseCountInDex = false;
     public boolean calculateItemUseCountInDexDumpSingleUseStrings = false;
 
+    public boolean enableBinopOptimization = true;
+
     private DeterminismChecker getDeterminismChecker() {
       // Lazily read the env-var so that it can be set after options init.
       if (determinismChecker == null && !hasReadCheckDeterminism) {
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index af857c4..1cbcad2 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -13,6 +13,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -393,11 +394,11 @@
   }
 
   public static boolean isFalsy(String string) {
-    return string.equals("0") || string.toLowerCase().equals("false");
+    return string.equals("0") || StringUtils.toLowerCase(string).equals("false");
   }
 
   public static boolean isTruthy(String string) {
-    return string.equals("1") || string.toLowerCase().equals("true");
+    return string.equals("1") || StringUtils.toLowerCase(string).equals("true");
   }
 
   public static boolean isWhitespace(int codePoint) {
@@ -482,7 +483,7 @@
     if (stringToCapitalize == null || stringToCapitalize.isEmpty()) {
       return stringToCapitalize;
     }
-    return stringToCapitalize.substring(0, 1).toUpperCase() + stringToCapitalize.substring(1);
+    return toUpperCase(stringToCapitalize.substring(0, 1)) + stringToCapitalize.substring(1);
   }
 
   public static int indexOf(String s, char ch1, char ch2) {
@@ -492,4 +493,12 @@
     if (i2 == -1) return i1;
     return Math.min(i1, i2);
   }
+
+  public static String toLowerCase(String s) {
+    return s.toLowerCase(Locale.ROOT);
+  }
+
+  public static String toUpperCase(String s) {
+    return s.toUpperCase(Locale.ROOT);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 39c6354..068350c 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -230,19 +230,20 @@
   }
 
   public static boolean isDexFile(String entry) {
-    String name = entry.toLowerCase();
+    String name = StringUtils.toLowerCase(entry);
     return name.endsWith(DEX_EXTENSION);
   }
 
   public static boolean isClassFile(String entry) {
-    String name = entry.toLowerCase();
-    if (name.endsWith(MODULE_INFO_CLASS)) {
+    if (entry.endsWith(MODULE_INFO_CLASS)) {
       return false;
     }
-    if (name.startsWith("meta-inf") || name.startsWith("/meta-inf")) {
+    // Only check for upper case META-INF. See JAR File Specification,
+    // https://docs.oracle.com/en/java/javase/17/docs/specs/jar/jar.html.
+    if (entry.startsWith("META-INF") || entry.startsWith("/META-INF")) {
       return false;
     }
-    return name.endsWith(CLASS_EXTENSION);
+    return entry.endsWith(CLASS_EXTENSION);
   }
 
   public static class ZipBuilder {
@@ -262,6 +263,14 @@
       return stream;
     }
 
+    public ZipBuilder addFile(String name, Path file) throws IOException {
+      ZipEntry zipEntry = new ZipEntry(name);
+      stream.putNextEntry(zipEntry);
+      Files.copy(file, stream);
+      stream.closeEntry();
+      return this;
+    }
+
     public ZipBuilder addFilesRelative(Path basePath, Collection<Path> filesToAdd)
         throws IOException {
       for (Path path : filesToAdd) {
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
index 906f88c..80a2845 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -534,7 +535,7 @@
   }
 
   private static boolean isClassFile(String file) {
-    file = file.toLowerCase();
+    file = StringUtils.toLowerCase(file);
     return file.endsWith(".class");
   }
 
@@ -543,7 +544,7 @@
   }
 
   private static boolean isDexFile(String file) {
-    file = file.toLowerCase();
+    file = StringUtils.toLowerCase(file);
     return file.endsWith(".dex");
   }
 
@@ -552,7 +553,7 @@
   }
 
   private static boolean isArchive(String file) {
-    file = file.toLowerCase();
+    file = StringUtils.toLowerCase(file);
     return file.endsWith(".zip") || file.endsWith(".jar");
   }
 
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
index 4b86cea..521ccba 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -528,7 +529,7 @@
   }
 
   private static boolean isClassFile(String file) {
-    file = file.toLowerCase();
+    file = StringUtils.toLowerCase(file);
     return file.endsWith(".class");
   }
 
@@ -537,7 +538,7 @@
   }
 
   private static boolean isArchive(String file) {
-    file = file.toLowerCase();
+    file = StringUtils.toLowerCase(file);
     return file.endsWith(".zip") || file.endsWith(".jar");
   }
 
diff --git a/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
index b1280e4..9cdc2d1 100644
--- a/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
+++ b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
@@ -48,9 +48,9 @@
   public void testMultiReleaseJars() throws IOException {
     Path jar = temporaryFolder.getRoot().toPath().resolve("classes.jar");
     try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(jar))) {
-      output.putNextEntry(new ZipEntry("meta-inf/9/Test.class"));
+      output.putNextEntry(new ZipEntry("META-INF/9/Test.class"));
       output.closeEntry();
-      output.putNextEntry(new ZipEntry("/meta-inf/9/Test.class"));
+      output.putNextEntry(new ZipEntry("/META-INF/9/Test.class"));
       output.closeEntry();
     }
     ArchiveClassFileProvider provider = new ArchiveClassFileProvider(jar);
diff --git a/src/test/java/com/android/tools/r8/ExtractMarkerTest.java b/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
index c7a260a..c97ab32 100644
--- a/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
+++ b/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
@@ -13,8 +13,8 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ExtractMarkerUtils;
+import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Collection;
 import java.util.Set;
 import org.junit.Assume;
@@ -51,7 +51,8 @@
   private void verifyMarkerDex(Marker marker, Tool tool) {
     assertEquals(tool, marker.getTool());
     assertEquals(Version.LABEL, marker.getVersion());
-    assertEquals(CompilationMode.DEBUG.toString().toLowerCase(), marker.getCompilationMode());
+    assertEquals(
+        StringUtils.toLowerCase(CompilationMode.DEBUG.toString()), marker.getCompilationMode());
     assertEquals(parameters.getApiLevel().getLevel(), marker.getMinApi().intValue());
     assertEquals(includeClassesChecksum, marker.getHasChecksums());
   }
@@ -94,7 +95,8 @@
   private static void verifyMarkerCf(Marker marker, Tool tool) {
     assertEquals(tool, marker.getTool());
     assertEquals(Version.LABEL, marker.getVersion());
-    assertEquals(CompilationMode.DEBUG.toString().toLowerCase(), marker.getCompilationMode());
+    assertEquals(
+        StringUtils.toLowerCase(CompilationMode.DEBUG.toString()), marker.getCompilationMode());
     assertFalse(marker.getHasChecksums());
   }
 
diff --git a/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
index ba323ee..b7404f3 100644
--- a/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
+++ b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.file.Path;
 import org.junit.Test;
@@ -119,8 +120,7 @@
                     diagnotics.getErrors().stream()
                         .allMatch(
                             s ->
-                                s.getDiagnosticMessage()
-                                    .toLowerCase()
+                                StringUtils.toLowerCase(s.getDiagnosticMessage())
                                     .contains("unsupported class file major version")));
               });
     } catch (CompilationFailedException e) {
diff --git a/src/test/java/com/android/tools/r8/MarkerMatcher.java b/src/test/java/com/android/tools/r8/MarkerMatcher.java
index bbd1952..242a752 100644
--- a/src/test/java/com/android/tools/r8/MarkerMatcher.java
+++ b/src/test/java/com/android/tools/r8/MarkerMatcher.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -113,7 +114,7 @@
     return new MarkerMatcher() {
       @Override
       protected boolean eval(Marker marker) {
-        return marker.getCompilationMode().equals(compilationMode.name().toLowerCase());
+        return marker.getCompilationMode().equals(StringUtils.toLowerCase(compilationMode.name()));
       }
 
       @Override
@@ -127,12 +128,14 @@
     return new MarkerMatcher() {
       @Override
       protected boolean eval(Marker marker) {
-        return marker.getBackend().equals(backend.name().toLowerCase());
+        return marker.getBackend().equals(StringUtils.toLowerCase(backend.name()));
       }
 
       @Override
       protected void explain(Description description) {
-        description.appendText(Marker.BACKEND + " ").appendText(backend.name().toLowerCase());
+        description
+            .appendText(Marker.BACKEND + " ")
+            .appendText(StringUtils.toLowerCase(backend.name()));
       }
     };
   }
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index a1a6580..802e714 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -44,6 +44,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -1888,7 +1889,8 @@
       fileNames.add(file.getCanonicalPath());
     }
 
-    File resultDir = temp.newFolder(firstCompilerUnderTest.toString().toLowerCase() + "-output");
+    File resultDir =
+        temp.newFolder(firstCompilerUnderTest.toString().toLowerCase(Locale.ROOT) + "-output");
 
     runArtTestDoRunOnArt(
         dexVm, firstCompilerUnderTest, specification, fileNames, resultDir, compilationMode);
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 0da6332..48689c3 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -60,7 +60,6 @@
         options.testing.allowUnnecessaryDontWarnWildcards = false;
         options.horizontalClassMergerOptions().enable();
         options.horizontalClassMergerOptions().setEnableInterfaceMerging();
-        options.getArtProfileOptions().setEnableCompletenessCheckForTesting(true);
         options
             .getCfCodeAnalysisOptions()
             .setAllowUnreachableCfBlocks(false)
@@ -88,6 +87,7 @@
   private ByteArrayOutputStream stderr = null;
   private PrintStream oldStderr = null;
   protected OutputMode outputMode = OutputMode.DexIndexed;
+  private boolean isBenchmarkRunner = false;
 
   private Optional<Integer> isAndroidBuildVersionAdded = null;
 
@@ -225,6 +225,7 @@
     if (System.getProperty("com.android.tools.r8.printtimes") != null) {
       allowStdoutMessages();
     }
+    isBenchmarkRunner = true;
     return internalCompileAndBenchmark(results);
   }
 
@@ -265,7 +266,10 @@
               : getMinApiLevel();
       builder.setMinApiLevel(minApi);
     }
-    if (!noMinApiLevel && backend.isDex() && (isD8TestBuilder() || isR8TestBuilder())) {
+    if (!noMinApiLevel
+        && backend.isDex()
+        && (isD8TestBuilder() || isR8TestBuilder())
+        && !isBenchmarkRunner) {
       int minApiLevel = builder.getMinApiLevel();
       allowedGlobalSynthetics.computeIfAbsent(
           minApiLevel, TestCompilerBuilder::computeAllGlobalSynthetics);
@@ -284,6 +288,12 @@
             }
           };
     }
+
+    if ((isD8TestBuilder() || isR8TestBuilder()) && !isBenchmarkRunner) {
+      addOptionsModification(
+          o -> o.getArtProfileOptions().setEnableCompletenessCheckForTesting(true));
+    }
+
     builder.setOptimizeMultidexForLinearAlloc(optimizeMultidexForLinearAlloc);
     if (useDefaultRuntimeLibrary) {
       builder.addLibraryFiles(getDefaultLibraryFiles());
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 4f95deb..3669e77 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.structural.Ordered;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
@@ -322,7 +323,7 @@
 
     @Override
     public String name() {
-      return vm.name().toLowerCase();
+      return StringUtils.toLowerCase(vm.name());
     }
 
     public Path getJavaHome() {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index bc066b0..655350b 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1526,7 +1526,7 @@
   public static ProcessResult forkR8WithJavaOptions(
       Path dir, List<String> javaOptions, String... args) throws IOException {
     String r8Jar = R8_JAR.toAbsolutePath().toString();
-    return forkJavaWithJarAndJavaOptions(dir, r8Jar, Arrays.asList(args), javaOptions);
+    return forkJavaWithJarAndJavaOptions(dir, javaOptions, r8Jar, Arrays.asList(args));
   }
 
   public static ProcessResult forkR8Jar(Path dir, String... args) throws IOException {
@@ -1553,11 +1553,11 @@
 
   private static ProcessResult forkJavaWithJar(Path dir, String jarPath, List<String> args)
       throws IOException {
-    return forkJavaWithJarAndJavaOptions(dir, jarPath, args, ImmutableList.of());
+    return forkJavaWithJarAndJavaOptions(dir, ImmutableList.of(), jarPath, args);
   }
 
   private static ProcessResult forkJavaWithJarAndJavaOptions(
-      Path dir, String jarPath, List<String> args, List<String> javaOptions) throws IOException {
+      Path dir, List<String> javaOptions, String jarPath, List<String> args) throws IOException {
     List<String> command =
         new ImmutableList.Builder<String>()
             .add(getJavaExecutable())
@@ -1569,6 +1569,19 @@
     return runProcess(new ProcessBuilder(command).directory(dir.toFile()));
   }
 
+  public static ProcessResult forkJavaWithJavaOptions(
+      Path dir, List<String> javaOptions, Class clazz, List<String> args) throws IOException {
+    List<String> command =
+        new ImmutableList.Builder<String>()
+            .add(getJavaExecutable())
+            .addAll(javaOptions)
+            .add("-cp")
+            .add(System.getProperty("java.class.path"))
+            .add(clazz.getCanonicalName())
+            .addAll(args)
+            .build();
+    return runProcess(new ProcessBuilder(command).directory(dir.toFile()));
+  }
 
   private static ProcessResult forkJava(Path dir, Class clazz, List<String> args)
       throws IOException {
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkDependency.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkDependency.java
index 4bf5334..26e9029 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkDependency.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkDependency.java
@@ -5,6 +5,7 @@
 
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Locale;
 
 public class BenchmarkDependency {
 
@@ -34,7 +35,7 @@
     this.directoryName = directoryName;
     this.location = location;
     String firstChar = name.substring(0, 1);
-    if (!firstChar.equals(firstChar.toLowerCase()) || name.contains("_")) {
+    if (!firstChar.equals(firstChar.toLowerCase(Locale.ROOT)) || name.contains("_")) {
       throw new BenchmarkConfigError("Benchmark name should use lowerCamelCase, found: " + name);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
index 0f1384c..f210e18 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkRunner.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.benchmarks;
 
+import com.android.tools.r8.utils.StringUtils;
+
 public class BenchmarkRunner {
 
   public interface BenchmarkRunnerFunction {
@@ -15,7 +17,7 @@
 
     @Override
     public String toString() {
-      return name().toLowerCase();
+      return StringUtils.toLowerCase(name());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java b/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
index 4e8caaa..fda115e 100644
--- a/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
+++ b/src/test/java/com/android/tools/r8/graph/InvokeSuperTest.java
@@ -362,7 +362,8 @@
                   assertTrue(split > 0);
                   String targetMethodRaw = name.substring("invoke".length(), split);
                   String targetMethod =
-                      targetMethodRaw.substring(0, 1).toLowerCase() + targetMethodRaw.substring(1);
+                      StringUtils.toLowerCase(targetMethodRaw.substring(0, 1))
+                          + targetMethodRaw.substring(1);
                   String targetHolderRaw = name.substring(split + 2);
                   String targetHolderType =
                       InvokeSuperTest.class.getTypeName() + "$" + targetHolderRaw;
diff --git a/src/test/java/com/android/tools/r8/ir/IdentityAbsorbingTest.java b/src/test/java/com/android/tools/r8/ir/IdentityAbsorbingTest.java
new file mode 100644
index 0000000..390d6ca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/IdentityAbsorbingTest.java
@@ -0,0 +1,1063 @@
+// Copyright (c) 2023, 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;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class IdentityAbsorbingTest extends TestBase {
+
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "2147483647",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "2147483646",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "-2147483648",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "-2147483647",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "9223372036854775807",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "9223372036854775806",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "-9223372036854775808",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "-9223372036854775807",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "0",
+          "-1",
+          "-1",
+          "-1",
+          "0",
+          "0",
+          "0");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntimes().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public IdentityAbsorbingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspector
+        .clazz(Main.class)
+        .forAllMethods(
+            m ->
+                assertTrue(
+                    m.streamInstructions()
+                        .noneMatch(
+                            i -> i.isIntOrLongLogicalBinop() || i.isIntOrLongArithmeticBinop())));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      intTests(Integer.MAX_VALUE);
+      intTests(Integer.MAX_VALUE - 1);
+      intTests(Integer.MIN_VALUE);
+      intTests(Integer.MIN_VALUE + 1);
+      intTests(System.currentTimeMillis() > 0 ? 0 : 1);
+      intTests(System.currentTimeMillis() > 0 ? 1 : 9);
+      intTests(System.currentTimeMillis() > 0 ? -1 : 1);
+
+      longTests(Long.MAX_VALUE);
+      longTests(Long.MAX_VALUE - 1);
+      longTests(Long.MIN_VALUE);
+      longTests(Long.MIN_VALUE + 1);
+      longTests(System.currentTimeMillis() > 0 ? 0L : 1L);
+      longTests(System.currentTimeMillis() > 0 ? 1L : 9L);
+      longTests(System.currentTimeMillis() > 0 ? -1L : 1L);
+    }
+
+    private static void longTests(long val) {
+      identityLongTest(val);
+      absorbingLongTest(val);
+      identityDoubleLongTest(val);
+      absorbingDoubleLongTest(val);
+    }
+
+    private static void intTests(int val) {
+      identityIntTest(val);
+      absorbingIntTest(val);
+      identityDoubleIntTest(val);
+      absorbingDoubleIntTest(val);
+      chainIntTest(val);
+    }
+
+    @NeverInline
+    private static void identityDoubleIntTest(int val) {
+      System.out.println(val + 0 + 0);
+      System.out.println(0 + val + 0);
+      System.out.println(0 + 0 + val);
+      System.out.println(val - 0 - 0);
+      System.out.println(val * 1 * 1);
+      System.out.println(1 * val * 1);
+      System.out.println(1 * 1 * val);
+      System.out.println(val / 1 / 1);
+
+      System.out.println(val & -1 & -1);
+      System.out.println(-1 & val & -1);
+      System.out.println(-1 & -1 & val);
+      System.out.println(val | 0 | 0);
+      System.out.println(0 | val | 0);
+      System.out.println(0 | 0 | val);
+      System.out.println(val ^ 0 ^ 0);
+      System.out.println(0 ^ val ^ 0);
+      System.out.println(0 ^ 0 ^ val);
+      System.out.println(val << 0 << 0);
+      System.out.println(val >> 0 >> 0);
+      System.out.println(val >>> 0 >>> 0);
+    }
+
+    @NeverInline
+    private static void identityDoubleLongTest(long val) {
+      System.out.println(val + 0L + 0L);
+      System.out.println(0L + val + 0L);
+      System.out.println(0L + 0L + val);
+      System.out.println(val - 0L - 0L);
+      System.out.println(val * 1L * 1L);
+      System.out.println(1L * val * 1L);
+      System.out.println(1L * 1L * val);
+      System.out.println(val / 1L / 1L);
+
+      System.out.println(val & -1L & -1L);
+      System.out.println(-1L & val & -1L);
+      System.out.println(-1L & -1L & val);
+      System.out.println(val | 0L | 0L);
+      System.out.println(0L | val | 0L);
+      System.out.println(0L | 0L | val);
+      System.out.println(val ^ 0L ^ 0L);
+      System.out.println(0L ^ val ^ 0L);
+      System.out.println(0L ^ 0L ^ val);
+      System.out.println(val << 0L << 0L);
+      System.out.println(val >> 0L >> 0L);
+      System.out.println(val >>> 0L >>> 0L);
+    }
+
+    @NeverInline
+    private static void identityIntTest(int val) {
+      System.out.println(val + 0);
+      System.out.println(0 + val);
+      System.out.println(val - 0);
+      System.out.println(val * 1);
+      System.out.println(1 * val);
+      System.out.println(val / 1);
+
+      System.out.println(val & -1);
+      System.out.println(-1 & val);
+      System.out.println(val | 0);
+      System.out.println(0 | val);
+      System.out.println(val ^ 0);
+      System.out.println(0 ^ val);
+      System.out.println(val << 0);
+      System.out.println(val >> 0);
+      System.out.println(val >>> 0);
+    }
+
+    @NeverInline
+    private static void identityLongTest(long val) {
+      System.out.println(val + 0L);
+      System.out.println(0L + val);
+      System.out.println(val - 0L);
+      System.out.println(val * 1L);
+      System.out.println(1L * val);
+      System.out.println(val / 1L);
+
+      System.out.println(val & -1L);
+      System.out.println(-1L & val);
+      System.out.println(val | 0L);
+      System.out.println(0L | val);
+      System.out.println(val ^ 0L);
+      System.out.println(0L ^ val);
+      System.out.println(val << 0L);
+      System.out.println(val >> 0L);
+      System.out.println(val >>> 0L);
+    }
+
+    @NeverInline
+    private static void absorbingDoubleIntTest(int val) {
+      System.out.println(val * 0 * 0);
+      System.out.println(0 * val * 0);
+      System.out.println(0 * 0 * val);
+      // val would need to be proven non zero.
+      // System.out.println(0 / val);
+      // System.out.println(0 % val);
+
+      System.out.println(0 & 0 & val);
+      System.out.println(0 & val & 0);
+      System.out.println(val & 0 & 0);
+      System.out.println(-1 | -1 | val);
+      System.out.println(-1 | val | -1);
+      System.out.println(val | -1 | -1);
+      System.out.println(0 << 0 << val);
+      System.out.println(0 >> 0 >> val);
+      System.out.println(0 >>> 0 >>> val);
+    }
+
+    @NeverInline
+    private static void absorbingDoubleLongTest(long val) {
+      System.out.println(val * 0L * 0L);
+      System.out.println(0L * val * 0L);
+      System.out.println(0L * 0L * val);
+      // val would need to be proven non zero.
+      // System.out.println(0L / val);
+      // System.out.println(0L % val);
+
+      System.out.println(0L & 0L & val);
+      System.out.println(0L & val & 0L);
+      System.out.println(val & 0L & 0L);
+      System.out.println(-1L | -1L | val);
+      System.out.println(-1L | val | -1L);
+      System.out.println(val | -1L | -1L);
+      System.out.println(0L << 0L << val);
+      System.out.println(0L >> 0L >> val);
+      System.out.println(0L >>> 0L >>> val);
+    }
+
+    @NeverInline
+    private static void absorbingIntTest(int val) {
+      System.out.println(val * 0);
+      System.out.println(0 * val);
+      // val would need to be proven non zero.
+      // System.out.println(0 / val);
+      // System.out.println(0 % val);
+
+      System.out.println(0 & val);
+      System.out.println(val & 0);
+      System.out.println(-1 | val);
+      System.out.println(val | -1);
+      System.out.println(0 << val);
+      System.out.println(0 >> val);
+      System.out.println(0 >>> val);
+    }
+
+    @NeverInline
+    private static void absorbingLongTest(long val) {
+      System.out.println(val * 0L);
+      System.out.println(0L * val);
+      // val would need to be proven non zero.
+      // System.out.println(0L / val);
+      // System.out.println(0L % val);
+
+      System.out.println(0L & val);
+      System.out.println(val & 0L);
+      System.out.println(-1L | val);
+      System.out.println(val | -1L);
+      System.out.println(0L << val);
+      System.out.println(0L >> val);
+      System.out.println(0L >>> val);
+    }
+
+    private static void chainIntTest(int val) {
+      int abs = System.currentTimeMillis() > 0 ? val * 0 : 0 * val;
+      System.out.println(abs * val);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeConstraintOnTrivialPhiTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeConstraintOnTrivialPhiTest.java
index 609b2c9..277faa0 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeConstraintOnTrivialPhiTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeConstraintOnTrivialPhiTest.java
@@ -38,7 +38,7 @@
     }
 
     public String getTestName() {
-      return toString().toLowerCase() + "ConstraintOnTrivialPhiTest";
+      return StringUtils.toLowerCase(toString()) + "ConstraintOnTrivialPhiTest";
     }
 
     public String getConstInstruction() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/DoubleDiamondTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/DoubleDiamondTest.java
new file mode 100644
index 0000000..cf928e6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/DoubleDiamondTest.java
@@ -0,0 +1,188 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.ifs;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.AlwaysInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DoubleDiamondTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DoubleDiamondTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableAlwaysInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "5", "1", "1", "5", "1", "5", "5", "1", "5", "5", "1", "1", "1", "5", "5", "5", "1",
+            "1", "1", "5");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    for (FoundMethodSubject method : inspector.clazz(Main.class).allMethods()) {
+      if (!method.getOriginalName().equals("main")) {
+        long count = method.streamInstructions().filter(InstructionSubject::isIf).count();
+        assertEquals(method.getOriginalName().contains("Double") ? 2 : 1, count);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(indirectEquals(2, 6));
+      System.out.println(indirectEquals(3, 3));
+
+      System.out.println(indirectEqualsNegated(2, 6));
+      System.out.println(indirectEqualsNegated(3, 3));
+
+      System.out.println(indirectLessThan(2, 6));
+      System.out.println(indirectLessThan(7, 3));
+
+      System.out.println(indirectLessThanNegated(2, 6));
+      System.out.println(indirectLessThanNegated(7, 3));
+
+      System.out.println(indirectDoubleEquals(2, 6, 6));
+      System.out.println(indirectDoubleEquals(7, 7, 3));
+      System.out.println(indirectDoubleEquals(1, 1, 1));
+
+      System.out.println(indirectDoubleEqualsNegated(2, 6, 6));
+      System.out.println(indirectDoubleEqualsNegated(2, 2, 6));
+      System.out.println(indirectDoubleEqualsNegated(7, 7, 7));
+
+      System.out.println(indirectDoubleEqualsSplit(2, 6, 6));
+      System.out.println(indirectDoubleEqualsSplit(7, 7, 3));
+      System.out.println(indirectDoubleEqualsSplit(1, 1, 1));
+
+      System.out.println(indirectDoubleEqualsSplitNegated(2, 6, 6));
+      System.out.println(indirectDoubleEqualsSplitNegated(2, 2, 6));
+      System.out.println(indirectDoubleEqualsSplitNegated(7, 7, 7));
+    }
+
+    @AlwaysInline
+    public static boolean doubleEqualsSplit(int i, int j, int k) {
+      if (i != j) {
+        return false;
+      }
+      return j == k;
+    }
+
+    @NeverInline
+    public static int indirectDoubleEqualsSplit(int i, int j, int k) {
+      if (doubleEqualsSplit(i, j, k)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @NeverInline
+    public static int indirectDoubleEqualsSplitNegated(int i, int j, int k) {
+      if (!doubleEqualsSplit(i, j, k)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @AlwaysInline
+    public static boolean doubleEquals(int i, int j, int k) {
+      return i == j && j == k;
+    }
+
+    @NeverInline
+    public static int indirectDoubleEquals(int i, int j, int k) {
+      if (doubleEquals(i, j, k)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @NeverInline
+    public static int indirectDoubleEqualsNegated(int i, int j, int k) {
+      if (!doubleEquals(i, j, k)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @AlwaysInline
+    public static boolean equals(int i, int j) {
+      return i == j;
+    }
+
+    @NeverInline
+    public static int indirectEquals(int i, int j) {
+      if (equals(i, j)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @NeverInline
+    public static int indirectEqualsNegated(int i, int j) {
+      if (!equals(i, j)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @AlwaysInline
+    public static boolean lessThan(int i, int j) {
+      return i <= j;
+    }
+
+    @NeverInline
+    public static int indirectLessThan(int i, int j) {
+      if (lessThan(i, j)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+
+    @NeverInline
+    public static int indirectLessThanNegated(int i, int j) {
+      if (!lessThan(i, j)) {
+        return 1;
+      } else {
+        return 5;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
index 572020d..8f8541c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/FieldReadForWriteTest.java
@@ -27,7 +27,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection parameters() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+    return getTestParameters().withDefaultRuntimes().withAllApiLevels().build();
   }
 
   @Test
@@ -39,7 +39,9 @@
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters)
-        .compile()
+        .addOptionsModification(o -> o.testing.roundtripThroughLir = true)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("42")
         .inspect(inspector -> assertThat(inspector.clazz(anim.class), isAbsent()));
   }
 
@@ -86,6 +88,7 @@
       anim.abc_fade_in ^= packageIdTransform;
       // Unop (number conversion, but also: inc, neg, not).
       anim.abc_fade_in = (int) ((long) anim.abc_fade_in);
+      System.out.println("42");
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
new file mode 100644
index 0000000..b3a1579
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByNativeAnnotationTest.java
@@ -0,0 +1,118 @@
+// Copyright (c) 2023, 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.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByNative;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsedByNativeAnnotationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepUsedByNativeAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(A.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
+  }
+
+  @UsedByNative(
+      description = "Ensure that the class A remains as we are assuming the contents of its name.",
+      preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+      // The kind will default to ONLY_CLASS, so setting this to include members will keep the
+      // otherwise unused bar method.
+      kind = KeepItemKind.CLASS_AND_MEMBERS)
+  static class A {
+
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+
+    public void bar() {
+      // Unused but kept by the annotation.
+    }
+  }
+
+  static class B {
+
+    @UsedByNative(
+        // Only if A.foo is live do we need to keep this.
+        preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+        // Both the class and method are reflectively accessed.
+        kind = KeepItemKind.CLASS_AND_MEMBERS)
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
new file mode 100644
index 0000000..c8ea937
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsedByReflectionAnnotationTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2023, 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.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsedByReflectionAnnotationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepUsedByReflectionAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+    assertThat(inspector.clazz(C.class), isAbsent());
+    assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+    assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
+  }
+
+  @UsedByReflection(
+      description = "Ensure that the class A remains as we are assuming the contents of its name.")
+  static class A {
+
+    public void foo() throws Exception {
+      Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
+      clazz.getDeclaredMethod("bar").invoke(clazz);
+    }
+  }
+
+  static class B {
+
+    @UsedByReflection(
+        // Only if A.foo is live do we need to keep this.
+        preconditions = {@KeepCondition(classConstant = A.class, methodName = "foo")},
+        // Both the class and method are reflectively accessed.
+        kind = KeepItemKind.CLASS_AND_MEMBERS)
+    public static void bar() {
+      System.out.println("Hello, world");
+    }
+
+    public static void bar(int ignore) {
+      throw new RuntimeException("UNUSED");
+    }
+  }
+
+  static class C {
+    // Unused.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/locale/TurkishLocaleMultiReleaseJarTest.java b/src/test/java/com/android/tools/r8/locale/TurkishLocaleMultiReleaseJarTest.java
new file mode 100644
index 0000000..48f8121
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/locale/TurkishLocaleMultiReleaseJarTest.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2023, 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.locale;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.R8;
+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.ToolHelper.ArtCommandBuilder;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TurkishLocaleMultiReleaseJarTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    Path workingDir = temp.getRoot().toPath();
+    ProcessResult result =
+        ToolHelper.forkJavaWithJavaOptions(
+            workingDir,
+            ImmutableList.of("-Duser.language=tr"),
+            D8.class,
+            ImmutableList.of(
+                "--min-api",
+                Integer.toString(parameters.getApiLevel().getLevel()),
+                "--lib",
+                ToolHelper.getAndroidJar(AndroidApiLevel.U).toAbsolutePath().toString(),
+                buildMultiReleaseJarWithUpperCaseMetaInf(workingDir).toAbsolutePath().toString()));
+    assertEquals(0, result.exitCode);
+    runArtOnClassesDotDex(workingDir);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Path workingDir = temp.getRoot().toPath();
+    ImmutableList.Builder<String> builder = ImmutableList.builder();
+    builder.add(
+        "--lib",
+        ToolHelper.getAndroidJar(AndroidApiLevel.U).toAbsolutePath().toString(),
+        "--pg-conf",
+        FileUtils.writeTextFile(temp.newFile("test.pro").toPath(), "-keep class * { *; }")
+            .toAbsolutePath()
+            .toString(),
+        buildMultiReleaseJarWithUpperCaseMetaInf(workingDir).toAbsolutePath().toString());
+    if (parameters.isCfRuntime()) {
+      builder.add("--classfile");
+    } else {
+      builder.add("--min-api", Integer.toString(parameters.getApiLevel().getLevel()));
+    }
+
+    ProcessResult result =
+        ToolHelper.forkJavaWithJavaOptions(
+            workingDir, ImmutableList.of("-Duser.language=tr"), R8.class, builder.build());
+    assertEquals(0, result.exitCode);
+    runArtOnClassesDotDex(workingDir);
+  }
+
+  private Path buildMultiReleaseJarWithUpperCaseMetaInf(Path dir) throws Exception {
+    // Compiler will to check String.toLowerCase() of zip entries.
+    Path jar = dir.resolve("test.jar");
+    ZipBuilder.builder(jar)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(), ToolHelper.getClassFileForTestClass(TestClass.class))
+        .addFile(
+            Paths.get("META-INF/versions/9")
+                .resolve(
+                    ToolHelper.getClassPathForTests()
+                        .relativize(ToolHelper.getClassFileForTestClass(TestClass.class)))
+                .toString(),
+            ToolHelper.getClassFileForTestClass(TestClass.class))
+        .build();
+    return jar;
+  }
+
+  private void runArtOnClassesDotDex(Path dir) throws Exception {
+    if (parameters.getRuntime().isCf()) {
+      return;
+    }
+    ArtCommandBuilder builder = new ArtCommandBuilder(parameters.getRuntime().asDex().getVm());
+    builder.appendClasspath(dir.resolve("classes.dex").toAbsolutePath().toString());
+    builder.setMainClass(TestClass.class.getTypeName());
+    String stdout = ToolHelper.runArt(builder);
+    assertEquals(StringUtils.lines("Hello, world!"), stdout);
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/locale/TurkishLocaleZipFileInputTest.java b/src/test/java/com/android/tools/r8/locale/TurkishLocaleZipFileInputTest.java
new file mode 100644
index 0000000..4231616
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/locale/TurkishLocaleZipFileInputTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2023, 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.locale;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.R8;
+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.ToolHelper.ArtCommandBuilder;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TurkishLocaleZipFileInputTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    Path workingDir = temp.getRoot().toPath();
+    ProcessResult result =
+        ToolHelper.forkJavaWithJavaOptions(
+            workingDir,
+            // See b/281774632 for context.
+            ImmutableList.of("-Duser.language=tr"),
+            D8.class,
+            ImmutableList.of(
+                "--min-api",
+                Integer.toString(parameters.getApiLevel().getLevel()),
+                "--lib",
+                ToolHelper.getAndroidJar(AndroidApiLevel.U).toAbsolutePath().toString(),
+                buildZipWithUpperCaseExtension(workingDir).toAbsolutePath().toString()));
+    assertEquals(0, result.exitCode);
+    runArtOnClassesDotDex(workingDir);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Path workingDir = temp.getRoot().toPath();
+    ImmutableList.Builder<String> builder = ImmutableList.builder();
+    builder.add(
+        "--lib",
+        ToolHelper.getAndroidJar(AndroidApiLevel.U).toAbsolutePath().toString(),
+        "--pg-conf",
+        FileUtils.writeTextFile(temp.newFile("test.pro").toPath(), "-keep class * { *; }")
+            .toAbsolutePath()
+            .toString(),
+        buildZipWithUpperCaseExtension(workingDir).toAbsolutePath().toString());
+    if (parameters.isCfRuntime()) {
+      builder.add("--classfile");
+    } else {
+      builder.add("--min-api", Integer.toString(parameters.getApiLevel().getLevel()));
+    }
+
+    ProcessResult result =
+        ToolHelper.forkJavaWithJavaOptions(
+            workingDir,
+            // See b/281774632 for context.
+            ImmutableList.of("-Duser.language=tr"),
+            R8.class,
+            builder.build());
+    assertEquals(0, result.exitCode);
+    runArtOnClassesDotDex(workingDir);
+  }
+
+  private Path buildZipWithUpperCaseExtension(Path dir) throws Exception {
+    // Compiler will to check String.toLowerCase() of file extensions.
+    Path jar = dir.resolve("test.ZIP");
+    ZipBuilder.builder(jar)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(), ToolHelper.getClassFileForTestClass(TestClass.class))
+        .build();
+    return jar;
+  }
+
+  private void runArtOnClassesDotDex(Path dir) throws Exception {
+    if (parameters.getRuntime().isCf()) {
+      return;
+    }
+    ArtCommandBuilder builder = new ArtCommandBuilder(parameters.getRuntime().asDex().getVm());
+    builder.appendClasspath(dir.resolve("classes.dex").toAbsolutePath().toString());
+    builder.setMainClass(TestClass.class.getTypeName());
+    String stdout = ToolHelper.runArt(builder);
+    assertEquals(StringUtils.lines("Hello, world!"), stdout);
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileContentsTest.java b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileContentsTest.java
index dc61ce0..2d73302 100644
--- a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileContentsTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileContentsTest.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.utils.ArchiveResourceProvider;
 import com.android.tools.r8.utils.DataResourceConsumerForTesting;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
@@ -173,9 +174,8 @@
             AdaptResourceFileContentsTestClass.B.class),
         getProguardConfig(true, null),
         null,
-        getDataResources()
-            .stream()
-            .filter(x -> !x.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION))
+        getDataResources().stream()
+            .filter(x -> !StringUtils.toLowerCase(x.getName()).endsWith(FileUtils.CLASS_EXTENSION))
             .collect(Collectors.toList()));
 
     // Visit each of the resources in the jar and check that their contents are as expected.
diff --git a/src/test/java/com/android/tools/r8/naming/DontUseMixedCaseClassNamesExistingClassTest.java b/src/test/java/com/android/tools/r8/naming/DontUseMixedCaseClassNamesExistingClassTest.java
index 6fa54e4..310fe36 100644
--- a/src/test/java/com/android/tools/r8/naming/DontUseMixedCaseClassNamesExistingClassTest.java
+++ b/src/test/java/com/android/tools/r8/naming/DontUseMixedCaseClassNamesExistingClassTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
@@ -58,7 +59,9 @@
         .inspect(
             inspector -> {
               String finalName = Main.class.getPackage().getName() + "." + FINAL_CLASS_NAME;
-              assertEquals(finalName.toLowerCase(), Main.class.getTypeName().toLowerCase());
+              assertEquals(
+                  StringUtils.toLowerCase(finalName),
+                  StringUtils.toLowerCase(Main.class.getTypeName()));
               if (dontUseMixedCase) {
                 assertNotEquals(finalName, inspector.clazz(A.class).getFinalName());
               } else {
diff --git a/src/test/java/com/android/tools/r8/naming/b155249069/DontUseMixedCaseClassNamesExistingClassPackageTest.java b/src/test/java/com/android/tools/r8/naming/b155249069/DontUseMixedCaseClassNamesExistingClassPackageTest.java
index 839c96d..0c2c31d 100644
--- a/src/test/java/com/android/tools/r8/naming/b155249069/DontUseMixedCaseClassNamesExistingClassPackageTest.java
+++ b/src/test/java/com/android/tools/r8/naming/b155249069/DontUseMixedCaseClassNamesExistingClassPackageTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -76,10 +77,12 @@
               ClassSubject bSubject = inspector.clazz(A.class);
               if (dontUseMixedCase) {
                 assertNotEquals(
-                    aSubject.getFinalName().toLowerCase(), bSubject.getFinalName().toLowerCase());
+                    StringUtils.toLowerCase(aSubject.getFinalName()),
+                    StringUtils.toLowerCase(bSubject.getFinalName()));
               } else {
                 assertEquals(
-                    aSubject.getFinalName().toLowerCase(), bSubject.getFinalName().toLowerCase());
+                    StringUtils.toLowerCase(aSubject.getFinalName()),
+                    StringUtils.toLowerCase(bSubject.getFinalName()));
               }
             });
   }
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
index 9f82884..3644aa8 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -72,6 +72,7 @@
 import com.android.tools.r8.retrace.stacktraces.OverloadSameLineTest;
 import com.android.tools.r8.retrace.stacktraces.OverloadedWithAndWithoutRangeStackTrace;
 import com.android.tools.r8.retrace.stacktraces.PreambleLineNumberStackTrace;
+import com.android.tools.r8.retrace.stacktraces.ResidualSignatureOnOuterFrameStackTrace;
 import com.android.tools.r8.retrace.stacktraces.RetraceAssertionErrorStackTrace;
 import com.android.tools.r8.retrace.stacktraces.SingleLineNoLineNumberStackTrace;
 import com.android.tools.r8.retrace.stacktraces.SourceFileNameSynthesizeStackTrace;
@@ -437,6 +438,11 @@
   }
 
   @Test
+  public void testResidualSignatureOnOuterFrameStackTrace() throws Exception {
+    runRetraceTest(new ResidualSignatureOnOuterFrameStackTrace());
+  }
+
+  @Test
   public void testMapVersionWarningStackTrace() throws Exception {
     // TODO(b/204289928): Internalize the diagnostics checking.
     assumeFalse(external);
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ResidualSignatureOnOuterFrameStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ResidualSignatureOnOuterFrameStackTrace.java
new file mode 100644
index 0000000..f464a62
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ResidualSignatureOnOuterFrameStackTrace.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2023, 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.retrace.stacktraces;
+
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Collections;
+import java.util.List;
+
+/** This is a reproduction of b/283837159 */
+public class ResidualSignatureOnOuterFrameStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Collections.singletonList("\tat mapping.g(SourceFile)");
+  }
+
+  @Override
+  public String mapping() {
+    return StringUtils.joinLines(
+        "# {'id':'com.android.tools.r8.mapping','version':'2.2'}",
+        "kotlinx.coroutines.BuildersKt -> mapping:",
+        "  1:1:void pruned.class.method(kotlinx.coroutines.CoroutineScope):10:10 -> g",
+        "  2:2:void pruned.class.method(kotlinx.coroutines.CoroutineScope):0:0 -> g",
+        "  2:2:void pruned.class.method(kotlinx.coroutines.CoroutineScope):0 -> g",
+        // The residual signature should be placed on the first mapped range.
+        "  # {'id':'com.android.tools.r8.residualsignature', 'signature':'(LX;)V'}",
+        "  3:3:void pruned.class.method(kotlinx.coroutines.CoroutineScope):30:30 -> g");
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Collections.singletonList("\tat pruned.class.method(class.java)");
+  }
+
+  @Override
+  public List<String> retraceVerboseStackTrace() {
+    return Collections.singletonList(
+        "\tat pruned.class.void method(kotlinx.coroutines.CoroutineScope)(class.java)");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index 223140b..1dba3c9 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.diagnostic.MissingDefinitionsDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -119,7 +120,7 @@
                           Paths.get(
                               EXAMPLES_DIR,
                               "shaking1",
-                              "print-mapping-" + backend.name().toLowerCase() + ".ref")),
+                              "print-mapping-" + StringUtils.toLowerCase(backend.name()) + ".ref")),
                       StandardCharsets.UTF_8);
               assertEquals(sorted(refMapping), sorted(actualMapping));
             });
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
index f6f59dd..b012f3d 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptSingletonIsNotCyclicTest.java
@@ -86,7 +86,7 @@
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     whyAreYouKeepingConsumer.printWhyAreYouKeeping(fooClassRef, new PrintStream(baos));
     assertThat(
-        baos.toString().replace(getClass().getTypeName(), "<test>").toLowerCase(),
+        StringUtils.toLowerCase(baos.toString().replace(getClass().getTypeName(), "<test>")),
         not(anyOf(containsString("cyclic"), containsString("cycle"))));
 
     // The only root should be the keep main-method rule.
diff --git a/src/test/java/com/android/tools/r8/smali/BinopLiteralTest.java b/src/test/java/com/android/tools/r8/smali/BinopLiteralTest.java
index afd71b9..798fe0c 100644
--- a/src/test/java/com/android/tools/r8/smali/BinopLiteralTest.java
+++ b/src/test/java/com/android/tools/r8/smali/BinopLiteralTest.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.code.DexConst16;
 import com.android.tools.r8.dex.code.DexFormat22b;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.dex.code.DexReturn;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.utils.AndroidApp;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -46,6 +48,11 @@
       Short.MAX_VALUE,
   };
 
+  protected AndroidApp processApplication(AndroidApp application)
+      throws CompilationFailedException {
+    return processApplication(application, opt -> opt.testing.enableBinopOptimization = false);
+  }
+
   @Test
   public void lit8PassthroughTest() {
     List<String> lit8Binops = Arrays.asList(
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index 2e2e926..23684e1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.cf.code.CfInvokeDynamic;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfLogicalBinop;
 import com.android.tools.r8.cf.code.CfMonitor;
 import com.android.tools.r8.cf.code.CfNew;
 import com.android.tools.r8.cf.code.CfNewArray;
@@ -39,6 +40,7 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.MonitorType;
+import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.ValueType;
 import java.util.Iterator;
 import org.objectweb.asm.Opcodes;
@@ -337,6 +339,20 @@
   }
 
   @Override
+  public boolean isIntOrLongArithmeticBinop() {
+    return instruction instanceof CfArithmeticBinop
+        && (((CfArithmeticBinop) instruction).getType() == NumericType.INT
+            || ((CfArithmeticBinop) instruction).getType() == NumericType.LONG);
+  }
+
+  @Override
+  public boolean isIntOrLongLogicalBinop() {
+    return instruction instanceof CfLogicalBinop
+        && (((CfLogicalBinop) instruction).getType() == NumericType.INT
+            || ((CfLogicalBinop) instruction).getType() == NumericType.LONG);
+  }
+
+  @Override
   public boolean isMultiplication() {
     if (!(instruction instanceof CfArithmeticBinop)) {
       return false;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index 7513e06..16a41b1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -4,6 +4,12 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import com.android.tools.r8.dex.code.DexAddInt;
+import com.android.tools.r8.dex.code.DexAddInt2Addr;
+import com.android.tools.r8.dex.code.DexAddIntLit16;
+import com.android.tools.r8.dex.code.DexAddIntLit8;
+import com.android.tools.r8.dex.code.DexAddLong;
+import com.android.tools.r8.dex.code.DexAddLong2Addr;
 import com.android.tools.r8.dex.code.DexAget;
 import com.android.tools.r8.dex.code.DexAgetBoolean;
 import com.android.tools.r8.dex.code.DexAgetByte;
@@ -11,6 +17,12 @@
 import com.android.tools.r8.dex.code.DexAgetObject;
 import com.android.tools.r8.dex.code.DexAgetShort;
 import com.android.tools.r8.dex.code.DexAgetWide;
+import com.android.tools.r8.dex.code.DexAndInt;
+import com.android.tools.r8.dex.code.DexAndInt2Addr;
+import com.android.tools.r8.dex.code.DexAndIntLit16;
+import com.android.tools.r8.dex.code.DexAndIntLit8;
+import com.android.tools.r8.dex.code.DexAndLong;
+import com.android.tools.r8.dex.code.DexAndLong2Addr;
 import com.android.tools.r8.dex.code.DexAput;
 import com.android.tools.r8.dex.code.DexAputBoolean;
 import com.android.tools.r8.dex.code.DexAputByte;
@@ -31,6 +43,12 @@
 import com.android.tools.r8.dex.code.DexConstWide16;
 import com.android.tools.r8.dex.code.DexConstWide32;
 import com.android.tools.r8.dex.code.DexConstWideHigh16;
+import com.android.tools.r8.dex.code.DexDivInt;
+import com.android.tools.r8.dex.code.DexDivInt2Addr;
+import com.android.tools.r8.dex.code.DexDivIntLit16;
+import com.android.tools.r8.dex.code.DexDivIntLit8;
+import com.android.tools.r8.dex.code.DexDivLong;
+import com.android.tools.r8.dex.code.DexDivLong2Addr;
 import com.android.tools.r8.dex.code.DexGoto;
 import com.android.tools.r8.dex.code.DexIfEq;
 import com.android.tools.r8.dex.code.DexIfEqz;
@@ -87,7 +105,19 @@
 import com.android.tools.r8.dex.code.DexNewArray;
 import com.android.tools.r8.dex.code.DexNewInstance;
 import com.android.tools.r8.dex.code.DexNop;
+import com.android.tools.r8.dex.code.DexOrInt;
+import com.android.tools.r8.dex.code.DexOrInt2Addr;
+import com.android.tools.r8.dex.code.DexOrIntLit16;
+import com.android.tools.r8.dex.code.DexOrIntLit8;
+import com.android.tools.r8.dex.code.DexOrLong;
+import com.android.tools.r8.dex.code.DexOrLong2Addr;
 import com.android.tools.r8.dex.code.DexPackedSwitch;
+import com.android.tools.r8.dex.code.DexRemInt;
+import com.android.tools.r8.dex.code.DexRemInt2Addr;
+import com.android.tools.r8.dex.code.DexRemIntLit16;
+import com.android.tools.r8.dex.code.DexRemIntLit8;
+import com.android.tools.r8.dex.code.DexRemLong;
+import com.android.tools.r8.dex.code.DexRemLong2Addr;
 import com.android.tools.r8.dex.code.DexReturn;
 import com.android.tools.r8.dex.code.DexReturnObject;
 import com.android.tools.r8.dex.code.DexReturnVoid;
@@ -98,6 +128,16 @@
 import com.android.tools.r8.dex.code.DexSgetObject;
 import com.android.tools.r8.dex.code.DexSgetShort;
 import com.android.tools.r8.dex.code.DexSgetWide;
+import com.android.tools.r8.dex.code.DexShlInt;
+import com.android.tools.r8.dex.code.DexShlInt2Addr;
+import com.android.tools.r8.dex.code.DexShlIntLit8;
+import com.android.tools.r8.dex.code.DexShlLong;
+import com.android.tools.r8.dex.code.DexShlLong2Addr;
+import com.android.tools.r8.dex.code.DexShrInt;
+import com.android.tools.r8.dex.code.DexShrInt2Addr;
+import com.android.tools.r8.dex.code.DexShrIntLit8;
+import com.android.tools.r8.dex.code.DexShrLong;
+import com.android.tools.r8.dex.code.DexShrLong2Addr;
 import com.android.tools.r8.dex.code.DexSparseSwitch;
 import com.android.tools.r8.dex.code.DexSput;
 import com.android.tools.r8.dex.code.DexSputBoolean;
@@ -106,7 +146,22 @@
 import com.android.tools.r8.dex.code.DexSputObject;
 import com.android.tools.r8.dex.code.DexSputShort;
 import com.android.tools.r8.dex.code.DexSputWide;
+import com.android.tools.r8.dex.code.DexSubInt;
+import com.android.tools.r8.dex.code.DexSubInt2Addr;
+import com.android.tools.r8.dex.code.DexSubLong;
+import com.android.tools.r8.dex.code.DexSubLong2Addr;
 import com.android.tools.r8.dex.code.DexThrow;
+import com.android.tools.r8.dex.code.DexUshrInt;
+import com.android.tools.r8.dex.code.DexUshrInt2Addr;
+import com.android.tools.r8.dex.code.DexUshrIntLit8;
+import com.android.tools.r8.dex.code.DexUshrLong;
+import com.android.tools.r8.dex.code.DexUshrLong2Addr;
+import com.android.tools.r8.dex.code.DexXorInt;
+import com.android.tools.r8.dex.code.DexXorInt2Addr;
+import com.android.tools.r8.dex.code.DexXorIntLit16;
+import com.android.tools.r8.dex.code.DexXorIntLit8;
+import com.android.tools.r8.dex.code.DexXorLong;
+import com.android.tools.r8.dex.code.DexXorLong2Addr;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.SingleConstant;
@@ -445,6 +500,73 @@
     return instruction instanceof DexSparseSwitch;
   }
 
+  public boolean isIntOrLongArithmeticBinop() {
+    return instruction instanceof DexMulInt
+        || instruction instanceof DexMulIntLit8
+        || instruction instanceof DexMulIntLit16
+        || instruction instanceof DexMulInt2Addr
+        || instruction instanceof DexMulLong
+        || instruction instanceof DexMulLong2Addr
+        || instruction instanceof DexAddInt
+        || instruction instanceof DexAddIntLit8
+        || instruction instanceof DexAddIntLit16
+        || instruction instanceof DexAddInt2Addr
+        || instruction instanceof DexAddLong
+        || instruction instanceof DexAddLong2Addr
+        || instruction instanceof DexSubInt
+        || instruction instanceof DexSubInt2Addr
+        || instruction instanceof DexSubLong
+        || instruction instanceof DexSubLong2Addr
+        || instruction instanceof DexDivInt
+        || instruction instanceof DexDivIntLit8
+        || instruction instanceof DexDivIntLit16
+        || instruction instanceof DexDivInt2Addr
+        || instruction instanceof DexDivLong
+        || instruction instanceof DexDivLong2Addr
+        || instruction instanceof DexRemInt
+        || instruction instanceof DexRemIntLit8
+        || instruction instanceof DexRemIntLit16
+        || instruction instanceof DexRemInt2Addr
+        || instruction instanceof DexRemLong
+        || instruction instanceof DexRemLong2Addr;
+  }
+
+  public boolean isIntOrLongLogicalBinop() {
+    return instruction instanceof DexAndInt
+        || instruction instanceof DexAndIntLit8
+        || instruction instanceof DexAndIntLit16
+        || instruction instanceof DexAndInt2Addr
+        || instruction instanceof DexAndLong
+        || instruction instanceof DexAndLong2Addr
+        || instruction instanceof DexOrInt
+        || instruction instanceof DexOrIntLit8
+        || instruction instanceof DexOrIntLit16
+        || instruction instanceof DexOrInt2Addr
+        || instruction instanceof DexOrLong
+        || instruction instanceof DexOrLong2Addr
+        || instruction instanceof DexXorInt
+        || instruction instanceof DexXorIntLit8
+        || instruction instanceof DexXorIntLit16
+        || instruction instanceof DexXorInt2Addr
+        || instruction instanceof DexXorLong
+        || instruction instanceof DexXorLong2Addr
+        || instruction instanceof DexShrInt
+        || instruction instanceof DexShrIntLit8
+        || instruction instanceof DexShrInt2Addr
+        || instruction instanceof DexShrLong
+        || instruction instanceof DexShrLong2Addr
+        || instruction instanceof DexShlInt
+        || instruction instanceof DexShlIntLit8
+        || instruction instanceof DexShlInt2Addr
+        || instruction instanceof DexShlLong
+        || instruction instanceof DexShlLong2Addr
+        || instruction instanceof DexUshrInt
+        || instruction instanceof DexUshrIntLit8
+        || instruction instanceof DexUshrInt2Addr
+        || instruction instanceof DexUshrLong
+        || instruction instanceof DexUshrLong2Addr;
+  }
+
   @Override
   public boolean isMultiplication() {
     return instruction instanceof DexMulInt
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index 05bd760..7fd166e 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -140,6 +140,10 @@
 
   boolean isSparseSwitch();
 
+  boolean isIntOrLongArithmeticBinop();
+
+  boolean isIntOrLongLogicalBinop();
+
   boolean isMultiplication();
 
   boolean isNewArray();
diff --git a/tools/archive.py b/tools/archive.py
index 558842c..27c3b25 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -166,6 +166,7 @@
           utils.R8RETRACE,
           utils.R8RETRACE_NO_DEPS,
           utils.LIBRARY_DESUGAR_CONVERSIONS,
+          utils.KEEPANNO_ANNOTATIONS_TARGET,
           '-Pno_internal'
       ])
 
@@ -238,6 +239,7 @@
       utils.DESUGAR_CONFIGURATION_JDK11_MINIMAL_MAVEN_ZIP,
       utils.DESUGAR_CONFIGURATION_JDK11_MAVEN_ZIP,
       utils.DESUGAR_CONFIGURATION_JDK11_NIO_MAVEN_ZIP,
+      utils.KEEPANNO_ANNOTATIONS_JAR,
       utils.GENERATED_LICENSE,
     ]:
       file_name = os.path.basename(file)
diff --git a/tools/utils.py b/tools/utils.py
index 5cb2cbe..b691b16 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -51,6 +51,7 @@
 R8_TESTS_DEPS_TARGET = 'RepackageTestDeps'
 R8LIB_TESTS_TARGET = 'configureTestForR8Lib'
 R8LIB_TESTS_DEPS_TARGET = R8_TESTS_DEPS_TARGET
+KEEPANNO_ANNOTATIONS_TARGET = 'keepAnnoJar'
 
 ALL_DEPS_JAR = os.path.join(LIBS, 'deps_all.jar')
 R8_JAR = os.path.join(LIBS, 'r8.jar')
@@ -70,6 +71,7 @@
 MAVEN_ZIP_LIB = os.path.join(LIBS, 'r8lib.zip')
 LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP = os.path.join(LIBS, 'library_desugar_conversions_legacy.jar')
 LIBRARY_DESUGAR_CONVERSIONS_ZIP = os.path.join(LIBS, 'library_desugar_conversions.jar')
+KEEPANNO_ANNOTATIONS_JAR = os.path.join(LIBS, 'keepanno-annotations.jar')
 
 DESUGAR_CONFIGURATION = os.path.join(
       'src', 'library_desugar', 'desugar_jdk_libs.json')