Merge commit '4f920edd9ded4a9c3436423ef3427ad7a5a40d0e' into dev-release

Change-Id: I2996101098642d5f04cc809ff797ac45fcc0e6fb
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index c994367..74c5484 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -439,10 +439,8 @@
 
     /** Add proguard rules for automatic main-dex-list calculation. */
     public Builder addMainDexRules(List<String> lines, Origin origin) {
-      guard(
-          () ->
-              mainDexRules.add(
-                  new ProguardConfigurationSourceStrings(lines, Paths.get("."), origin)));
+      String config = String.join(System.lineSeparator(), lines);
+      mainDexRules.add(new ProguardConfigurationSourceStrings(config, Paths.get("."), origin));
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/ExtractKeepAnnoRules.java b/src/main/java/com/android/tools/r8/ExtractKeepAnnoRules.java
index 90ffd66..a8c18f5 100644
--- a/src/main/java/com/android/tools/r8/ExtractKeepAnnoRules.java
+++ b/src/main/java/com/android/tools/r8/ExtractKeepAnnoRules.java
@@ -11,13 +11,11 @@
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
-import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
-import java.util.Collections;
 import java.util.List;
 
 /** Experimental API to extract keep rules from keep annotations. */
@@ -50,14 +48,7 @@
           List<KeepDeclaration> declarations = KeepEdgeReader.readKeepEdges(resource.getBytes());
           if (!declarations.isEmpty()) {
             KeepRuleExtractor extractor =
-                new KeepRuleExtractor(
-                    rule -> {
-                      ProguardConfigurationSourceStrings source =
-                          new ProguardConfigurationSourceStrings(
-                              Collections.singletonList(rule), null, resource.getOrigin());
-                      consumer.accept(source.get(), reporter);
-                    },
-                    extractorOptions);
+                new KeepRuleExtractor(rule -> consumer.accept(rule, reporter), extractorOptions);
             declarations.forEach(extractor::extract);
           }
         }
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index 4e1e118..ce770ed 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -82,8 +82,8 @@
      * Add proguard configuration for automatic main dex list calculation.
      */
     public GenerateMainDexListCommand.Builder addMainDexRules(List<String> lines, Origin origin) {
-      guard(() -> mainDexRules.add(
-          new ProguardConfigurationSourceStrings(lines, Paths.get("."), origin)));
+      String config = String.join(System.lineSeparator(), lines);
+      mainDexRules.add(new ProguardConfigurationSourceStrings(config, Paths.get("."), origin));
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index fa8a50c..9f1f42d 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -74,7 +74,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -247,10 +246,8 @@
 
     /** Add proguard rules for automatic main-dex-list calculation. */
     public Builder addMainDexRules(List<String> lines, Origin origin) {
-      guard(
-          () ->
-              mainDexRules.add(
-                  new ProguardConfigurationSourceStrings(lines, Paths.get("."), origin)));
+      String config = String.join(System.lineSeparator(), lines);
+      mainDexRules.add(new ProguardConfigurationSourceStrings(config, Paths.get("."), origin));
       return self();
     }
 
@@ -320,8 +317,8 @@
 
     /** Add proguard configuration. */
     public Builder addProguardConfiguration(List<String> lines, Origin origin) {
-      guard(() -> proguardConfigs.add(
-          new ProguardConfigurationSourceStrings(lines, Paths.get("."), origin)));
+      String config = String.join(System.lineSeparator(), lines);
+      proguardConfigs.add(new ProguardConfigurationSourceStrings(config, Paths.get("."), origin));
       return self();
     }
 
@@ -1051,7 +1048,7 @@
                         rule -> {
                           ProguardConfigurationSourceStrings source =
                               new ProguardConfigurationSourceStrings(
-                                  Collections.singletonList(rule), null, resource.getOrigin());
+                                  rule, null, resource.getOrigin());
                           parser.parse(source);
                         });
                 declarations.forEach(extractor::extract);
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 6d4a086..73aeeee 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -67,7 +67,7 @@
 public class ApplicationReader {
 
   private final InternalOptions options;
-  private final DexItemFactory itemFactory;
+  private final DexItemFactory factory;
   private final Timing timing;
   private final AndroidApp inputApp;
 
@@ -79,7 +79,7 @@
 
   public ApplicationReader(AndroidApp inputApp, InternalOptions options, Timing timing) {
     this.options = options;
-    itemFactory = options.itemFactory;
+    factory = options.itemFactory;
     this.timing = timing;
     this.inputApp = inputApp;
   }
@@ -127,7 +127,7 @@
 
     timing.begin("DexApplication.read");
     LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, timing);
-    TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
+    TaskCollection<?> tasks = createReaderTaskCollection(executorService);
     try {
       // Still preload some of the classes, primarily for two reasons:
       // (a) class lazy loading is not supported for DEX files
@@ -140,7 +140,7 @@
       ClassReader classReader = new ClassReader(tasks);
       classReader.initializeLazyClassCollection(builder);
       classReader.readSources();
-      timing.time("Await read", () -> tasks.await());
+      awaitReaderTaskCollection(tasks);
       flags = classReader.getDexApplicationReadFlags();
       return builder
           .addDataResourceProviders(inputApp.getProgramResourceProviders())
@@ -178,7 +178,7 @@
     timing.begin("DexApplication.readDirect");
     DirectMappedDexApplication.Builder builder =
         DirectMappedDexApplication.directBuilder(options, timing);
-    TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
+    TaskCollection<?> tasks = createReaderTaskCollection(executorService);
     try {
       readProguardMap(inputApp.getProguardMapInputData(), builder, tasks);
       ClassReader classReader = new ClassReader(tasks);
@@ -187,7 +187,7 @@
           allClassesBuilder::setClasspathClasses, allClassesBuilder::setLibraryClasses);
       allClassesBuilder.forceLoadNonProgramClassCollections(options, tasks, timing);
       classReader.readSources();
-      timing.time("Await read", () -> tasks.await());
+      awaitReaderTaskCollection(tasks);
       allClassesBuilder.setProgramClasses(
           ProgramClassCollection.resolveConflicts(classReader.programClasses, options));
       AllClasses allClasses = allClassesBuilder.build(options, timing);
@@ -209,6 +209,18 @@
     }
   }
 
+  private TaskCollection<?> createReaderTaskCollection(ExecutorService executorService) {
+    // Commit all statically known types before starting to read (e.g., java.lang.Object).
+    factory.commitPendingItems();
+    return new TaskCollection<>(options, executorService);
+  }
+
+  private void awaitReaderTaskCollection(TaskCollection<?> tasks) throws ExecutionException {
+    timing.time("Await read", () -> tasks.await());
+    // Commit all references corresponding to program definitions.
+    factory.commitPendingItems();
+  }
+
   public final void dump(DumpInputFlags dumpInputFlags) {
     assert verifyMainDexOptionsCompatible(inputApp, options);
     dumpApplication(dumpInputFlags);
@@ -250,7 +262,7 @@
         if (emitDeprecatedDiagnostics) {
           options.reporter.error(new UnsupportedMainDexListUsageDiagnostic(resource.getOrigin()));
         }
-        addToMainDexClasses(app, builder, MainDexListParser.parseList(resource, itemFactory));
+        addToMainDexClasses(app, builder, MainDexListParser.parseList(resource, factory));
       }
       if (!inputApp.getMainDexClasses().isEmpty()) {
         if (emitDeprecatedDiagnostics) {
@@ -260,7 +272,7 @@
             app,
             builder,
             inputApp.getMainDexClasses().stream()
-                .map(clazz -> itemFactory.createType(DescriptorUtils.javaTypeToDescriptor(clazz)))
+                .map(clazz -> factory.createType(DescriptorUtils.javaTypeToDescriptor(clazz)))
                 .collect(Collectors.toList()));
       }
     }
@@ -435,7 +447,7 @@
         return true;
       }
       DexAnnotation retentionAnnotation =
-          clazz.annotations().getFirstMatching(itemFactory.retentionType);
+          clazz.annotations().getFirstMatching(factory.retentionType);
       // Default is CLASS retention, read if retained.
       if (retentionAnnotation == null) {
         return DexAnnotation.retainCompileTimeAnnotation(clazz.getType(), application.options);
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index ce75e87..5b17a26 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -3360,28 +3360,6 @@
     return markers;
   }
 
-  // Non-synchronized internal create.
-  private DexType internalCreateType(DexString descriptor) {
-    assert descriptor != null;
-    DexType result = types.get(descriptor);
-    if (result == null) {
-      if (descriptor.getFirstByteAsChar() != '[') {
-        result = new DexType(descriptor);
-      } else {
-        DexType elementType = createType(descriptor.toArrayElementDescriptor(this));
-        result = new DexArrayType(descriptor, elementType);
-      }
-      assert result.isArrayType()
-              || result.isClassType()
-              || result.isPrimitiveType()
-              || result.isVoidType()
-          : descriptor.toString();
-      assert !isInternalSentinel(result);
-      types.put(descriptor, result);
-    }
-    return result;
-  }
-
   private DexType createStaticallyKnownType(String descriptor) {
     return createStaticallyKnownType(createString(descriptor));
   }
@@ -3394,7 +3372,7 @@
   }
 
   private DexType createStaticallyKnownType(DexString descriptor) {
-    DexType type = internalCreateType(descriptor);
+    DexType type = createType(descriptor);
     // Conservatively add all statically known types to "compiler synthesized types set".
     addPossiblySynthesizedType(type);
     return type;
@@ -3403,7 +3381,7 @@
   // Safe synchronized external create. May be used for statically known types in synthetic code.
   // See the generated BackportedMethods.java for reference.
   public synchronized DexType createSynthesizedType(String descriptor) {
-    DexType type = internalCreateType(createString(descriptor));
+    DexType type = createType(createString(descriptor));
     addPossiblySynthesizedType(type);
     return type;
   }
@@ -3431,9 +3409,21 @@
     possibleCompilerSynthesizedTypes.forEach(fn);
   }
 
-  // Safe synchronized external create. Should never be used to create a statically known type!
-  public synchronized DexType createType(DexString descriptor) {
-    return internalCreateType(descriptor);
+  public DexType createType(DexString descriptor) {
+    assert descriptor != null;
+    DexType committed = committedTypes.get(descriptor);
+    if (committed != null) {
+      return committed;
+    }
+    if (descriptor.getFirstByteAsChar() != '[') {
+      return types.computeIfAbsent(descriptor, DexType::new);
+    }
+    DexType pending = types.get(descriptor);
+    if (pending != null) {
+      return pending;
+    }
+    DexType elementType = createType(descriptor.toArrayElementDescriptor(this));
+    return types.computeIfAbsent(descriptor, d -> new DexArrayType(d, elementType));
   }
 
   public DexType createType(String descriptor) {
@@ -3686,7 +3676,7 @@
   public void commitPendingItems() {
     commitPendingItems(committedMethodHandles, methodHandles);
     commitPendingItems(committedStrings, strings);
-    // commitPendingItems(committedTypes, types);
+    commitPendingItems(committedTypes, types);
     commitPendingItems(committedFields, fields);
     commitPendingItems(committedProtos, protos);
     commitPendingItems(committedMethods, methods);
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
index 5a42ddc..4cd24cc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 
-public interface FieldGet {
+public interface FieldGet extends InstructionOrValue {
 
   DexField getField();
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldPut.java b/src/main/java/com/android/tools/r8/ir/code/FieldPut.java
index 91e6d2d..37b1cf5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldPut.java
@@ -10,9 +10,7 @@
 import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 
-public interface FieldPut {
-
-  BasicBlock getBlock();
+public interface FieldPut extends InstructionOrValue {
 
   DexField getField();
 
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 1de0af6..003e17f 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
@@ -374,12 +374,9 @@
     }
   }
 
-  /**
-   * Returns the basic block containing this instruction.
-   */
+  /** Returns the basic block containing this instruction. */
   @Override
-  public BasicBlock getBlock() {
-    assert block != null;
+  public BasicBlock getBlockOrNull() {
     return block;
   }
 
@@ -423,13 +420,6 @@
     block.getInstructions().replace(this, newInstruction, affectedValues);
   }
 
-  /**
-   * Returns true if the instruction is in the IR and therefore has a block.
-   */
-  public boolean hasBlock() {
-    return block != null;
-  }
-
   public String getInstructionName() {
     return getClass().getSimpleName();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionOrValue.java b/src/main/java/com/android/tools/r8/ir/code/InstructionOrValue.java
index b45fd92..65c63f4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionOrValue.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionOrValue.java
@@ -29,5 +29,14 @@
     return null;
   }
 
-  BasicBlock getBlock();
+  default boolean hasBlock() {
+    return getBlockOrNull() != null;
+  }
+
+  default BasicBlock getBlock() {
+    assert hasBlock();
+    return getBlockOrNull();
+  }
+
+  BasicBlock getBlockOrNull();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index e49e987..83dd50b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -84,12 +84,7 @@
   }
 
   @Override
-  public boolean hasBlock() {
-    return block != null;
-  }
-
-  @Override
-  public BasicBlock getBlock() {
+  public BasicBlock getBlockOrNull() {
     return block;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index fff0036..6546ce4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -1087,13 +1087,9 @@
         && !appView.enableWholeProgramOptimizations();
   }
 
-  public boolean hasBlock() {
-    return definition.hasBlock();
-  }
-
   @Override
-  public BasicBlock getBlock() {
-    return definition.getBlock();
+  public BasicBlock getBlockOrNull() {
+    return definition.getBlockOrNull();
   }
 
   public TypeElement getType() {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPassCollection.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPassCollection.java
index 54fa781..133faa0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPassCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPassCollection.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.optimize.ListIterationRewriter;
 import com.android.tools.r8.ir.optimize.RedundantFieldLoadAndStoreElimination;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
+import com.android.tools.r8.ir.optimize.ShareInstanceGetInstructions;
 import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
 import com.android.tools.r8.ir.optimize.string.StringBuilderAppendOptimizer;
 import com.android.tools.r8.utils.InternalOptions;
@@ -63,6 +64,7 @@
       passes.add(new SplitReturnRewriter(appView));
       passes.add(new ReturnBlockCanonicalizerRewriter(appView));
     }
+    passes.add(new ShareInstanceGetInstructions(appView));
     return new CodeRewriterPassCollection(passes);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index e459b66..f34dd58 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -3,6 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_CUSTOM;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_MULTI_NEW_ARRAY;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_POLYMORPHIC;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
+import static com.android.tools.r8.ir.code.Opcodes.NEW_ARRAY_FILLED;
+import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
@@ -25,13 +36,12 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableList;
-import java.util.ArrayDeque;
 import java.util.Collection;
-import java.util.Deque;
 import java.util.Iterator;
-import java.util.Queue;
+import java.util.List;
 
 public class DeadCodeRemover {
 
@@ -51,13 +61,15 @@
     // We may encounter unneeded catch handlers after each iteration, e.g., if a dead instruction
     // is the only throwing instruction in a block. Removing unneeded catch handlers can lead to
     // more dead instructions.
-    Deque<BasicBlock> worklist = new ArrayDeque<>();
+    List<BasicBlock> topologicallySortedBlocks = code.topologicallySortedBlocks();
+    WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList();
     do {
       AffectedValues affectedValues = new AffectedValues();
       ValueIsDeadAnalysis valueIsDeadAnalysis = new ValueIsDeadAnalysis(appView, code);
-      worklist.addAll(code.topologicallySortedBlocks());
+      worklist.addIfNotSeen(topologicallySortedBlocks);
       while (!worklist.isEmpty()) {
         BasicBlock block = worklist.removeLast();
+        worklist.removeSeen(block);
         removeDeadInstructions(worklist, code, block, affectedValues, valueIsDeadAnalysis);
         removeDeadAndTrivialPhis(worklist, block, valueIsDeadAnalysis);
       }
@@ -95,30 +107,31 @@
   }
 
   // Add the block from where the value originates to the worklist.
-  private static void updateWorklist(Queue<BasicBlock> worklist, Value value) {
-    BasicBlock block = null;
-    if (value.isPhi()) {
-      block = value.asPhi().getBlock();
-    } else if (value.definition.hasBlock()) {
-      block = value.definition.getBlock();
-    }
+  private static void updateWorklist(WorkList<BasicBlock> worklist, Value value) {
+    BasicBlock block = value.getBlockOrNull();
     if (block != null) {
-      worklist.add(block);
+      worklist.addIfNotSeen(block);
     }
   }
 
   // Add all blocks from where the in/debug-values to the instruction originates.
-  private static void updateWorklist(Queue<BasicBlock> worklist, Instruction instruction) {
+  private static void updateWorklist(WorkList<BasicBlock> worklist, Instruction instruction) {
     for (Value inValue : instruction.inValues()) {
-      updateWorklist(worklist, inValue);
+      BasicBlock block = inValue.getBlockOrNull();
+      if (block != null && block != instruction.getBlock()) {
+        updateWorklist(worklist, inValue);
+      }
     }
     for (Value debugValue : instruction.getDebugValues()) {
-      updateWorklist(worklist, debugValue);
+      BasicBlock block = debugValue.getBlockOrNull();
+      if (block != null && block != instruction.getBlock()) {
+        updateWorklist(worklist, debugValue);
+      }
     }
   }
 
   private void removeDeadAndTrivialPhis(
-      Queue<BasicBlock> worklist, BasicBlock block, ValueIsDeadAnalysis valueIsDeadAnalysis) {
+      WorkList<BasicBlock> worklist, BasicBlock block, ValueIsDeadAnalysis valueIsDeadAnalysis) {
     Iterator<Phi> phiIt = block.getPhis().iterator();
     while (phiIt.hasNext()) {
       Phi phi = phiIt.next();
@@ -137,7 +150,7 @@
 
   @SuppressWarnings("ReferenceEquality")
   private void removeDeadInstructions(
-      Queue<BasicBlock> worklist,
+      WorkList<BasicBlock> worklist,
       IRCode code,
       BasicBlock block,
       AffectedValues affectedValues,
@@ -146,36 +159,55 @@
     while (iterator.hasPrevious()) {
       Instruction current = iterator.previous();
       if (current.hasOutValue()) {
-        // Replace unnecessary cast values.
-        if (current.isCheckCast()) {
-          CheckCast checkCast = current.asCheckCast();
-          if (!checkCast.isRefiningStaticType(appView.options())
-              && checkCast.outValue().getLocalInfo() == checkCast.object().getLocalInfo()) {
-            updateWorklistWithNonDebugUses(worklist, checkCast);
-            checkCast.outValue().replaceUsers(checkCast.object(), affectedValues);
-            checkCast.object().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
-          }
-        }
-        // Remove unused invoke results.
-        if (current.isInvoke() && !current.outValue().isUsed()) {
-          current.setOutValue(null);
-        }
-        if (current.isStaticGet() && !current.outValue().isUsed() && appView.hasLiveness()) {
-          Box<InitClass> initClass = new Box<>();
-          if (iterator.removeOrReplaceCurrentInstructionByInitClassIfPossible(
-              appView.withLiveness(),
-              code,
-              current.asStaticGet().getField().getHolderType(),
-              initClass::set)) {
-            if (initClass.isSet()) {
-              // Apply dead code remover to the new init-class instruction.
-              current = iterator.previous();
-              assert current == initClass.get();
-            } else {
-              // Instruction removed.
-              continue;
+        switch (current.opcode()) {
+          case CHECK_CAST:
+            {
+              // Replace unnecessary cast values.
+              CheckCast checkCast = current.asCheckCast();
+              if (!checkCast.isRefiningStaticType(appView.options())
+                  && checkCast.outValue().getLocalInfo() == checkCast.object().getLocalInfo()) {
+                updateWorklistWithNonDebugUses(worklist, checkCast);
+                checkCast.outValue().replaceUsers(checkCast.object(), affectedValues);
+                checkCast.object().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
             }
+              break;
           }
+          case INVOKE_CUSTOM:
+          case INVOKE_DIRECT:
+          case INVOKE_INTERFACE:
+          case INVOKE_MULTI_NEW_ARRAY:
+          case INVOKE_STATIC:
+          case INVOKE_SUPER:
+          case INVOKE_POLYMORPHIC:
+          case INVOKE_VIRTUAL:
+          case NEW_ARRAY_FILLED:
+            // Remove unused invoke results.
+            if (!current.outValue().isUsed()) {
+              current.clearOutValue();
+            }
+            break;
+          case STATIC_GET:
+            if (!current.outValue().isUsed() && appView.hasLiveness()) {
+              Box<InitClass> initClass = new Box<>();
+              if (iterator.removeOrReplaceCurrentInstructionByInitClassIfPossible(
+                  appView.withLiveness(),
+                  code,
+                  current.asStaticGet().getField().getHolderType(),
+                  initClass::set)) {
+                if (initClass.isSet()) {
+                  // Apply dead code remover to the new init-class instruction.
+                  current = iterator.previous();
+                  assert current == initClass.get();
+                } else {
+                  // Instruction removed.
+                  continue;
+                }
+              }
+            }
+            break;
+          default:
+            assert !current.isInvoke();
+            break;
         }
       }
       DeadInstructionResult deadInstructionResult = current.canBeDeadCode(appView, code);
@@ -209,12 +241,12 @@
   }
 
   private static void updateWorklistWithNonDebugUses(
-      Queue<BasicBlock> worklist, CheckCast checkCast) {
+      WorkList<BasicBlock> worklist, CheckCast checkCast) {
     for (Instruction user : checkCast.outValue().uniqueUsers()) {
-      worklist.add(user.getBlock());
+      worklist.addIfNotSeen(user.getBlock());
     }
     for (Phi user : checkCast.outValue().uniquePhiUsers()) {
-      worklist.add(user.getBlock());
+      worklist.addIfNotSeen(user.getBlock());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ShareInstanceGetInstructions.java b/src/main/java/com/android/tools/r8/ir/optimize/ShareInstanceGetInstructions.java
new file mode 100644
index 0000000..0fe2c54
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ShareInstanceGetInstructions.java
@@ -0,0 +1,193 @@
+// Copyright (c) 2025, 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;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.google.common.collect.Sets;
+import java.util.List;
+import java.util.Set;
+
+public class ShareInstanceGetInstructions extends CodeRewriterPass<AppInfo> {
+
+  public ShareInstanceGetInstructions(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getRewriterId() {
+    return "ShareInstanceGetInstructions";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    return code.metadata().mayHaveInstanceGet();
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean changed = false;
+    for (BasicBlock block : code.getBlocks()) {
+      // Try to hoist identical InstanceGets in two successors to this block:
+      List<BasicBlock> successors = block.getSuccessors();
+      // TODO(b/448586591: We should also be able to handle catch handlers by splitting the block we
+      // hoist into.
+      if (successors.size() == 2 && !block.hasCatchHandlers()) {
+        InstanceGet firstInstanceGet = findFirstInstanceGetInstruction(code, successors.get(0));
+        InstanceGet secondInstanceGet = findFirstInstanceGetInstruction(code, successors.get(1));
+        if (firstInstanceGet == null || secondInstanceGet == null) {
+          continue;
+        }
+        Value firstReceiver = firstInstanceGet.object();
+        Value firstReceiverRoot = firstReceiver.getAliasedValue();
+
+        Value secondReceiver = secondInstanceGet.object();
+        DexField field = firstInstanceGet.getField();
+        if (firstReceiverRoot != secondReceiver.getAliasedValue()
+            || firstReceiver.isMaybeNull()
+            || secondReceiver.isMaybeNull()
+            || field.isNotIdenticalTo(secondInstanceGet.getField())) {
+          continue;
+        }
+        Value firstOutValue = firstInstanceGet.outValue();
+        Value secondOutValue = secondInstanceGet.outValue();
+        if (firstOutValue.hasLocalInfo() || secondOutValue.hasLocalInfo()) {
+          continue;
+        }
+        Value outValue = code.createValue(firstOutValue.getType());
+        Value newReceiver =
+            firstReceiver.getBlock() == firstInstanceGet.getBlock()
+                ? firstReceiverRoot
+                : firstReceiver;
+        InstanceGet hoistedInstanceGet = new InstanceGet(outValue, newReceiver, field);
+        hoistedInstanceGet.setPosition(firstInstanceGet.getPosition());
+        block.getInstructions().addBefore(hoistedInstanceGet, block.getLastInstruction());
+        removeOldInstructions(outValue, firstInstanceGet, secondInstanceGet);
+        changed = true;
+      }
+      // Try to sink shareable InstanceGets from two predecessors into this block:
+      List<BasicBlock> predecessors = block.getPredecessors();
+      if (predecessors.size() == 2 && !block.hasCatchHandlers()) {
+        BasicBlock firstPredecessor = predecessors.get(0);
+        BasicBlock secondPredecessor = predecessors.get(1);
+        // TODO(b/448586591: We should also be able to handle catch handlers by splitting the block
+        // we hoist into.
+        if (firstPredecessor.hasCatchHandlers() || secondPredecessor.hasCatchHandlers()) {
+          continue;
+        }
+        InstanceGet firstInstanceGet = getLastInstanceGet(firstPredecessor);
+        InstanceGet secondInstanceGet = getLastInstanceGet(secondPredecessor);
+        if (firstInstanceGet == null || secondInstanceGet == null) {
+          continue;
+        }
+        Value firstOutValue = firstInstanceGet.outValue();
+        Value secondOutValue = secondInstanceGet.outValue();
+        if (firstOutValue.hasLocalInfo()
+            || secondOutValue.hasLocalInfo()
+            || hasPhisThatWillBecomeInvalid(block, firstOutValue, secondOutValue)) {
+          continue;
+        }
+        Value firstReceiver = firstInstanceGet.object();
+        Value secondReceiver = secondInstanceGet.object();
+        if (firstReceiver.isMaybeNull() || secondReceiver.isMaybeNull()) {
+          continue;
+        }
+        DexField field = firstInstanceGet.getField();
+        if (field.isNotIdenticalTo(secondInstanceGet.getField())) {
+          continue;
+        }
+        Value receiver;
+        if (firstReceiver == secondReceiver) {
+          receiver = firstReceiver;
+        } else {
+          Value firstReceiverRoot = firstReceiver.getAliasedValue();
+          if (firstReceiverRoot == secondReceiver.getAliasedValue()) {
+            receiver = firstReceiverRoot;
+          } else {
+            TypeElement type = firstReceiver.getType().join(secondReceiver.getType(), appView);
+            Phi phi = code.createPhi(block, type);
+            phi.appendOperand(firstReceiver);
+            phi.appendOperand(secondReceiver);
+            receiver = phi;
+          }
+        }
+        Value outValue = code.createValue(firstOutValue.getType());
+        Instruction hoistedInstanceGet = new InstanceGet(outValue, receiver, field);
+        hoistedInstanceGet.setPosition(firstInstanceGet.getPosition());
+        block.getInstructions().addFirst(hoistedInstanceGet);
+        removeOldInstructions(outValue, firstInstanceGet, secondInstanceGet);
+        changed = true;
+      }
+    }
+    if (changed) {
+      code.removeRedundantBlocks();
+    }
+    return CodeRewriterResult.hasChanged(changed);
+  }
+
+  private static void removeOldInstructions(
+      Value outValue, InstanceGet firstInstanceGet, InstanceGet secondInstanceGet) {
+    firstInstanceGet.outValue().replaceUsers(outValue);
+    secondInstanceGet.outValue().replaceUsers(outValue);
+    outValue.uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+    firstInstanceGet.removeOrReplaceByDebugLocalRead();
+    secondInstanceGet.removeOrReplaceByDebugLocalRead();
+  }
+
+  private boolean hasPhisThatWillBecomeInvalid(
+      BasicBlock block, Value firstOutValue, Value secondOutValue) {
+    for (Phi phi : block.getPhis()) {
+      if (phi.getOperands().contains(firstOutValue)) {
+        if (phi.getOperands().size() != 2 || !phi.getOperands().contains(secondOutValue)) {
+          return true;
+        }
+      } else if (phi.getOperands().contains(secondOutValue)) {
+        if (phi.getOperands().size() != 2 || !phi.getOperands().contains(firstOutValue)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private InstanceGet getLastInstanceGet(BasicBlock block) {
+    Set<Value> seenValues = Sets.newIdentityHashSet();
+    for (Instruction instruction = block.getLastInstruction();
+        instruction != null;
+        instruction = instruction.getPrev()) {
+      if (instruction.isInstanceGet() && !seenValues.contains(instruction.outValue())) {
+        return instruction.asInstanceGet();
+      } else {
+        seenValues.addAll(instruction.inValues());
+      }
+    }
+    return null;
+  }
+
+  private InstanceGet findFirstInstanceGetInstruction(IRCode code, BasicBlock block) {
+    for (Instruction instruction = block.entry();
+        instruction != null;
+        instruction = instruction.getNext()) {
+      if (instruction.instructionMayHaveSideEffects(appView, code.context())) {
+        break;
+      }
+      if (instruction.isInstanceGet()) {
+        return instruction.asInstanceGet();
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/ConsumerRuleDiagnostic.java b/src/main/java/com/android/tools/r8/processkeeprules/ConsumerRuleDiagnostic.java
new file mode 100644
index 0000000..1a119a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/processkeeprules/ConsumerRuleDiagnostic.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2025, 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.processkeeprules;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+@KeepForApi
+public abstract class ConsumerRuleDiagnostic implements Diagnostic {
+
+  private final Origin origin;
+  private final Position position;
+
+  public ConsumerRuleDiagnostic(Origin origin, Position position) {
+    this.origin = origin;
+    this.position = position;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/FilteredKeepRulesBuilder.java b/src/main/java/com/android/tools/r8/processkeeprules/FilteredKeepRulesBuilder.java
new file mode 100644
index 0000000..fa4d3ab
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/processkeeprules/FilteredKeepRulesBuilder.java
@@ -0,0 +1,227 @@
+// Copyright (c) 2025, 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.processkeeprules;
+
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.shaking.FilteredClassPath;
+import com.android.tools.r8.shaking.ProguardClassNameList;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
+import com.android.tools.r8.shaking.ProguardConfigurationParserConsumer;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.shaking.ProguardPathList;
+import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.List;
+
+// TODO(b/437139566): This is not yet feature complete. Add support for writing all directives.
+// TODO(b/437139566): Implement filtering by commenting out rules.
+public class FilteredKeepRulesBuilder implements ProguardConfigurationParserConsumer {
+
+  private final StringConsumer consumer;
+  private final Reporter reporter;
+
+  private boolean isInComment;
+
+  FilteredKeepRulesBuilder(StringConsumer consumer, Reporter reporter) {
+    this.consumer = consumer;
+    this.reporter = reporter;
+  }
+
+  private void appendToCurrentLine(String string) {
+    assert string.indexOf('\n') < 0;
+    consumer.accept(string, reporter);
+    if (!isInComment && string.indexOf('#') >= 0) {
+      isInComment = true;
+    }
+  }
+
+  private void ensureComment() {
+    if (!isInComment) {
+      appendToCurrentLine("#");
+      isInComment = true;
+    }
+  }
+
+  private void ensureNewlineAfterComment() {
+    if (isInComment) {
+      exitComment();
+      consumer.accept(StringUtils.UNIX_LINE_SEPARATOR, reporter);
+    }
+  }
+
+  private void exitComment() {
+    isInComment = false;
+  }
+
+  private void write(ProguardConfigurationSourceParser parser, TextPosition positionStart) {
+    write(parser.getContentSince(positionStart));
+  }
+
+  private void write(String string) {
+    int lastNewlineIndex = string.lastIndexOf('\n');
+    if (lastNewlineIndex < 0) {
+      appendToCurrentLine(string);
+    } else {
+      // Write the lines leading up to the last line.
+      String untilNewlineInclusive = string.substring(0, lastNewlineIndex + 1);
+      consumer.accept(untilNewlineInclusive, reporter);
+      // Due to the newline character we are no longer inside a comment.
+      exitComment();
+      // Emit everything after the newline character.
+      String fromNewlineExclusive = string.substring(lastNewlineIndex + 1);
+      appendToCurrentLine(fromNewlineExclusive);
+    }
+  }
+
+  @Override
+  public void addKeepAttributePatterns(
+      List<String> attributesPatterns,
+      Origin origin,
+      ProguardConfigurationSourceParser parser,
+      Position position,
+      TextPosition positionStart) {
+    ensureNewlineAfterComment();
+    write(parser, positionStart);
+  }
+
+  @Override
+  public void addRule(ProguardConfigurationRule rule) {
+    ensureNewlineAfterComment();
+    write(rule.getSource());
+  }
+
+  @Override
+  public void addWhitespace(ProguardConfigurationSourceParser parser, TextPosition positionStart) {
+    write(parser, positionStart);
+  }
+
+  @Override
+  public void disableObfuscation(Origin origin, Position position) {
+    ensureComment();
+    write("-dontobfuscate");
+  }
+
+  @Override
+  public void disableOptimization(Origin origin, Position position) {
+    ensureComment();
+    write("-dontoptimize");
+  }
+
+  @Override
+  public void disableShrinking(Origin origin, Position position) {
+    ensureComment();
+    write("-dontshrink");
+  }
+
+  @Override
+  public void setRenameSourceFileAttribute(String s, Origin origin, Position position) {}
+
+  @Override
+  public void addKeepPackageNamesPattern(ProguardClassNameList proguardClassNameList) {}
+
+  @Override
+  public void setKeepParameterNames(boolean b, Origin origin, Position position) {}
+
+  @Override
+  public void enableKeepDirectories() {}
+
+  @Override
+  public void addKeepDirectories(ProguardPathList proguardPathList) {}
+
+  @Override
+  public void addParsedConfiguration(String s) {}
+
+  @Override
+  public void setPrintUsage(boolean b) {}
+
+  @Override
+  public void setPrintUsageFile(Path path) {}
+
+  @Override
+  public void enableProtoShrinking() {}
+
+  @Override
+  public void setIgnoreWarnings(boolean b) {}
+
+  @Override
+  public void addDontWarnPattern(ProguardClassNameList pattern) {}
+
+  @Override
+  public void addDontNotePattern(ProguardClassNameList pattern) {}
+
+  @Override
+  public void enableAllowAccessModification(Origin origin, Position position) {}
+
+  @Override
+  public void enablePrintConfiguration(Origin origin, Position position) {}
+
+  @Override
+  public void setPrintConfigurationFile(Path path) {}
+
+  @Override
+  public void enablePrintMapping(Origin origin, Position position) {}
+
+  @Override
+  public void setPrintMappingFile(Path path) {}
+
+  @Override
+  public void setApplyMappingFile(Path path, Origin origin, Position position) {}
+
+  @Override
+  public void addInjars(
+      List<FilteredClassPath> filteredClassPaths, Origin origin, Position position) {}
+
+  @Override
+  public void addLibraryJars(
+      List<FilteredClassPath> filteredClassPaths, Origin origin, Position position) {}
+
+  @Override
+  public void setPrintSeeds(boolean b, Origin origin, Position position) {}
+
+  @Override
+  public void setSeedFile(Path path) {}
+
+  @Override
+  public void setObfuscationDictionary(Path path, Origin origin, Position position) {}
+
+  @Override
+  public void setClassObfuscationDictionary(Path path, Origin origin, Position position) {}
+
+  @Override
+  public void setPackageObfuscationDictionary(Path path, Origin origin, Position position) {}
+
+  @Override
+  public void addAdaptClassStringsPattern(ProguardClassNameList pattern) {}
+
+  @Override
+  public void addAdaptResourceFileContents(ProguardPathList pattern) {}
+
+  @Override
+  public void addAdaptResourceFilenames(ProguardPathList pattern) {}
+
+  @Override
+  public void joinMaxRemovedAndroidLogLevel(int maxRemovedAndroidLogLevel) {}
+
+  @Override
+  public PackageObfuscationMode getPackageObfuscationMode() {
+    return null;
+  }
+
+  @Override
+  public void setPackagePrefix(String s) {}
+
+  @Override
+  public void setFlattenPackagePrefix(String s) {}
+
+  @Override
+  public void enableRepackageClasses(Origin origin, Position position) {}
+
+  @Override
+  public void enableFlattenPackageHierarchy(Origin origin, Position position) {}
+}
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/GlobalLibraryConsumerRuleDiagnostic.java b/src/main/java/com/android/tools/r8/processkeeprules/GlobalLibraryConsumerRuleDiagnostic.java
index 3921207..bc1a21d 100644
--- a/src/main/java/com/android/tools/r8/processkeeprules/GlobalLibraryConsumerRuleDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/processkeeprules/GlobalLibraryConsumerRuleDiagnostic.java
@@ -3,34 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.processkeeprules;
 
-import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 
 @KeepForApi
-public class GlobalLibraryConsumerRuleDiagnostic implements Diagnostic {
-  private final Origin origin;
-  private final Position position;
+public class GlobalLibraryConsumerRuleDiagnostic extends ConsumerRuleDiagnostic {
   private final String rule;
 
   public GlobalLibraryConsumerRuleDiagnostic(Origin origin, Position position, String rule) {
-    this.origin = origin;
-    this.position = position;
+    super(origin, position);
     this.rule = rule;
   }
 
   @Override
-  public Origin getOrigin() {
-    return origin;
-  }
-
-  @Override
-  public Position getPosition() {
-    return position;
-  }
-
-  @Override
   public String getDiagnosticMessage() {
     return rule + " not allowed in library consumer rules.";
   }
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/KeepAttributeLibraryConsumerRuleDiagnostic.java b/src/main/java/com/android/tools/r8/processkeeprules/KeepAttributeLibraryConsumerRuleDiagnostic.java
index c7c1c1a..c1f3d5a 100644
--- a/src/main/java/com/android/tools/r8/processkeeprules/KeepAttributeLibraryConsumerRuleDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/processkeeprules/KeepAttributeLibraryConsumerRuleDiagnostic.java
@@ -3,36 +3,22 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.processkeeprules;
 
-import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 
 @KeepForApi
-public class KeepAttributeLibraryConsumerRuleDiagnostic implements Diagnostic {
+public class KeepAttributeLibraryConsumerRuleDiagnostic extends ConsumerRuleDiagnostic {
 
-  private final Origin origin;
-  private final Position position;
   private final String attribute;
 
   public KeepAttributeLibraryConsumerRuleDiagnostic(
       Origin origin, Position position, String attribute) {
-    this.origin = origin;
-    this.position = position;
+    super(origin, position);
     this.attribute = attribute;
   }
 
   @Override
-  public Origin getOrigin() {
-    return origin;
-  }
-
-  @Override
-  public Position getPosition() {
-    return position;
-  }
-
-  @Override
   public String getDiagnosticMessage() {
     return "Illegal attempt to keep the attribute '" + attribute + "' in library consumer rules.";
   }
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRules.java b/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRules.java
index 158894d..9f1cf1e 100644
--- a/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRules.java
+++ b/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRules.java
@@ -4,28 +4,62 @@
 package com.android.tools.r8.processkeeprules;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.shaking.ProguardConfigurationParserConsumer;
+import com.android.tools.r8.shaking.ProguardConfigurationSource;
 import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.Reporter;
+import java.util.List;
 
 @KeepForApi
 public class ProcessKeepRules {
+
+  private final DexItemFactory factory;
+  private final Reporter reporter;
+
+  private final List<ProguardConfigurationSource> keepRules;
+  private final StringConsumer filteredKeepRulesConsumer;
+  private final boolean validateLibraryConsumerRules;
+
+  private ProcessKeepRules(DexItemFactory factory, ProcessKeepRulesCommand command) {
+    this.factory = factory;
+    this.reporter = command.getReporter();
+    this.keepRules = command.getKeepRules();
+    this.filteredKeepRulesConsumer = command.getFilteredKeepRulesConsumer();
+    this.validateLibraryConsumerRules = command.isValidateLibraryConsumerRules();
+  }
+
   public static void run(ProcessKeepRulesCommand command) throws CompilationFailedException {
     ExceptionUtils.withCompilationHandler(
         command.getReporter(),
         () -> {
-          // This is the only valid form of keep rule processing for now.
-          assert command.getValidateLibraryConsumerRules();
-          DexItemFactory dexItemFactory = new DexItemFactory();
-          ProguardConfigurationParserConsumer configurationConsumer =
-              new ValidateLibraryConsumerRulesKeepRuleProcessor(command.getReporter());
-          ProguardConfigurationParser parser =
-              new ProguardConfigurationParser(
-                  dexItemFactory, command.getReporter(), configurationConsumer);
-          parser.parse(command.getKeepRules());
-          command.getReporter().failIfPendingErrors();
+          DexItemFactory factory = new DexItemFactory();
+          new ProcessKeepRules(factory, command).internalRun();
         });
   }
+
+  private void internalRun() {
+    FilteredKeepRulesBuilder filteredKeepRulesBuilder =
+        filteredKeepRulesConsumer != null
+            ? new FilteredKeepRulesBuilder(filteredKeepRulesConsumer, reporter)
+            : null;
+    ProguardConfigurationParserConsumer parserConsumer =
+        validateLibraryConsumerRules
+            ? new ValidateLibraryConsumerRulesKeepRuleProcessor(reporter)
+            : filteredKeepRulesBuilder;
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(factory, reporter, parserConsumer);
+    parser.parse(keepRules);
+    supplyConsumers();
+    reporter.failIfPendingErrors();
+  }
+
+  private void supplyConsumers() {
+    if (filteredKeepRulesConsumer != null) {
+      filteredKeepRulesConsumer.finished(reporter);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommand.java b/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommand.java
index 81f18f2..0c429f3 100644
--- a/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommand.java
+++ b/src/main/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommand.java
@@ -4,27 +4,36 @@
 package com.android.tools.r8.processkeeprules;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfigurationSource;
 import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
 import com.android.tools.r8.utils.Reporter;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
 @KeepForApi
 public class ProcessKeepRulesCommand {
+
   private final Reporter reporter;
   private final List<ProguardConfigurationSource> keepRules;
+  private final StringConsumer filteredKeepRulesConsumer;
   private final boolean validateLibraryConsumerRules;
 
   private ProcessKeepRulesCommand(
       Reporter reporter,
       List<ProguardConfigurationSource> keepRules,
+      StringConsumer filteredKeepRulesConsumer,
       boolean validateLibraryConsumerRules) {
     this.reporter = reporter;
     this.keepRules = keepRules;
+    this.filteredKeepRulesConsumer = filteredKeepRulesConsumer;
     this.validateLibraryConsumerRules = validateLibraryConsumerRules;
   }
 
@@ -49,10 +58,14 @@
     return reporter;
   }
 
-  boolean getValidateLibraryConsumerRules() {
+  boolean isValidateLibraryConsumerRules() {
     return validateLibraryConsumerRules;
   }
 
+  StringConsumer getFilteredKeepRulesConsumer() {
+    return filteredKeepRulesConsumer;
+  }
+
   List<ProguardConfigurationSource> getKeepRules() {
     return keepRules;
   }
@@ -61,7 +74,9 @@
   public static class Builder {
 
     private final Reporter reporter;
-    private List<ProguardConfigurationSource> keepRuleFiles = new ArrayList<>();
+    private final List<ProguardConfigurationSource> keepRules = new ArrayList<>();
+
+    private StringConsumer filteredKeepRulesConsumer = null;
     private boolean validateLibraryConsumerRules = false;
 
     // TODO(b/447161121) introduce a DefaultDiagnosticHandler instead.
@@ -73,14 +88,28 @@
       this.reporter = new Reporter(diagnosticsHandler);
     }
 
+    public Builder addKeepRules(String data, Origin origin) {
+      keepRules.add(new ProguardConfigurationSourceStrings(data, Paths.get("."), origin));
+      return this;
+    }
+
     /** Add proguard configuration-file resources. */
     public Builder addKeepRuleFiles(Collection<Path> paths) {
       for (Path path : paths) {
-        keepRuleFiles.add(new ProguardConfigurationSourceFile(path));
+        keepRules.add(new ProguardConfigurationSourceFile(path));
       }
       return this;
     }
 
+    public Builder addKeepRuleFiles(Path... paths) {
+      return addKeepRuleFiles(Arrays.asList(paths));
+    }
+
+    public Builder setFilteredKeepRulesConsumer(StringConsumer filteredKeepRulesConsumer) {
+      this.filteredKeepRulesConsumer = filteredKeepRulesConsumer;
+      return this;
+    }
+
     public Builder setLibraryConsumerRuleValidation(boolean enable) {
       validateLibraryConsumerRules = enable;
       return this;
@@ -88,15 +117,20 @@
 
     public ProcessKeepRulesCommand build() {
       validate();
-      return new ProcessKeepRulesCommand(reporter, keepRuleFiles, validateLibraryConsumerRules);
+      return new ProcessKeepRulesCommand(
+          reporter, keepRules, filteredKeepRulesConsumer, validateLibraryConsumerRules);
     }
 
     private void validate() {
-      if (keepRuleFiles.isEmpty()) {
-        reporter.error("No keep rule files provided.");
+      if (keepRules.isEmpty()) {
+        reporter.error("No keep rules provided.");
       }
-      if (!validateLibraryConsumerRules) {
-        reporter.error("No rule validation enabled.");
+      if (filteredKeepRulesConsumer == null) {
+        if (!validateLibraryConsumerRules) {
+          reporter.error("Filtering or validation not enabled.");
+        }
+      } else if (validateLibraryConsumerRules) {
+        reporter.error("Filtering and validation not supported simultaneously.");
       }
       reporter.failIfPendingErrors();
     }
diff --git a/src/main/java/com/android/tools/r8/processkeeprules/ValidateLibraryConsumerRulesKeepRuleProcessor.java b/src/main/java/com/android/tools/r8/processkeeprules/ValidateLibraryConsumerRulesKeepRuleProcessor.java
index 371e1cb..c2ced3e 100644
--- a/src/main/java/com/android/tools/r8/processkeeprules/ValidateLibraryConsumerRulesKeepRuleProcessor.java
+++ b/src/main/java/com/android/tools/r8/processkeeprules/ValidateLibraryConsumerRulesKeepRuleProcessor.java
@@ -5,8 +5,10 @@
 
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.shaking.FilteredClassPath;
 import com.android.tools.r8.shaking.ProguardClassNameList;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
 import com.android.tools.r8.shaking.ProguardConfigurationParserConsumer;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
@@ -75,7 +77,11 @@
 
   @Override
   public void addKeepAttributePatterns(
-      List<String> attributesPatterns, Origin origin, Position position) {
+      List<String> attributesPatterns,
+      Origin origin,
+      ProguardConfigurationSourceParser parser,
+      Position position,
+      TextPosition positionStart) {
     // TODO(b/270289387): Add support for more attributes.
     ProguardKeepAttributes keepAttributes = ProguardKeepAttributes.fromPatterns(attributesPatterns);
     if (keepAttributes.lineNumberTable) {
@@ -128,40 +134,60 @@
   public void addDontNotePattern(ProguardClassNameList pattern) {}
 
   @Override
-  public void setPrintConfiguration(boolean b) {}
+  public void enablePrintConfiguration(Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-printconfiguration");
+  }
 
   @Override
   public void setPrintConfigurationFile(Path path) {}
 
   @Override
-  public void setPrintMapping(boolean b) {}
+  public void enablePrintMapping(Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-printmapping");
+  }
 
   @Override
   public void setPrintMappingFile(Path path) {}
 
   @Override
-  public void setApplyMappingFile(Path path) {}
+  public void setApplyMappingFile(Path path, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-applymapping");
+  }
 
   @Override
-  public void addInjars(List<FilteredClassPath> filteredClassPaths) {}
+  public void addInjars(
+      List<FilteredClassPath> filteredClassPaths, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-injars");
+  }
 
   @Override
-  public void addLibraryJars(List<FilteredClassPath> filteredClassPaths) {}
+  public void addLibraryJars(
+      List<FilteredClassPath> filteredClassPaths, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-libraryjars");
+  }
 
   @Override
-  public void setPrintSeeds(boolean b) {}
+  public void setPrintSeeds(boolean b, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-printseeds");
+  }
 
   @Override
   public void setSeedFile(Path path) {}
 
   @Override
-  public void setObfuscationDictionary(Path path) {}
+  public void setObfuscationDictionary(Path path, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-obfuscationdictionary");
+  }
 
   @Override
-  public void setClassObfuscationDictionary(Path path) {}
+  public void setClassObfuscationDictionary(Path path, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-classobfuscationdictionary");
+  }
 
   @Override
-  public void setPackageObfuscationDictionary(Path path) {}
+  public void setPackageObfuscationDictionary(Path path, Origin origin, Position position) {
+    handleGlobalRule(origin, position, "-packageobfuscationdictionary");
+  }
 
   @Override
   public void addAdaptClassStringsPattern(ProguardClassNameList pattern) {}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index 2d4e353..5b0c3f1 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -8,6 +8,8 @@
 import com.android.tools.r8.naming.DictionaryReader;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
@@ -88,12 +90,13 @@
     }
 
     @Override
-    public void addInjars(List<FilteredClassPath> injars) {
+    public void addInjars(List<FilteredClassPath> injars, Origin origin, Position position) {
       this.injars.addAll(injars);
     }
 
     @Override
-    public void addLibraryJars(List<FilteredClassPath> libraryJars) {
+    public void addLibraryJars(
+        List<FilteredClassPath> libraryJars, Origin origin, Position position) {
       this.libraryJars.addAll(libraryJars);
     }
 
@@ -154,8 +157,8 @@
     }
 
     @Override
-    public void setPrintConfiguration(boolean printConfiguration) {
-      this.printConfiguration = printConfiguration;
+    public void enablePrintConfiguration(Origin origin, Position position) {
+      this.printConfiguration = true;
     }
 
     @Override
@@ -175,8 +178,8 @@
     }
 
     @Override
-    public void setPrintMapping(boolean printMapping) {
-      this.printMapping = printMapping;
+    public void enablePrintMapping(Origin origin, Position position) {
+      this.printMapping = true;
     }
 
     @Override
@@ -186,7 +189,7 @@
     }
 
     @Override
-    public void setApplyMappingFile(Path file) {
+    public void setApplyMappingFile(Path file, Origin origin, Position position) {
       this.applyMappingFile = file;
     }
 
@@ -202,7 +205,11 @@
 
     @Override
     public void addKeepAttributePatterns(
-        List<String> keepAttributePatterns, Origin origin, Position position) {
+        List<String> keepAttributePatterns,
+        Origin origin,
+        ProguardConfigurationSourceParser parser,
+        Position position,
+        TextPosition positionStart) {
       this.keepAttributePatterns.addAll(keepAttributePatterns);
     }
 
@@ -279,22 +286,25 @@
     }
 
     @Override
-    public void setPrintSeeds(boolean printSeeds) {
+    public void setPrintSeeds(boolean printSeeds, Origin origin, Position position) {
       this.printSeeds = printSeeds;
     }
 
     @Override
-    public void setObfuscationDictionary(Path obfuscationDictionary) {
+    public void setObfuscationDictionary(
+        Path obfuscationDictionary, Origin origin, Position position) {
       this.obfuscationDictionary = obfuscationDictionary;
     }
 
     @Override
-    public void setClassObfuscationDictionary(Path classObfuscationDictionary) {
+    public void setClassObfuscationDictionary(
+        Path classObfuscationDictionary, Origin origin, Position position) {
       this.classObfuscationDictionary = classObfuscationDictionary;
     }
 
     @Override
-    public void setPackageObfuscationDictionary(Path packageObfuscationDictionary) {
+    public void setPackageObfuscationDictionary(
+        Path packageObfuscationDictionary, Origin origin, Position position) {
       this.packageObfuscationDictionary = packageObfuscationDictionary;
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 0f505fd..3c01885 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -222,7 +222,7 @@
     ANY
   }
 
-  private class ProguardConfigurationSourceParser {
+  public class ProguardConfigurationSourceParser {
     private final String name;
     private final String contents;
     private int position = 0;
@@ -240,9 +240,16 @@
       this.origin = source.getOrigin();
     }
 
+    public String getContentSince(TextPosition start) {
+      return contents.substring(start.getOffsetAsInt(), position);
+    }
+
     public void parse() throws ProguardRuleParserException {
       do {
-        skipWhitespace();
+        TextPosition whitespaceStart = getPosition();
+        if (skipWhitespace()) {
+          configurationConsumer.addWhitespace(this, whitespaceStart);
+        }
       } while (parseOption());
       // This may be unknown, but we want to always ensure that we don't attribute lines to the
       // wrong configuration.
@@ -287,11 +294,15 @@
         configurationConsumer.addKeepAttributePatterns(
             Collections.singletonList(RUNTIME_VISIBLE_ANNOTATIONS),
             origin,
-            getPosition(optionStart));
+            this,
+            getPosition(optionStart),
+            optionStart);
         configurationConsumer.addKeepAttributePatterns(
             Collections.singletonList(RUNTIME_INVISIBLE_ANNOTATIONS),
             origin,
-            getPosition(optionStart));
+            this,
+            getPosition(optionStart),
+            optionStart);
       } else if (acceptString("renamesourcefileattribute")) {
         skipWhitespace();
         String renameSourceFileAttribute =
@@ -299,7 +310,7 @@
         configurationConsumer.setRenameSourceFileAttribute(
             renameSourceFileAttribute, origin, getPosition(optionStart));
       } else if (acceptString("keepattributes")) {
-        parseKeepAttributes(getPosition(optionStart));
+        parseKeepAttributes(optionStart);
       } else if (acceptString("keeppackagenames")) {
         parseClassFilter(configurationConsumer::addKeepPackageNamesPattern);
       } else if (acceptString("keepparameternames")) {
@@ -391,20 +402,22 @@
       } else if (acceptString("allowaccessmodification")) {
         configurationConsumer.enableAllowAccessModification(origin, getPosition(optionStart));
       } else if (acceptString("printconfiguration")) {
-        configurationConsumer.setPrintConfiguration(true);
+        configurationConsumer.enablePrintConfiguration(origin, getPosition(optionStart));
         skipWhitespace();
         if (isOptionalArgumentGiven()) {
           configurationConsumer.setPrintConfigurationFile(parseFileName(false));
         }
       } else if (acceptString("printmapping")) {
-        configurationConsumer.setPrintMapping(true);
+        configurationConsumer.enablePrintMapping(origin, getPosition(optionStart));
         skipWhitespace();
         if (isOptionalArgumentGiven()) {
           configurationConsumer.setPrintMappingFile(parseFileName(false));
         }
       } else if (acceptString("applymapping")) {
         configurationConsumer.setApplyMappingFile(
-            parseFileInputDependency(inputDependencyConsumer::acceptProguardApplyMapping));
+            parseFileInputDependency(inputDependencyConsumer::acceptProguardApplyMapping),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("assumenosideeffects")) {
         ProguardAssumeNoSideEffectRule rule = parseAssumeNoSideEffectsRule(optionStart);
         configurationConsumer.addRule(rule);
@@ -423,27 +436,37 @@
         baseDirectory = parseFileName(false);
       } else if (acceptString("injars")) {
         configurationConsumer.addInjars(
-            parseClassPath(inputDependencyConsumer::acceptProguardInJars));
+            parseClassPath(inputDependencyConsumer::acceptProguardInJars),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("libraryjars")) {
         configurationConsumer.addLibraryJars(
-            parseClassPath(inputDependencyConsumer::acceptProguardLibraryJars));
+            parseClassPath(inputDependencyConsumer::acceptProguardLibraryJars),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("printseeds")) {
-        configurationConsumer.setPrintSeeds(true);
+        configurationConsumer.setPrintSeeds(true, origin, getPosition(optionStart));
         skipWhitespace();
         if (isOptionalArgumentGiven()) {
           configurationConsumer.setSeedFile(parseFileName(false));
         }
       } else if (acceptString("obfuscationdictionary")) {
         configurationConsumer.setObfuscationDictionary(
-            parseFileInputDependency(inputDependencyConsumer::acceptProguardObfuscationDictionary));
+            parseFileInputDependency(inputDependencyConsumer::acceptProguardObfuscationDictionary),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("classobfuscationdictionary")) {
         configurationConsumer.setClassObfuscationDictionary(
             parseFileInputDependency(
-                inputDependencyConsumer::acceptProguardClassObfuscationDictionary));
+                inputDependencyConsumer::acceptProguardClassObfuscationDictionary),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("packageobfuscationdictionary")) {
         configurationConsumer.setPackageObfuscationDictionary(
             parseFileInputDependency(
-                inputDependencyConsumer::acceptProguardPackageObfuscationDictionary));
+                inputDependencyConsumer::acceptProguardPackageObfuscationDictionary),
+            origin,
+            getPosition(optionStart));
       } else if (acceptString("alwaysinline")) {
         InlineRule rule =
             parseRuleWithClassSpec(
@@ -735,7 +758,7 @@
       return true;
     }
 
-    private void parseKeepAttributes(Position position) throws ProguardRuleParserException {
+    private void parseKeepAttributes(TextPosition start) throws ProguardRuleParserException {
       List<String> attributesPatterns = acceptKeepAttributesPatternList();
       if (attributesPatterns.isEmpty()) {
         throw parseError("Expected attribute pattern list");
@@ -753,7 +776,8 @@
                       + ")"));
         }
       }
-      configurationConsumer.addKeepAttributePatterns(attributesPatterns, origin, position);
+      configurationConsumer.addKeepAttributePatterns(
+          attributesPatterns, origin, this, getPosition(start), start);
     }
 
     private boolean skipFlag(String name) {
@@ -1686,27 +1710,31 @@
       return builder.build();
     }
 
-    private void skipWhitespace() {
+    private boolean skipWhitespace() {
+      boolean skipped = false;
       while (!eof() && StringUtils.isWhitespace(peekChar())) {
         if (peekChar() == '\n') {
           line++;
           lineStartPosition = position + 1;
         }
         position++;
+        skipped = true;
       }
-      skipComment();
+      if (skipComment()) {
+        skipped = true;
+      }
+      return skipped;
     }
 
-    private void skipComment() {
-      if (eof()) {
-        return;
+    private boolean skipComment() {
+      if (eof() || peekChar() != '#') {
+        return false;
       }
-      if (peekChar() == '#') {
-        while (!eof() && peekChar() != '\n') {
-          position++;
-        }
-        skipWhitespace();
+      while (!eof() && peekChar() != '\n') {
+        position++;
       }
+      skipWhitespace();
+      return true;
     }
 
     private boolean eof() {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserConsumer.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserConsumer.java
index f0fbdd6..ea658c5 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserConsumer.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserConsumer.java
@@ -5,6 +5,8 @@
 
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.ProguardConfigurationSourceParser;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import java.nio.file.Path;
 import java.util.List;
@@ -15,7 +17,12 @@
 
   void addRule(ProguardConfigurationRule rule);
 
-  void addKeepAttributePatterns(List<String> attributesPatterns, Origin origin, Position position);
+  void addKeepAttributePatterns(
+      List<String> attributesPatterns,
+      Origin origin,
+      ProguardConfigurationSourceParser parser,
+      Position position,
+      TextPosition positionStart);
 
   void setRenameSourceFileAttribute(String s, Origin origin, Position position);
 
@@ -47,29 +54,29 @@
 
   void enableAllowAccessModification(Origin origin, Position position);
 
-  void setPrintConfiguration(boolean b);
+  void enablePrintConfiguration(Origin origin, Position position);
 
   void setPrintConfigurationFile(Path path);
 
-  void setPrintMapping(boolean b);
+  void enablePrintMapping(Origin origin, Position position);
 
   void setPrintMappingFile(Path path);
 
-  void setApplyMappingFile(Path path);
+  void setApplyMappingFile(Path path, Origin origin, Position position);
 
-  void addInjars(List<FilteredClassPath> filteredClassPaths);
+  void addInjars(List<FilteredClassPath> filteredClassPaths, Origin origin, Position position);
 
-  void addLibraryJars(List<FilteredClassPath> filteredClassPaths);
+  void addLibraryJars(List<FilteredClassPath> filteredClassPaths, Origin origin, Position position);
 
-  void setPrintSeeds(boolean b);
+  void setPrintSeeds(boolean b, Origin origin, Position position);
 
   void setSeedFile(Path path);
 
-  void setObfuscationDictionary(Path path);
+  void setObfuscationDictionary(Path path, Origin origin, Position position);
 
-  void setClassObfuscationDictionary(Path path);
+  void setClassObfuscationDictionary(Path path, Origin origin, Position position);
 
-  void setPackageObfuscationDictionary(Path path);
+  void setPackageObfuscationDictionary(Path path, Origin origin, Position position);
 
   void addAdaptClassStringsPattern(ProguardClassNameList pattern);
 
@@ -88,4 +95,6 @@
   void enableRepackageClasses(Origin origin, Position position);
 
   void enableFlattenPackageHierarchy(Origin origin, Position position);
+
+  default void addWhitespace(ProguardConfigurationSourceParser parser, TextPosition position) {}
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
index cc9fafa..b826231 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
@@ -8,33 +8,27 @@
 import com.google.common.annotations.VisibleForTesting;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.List;
 
 public class ProguardConfigurationSourceStrings implements ProguardConfigurationSource {
 
   private final Path basePath;
-  private final List<String> config;
+  private final String config;
   private final Origin origin;
 
   /**
-   * Creates {@link ProguardConfigurationSource} with raw {@param config}, along with
-   * {@param basePath}, which allows all other options that use a relative path to reach out
-   * to desired paths appropriately.
+   * Creates {@link ProguardConfigurationSource} with raw {@param config}, along with {@param
+   * basePath}, which allows all other options that use a relative path to reach out to desired
+   * paths appropriately.
    */
-  public ProguardConfigurationSourceStrings(List<String> config, Path basePath, Origin origin) {
+  public ProguardConfigurationSourceStrings(String config, Path basePath, Origin origin) {
     this.basePath = basePath;
     this.config = config;
     this.origin = origin;
   }
 
-  private ProguardConfigurationSourceStrings(List<String> config) {
-    this(config, Paths.get(""), Origin.unknown());
-  }
-
   @VisibleForTesting
-  public static ProguardConfigurationSourceStrings createConfigurationForTesting(
-      List<String> config) {
-    return new ProguardConfigurationSourceStrings(config);
+  public static ProguardConfigurationSourceStrings createConfigurationForTesting(String config) {
+    return new ProguardConfigurationSourceStrings(config, Paths.get(""), Origin.unknown());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
index 9d0ef51..ade3e8b 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
@@ -24,11 +24,13 @@
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
 
 @KeepForApi
@@ -38,6 +40,11 @@
     runForTesting(command, command.getInternalOptions());
   }
 
+  public static void run(TraceReferencesCommand command, ExecutorService executorService)
+      throws CompilationFailedException {
+    runForTesting(command, command.getInternalOptions(), executorService);
+  }
+
   private static void forEachDescriptor(ProgramResourceProvider provider, Consumer<String> consumer)
       throws ResourceException, IOException {
     for (ProgramResource programResource : provider.getProgramResources()) {
@@ -63,11 +70,18 @@
 
   static void runForTesting(TraceReferencesCommand command, InternalOptions options)
       throws CompilationFailedException {
-    ExceptionUtils.withCompilationHandler(
-        command.getReporter(), () -> runInternal(command, options));
+    runForTesting(command, options, ThreadUtils.getExecutorService(options));
   }
 
-  static void runInternal(TraceReferencesCommand command, InternalOptions options)
+  static void runForTesting(
+      TraceReferencesCommand command, InternalOptions options, ExecutorService executorService)
+      throws CompilationFailedException {
+    ExceptionUtils.withCompilationHandler(
+        command.getReporter(), () -> runInternal(command, options, executorService));
+  }
+
+  static void runInternal(
+      TraceReferencesCommand command, InternalOptions options, ExecutorService executorService)
       throws IOException, ResourceException {
     AndroidApp.Builder builder = AndroidApp.builder();
     command.getLibrary().forEach(builder::addLibraryResourceProvider);
@@ -84,7 +98,7 @@
         AppView.createForTracer(
             AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(
                 new ApplicationReader(builder.build(), options, Timing.empty())
-                    .readDirectSingleThreaded(),
+                    .readDirect(executorService),
                 ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap(),
                 MainDexInfo.none(),
                 GlobalSyntheticsStrategy.forSingleOutputMode()));
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
index 429b912..462e396 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
@@ -39,6 +39,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 
@@ -214,15 +215,20 @@
       public ProgramResource getProgramResource(String descriptor) {
         return descriptor.equals(this.descriptor) ? programResource : null;
       }
+
+      @Override
+      public void getProgramResources(Consumer<ProgramResource> consumer) {
+        consumer.accept(programResource);
+      }
     }
 
-    private ClassFileResourceProvider singleClassFileClassFileResourceProvider(Path file)
+    private static ClassFileResourceProvider singleClassFileClassFileResourceProvider(Path file)
         throws IOException {
       return new SingleClassClassFileResourceProvider(
           new PathOrigin(file), Files.readAllBytes(file));
     }
 
-    private ProgramResourceProvider singleClassFileProgramResourceProvider(Path file)
+    private static ProgramResourceProvider singleClassFileProgramResourceProvider(Path file)
         throws IOException {
       byte[] bytes = Files.readAllBytes(file);
       String descriptor = extractClassDescriptor(bytes);
@@ -230,9 +236,33 @@
 
         @Override
         public Collection<ProgramResource> getProgramResources() {
-          return ImmutableList.of(
-              ProgramResource.fromBytes(
-                  new PathOrigin(file), Kind.CF, bytes, ImmutableSet.of(descriptor)));
+          return Collections.singletonList(createProgramResource());
+        }
+
+        @Override
+        public void getProgramResources(Consumer<ProgramResource> consumer) {
+          consumer.accept(createProgramResource());
+        }
+
+        private ProgramResource createProgramResource() {
+          return ProgramResource.fromBytes(
+              new PathOrigin(file), Kind.CF, bytes, ImmutableSet.of(descriptor));
+        }
+      };
+    }
+
+    private static ProgramResourceProvider singleDexFileProgramResourceProvider(Path file) {
+      ProgramResource programResource = ProgramResource.fromFile(Kind.DEX, file);
+      return new ProgramResourceProvider() {
+
+        @Override
+        public Collection<ProgramResource> getProgramResources() {
+          return Collections.singletonList(programResource);
+        }
+
+        @Override
+        public void getProgramResources(Consumer<ProgramResource> consumer) {
+          consumer.accept(programResource);
         }
       };
     }
@@ -277,15 +307,7 @@
           error(new ExceptionDiagnostic(e));
         }
       } else if (isDexFile(file)) {
-        traceSourceBuilder.add(
-            new ProgramResourceProvider() {
-              ProgramResource dexResource = ProgramResource.fromFile(Kind.DEX, file);
-
-              @Override
-              public Collection<ProgramResource> getProgramResources() {
-                return Collections.singletonList(dexResource);
-              }
-            });
+        traceSourceBuilder.add(singleDexFileProgramResourceProvider(file));
       } else {
         error(new StringDiagnostic("Unsupported source file type", new PathOrigin(file)));
       }
diff --git a/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java b/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java
index b1c3cda..6216687 100644
--- a/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.SourceFileProvider;
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
+import java.util.Map.Entry;
 
 public class SourceFileTemplateProvider implements SourceFileProvider {
 
@@ -53,7 +54,7 @@
   }
 
   private final String template;
-  private String cachedValue = null;
+  private volatile String cachedValue = null;
 
   private SourceFileTemplateProvider(String template) {
     this.template = template;
@@ -62,11 +63,15 @@
   @Override
   public String get(SourceFileEnvironment environment) {
     if (cachedValue == null) {
-      cachedValue = template;
-      HANDLERS.forEach(
-          (variable, getter) -> {
-            cachedValue = cachedValue.replace(variable, getter.get(environment));
-          });
+      synchronized (this) {
+        if (cachedValue == null) {
+          String temp = template;
+          for (Entry<String, SourceFileProvider> entry : HANDLERS.entrySet()) {
+            temp = temp.replace(entry.getKey(), entry.getValue().get(environment));
+          }
+          cachedValue = temp;
+        }
+      }
     }
     return cachedValue;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index aac9d19..c9a17a0 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -167,6 +167,11 @@
     return next;
   }
 
+  public void removeSeen(T element) {
+    boolean removed = seen.remove(element);
+    assert removed;
+  }
+
   public void clearSeen() {
     seen.clear();
   }
diff --git a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
index 3047ca7..b10f50b 100644
--- a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
+++ b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
@@ -4,8 +4,12 @@
 
 package com.android.tools.r8.bootstrap;
 
+import static com.android.tools.r8.utils.DescriptorUtils.JAVA_PACKAGE_SEPARATOR;
+import static com.android.tools.r8.utils.FileUtils.JAVA_EXTENSION;
 import static org.hamcrest.CoreMatchers.startsWith;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -16,19 +20,19 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.CfUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StreamUtils;
 import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
-import java.util.Enumeration;
 import java.util.Set;
 import java.util.function.Predicate;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -53,11 +57,74 @@
     parameters.assertNoneRuntime();
   }
 
-  private void checkJarContent(Path jar, boolean allowDirectories, Predicate<String> entryTester)
+  private void checkJarContent(
+      Path jar, boolean allowDirectories, ClassNameMapper mapping, Predicate<String> entryTester)
       throws Exception {
-    ZipFile zipFile;
+    BooleanBox licenseSeen = new BooleanBox();
+    Set<String> apiDatabaseFiles = Sets.newHashSet("resources/new_api_database.ser");
+    Set<String> r8AssistantRuntime =
+        ImmutableSet.of(
+            "ReflectiveEventType.java",
+            "ReflectiveOperationJsonLogger.java",
+            "ReflectiveOracle.java",
+            "EmptyReflectiveOperationReceiver.java",
+            "ReflectiveOperationReceiver.java",
+            "ReflectiveOperationLogger.java");
     try {
-      zipFile = new ZipFile(jar.toFile(), StandardCharsets.UTF_8);
+      ZipUtils.iter(
+          jar,
+          (entry, input) -> {
+            String name = entry.getName();
+            if (ZipUtils.isClassFile(name) || FileUtils.isKotlinBuiltinsFile(name)) {
+              assertThat(name, startsWith("com/android/tools/r8/"));
+              if (ZipUtils.isClassFile(name)) {
+                byte[] classFileBytes = StreamUtils.streamToByteArrayClose(input);
+                String sourceFile = extractSourceFile(classFileBytes);
+                if (!r8AssistantRuntime.contains(sourceFile)) {
+                  if (mapping != null) {
+                    assertNotNull(sourceFile);
+                    assertTrue(sourceFile, sourceFile.startsWith("R8_"));
+                    assertEquals(sourceFile.contains("+excldeps") ? 117 : 108, sourceFile.length());
+                  } else {
+                    // Some class files from third party libraries does not have a SourceFile
+                    // attribute.
+                    if (sourceFile != null) {
+                      assertTrue(
+                          sourceFile, sourceFile.endsWith(".java") || sourceFile.endsWith(".kt"));
+                    }
+                  }
+                } else {
+                  String className = CfUtils.extractClassName(classFileBytes);
+                  int lastPackageSeparator = className.lastIndexOf(JAVA_PACKAGE_SEPARATOR);
+                  String expectedSourceFile = className.substring(lastPackageSeparator + 1);
+                  int firstInnerclassSeparator = expectedSourceFile.indexOf('$');
+                  if (firstInnerclassSeparator >= 0) {
+                    expectedSourceFile = expectedSourceFile.substring(0, firstInnerclassSeparator);
+                  }
+                  expectedSourceFile += JAVA_EXTENSION;
+                  assertEquals(expectedSourceFile, sourceFile);
+                }
+              }
+            } else if (name.equals("META-INF/MANIFEST.MF")) {
+              // Allow.
+            } else if (name.equals("LICENSE")) {
+              licenseSeen.set();
+            } else if (name.equals(THREADING_MODULE_SERVICE_FILE)) {
+              // Allow.
+            } else if (entryTester.test(name)) {
+              // Allow.
+            } else if (apiDatabaseFiles.contains(name)) {
+              // Allow all api database files.
+              apiDatabaseFiles.remove(name);
+            } else if (name.endsWith("/")) {
+              assertTrue("Unexpected directory entry in" + jar, allowDirectories);
+            } else {
+              fail("Unexpected entry '" + name + "' in " + jar);
+            }
+          });
+      assertTrue(apiDatabaseFiles.isEmpty());
+      assertTrue(
+          "No LICENSE entry found in " + jar, licenseSeen.isAssigned() && licenseSeen.isTrue());
     } catch (IOException e) {
       if (!Files.exists(jar)) {
         throw new NoSuchFileException(jar.toString());
@@ -65,33 +132,6 @@
         throw e;
       }
     }
-    boolean licenseSeen = false;
-    final Enumeration<? extends ZipEntry> entries = zipFile.entries();
-    Set<String> apiDatabaseFiles = Sets.newHashSet("resources/new_api_database.ser");
-    while (entries.hasMoreElements()) {
-      ZipEntry entry = entries.nextElement();
-      String name = entry.getName();
-      if (ZipUtils.isClassFile(name) || FileUtils.isKotlinBuiltinsFile(name)) {
-        assertThat(name, startsWith("com/android/tools/r8/"));
-      } else if (name.equals("META-INF/MANIFEST.MF")) {
-        // Allow.
-      } else if (name.equals("LICENSE")) {
-        licenseSeen = true;
-      } else if (name.equals(THREADING_MODULE_SERVICE_FILE)) {
-        // Allow.
-      } else if (entryTester.test(name)) {
-        // Allow.
-      } else if (apiDatabaseFiles.contains(name)) {
-        // Allow all api database files.
-        apiDatabaseFiles.remove(name);
-      } else if (name.endsWith("/")) {
-        assertTrue("Unexpected directory entry in" + jar, allowDirectories);
-      } else {
-        fail("Unexpected entry '" + name + "' in " + jar);
-      }
-    }
-    assertTrue(apiDatabaseFiles.isEmpty());
-    assertTrue("No LICENSE entry found in " + jar, licenseSeen);
   }
 
   private void checkLibJarContent(Path jar, Path map) throws Exception {
@@ -100,14 +140,14 @@
     }
     assertTrue(Files.exists(map));
     ClassNameMapper mapping = ClassNameMapper.mapperFromFile(map);
-    checkJarContent(jar, false, name -> metadataExtensionTester(name, mapping));
+    checkJarContent(jar, false, mapping, name -> metadataExtensionTester(name, mapping));
   }
 
   private void checkJarContent(Path jar) throws Exception {
     if (!Files.exists(jar)) {
       return;
     }
-    checkJarContent(jar, true, name -> metadataExtensionTester(name, null));
+    checkJarContent(jar, true, null, name -> metadataExtensionTester(name, null));
   }
 
   private boolean metadataExtensionTester(String name, ClassNameMapper mapping) {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/InvokeInterfaceNoDevirtualizationEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/InvokeInterfaceNoDevirtualizationEnumUnboxingTest.java
new file mode 100644
index 0000000..0d04b20
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/InvokeInterfaceNoDevirtualizationEnumUnboxingTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2025, 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.enumunboxing;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import java.util.List;
+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 InvokeInterfaceNoDevirtualizationEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public InvokeInterfaceNoDevirtualizationEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void test() throws Exception {
+    AssertUtils.assertFailsCompilation(
+        () ->
+            testForR8(parameters)
+                .addInnerClasses(getClass())
+                .addKeepMainRule(Main.class)
+                .addKeepRules(enumKeepRules.getKeepRules())
+                // Disable devirtualization so that the enum unboxing rewriter sees the call to
+                // I#greet instead of MyEnum#greet.
+                .addOptionsModification(options -> options.enableDevirtualization = false)
+                .addOptionsModification(
+                    options -> enableEnumOptions(options, enumValueOptimization))
+                .enableInliningAnnotations()
+                .enableNoVerticalClassMergingAnnotations()
+                .compile()
+                .run(parameters.getRuntime(), Main.class)
+                .assertSuccessWithOutputLines("Hello, world!"));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      MyEnum e = System.currentTimeMillis() > 0 ? MyEnum.A : MyEnum.B;
+      greet(e);
+    }
+
+    static void greet(I i) {
+      i.greet();
+    }
+  }
+
+  @NoVerticalClassMerging
+  interface I {
+
+    void greet();
+  }
+
+  enum MyEnum implements I {
+    A,
+    B;
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
index 0990e64..e290f65 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -120,7 +120,7 @@
       ToolHelper.addProguardConfigurationConsumer(
           builder,
           pgConfig -> {
-            pgConfig.setPrintSeeds(false);
+            pgConfig.setPrintSeeds(false, null, null);
             pgConfig.setIgnoreWarnings(true);
           });
       outputApp = new AndroidAppConsumers(builder);
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
index 69e016e..f63abf2 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.timing.Timing;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
@@ -54,8 +53,7 @@
               ProguardConfigurationParser parser =
                   new ProguardConfigurationParser(factory, reporter, configurationBuilder);
               parser.parse(
-                  createConfigurationForTesting(
-                      ImmutableList.of("-keep class ** { void m1(); void m5(); }")));
+                  createConfigurationForTesting("-keep class ** { void m1(); void m5(); }"));
               return configurationBuilder.build();
             });
     this.options = appView.options();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/InstanceGetSharingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/InstanceGetSharingTest.java
new file mode 100644
index 0000000..eb2e295
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/InstanceGetSharingTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2025, 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoMethodStaticizing;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.code.Instruction;
+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 InstanceGetSharingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, A.class, B.class, C.class)
+        .addDontObfuscate()
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoMethodStaticizingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              assertEquals(
+                  1,
+                  inspector
+                      .clazz(A.class)
+                      .uniqueMethodWithOriginalName("foo")
+                      .buildIR()
+                      .streamInstructions()
+                      .filter(Instruction::isInstanceGet)
+                      .count());
+              assertEquals(
+                  1,
+                  inspector
+                      .clazz(C.class)
+                      .uniqueMethodWithOriginalName("bar")
+                      .buildIR()
+                      .streamInstructions()
+                      .filter(Instruction::isInstanceGet)
+                      .count());
+            });
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println(new A().foo());
+      System.out.println(new C().bar());
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+    private B b = new B();
+
+    @NeverInline
+    public long foo() {
+      if (System.currentTimeMillis() > 0) {
+        return b.getNum() + 1;
+      } else {
+        return b.getNum() + 2;
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NeverClassInline
+  static class C {
+
+    @NeverInline
+    public long bar() {
+      long num;
+      if (System.currentTimeMillis() > 0) {
+        B b1 = new B();
+        num = b1.num;
+      } else {
+        B b2 = new B();
+        num = b2.num;
+      }
+      return num + 1;
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+    public long num = System.currentTimeMillis();
+
+    @NeverInline
+    @NoMethodStaticizing
+    long getNum() {
+      return System.currentTimeMillis();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/CanonicalizeInstanceGetBeforeInstancePutTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/CanonicalizeInstanceGetBeforeInstancePutTest.java
index 93fa047..7ff0eeb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/CanonicalizeInstanceGetBeforeInstancePutTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/CanonicalizeInstanceGetBeforeInstancePutTest.java
@@ -49,10 +49,8 @@
     MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
     assertThat(mainMethodSubject, isPresent());
 
-    // We expect two instance-get instructions when compiling to CF since we only run
-    // canonicalization when compiling to DEX.
     assertEquals(
-        parameters.isCfRuntime() ? 2 : 1,
+        1,
         mainMethodSubject.streamInstructions().filter(InstructionSubject::isInstanceGet).count());
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
index 489b670..42243c0 100644
--- a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
@@ -78,7 +78,7 @@
         ToolHelper.addProguardConfigurationConsumer(
                 R8Command.builder(),
                 pgConfig -> {
-                  pgConfig.setPrintMapping(true);
+                  pgConfig.enablePrintMapping(null, null);
                   if (!minify.isMinify()) {
                     pgConfig.disableObfuscation();
                   }
diff --git a/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java b/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
index 56eae04..2f684c0 100644
--- a/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
+++ b/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
@@ -48,7 +48,7 @@
         ToolHelper.addProguardConfigurationConsumer(
                 ToolHelper.prepareR8CommandBuilder(app),
                 pgConfig -> {
-                  pgConfig.setPrintMapping(true);
+                  pgConfig.enablePrintMapping(null, null);
                   pgConfig.setPrintMappingFile(out.resolve(ToolHelper.DEFAULT_PROGUARD_MAP_FILE));
                 })
             .addProguardConfiguration(
diff --git a/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommandTest.java b/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommandTest.java
index 13ec056..1807513 100644
--- a/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesCommandTest.java
@@ -37,6 +37,21 @@
           .put("-dontobfuscate", "-dontobfuscate not allowed in library consumer rules.")
           .put("-dontshrink", "-dontshrink not allowed in library consumer rules.")
           .put("-repackageclasses", "-repackageclasses not allowed in library consumer rules.")
+          .put("-printconfiguration", "-printconfiguration not allowed in library consumer rules.")
+          .put("-printmapping", "-printmapping not allowed in library consumer rules.")
+          .put("-applymapping foo", "-applymapping not allowed in library consumer rules.")
+          .put("-injars foo", "-injars not allowed in library consumer rules.")
+          .put("-libraryjars foo", "-libraryjars not allowed in library consumer rules.")
+          .put("-printseeds", "-printseeds not allowed in library consumer rules.")
+          .put(
+              "-obfuscationdictionary foo",
+              "-obfuscationdictionary not allowed in library consumer rules.")
+          .put(
+              "-classobfuscationdictionary foo",
+              "-classobfuscationdictionary not allowed in library consumer rules.")
+          .put(
+              "-packageobfuscationdictionary foo",
+              "-packageobfuscationdictionary not allowed in library consumer rules.")
           .put(
               "-flattenpackagehierarchy",
               "-flattenpackagehierarchy not allowed in library consumer rules.")
@@ -45,20 +60,22 @@
               "-allowaccessmodification not allowed in library consumer rules.")
           .put(
               "-keepattributes LineNumberTable",
-              "Illegal attempt to keep LineNumberTable in library consumer rules.")
+              "Illegal attempt to keep the attribute 'LineNumberTable' in library consumer rules.")
           .put(
               "-keepattributes RuntimeInvisibleAnnotations",
-              "Illegal attempt to keep RuntimeInvisibleAnnotations in library consumer rules.")
+              "Illegal attempt to keep the attribute 'RuntimeInvisibleAnnotations' in library"
+                  + " consumer rules.")
           .put(
               "-keepattributes RuntimeInvisibleTypeAnnotations",
-              "Illegal attempt to keep RuntimeInvisibleTypeAnnotations in library consumer rules.")
+              "Illegal attempt to keep the attribute 'RuntimeInvisibleTypeAnnotations' in library"
+                  + " consumer rules.")
           .put(
               "-keepattributes RuntimeInvisibleParameterAnnotations",
-              "Illegal attempt to keep RuntimeInvisibleParameterAnnotations in library consumer"
-                  + " rules.")
+              "Illegal attempt to keep the attribute 'RuntimeInvisibleParameterAnnotations' in"
+                  + " library consumer rules.")
           .put(
               "-keepattributes SourceFile",
-              "Illegal attempt to keep SourceFile in library consumer rules.")
+              "Illegal attempt to keep the attribute 'SourceFile' in library consumer rules.")
           .put(
               "-renamesourcefileattribute",
               "-renamesourcefileattribute not allowed in library consumer rules.")
diff --git a/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesFilteringTest.java b/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesFilteringTest.java
new file mode 100644
index 0000000..16cb049
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/processkeeprules/ProcessKeepRulesFilteringTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2025, 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.processkeeprules;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+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 ProcessKeepRulesFilteringTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    TestDiagnosticMessagesImpl diagnostics = new TestDiagnosticMessagesImpl();
+    String keepRules =
+        StringUtils.lines(
+            " -dontobfuscate -dontoptimize -dontshrink -keep class com.example.MainActivity # keep",
+            "# Keep all attributes",
+            "-keepattributes *",
+            "# Keep all",
+            "-keep  class **",
+            "# End");
+    FilteredKeepRules filteredKeepRules = new FilteredKeepRules();
+    ProcessKeepRulesCommand command =
+        ProcessKeepRulesCommand.builder(diagnostics)
+            .addKeepRules(keepRules, Origin.unknown())
+            .setFilteredKeepRulesConsumer(filteredKeepRules)
+            .build();
+    ProcessKeepRules.run(command);
+    assertEquals(
+        StringUtils.lines(
+            " #-dontobfuscate -dontoptimize -dontshrink ",
+            "-keep class com.example.MainActivity # keep",
+            "# Keep all attributes",
+            "-keepattributes *",
+            "# Keep all",
+            "-keep  class **",
+            "# End"),
+        filteredKeepRules.get());
+    diagnostics.assertNoMessages();
+  }
+
+  private static class FilteredKeepRules implements StringConsumer {
+
+    private StringBuilder sb = new StringBuilder();
+    private String result;
+
+    @Override
+    public void accept(String string, DiagnosticsHandler handler) {
+      sb.append(string);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      result = sb.toString();
+      sb = null;
+    }
+
+    public String get() {
+      return result;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 907e566..f9f7c65 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -46,7 +46,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -225,7 +224,7 @@
 
     // Parse from strings.
 
-    List<String> lines = FileUtils.readAllLines(Paths.get(PROGUARD_SPEC_FILE));
+    String lines = FileUtils.readTextFile(Paths.get(PROGUARD_SPEC_FILE));
     parser.parse(createConfigurationForTesting(lines));
     rules = builder.build().getRules();
     assertEquals(24, rules.size());
@@ -261,7 +260,7 @@
             "  -NameWithDash- -field-;",
             "  p-.-OtherNameWithDash- -method-(-p.-WithDash-, -package-.-ClassNameWithDash-[]); ",
             "}"));
-    parser.parse(createConfigurationForTesting(ImmutableList.of(nonJavaIdentifiers)));
+    parser.parse(createConfigurationForTesting(nonJavaIdentifiers));
     verifyParserEndsCleanly();
     List<ProguardConfigurationRule> rules = builder.build().getRules();
     assertEquals(1, rules.size());
@@ -296,7 +295,7 @@
   private void testDontXXX(
       String xxx, Function<ProguardConfiguration, Predicate<DexType>> matcherFactory) {
     String configuration = "-dont" + xxx + " !foobar,*bar";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+    parser.parse(createConfigurationForTesting(configuration));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     Predicate<DexType> matcher = matcherFactory.apply(config);
@@ -314,9 +313,9 @@
   private void testDontXXXMultiple(
       String xxx, Function<ProguardConfiguration, Predicate<DexType>> matcherFactory) {
     DexItemFactory dexItemFactory = new DexItemFactory();
-    List<String> configuration1 = ImmutableList.of("-dont" + xxx + " foo.**, bar.**");
-    List<String> configuration2 = ImmutableList.of("-dont" + xxx + " foo.**", "-dontwarn bar.**");
-    for (List<String> configuration : ImmutableList.of(configuration1, configuration2)) {
+    String configuration1 = StringUtils.lines("-dont" + xxx + " foo.**, bar.**");
+    String configuration2 = StringUtils.lines("-dont" + xxx + " foo.**", "-dontwarn bar.**");
+    for (String configuration : ImmutableList.of(configuration1, configuration2)) {
       parser.parse(createConfigurationForTesting(configuration));
       verifyParserEndsCleanly();
       ProguardConfiguration config = builder.build();
@@ -337,7 +336,7 @@
       String xxx, Function<ProguardConfiguration, Predicate<DexType>> matcherFactory) {
     DexItemFactory dexItemFactory = new DexItemFactory();
     String dontwarnAll = "-dont" + xxx + " *";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(dontwarnAll)));
+    parser.parse(createConfigurationForTesting(dontwarnAll));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     Predicate<DexType> matcher = matcherFactory.apply(config);
@@ -357,7 +356,7 @@
     DexItemFactory dexItemFactory = new DexItemFactory();
     String dontwarnAll = "-dont" + xxx;
     String otherOption = "-keep class *";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(dontwarnAll, otherOption)));
+    parser.parse(createConfigurationForTesting(StringUtils.lines(dontwarnAll, otherOption)));
     ProguardConfiguration config = builder.build();
     Predicate<DexType> matcher = matcherFactory.apply(config);
     assertTrue(matcher.test(dexItemFactory.createType("Lboobaz;")));
@@ -612,7 +611,7 @@
   @Test
   public void testAdaptClassStrings() {
     String adaptClassStrings = "-adaptclassstrings !foobar,*bar";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(adaptClassStrings)));
+    parser.parse(createConfigurationForTesting(adaptClassStrings));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertFalse(
@@ -625,10 +624,10 @@
 
   @Test
   public void testAdaptClassStringsMultiple() {
-    List<String> configuration1 = ImmutableList.of("-adaptclassstrings foo.**, bar.**");
-    List<String> configuration2 =
-        ImmutableList.of("-adaptclassstrings foo.**", "-adaptclassstrings bar.**");
-    for (List<String> configuration : ImmutableList.of(configuration1, configuration2)) {
+    String configuration1 = "-adaptclassstrings foo.**, bar.**";
+    String configuration2 =
+        StringUtils.lines("-adaptclassstrings foo.**", "-adaptclassstrings bar.**");
+    for (String configuration : ImmutableList.of(configuration1, configuration2)) {
       parser.parse(createConfigurationForTesting(configuration));
       verifyParserEndsCleanly();
       ProguardConfiguration config = builder.build();
@@ -644,7 +643,7 @@
   @Test
   public void testAdaptClassStringsAllExplicitly() {
     String adaptAll = "-adaptclassstrings *";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(adaptAll)));
+    parser.parse(createConfigurationForTesting(adaptAll));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertTrue(
@@ -658,7 +657,7 @@
   @Test
   public void testAdaptClassStringsAllImplicitly() {
     String adaptAll = "-adaptclassstrings";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(adaptAll)));
+    parser.parse(createConfigurationForTesting(adaptAll));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertTrue(
@@ -683,7 +682,7 @@
         "-identifiernamestring class * {\n"
             + "  @my.annotations.IdentifierNameString *;\n"
             + "}";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(config1, config2, config3)));
+    parser.parse(createConfigurationForTesting(StringUtils.lines(config1, config2, config3)));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     List<ProguardConfigurationRule> identifierNameStrings = config.getRules();
@@ -736,7 +735,7 @@
             null,
             builder);
     String rule = "-convertchecknotnull class C { ** m(**, ...); }";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(rule)));
+    parser.parse(createConfigurationForTesting(rule));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertEquals(1, config.getRules().size());
@@ -758,7 +757,7 @@
             null,
             builder);
     String rule = "-convertchecknotnull class C { void m(**, ...); }";
-    parser.parse(createConfigurationForTesting(ImmutableList.of(rule)));
+    parser.parse(createConfigurationForTesting(rule));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertEquals(1, config.getRules().size());
@@ -918,9 +917,7 @@
   @Test
   public void parseInvalidFilePattern() {
     try {
-      parser.parse(
-          createConfigurationForTesting(
-              Collections.singletonList("-injars abc.jar(*.zip;*.class)")));
+      parser.parse(createConfigurationForTesting("-injars abc.jar(*.zip;*.class)"));
       fail();
     } catch (RuntimeException e) {
       assertEquals(1, handler.errors.size());
@@ -933,29 +930,28 @@
       for (String after : whiteSpace) {
         reset();
         parseAndVerifyParserEndsCleanly(
-            ImmutableList.of(
-                "-keep"
-                    + before
-                    + ","
-                    + after
-                    + "includedescriptorclasses"
-                    + before
-                    + ","
-                    + after
-                    + "allowaccessmodification"
-                    + before
-                    + ","
-                    + after
-                    + "allowshrinking"
-                    + before
-                    + ","
-                    + after
-                    + "allowobfuscation"
-                    + before
-                    + ","
-                    + after
-                    + "allowoptimization "
-                    + "class A { *; }"));
+            "-keep"
+                + before
+                + ","
+                + after
+                + "includedescriptorclasses"
+                + before
+                + ","
+                + after
+                + "allowaccessmodification"
+                + before
+                + ","
+                + after
+                + "allowshrinking"
+                + before
+                + ","
+                + after
+                + "allowobfuscation"
+                + before
+                + ","
+                + after
+                + "allowoptimization "
+                + "class A { *; }");
       }
     }
   }
@@ -964,18 +960,16 @@
   public void parseKeepAnnotation() {
     for (String space : whiteSpace) {
       reset();
-      parseAndVerifyParserEndsCleanly(ImmutableList.of(
-          "-keep @" + space + "interface A"
-      ));
+      parseAndVerifyParserEndsCleanly("-keep @" + space + "interface A");
     }
   }
 
   @Test
   public void regress78442725() {
-    parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-keep, includedescriptorclasses class in.uncod.android.bypass.Document { *; }",
-        "-keep, includedescriptorclasses class in.uncod.android.bypass.Element { *; }"
-    ));
+    parseAndVerifyParserEndsCleanly(
+        StringUtils.lines(
+            "-keep, includedescriptorclasses class in.uncod.android.bypass.Document { *; }",
+            "-keep, includedescriptorclasses class in.uncod.android.bypass.Element { *; }"));
   }
 
   @Test
@@ -1086,7 +1080,7 @@
   private void testUnsupportedOption(String option) {
     try {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(option)));
+      parser.parse(createConfigurationForTesting(option));
       fail("Expect to fail due to unsupported option.");
     } catch (RuntimeException e) {
       checkDiagnostics(handler.errors, null, 1, 1, "Unsupported option", option);
@@ -1101,7 +1095,7 @@
 
   @Test
   public void parse_printconfiguration_noArguments() {
-    parser.parse(createConfigurationForTesting(ImmutableList.of("-printconfiguration")));
+    parser.parse(createConfigurationForTesting("-printconfiguration"));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertTrue(config.isPrintConfiguration());
@@ -1110,7 +1104,7 @@
 
   @Test
   public void parse_printconfiguration_argument() {
-    parser.parse(createConfigurationForTesting(ImmutableList.of("-printconfiguration file_name")));
+    parser.parse(createConfigurationForTesting("-printconfiguration file_name"));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertTrue(config.isPrintConfiguration());
@@ -1359,7 +1353,7 @@
   }
 
   private void testKeepattributes(List<String> expected, String config) {
-    parser.parse(createConfigurationForTesting(ImmutableList.of(config)));
+    parser.parse(createConfigurationForTesting(config));
     verifyParserEndsCleanly();
     assertEquals(
         ProguardKeepAttributes.fromPatterns(expected), builder.buildRaw().getKeepAttributes());
@@ -1402,7 +1396,7 @@
   }
 
   private void testKeeppackagenames(String config) {
-    parser.parse(createConfigurationForTesting(ImmutableList.of(config)));
+    parser.parse(createConfigurationForTesting(config));
     verifyParserEndsCleanly();
   }
 
@@ -1429,7 +1423,7 @@
   @Test
   public void parseInvalidKeepattributes_brokenList() {
     try {
-      parser.parse(createConfigurationForTesting(ImmutableList.of("-keepattributes xxx,")));
+      parser.parse(createConfigurationForTesting("-keepattributes xxx,"));
       fail();
     } catch (RuntimeException e) {
       assertTrue(
@@ -1448,14 +1442,14 @@
   @Test
   public void parseKeepParameterNamesWithoutMinification() {
     parser.parse(
-        createConfigurationForTesting(ImmutableList.of("-keepparameternames", "-dontobfuscate")));
+        createConfigurationForTesting(StringUtils.lines("-keepparameternames", "-dontobfuscate")));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     assertTrue(config.isKeepParameterNames());
 
-    parser.parse(createConfigurationForTesting(ImmutableList.of("-keepparameternames")));
+    parser.parse(createConfigurationForTesting("-keepparameternames"));
     verifyParserEndsCleanly();
-    parser.parse(createConfigurationForTesting(ImmutableList.of("-dontobfuscate")));
+    parser.parse(createConfigurationForTesting("-dontobfuscate"));
     verifyParserEndsCleanly();
     config = builder.build();
     assertTrue(config.isKeepParameterNames());
@@ -1464,7 +1458,7 @@
   @Test
   public void parseShortLine() {
     try {
-      parser.parse(createConfigurationForTesting(Collections.singletonList("-")));
+      parser.parse(createConfigurationForTesting("-"));
       fail();
     } catch (AbortException e) {
       assertEquals(1, handler.errors.size());
@@ -1475,7 +1469,7 @@
   @Test
   public void parseNoLocals() {
     try {
-      parser.parse(createConfigurationForTesting(Collections.singletonList("--no-locals")));
+      parser.parse(createConfigurationForTesting("--no-locals"));
       fail();
     } catch (AbortException e) {
       assertEquals(1, handler.errors.size());
@@ -1491,11 +1485,10 @@
 
   @Test
   public void parse_adaptresourcexxx_keepdirectories_noArguments1() {
-    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-adaptresourcefilenames",
-        "-adaptresourcefilecontents",
-        "-keepdirectories"
-    ));
+    ProguardConfiguration config =
+        parseAndVerifyParserEndsCleanly(
+            StringUtils.lines(
+                "-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories"));
     checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
     checkFileFilterMatchAnything(config.getAdaptResourceFileContents());
     checkFileFilterMatchAnything(config.getKeepDirectories());
@@ -1503,11 +1496,10 @@
 
   @Test
   public void parse_adaptresourcexxx_keepdirectories_noArguments2() {
-    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-keepdirectories",
-        "-adaptresourcefilenames",
-        "-adaptresourcefilecontents"
-    ));
+    ProguardConfiguration config =
+        parseAndVerifyParserEndsCleanly(
+            StringUtils.lines(
+                "-keepdirectories", "-adaptresourcefilenames", "-adaptresourcefilecontents"));
     checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
     checkFileFilterMatchAnything(config.getAdaptResourceFileContents());
     checkFileFilterMatchAnything(config.getKeepDirectories());
@@ -1515,11 +1507,10 @@
 
   @Test
   public void parse_adaptresourcexxx_keepdirectories_noArguments3() {
-    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-adaptresourcefilecontents",
-        "-keepdirectories",
-        "-adaptresourcefilenames"
-    ));
+    ProguardConfiguration config =
+        parseAndVerifyParserEndsCleanly(
+            StringUtils.lines(
+                "-adaptresourcefilecontents", "-keepdirectories", "-adaptresourcefilenames"));
     checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
     checkFileFilterMatchAnything(config.getAdaptResourceFileContents());
     checkFileFilterMatchAnything(config.getKeepDirectories());
@@ -1536,19 +1527,17 @@
 
   @Test
   public void parse_adaptresourcexxx_keepdirectories_singleArgument() {
-    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-adaptresourcefilenames " + FILE_FILTER_SINGLE,
-        "-adaptresourcefilecontents " + FILE_FILTER_SINGLE,
-        "-keepdirectories " + FILE_FILTER_SINGLE
-    ));
+    ProguardConfiguration config =
+        parseAndVerifyParserEndsCleanly(
+            StringUtils.lines(
+                "-adaptresourcefilenames " + FILE_FILTER_SINGLE,
+                "-adaptresourcefilecontents " + FILE_FILTER_SINGLE,
+                "-keepdirectories " + FILE_FILTER_SINGLE));
     checkFileFilterSingle(config.getAdaptResourceFilenames());
     checkFileFilterSingle(config.getAdaptResourceFileContents());
     checkFileFilterSingle(config.getKeepDirectories());
   }
 
-  private String FILE_FILTER_MULTIPLE =
-      "xxx/*, !**.gif  ,images/**  ,  com/myapp/**/*.xml,com/mylib/*/*.xml";
-
   private void checkFileFilterMultiple(ProguardPathFilter filter) {
     assertTrue(filter.matches("xxx/x"));
     assertTrue(filter.matches("xxx/x.gif"));
@@ -1568,11 +1557,14 @@
 
   @Test
   public void parse_adaptresourcexxx_keepdirectories_multipleArgument() {
-    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
-        "-adaptresourcefilenames " + FILE_FILTER_MULTIPLE,
-        "-adaptresourcefilecontents " + FILE_FILTER_MULTIPLE,
-        "-keepdirectories " + FILE_FILTER_MULTIPLE
-    ));
+    String FILE_FILTER_MULTIPLE =
+        "xxx/*, !**.gif  ,images/**  ,  com/myapp/**/*.xml,com/mylib/*/*.xml";
+    ProguardConfiguration config =
+        parseAndVerifyParserEndsCleanly(
+            StringUtils.lines(
+                "-adaptresourcefilenames " + FILE_FILTER_MULTIPLE,
+                "-adaptresourcefilecontents " + FILE_FILTER_MULTIPLE,
+                "-keepdirectories " + FILE_FILTER_MULTIPLE));
     checkFileFilterMultiple(config.getAdaptResourceFilenames());
     checkFileFilterMultiple(config.getAdaptResourceFileContents());
     checkFileFilterMultiple(config.getKeepDirectories());
@@ -1585,7 +1577,7 @@
     for (String option : options) {
       try {
         reset();
-        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " ,")));
+        parser.parse(createConfigurationForTesting(option + " ,"));
         fail("Expect to fail due to the lack of path filter.");
       } catch (RuntimeException e) {
         checkDiagnostics(handler.errors, null, 1, option.length() + 2, "Path filter expected");
@@ -1600,7 +1592,7 @@
     for (String option : options) {
       try {
         reset();
-        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,,yyy")));
+        parser.parse(createConfigurationForTesting(option + " xxx,,yyy"));
         fail("Expect to fail due to the lack of path filter.");
       } catch (RuntimeException e) {
         checkDiagnostics(handler.errors, null, 1, option.length() + 6, "Path filter expected");
@@ -1615,7 +1607,7 @@
     for (String option : options) {
       try {
         reset();
-        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,")));
+        parser.parse(createConfigurationForTesting(option + " xxx,"));
         fail("Expect to fail due to the lack of path filter.");
       } catch (RuntimeException e) {
         checkDiagnostics(handler.errors, null, 1, option.length() + 6, "Path filter expected");
@@ -1629,7 +1621,7 @@
     for (String option : options) {
       try {
         reset();
-        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " class A { *; }")));
+        parser.parse(createConfigurationForTesting(option + " class A { *; }"));
         fail("Expect to fail due to testing option being turned off.");
       } catch (AbortException e) {
         assertEquals(1, handler.errors.size());
@@ -2193,12 +2185,11 @@
     assertEquals(0, handler.warnings.size());
   }
 
-  private void checkRulesSourceSnippet(List<String> sourceRules) {
-    checkRulesSourceSnippet(sourceRules, sourceRules, false);
+  private void checkRulesSourceSnippet(String sourceRule) {
+    checkRulesSourceSnippet(sourceRule, ImmutableList.of(sourceRule), false);
   }
 
-  private void checkRulesSourceSnippet(
-      List<String> sourceRules, List<String> expected, boolean trim) {
+  private void checkRulesSourceSnippet(String sourceRules, List<String> expected, boolean trim) {
     reset();
     parser.parse(createConfigurationForTesting(sourceRules));
     verifyParserEndsCleanly();
@@ -2211,18 +2202,17 @@
 
   @Test
   public void accurateSourceSnippet() {
-    String rule1 = String.join(System.lineSeparator(), ImmutableList.of("-keep class A  {  *;  }"));
+    String rule1 = "-keep class A  {  *;  }";
     String rule2 =
         String.join(System.lineSeparator(), ImmutableList.of("-keep class A  ", "{  *;  ", "}"));
     String rule3 =
         String.join(
             System.lineSeparator(), ImmutableList.of("-checkdiscard class A  ", "{  *;  ", "}"));
-
-    checkRulesSourceSnippet(ImmutableList.of(rule1));
-    checkRulesSourceSnippet(ImmutableList.of(rule2));
-    checkRulesSourceSnippet(ImmutableList.of(rule3));
+    checkRulesSourceSnippet(rule1);
+    checkRulesSourceSnippet(rule2);
+    checkRulesSourceSnippet(rule3);
     checkRulesSourceSnippet(
-        ImmutableList.of(rule1, rule2, rule3), ImmutableList.of(rule1, rule3), false);
+        StringUtils.lines(rule1, rule2, rule3), ImmutableList.of(rule1, rule3), false);
   }
 
   @Test
@@ -2238,17 +2228,17 @@
                   System.lineSeparator(), ImmutableList.of("-keep class A  ", "{  *;  ", "}", ""))
               .replaceAll(" {2}", space);
 
-      checkRulesSourceSnippet(ImmutableList.of(rule1), ImmutableList.of(rule1), true);
+      checkRulesSourceSnippet(rule1, ImmutableList.of(rule1), true);
       checkRulesSourceSnippet(
-          ImmutableList.of("#Test comment ", "", rule1), ImmutableList.of(rule1), true);
+          StringUtils.lines("#Test comment ", "", rule1), ImmutableList.of(rule1), true);
       checkRulesSourceSnippet(
-          ImmutableList.of("#Test comment ", "", rule1, "", "#Test comment ", ""),
+          StringUtils.lines("#Test comment ", "", rule1, "", "#Test comment ", ""),
           ImmutableList.of(rule1),
           true);
-      checkRulesSourceSnippet(ImmutableList.of(rule2), ImmutableList.of(rule2), true);
-      checkRulesSourceSnippet(ImmutableList.of(rule1, rule2), ImmutableList.of(rule1), true);
+      checkRulesSourceSnippet(rule2, ImmutableList.of(rule2), true);
+      checkRulesSourceSnippet(StringUtils.lines(rule1, rule2), ImmutableList.of(rule1), true);
       checkRulesSourceSnippet(
-          ImmutableList.of(
+          StringUtils.lines(
               "#Test comment ", "", rule1, " ", "#Test comment ", "", rule2, "#Test comment ", ""),
           ImmutableList.of(rule1),
           true);
@@ -2280,7 +2270,7 @@
       BiConsumer<String, String> check) {
     for (String value : values) {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(flag + " " + value)));
+      parser.parse(createConfigurationForTesting(flag + " " + value));
       verifyParserEndsCleanly();
       ProguardConfiguration config = builder.build();
       check.accept(value, extractPath.apply(config).toString());
@@ -2354,7 +2344,7 @@
       BiConsumer<String, String> check) {
     for (String value : values) {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(flag + " " + value)));
+      parser.parse(createConfigurationForTesting(flag + " " + value));
       verifyParserEndsCleanly();
       ProguardConfiguration config = builder.build();
       Path path = extractPath.apply(config);
@@ -2413,7 +2403,7 @@
   @Test
   public void pasteFlagWithFilenamesWithSystemProperty_empty() {
     try {
-      parser.parse(createConfigurationForTesting(ImmutableList.of("-printusage <>")));
+      parser.parse(createConfigurationForTesting("-printusage <>"));
       fail("Expect to fail due to the lack of file name.");
     } catch (RuntimeException e) {
       checkDiagnostics(handler.errors, null, 1, 15, "Value of system property '' not found");
@@ -2429,8 +2419,7 @@
     }
 
     try {
-      parser.parse(
-          createConfigurationForTesting(ImmutableList.of("-printusage <" + property + ">")));
+      parser.parse(createConfigurationForTesting("-printusage <" + property + ">"));
       fail("Expect to fail due to the lack of file name.");
     } catch (RuntimeException e) {
       checkDiagnostics(
@@ -2441,7 +2430,7 @@
   private void testFlagWithQuotedValue(
       String flag, String value, BiConsumer<PackageObfuscationMode, String> checker) {
     reset();
-    parser.parse(createConfigurationForTesting(ImmutableList.of(flag + " " + value)));
+    parser.parse(createConfigurationForTesting(flag + " " + value));
     verifyParserEndsCleanly();
     ProguardConfiguration config = builder.build();
     checker.accept(config.getPackageObfuscationMode(), config.getPackagePrefix());
@@ -2451,7 +2440,7 @@
       String flag, String value, Supplier<Void> checker) {
     try {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(flag + " " + value)));
+      parser.parse(createConfigurationForTesting(flag + " " + value));
       fail("Expect to fail due to un-closed quote.");
     } catch (RuntimeException e) {
       checker.get();
@@ -2514,7 +2503,7 @@
     });
   }
 
-  private ProguardConfiguration parseAndVerifyParserEndsCleanly(List<String> config) {
+  private ProguardConfiguration parseAndVerifyParserEndsCleanly(String config) {
     parser.parse(createConfigurationForTesting(config));
     verifyParserEndsCleanly();
     return builder.build();
@@ -2537,7 +2526,7 @@
     // Test spaces and quotes in class name list.
     parser.parse(
         createConfigurationForTesting(
-            ImmutableList.of(
+            StringUtils.lines(
                 "-keepclassmembers class \"a.b.c.**\" ,"
                     + " !**d , '!**e' , \"!**f\" , g , 'h' , \"i\" { ",
                 "<fields>;",
@@ -2560,7 +2549,7 @@
               ? "-flattenpackagehierarchy"
               : "-repackageclasses";
 
-      parser.parse(createConfigurationForTesting(ImmutableList.of(directive + " -keep class *")));
+      parser.parse(createConfigurationForTesting(directive + " -keep class *"));
       ProguardConfiguration configuration = builder.build();
       assertEquals(packageObfuscationMode, configuration.getPackageObfuscationMode());
       assertEquals("", configuration.getPackagePrefix());
@@ -2585,9 +2574,7 @@
               ? "-flattenpackagehierarchy"
               : "-repackageclasses";
 
-      parser.parse(
-          createConfigurationForTesting(
-              ImmutableList.of(directive + " @" + includeFile.toAbsolutePath())));
+      parser.parse(createConfigurationForTesting(directive + " @" + includeFile.toAbsolutePath()));
       ProguardConfiguration configuration = builder.build();
       assertEquals(packageObfuscationMode, configuration.getPackageObfuscationMode());
       assertEquals("", configuration.getPackagePrefix());
@@ -2720,7 +2707,7 @@
   @Test
   public void backReferenceElimination() {
     String configuration = StringUtils.lines("-if class *.*.*", "-keep class <1>.<2>$<3>");
-    parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+    parser.parse(createConfigurationForTesting(configuration));
     verifyParserEndsCleanly();
 
     ProguardConfiguration config = builder.build();
@@ -2783,7 +2770,7 @@
             "-keep @Foo @Bar public final class *");
     for (String configurationContent : configurationContents) {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(configurationContent)));
+      parser.parse(createConfigurationForTesting(configurationContent));
       verifyParserEndsCleanly();
 
       ProguardConfiguration configuration = builder.build();
@@ -2856,7 +2843,7 @@
         (configurationContent, expectedExceptionClass) -> {
           reset();
           try {
-            parser.parse(createConfigurationForTesting(ImmutableList.of(configurationContent)));
+            parser.parse(createConfigurationForTesting(configurationContent));
             assertFalse(expectedExceptionClass.isPresent());
           } catch (Throwable e) {
             assertTrue(expectedExceptionClass.isPresent());
@@ -2945,7 +2932,7 @@
         (configurationContent, expectedExceptionClass) -> {
           reset();
           try {
-            parser.parse(createConfigurationForTesting(ImmutableList.of(configurationContent)));
+            parser.parse(createConfigurationForTesting(configurationContent));
             assertFalse(expectedExceptionClass.isPresent());
           } catch (Throwable e) {
             assertTrue(expectedExceptionClass.isPresent());
@@ -2995,8 +2982,7 @@
                     ? rule.substring(0, rule.length() - 1)
                     : rule)
                 + modifier;
-        parser.parse(
-            createConfigurationForTesting(ImmutableList.of(ruleWithModifier + " class A")));
+        parser.parse(createConfigurationForTesting(ruleWithModifier + " class A"));
         verifyParserEndsCleanly();
 
         ProguardConfiguration configuration = builder.build();
@@ -3014,7 +3000,7 @@
   @Test
   public void parseMaximumRemovedAndroidLogLevelWithoutClassSpecification() {
     String configuration = StringUtils.lines("-maximumremovedandroidloglevel 2");
-    parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+    parser.parse(createConfigurationForTesting(configuration));
     verifyParserEndsCleanly();
 
     ProguardConfiguration config = builder.build();
@@ -3031,7 +3017,7 @@
         }) {
       reset();
       String configuration = StringUtils.lines(input);
-      parser.parse(createConfigurationForTesting(ImmutableList.of(configuration)));
+      parser.parse(createConfigurationForTesting(configuration));
       verifyParserEndsCleanly();
 
       ProguardConfiguration config = builder.build();
diff --git a/src/test/testbase/java/com/android/tools/r8/OriginMatcher.java b/src/test/testbase/java/com/android/tools/r8/OriginMatcher.java
index 4f5ad82..4f994b1 100644
--- a/src/test/testbase/java/com/android/tools/r8/OriginMatcher.java
+++ b/src/test/testbase/java/com/android/tools/r8/OriginMatcher.java
@@ -26,7 +26,7 @@
 
       @Override
       public void describeTo(Description description) {
-        description.appendText("not a parent " + parent);
+        description.appendText("a parent " + parent);
       }
     };
   }
@@ -40,7 +40,7 @@
 
       @Override
       public void describeTo(Description description) {
-        description.appendText("part is not " + part);
+        description.appendText("part " + part);
       }
     };
   }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBase.java b/src/test/testbase/java/com/android/tools/r8/TestBase.java
index c38ef15..31158a3 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -1990,6 +1990,30 @@
     return extractor.getClassFileVersion();
   }
 
+  protected static String extractSourceFile(byte[] classFileBytes) {
+    class SourceFileExtractor extends ClassVisitor {
+      private String source;
+
+      private SourceFileExtractor() {
+        super(ASM_VERSION);
+      }
+
+      @Override
+      public void visitSource(String source, String debug) {
+        this.source = source;
+      }
+
+      String getSourceFile() {
+        return source;
+      }
+    }
+
+    ClassReader reader = new ClassReader(classFileBytes);
+    SourceFileExtractor extractor = new SourceFileExtractor();
+    reader.accept(extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
+    return extractor.getSourceFile();
+  }
+
   public static void verifyAllInfoFromGenericSignatureTypeParameterValidation(
       TestCompileResult<?, ?> compileResult) {
     compileResult.assertAtLeastOneInfoMessage();