Merge commit '8743dd9bd14d0194ee5ebf5ec505234c2d76365b' into dev-release

Change-Id: Iab5d12a85885a7b21c81e456b6beab489486ec18
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt b/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
index f8a35af..65cf737 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
@@ -120,18 +120,38 @@
         || project.hasProperty("one_line_per_test")
         || project.hasProperty("update_test_timestamp")) {
         test.addTestListener(object : TestListener {
+          val testTimes = mutableMapOf<TestDescriptor?,Long>()
+          val maxPrintTimesCount = 1000
           override fun beforeSuite(desc: TestDescriptor?) {}
-          override fun afterSuite(desc: TestDescriptor?, result: TestResult?) {}
+          override fun afterSuite(desc: TestDescriptor?, result: TestResult?) {
+            if (project.hasProperty("print_times")) {
+              // desc.parent == null when we are all done
+              if (desc?.parent == null) {
+                testTimes.toList()
+                  .sortedByDescending { it.second }
+                  .take(maxPrintTimesCount)
+                  .forEach {
+                    println("${it.first} took: ${it.second}")
+                }
+              }
+            }
+          }
           override fun beforeTest(desc: TestDescriptor?) {
             if (project.hasProperty("one_line_per_test")) {
               println("Start executing ${desc}")
             }
+            if (project.hasProperty("print_times")) {
+              testTimes[desc] = Date().getTime()
+            }
           }
 
           override fun afterTest(desc: TestDescriptor?, result: TestResult?) {
             if (project.hasProperty("one_line_per_test")) {
               println("Done executing ${desc} with result: ${result?.resultType}")
             }
+            if (project.hasProperty("print_times")) {
+              testTimes[desc] = Date().getTime() - testTimes[desc]!!
+            }
             if (project.hasProperty("update_test_timestamp")) {
               File(project.property("update_test_timestamp")!!.toString())
                 .writeText(Date().getTime().toString())
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 30432af..1094432 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -263,6 +263,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -300,6 +301,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -337,6 +339,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -374,6 +377,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -411,6 +415,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -448,6 +453,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -485,6 +491,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -522,6 +529,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -559,6 +567,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -596,6 +605,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -633,6 +643,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -670,6 +681,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -707,6 +719,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -744,6 +757,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -781,6 +795,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -818,6 +833,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -855,6 +871,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -892,6 +909,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -929,6 +947,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -966,6 +985,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1003,6 +1023,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1040,6 +1061,7 @@
         '    "--all_tests",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1076,6 +1098,7 @@
         '    "--runtimes=dex-default",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1112,6 +1135,7 @@
         '    "--runtimes=dex-default",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1212,6 +1236,7 @@
         '    "--runtimes=jdk11",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1248,6 +1273,7 @@
         '    "--runtimes=jdk11",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1284,6 +1310,7 @@
         '    "--runtimes=jdk17",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1320,6 +1347,7 @@
         '    "--runtimes=jdk17",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1356,6 +1384,7 @@
         '    "--runtimes=jdk21",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1392,6 +1421,7 @@
         '    "--runtimes=jdk21",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1428,6 +1458,7 @@
         '    "--runtimes=jdk8",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1464,6 +1495,7 @@
         '    "--runtimes=jdk8",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1500,6 +1532,7 @@
         '    "--runtimes=jdk9",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1536,6 +1569,7 @@
         '    "--runtimes=jdk9",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1646,6 +1680,7 @@
         '    "--runtimes=none",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1682,6 +1717,7 @@
         '    "--runtimes=none",'
         '    "--command_cache_dir=/tmp/ccache",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
@@ -1814,13 +1850,14 @@
         '  "test_options": ['
         '    "--all_tests",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
         '  ]'
         '}'
       priority: 26
-      execution_timeout_secs: 21600
+      execution_timeout_secs: 28800
       expiration_secs: 126000
       build_numbers: YES
       service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
@@ -1849,13 +1886,14 @@
         '  "test_options": ['
         '    "--all_tests",'
         '    "--tool=r8",'
+        '    "--print-times",'
         '    "--no_internal",'
         '    "--one_line_per_test",'
         '    "--archive_failures"'
         '  ]'
         '}'
       priority: 26
-      execution_timeout_secs: 21600
+      execution_timeout_secs: 28800
       expiration_secs: 126000
       build_numbers: YES
       service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index 613f25e..57e6c02 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -7,7 +7,7 @@
 name: "r8"
 access: "group:all"
 lucicfg {
-  version: "1.43.4"
+  version: "1.43.5"
   package_dir: ".."
   config_dir: "generated"
   entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 4a1764f..d6fb5d7 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -155,11 +155,14 @@
 
 common_test_options = [
     "--tool=r8",
+    "--print-times",
     "--no_internal",
     "--one_line_per_test",
     "--archive_failures"
 ]
 
+default_timeout = time.hour * 6
+
 def get_dimensions(windows=False, internal=False, archive=False):
   # We use the following setup:
   #   windows -> always windows machine
@@ -218,7 +221,7 @@
 def r8_tester(name,
     test_options,
     dimensions = None,
-    execution_timeout = time.hour * 6,
+    execution_timeout = default_timeout,
     expiration_timeout = time.hour * 35,
     max_concurrent_invocations = 1,
     category=None,
@@ -244,10 +247,12 @@
     dimensions=None,
     category=None,
     release_trigger=None,
-    max_concurrent_invocations = 1):
+    max_concurrent_invocations = 1,
+    execution_timeout = default_timeout):
   r8_tester(name, test_options + common_test_options,
             dimensions = dimensions, category = category, release_trigger=release_trigger,
-            max_concurrent_invocations = max_concurrent_invocations)
+            max_concurrent_invocations = max_concurrent_invocations,
+            execution_timeout = execution_timeout)
 
 def archivers():
   for name in [
@@ -350,6 +355,7 @@
 
 r8_tester_with_default("windows", ["--all_tests"],
     dimensions=get_dimensions(windows=True),
+    execution_timeout = time.hour * 8,
     max_concurrent_invocations = 2)
 
 def internal():
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 766371d..46f1ce4 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -460,7 +460,9 @@
           reporter.error(
               "D8 does not support main-dex inputs and outputs when compiling to API level "
                   + AndroidApiLevel.L_MR1.getLevel()
-                  + " and above");
+                  + " and above (min API level "
+                  + getMinApiLevel()
+                  + " was provided)");
         }
       }
       if (hasDesugaredLibraryConfiguration() && getDisableDesugaring()) {
@@ -479,7 +481,9 @@
           reporter.error(
               "D8 startup layout requires native multi dex support (API level "
                   + AndroidApiLevel.L.getLevel()
-                  + " and above)");
+                  + " and above, min API level "
+                  + getMinApiLevel()
+                  + " was provided)");
         }
       }
       super.validate();
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index 2ef79c8..fa0a094 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.StringConsumer.FileConsumer;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.profile.art.ArtProfileConsumerUtils;
 import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
 import com.android.tools.r8.profile.startup.StartupProfileProviderUtils;
@@ -19,12 +20,15 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 public class R8CommandParser extends BaseCompilerCommandParser<R8Command, R8Command.Builder> {
@@ -41,6 +45,7 @@
           "--main-dex-rules",
           "--main-dex-list",
           "--feature",
+          "--android-resources",
           "--main-dex-list-output",
           "--pg-conf",
           "--pg-conf-output",
@@ -57,7 +62,7 @@
 
   // Note: this must be a subset of OPTIONS_WITH_ONE_PARAMETER.
   private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS =
-      ImmutableSet.of(ART_PROFILE_FLAG, "--feature");
+      ImmutableSet.of(ART_PROFILE_FLAG, "--feature", "--android-resources");
 
   // Due to the family of flags (for assertions and diagnostics) we can't base the one/two args
   // on this setup of flags. Thus, the flag collection just encodes the descriptive content.
@@ -96,11 +101,21 @@
         .add(ParseFlagInfoImpl.getMainDexList())
         .add(
             flag2(
-                "--feature",
+                "--android-resources",
                 "<input>",
                 "<output>",
+                "Add android resource input and output to be used in resource shrinking. Both ",
+                "input and output must be specified."))
+        .add(
+            flag2(
+                "--feature",
+                "<input>[:|;<res-input>]",
+                "<output>[:|;<res-output>]",
                 "Add feature <input> file to <output> file. Several ",
-                "occurrences can map to the same output."))
+                "occurrences can map to the same output. If <res-input> and <res-output> are ",
+                "specified use these as resource shrinker input and output. Separator is : on ",
+                "linux/mac, ; on windows. It is possible to supply resource only features by ",
+                " using an empty string for <input> and <output>, e.g. --feature :in.ap_ :out.ap_"))
         .add(ParseFlagInfoImpl.getIsolatedSplits())
         .add(flag1("--main-dex-list-output", "<file>", "Output the full main-dex list in <file>."))
         .addAll(ParseFlagInfoImpl.getAssertionsFlags())
@@ -193,7 +208,7 @@
   private void parse(
       String[] args, Origin argsOrigin, R8Command.Builder builder, ParseState state) {
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
-    Map<Path, List<Path>> featureSplitJars = new HashMap<>();
+    FeatureSplitConfigCollector featureSplitConfigCollector = new FeatureSplitConfigCollector();
     for (int i = 0; i < expandedArgs.length; i++) {
       String arg = expandedArgs[i].trim();
       String nextArg = null;
@@ -290,10 +305,15 @@
         builder.setDisableDesugaring(true);
       } else if (arg.equals("--main-dex-rules")) {
         builder.addMainDexRulesFiles(Paths.get(nextArg));
+      } else if (arg.equals("--android-resources")) {
+        Path inputPath = Paths.get(nextArg);
+        Path outputPath = Paths.get(nextNextArg);
+        builder.setAndroidResourceProvider(
+            new ArchiveProtoAndroidResourceProvider(inputPath, new PathOrigin(inputPath)));
+        builder.setAndroidResourceConsumer(
+            new ArchiveProtoAndroidResourceConsumer(outputPath, inputPath));
       } else if (arg.equals("--feature")) {
-        featureSplitJars
-            .computeIfAbsent(Paths.get(nextNextArg), k -> new ArrayList<>())
-            .add(Paths.get(nextArg));
+        featureSplitConfigCollector.addInputOutput(nextArg, nextNextArg);
       } else if (arg.equals(ISOLATED_SPLITS_FLAG)) {
         builder.setEnableIsolatedSplits(true);
       } else if (arg.equals("--main-dex-list")) {
@@ -358,20 +378,117 @@
         builder.addProgramFiles(Paths.get(arg));
       }
     }
-    featureSplitJars.forEach(
-        (outputPath, inputJars) -> addFeatureJar(builder, outputPath, inputJars));
+    addFeatureSplitConfigs(builder, featureSplitConfigCollector.getConfigs());
   }
 
-  public void addFeatureJar(R8Command.Builder builder, Path outputPath, List<Path> inputJarPaths) {
-    builder.addFeatureSplit(
-        featureSplitGenerator -> {
-          featureSplitGenerator.setProgramConsumer(
-              builder.createProgramOutputConsumer(outputPath, OutputMode.DexIndexed, true));
-          for (Path inputPath : inputJarPaths) {
-            featureSplitGenerator.addProgramResourceProvider(
-                ArchiveProgramResourceProvider.fromArchive(inputPath));
-          }
-          return featureSplitGenerator.build();
-        });
+  private void addFeatureSplitConfigs(
+      R8Command.Builder builder, Collection<FeatureSplitConfig> featureSplitConfigs) {
+    for (FeatureSplitConfig featureSplitConfig : featureSplitConfigs) {
+      builder.addFeatureSplit(
+          featureSplitGenerator -> {
+            if (featureSplitConfig.outputJar != null) {
+              featureSplitGenerator.setProgramConsumer(
+                  builder.createProgramOutputConsumer(
+                      featureSplitConfig.outputJar, OutputMode.DexIndexed, true));
+            }
+            for (Path inputPath : featureSplitConfig.inputJars) {
+              featureSplitGenerator.addProgramResourceProvider(
+                  ArchiveProgramResourceProvider.fromArchive(inputPath));
+            }
+            if (featureSplitConfig.inputResources != null) {
+              featureSplitGenerator.setAndroidResourceProvider(
+                  new ArchiveProtoAndroidResourceProvider(
+                      featureSplitConfig.inputResources,
+                      new PathOrigin(featureSplitConfig.inputResources)));
+            }
+            if (featureSplitConfig.outputResources != null) {
+              featureSplitGenerator.setAndroidResourceConsumer(
+                  new ArchiveProtoAndroidResourceConsumer(
+                      featureSplitConfig.outputResources, featureSplitConfig.inputResources));
+            }
+            return featureSplitGenerator.build();
+          });
+    }
+  }
+
+  // Represents a set of paths parsed from a string that may contain a ":" (";" on windows).
+  // Supported examples are:
+  //   pathA -> first = pathA, second = null
+  //   pathA:pathB -> first = pathA, second = pathB
+  //   :pathB -> first = null, second = pathB
+  //   pathA: -> first = pathA, second = null
+  private static class PossibleDoublePath {
+
+    public final Path first;
+    public final Path second;
+
+    private PossibleDoublePath(Path first, Path second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    public static PossibleDoublePath parse(String input) {
+      Path first = null, second = null;
+      List<String> inputSplit = StringUtils.split(input, File.pathSeparatorChar);
+      if (inputSplit.size() == 0 || inputSplit.size() > 2) {
+        throw new IllegalArgumentException("Feature input/output takes one or two paths.");
+      }
+      String firstString = inputSplit.get(0);
+      if (!firstString.isEmpty()) {
+        first = Paths.get(firstString);
+      }
+      if (inputSplit.size() == 2) {
+        // "a:".split() gives just ["a"], so we should never get here if we don't have
+        // a second string. ":b".split gives ["", "b"] which is handled for first above.
+        assert inputSplit.get(1).length() > 0;
+        second = Paths.get(inputSplit.get(1));
+      }
+      return new PossibleDoublePath(first, second);
+    }
+  }
+
+  private static class FeatureSplitConfig {
+    private List<Path> inputJars = new ArrayList<>();
+    private Path inputResources;
+    private Path outputResources;
+    private Path outputJar;
+  }
+
+  private static class FeatureSplitConfigCollector {
+
+    private List<FeatureSplitConfig> resourceOnlySplits = new ArrayList<>();
+    private Map<Path, FeatureSplitConfig> withCodeSplits = new HashMap<>();
+
+    public void addInputOutput(String input, String output) {
+      PossibleDoublePath inputPaths = PossibleDoublePath.parse(input);
+      PossibleDoublePath outputPaths = PossibleDoublePath.parse(output);
+      FeatureSplitConfig featureSplitConfig;
+      if (outputPaths.first != null) {
+        featureSplitConfig =
+            withCodeSplits.computeIfAbsent(outputPaths.first, k -> new FeatureSplitConfig());
+        featureSplitConfig.outputJar = outputPaths.first;
+        // We support adding resources independently of the input jars, which later --feature
+        // can add, so we might have no input jars here, example:
+        //  ... --feature :input_feature.ap_ out.jar:out_feature.ap_ --feature in.jar out.jar
+        if (inputPaths.first != null) {
+          featureSplitConfig.inputJars.add(inputPaths.first);
+        }
+      } else {
+        featureSplitConfig = new FeatureSplitConfig();
+        resourceOnlySplits.add(featureSplitConfig);
+      }
+      if (Objects.isNull(inputPaths.second) != Objects.isNull(outputPaths.second)) {
+        throw new IllegalArgumentException(
+            "Both input and output for feature resources must be provided");
+      }
+      featureSplitConfig.inputResources = inputPaths.second;
+      featureSplitConfig.outputResources = outputPaths.second;
+    }
+
+    public Collection<FeatureSplitConfig> getConfigs() {
+      ArrayList<FeatureSplitConfig> featureSplitConfigs = new ArrayList<>(resourceOnlySplits);
+      featureSplitConfigs.addAll(withCodeSplits.values());
+      return featureSplitConfigs;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 1290cc8..e096d1e 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -196,7 +196,7 @@
         timing.time(
             "Compilation context", () -> CompilationContext.createInitialContext(options()));
     this.wholeProgramOptimizations = wholeProgramOptimizations;
-    abstractValueFactory = new AbstractValueFactory(options());
+    abstractValueFactory = new AbstractValueFactory();
     abstractValueConstantPropagationJoiner = new AbstractValueConstantPropagationJoiner(this);
     if (enableWholeProgramOptimizations()) {
       abstractValueFieldJoiner = new AbstractValueFieldJoiner(withClassHierarchy());
@@ -542,7 +542,7 @@
   }
 
   public ComposeReferences getComposeReferences() {
-    assert testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
+    assert options().getJetpackComposeOptions().isAnyOptimizationsEnabled();
     if (composeReferences == null) {
       composeReferences = new ComposeReferences(dexItemFactory());
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
index e6ac132..b9f074c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
@@ -35,7 +35,8 @@
     this.appView = appView;
     this.fieldBitAccessAnalysis =
         options.enableFieldBitAccessAnalysis ? new FieldBitAccessAnalysis() : null;
-    this.fieldAssignmentTracker = new FieldAssignmentTracker(appView);
+    this.fieldAssignmentTracker =
+        options.enableFieldAssignmentTracker ? new FieldAssignmentTracker(appView) : null;
     this.fieldReadForInvokeReceiverAnalysis = new FieldReadForInvokeReceiverAnalysis(appView);
     this.fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index c1a2b62..1a64882 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues.EmptyStaticValues;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -64,6 +65,9 @@
     assert appView.appInfo().hasLiveness();
     assert appView.enableWholeProgramOptimizations();
     assert code.context().getDefinition().isClassInitializer();
+    if (!appView.options().enableFieldValueAnalysis) {
+      return EmptyStaticValues.getInstance();
+    }
     timing.begin("Analyze class initializer");
     StaticFieldValues result =
         new StaticFieldValueAnalysis(appView.withLiveness(), code, feedback)
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index e050ed4..fdb9d4b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -193,6 +193,14 @@
     return null;
   }
 
+  public boolean isSingleStatefulFieldValue() {
+    return false;
+  }
+
+  public boolean isSingleStatelessFieldValue() {
+    return false;
+  }
+
   public SingleNullValue asSingleNullValue() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
index 567d39a..27534c2 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
@@ -14,14 +14,10 @@
 import com.android.tools.r8.ir.analysis.value.objectstate.KnownLengthArrayState;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class AbstractValueFactory {
 
-  private final TestingOptions testingOptions;
-
   private final ConcurrentHashMap<DexType, SingleConstClassValue> singleConstClassValues =
       new ConcurrentHashMap<>();
   private final ConcurrentHashMap<Long, SingleNumberValue> singleNumberValues =
@@ -33,10 +29,6 @@
   private final ConcurrentHashMap<Integer, KnownLengthArrayState> knownArrayLengthStates =
       new ConcurrentHashMap<>();
 
-  public AbstractValueFactory(InternalOptions options) {
-    testingOptions = options.testing;
-  }
-
   public SingleBoxedBooleanValue createBoxedBooleanFalse() {
     return SingleBoxedBooleanValue.getFalseInstance();
   }
@@ -83,19 +75,10 @@
     if (definitelySetBits != 0 || definitelyUnsetBits != 0) {
       // If all bits are known, then create a single number value.
       boolean allBitsSet = (definitelySetBits | definitelyUnsetBits) == ALL_BITS_SET_MASK;
-      // Account for the temporary hack in the Compose modeling where we create a
-      // DefiniteBitsNumberValue with set bits=0b1^32 and unset bits = 0b1^(31)0. This value is used
-      // to simulate the effect of `x | 1` in joins.
-      if (testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions) {
-        boolean overlappingSetAndUnsetBits = (definitelySetBits & definitelyUnsetBits) != 0;
-        if (overlappingSetAndUnsetBits) {
-          allBitsSet = false;
-        }
-      }
       if (allBitsSet) {
         return createUncheckedSingleNumberValue(definitelySetBits);
       }
-      return new DefiniteBitsNumberValue(definitelySetBits, definitelyUnsetBits, testingOptions);
+      return new DefiniteBitsNumberValue(definitelySetBits, definitelyUnsetBits);
     }
     return AbstractValue.unknown();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
index a18281e..778e61b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import java.util.Objects;
 
@@ -17,10 +16,8 @@
   private final int definitelySetBits;
   private final int definitelyUnsetBits;
 
-  public DefiniteBitsNumberValue(
-      int definitelySetBits, int definitelyUnsetBits, TestingOptions testingOptions) {
-    assert (definitelySetBits & definitelyUnsetBits) == 0
-        || testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
+  public DefiniteBitsNumberValue(int definitelySetBits, int definitelyUnsetBits) {
+    assert (definitelySetBits & definitelyUnsetBits) == 0;
     this.definitelySetBits = definitelySetBits;
     this.definitelyUnsetBits = definitelyUnsetBits;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
index 7d06ddf..580d2d9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatefulFieldValue.java
@@ -40,6 +40,11 @@
   }
 
   @Override
+  public boolean isSingleStatefulFieldValue() {
+    return true;
+  }
+
+  @Override
   public String toString() {
     return "SingleStatefulFieldValue(" + field.toSourceString() + ")";
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
index 7b55767..be620a8 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStatelessFieldValue.java
@@ -25,6 +25,11 @@
   }
 
   @Override
+  public boolean isSingleStatelessFieldValue() {
+    return true;
+  }
+
+  @Override
   public String toString() {
     return "SingleStatelessFieldValue(" + field.toSourceString() + ")";
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java b/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java
new file mode 100644
index 0000000..ed6c059
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/arithmetic/AbstractCalculator.java
@@ -0,0 +1,192 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.analysis.value.arithmetic;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.utils.BitUtils;
+
+public class AbstractCalculator {
+
+  public static AbstractValue andIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (left.isZero()) {
+      return left;
+    }
+    if (right.isZero()) {
+      return right;
+    }
+    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
+      int result =
+          left.asSingleNumberValue().getIntValue() & right.asSingleNumberValue().getIntValue();
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()
+        && right.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              left.getDefinitelySetIntBits() & right.getDefinitelySetIntBits(),
+              left.getDefinitelyUnsetIntBits() | right.getDefinitelyUnsetIntBits());
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(0, left.getDefinitelyUnsetIntBits());
+    }
+    if (right.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(0, right.getDefinitelyUnsetIntBits());
+    }
+    return AbstractValue.unknown();
+  }
+
+  public static AbstractValue orIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (left.isZero()) {
+      return right;
+    }
+    if (right.isZero()) {
+      return left;
+    }
+    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
+      int result =
+          left.asSingleNumberValue().getIntValue() | right.asSingleNumberValue().getIntValue();
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()
+        && right.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              left.getDefinitelySetIntBits() | right.getDefinitelySetIntBits(),
+              left.getDefinitelyUnsetIntBits() & right.getDefinitelyUnsetIntBits());
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(left.getDefinitelySetIntBits(), 0);
+    }
+    if (right.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(right.getDefinitelySetIntBits(), 0);
+    }
+    return AbstractValue.unknown();
+  }
+
+  public static AbstractValue orIntegers(
+      AppView<?> appView,
+      AbstractValue first,
+      AbstractValue second,
+      AbstractValue third,
+      AbstractValue fourth) {
+    return orIntegers(
+        appView, first, orIntegers(appView, second, orIntegers(appView, third, fourth)));
+  }
+
+  public static AbstractValue shlIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (!right.isSingleNumberValue()) {
+      return AbstractValue.unknown();
+    }
+    int rightConst = right.asSingleNumberValue().getIntValue();
+    return shlIntegers(appView, left, rightConst);
+  }
+
+  public static AbstractValue shlIntegers(AppView<?> appView, AbstractValue left, int rightConst) {
+    if (rightConst == 0) {
+      return left;
+    }
+    if (left.isSingleNumberValue()) {
+      int result = left.asSingleNumberValue().getIntValue() << rightConst;
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation() && rightConst > 0) {
+      // Shift the known bits and add that we now know that the lowermost n bits are definitely
+      // unset. Note that when rightConst is 31, 1 << rightConst is Integer.MIN_VALUE. When
+      // subtracting 1 we overflow and get 0111...111, as desired.
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              left.getDefinitelySetIntBits() << rightConst,
+              (left.getDefinitelyUnsetIntBits() << rightConst) | ((1 << rightConst) - 1));
+    }
+    return AbstractValue.unknown();
+  }
+
+  public static AbstractValue shrIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (!right.isSingleNumberValue()) {
+      return AbstractValue.unknown();
+    }
+    int rightConst = right.asSingleNumberValue().getIntValue();
+    return shrIntegers(appView, left, rightConst);
+  }
+
+  public static AbstractValue shrIntegers(AppView<?> appView, AbstractValue left, int rightConst) {
+    if (rightConst == 0) {
+      return left;
+    }
+    if (left.isSingleNumberValue()) {
+      int result = left.asSingleNumberValue().getIntValue() >> rightConst;
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              left.getDefinitelySetIntBits() >> rightConst,
+              left.getDefinitelyUnsetIntBits() >> rightConst);
+    }
+    return AbstractValue.unknown();
+  }
+
+  public static AbstractValue ushrIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (!right.isSingleNumberValue()) {
+      return AbstractValue.unknown();
+    }
+    int rightConst = right.asSingleNumberValue().getIntValue();
+    if (rightConst == 0) {
+      return left;
+    }
+    if (left.isSingleNumberValue()) {
+      int result = left.asSingleNumberValue().getIntValue() >>> rightConst;
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation() && rightConst > 0) {
+      // Shift the known bits information and add that we now know that the uppermost n bits are
+      // definitely unset.
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              left.getDefinitelySetIntBits() >>> rightConst,
+              (left.getDefinitelyUnsetIntBits() >>> rightConst)
+                  | (BitUtils.ONLY_SIGN_BIT_SET_MASK >> (rightConst - 1)));
+    }
+    return AbstractValue.unknown();
+  }
+
+  public static AbstractValue xorIntegers(
+      AppView<?> appView, AbstractValue left, AbstractValue right) {
+    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
+      int result =
+          left.asSingleNumberValue().getIntValue() ^ right.asSingleNumberValue().getIntValue();
+      return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+    }
+    if (left.hasDefinitelySetAndUnsetBitsInformation()
+        && right.hasDefinitelySetAndUnsetBitsInformation()) {
+      return appView
+          .abstractValueFactory()
+          .createDefiniteBitsNumberValue(
+              (left.getDefinitelySetIntBits() & right.getDefinitelyUnsetIntBits())
+                  | (left.getDefinitelyUnsetIntBits() & right.getDefinitelySetIntBits()),
+              (left.getDefinitelySetIntBits() & right.getDefinitelySetIntBits())
+                  | (left.getDefinitelyUnsetIntBits() & right.getDefinitelyUnsetIntBits()));
+    }
+    return AbstractValue.unknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EmptyObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EmptyObjectState.java
index 1f2b960..d16e2b0 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EmptyObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EmptyObjectState.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.analysis.value.objectstate;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -29,7 +28,7 @@
   }
 
   @Override
-  public AbstractValue getAbstractFieldValue(DexEncodedField field) {
+  public AbstractValue getAbstractFieldValue(DexField field) {
     return UnknownValue.getInstance();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EnumValuesObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EnumValuesObjectState.java
index 28652ca..25a9566 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EnumValuesObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/EnumValuesObjectState.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.analysis.value.objectstate;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
@@ -40,7 +39,7 @@
   public void forEachAbstractFieldValue(BiConsumer<DexField, AbstractValue> consumer) {}
 
   @Override
-  public AbstractValue getAbstractFieldValue(DexEncodedField field) {
+  public AbstractValue getAbstractFieldValue(DexField field) {
     return UnknownValue.getInstance();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
index f05e962..a773fd2 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/KnownLengthArrayState.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.analysis.value.objectstate;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -27,7 +26,7 @@
   }
 
   @Override
-  public AbstractValue getAbstractFieldValue(DexEncodedField field) {
+  public AbstractValue getAbstractFieldValue(DexField field) {
     return UnknownValue.getInstance();
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/NonEmptyObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/NonEmptyObjectState.java
index 3b73b9d..7acebdd 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/NonEmptyObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/NonEmptyObjectState.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.ir.analysis.value.objectstate;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -32,8 +31,8 @@
   }
 
   @Override
-  public AbstractValue getAbstractFieldValue(DexEncodedField field) {
-    return state.getOrDefault(field.getReference(), UnknownValue.getInstance());
+  public AbstractValue getAbstractFieldValue(DexField field) {
+    return state.getOrDefault(field, UnknownValue.getInstance());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectState.java
index 3b02fc4..271cd94 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectState.java
@@ -47,7 +47,11 @@
     return predicate.test(singleValue);
   }
 
-  public abstract AbstractValue getAbstractFieldValue(DexEncodedField field);
+  public final AbstractValue getAbstractFieldValue(DexEncodedField field) {
+    return getAbstractFieldValue(field.getReference());
+  }
+
+  public abstract AbstractValue getAbstractFieldValue(DexField field);
 
   public abstract boolean isEmpty();
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectStateAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectStateAnalysis.java
index dea5836..f53f615 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectStateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/objectstate/ObjectStateAnalysis.java
@@ -20,13 +20,13 @@
 
   public static ObjectState computeObjectState(
       Value value, AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    assert !value.hasAliasedValue();
-    if (value.isDefinedByInstructionSatisfying(
+    Value valueRoot = value.getAliasedValue();
+    if (valueRoot.isDefinedByInstructionSatisfying(
         i -> i.isNewArrayEmpty() || i.isNewArrayFilledData() || i.isNewArrayFilled())) {
-      return computeNewArrayObjectState(value, appView, context);
+      return computeNewArrayObjectState(valueRoot, appView, context);
     }
-    if (value.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
-      return computeNewInstanceObjectState(value, appView, context);
+    if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
+      return computeNewInstanceObjectState(valueRoot, appView, context);
     }
     return ObjectState.empty();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/And.java b/src/main/java/com/android/tools/r8/ir/code/And.java
index 2639a9d..522c7c0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/And.java
+++ b/src/main/java/com/android/tools/r8/ir/code/And.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.dex.code.DexInstruction;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 import java.util.Set;
 
 public class And extends LogicalBinop {
@@ -104,37 +105,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (left.isZero()) {
-      return left;
-    }
-    if (right.isZero()) {
-      return right;
-    }
-    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
-      int result =
-          foldIntegers(
-              left.asSingleNumberValue().getIntValue(), right.asSingleNumberValue().getIntValue());
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()
-        && right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              foldIntegers(left.getDefinitelySetIntBits(), right.getDefinitelySetIntBits()),
-              left.getDefinitelyUnsetIntBits() | right.getDefinitelyUnsetIntBits());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(0, left.getDefinitelyUnsetIntBits());
-    }
-    if (right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(0, right.getDefinitelyUnsetIntBits());
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.andIntegers(appView, left, right);
   }
 
   @Override
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 6b7f62c..c1dee99 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
@@ -19,6 +19,8 @@
 
   boolean isInstanceGet();
 
+  InstanceGet asInstanceGet();
+
   boolean isStaticGet();
 
   boolean hasUsedOutValue();
diff --git a/src/main/java/com/android/tools/r8/ir/code/Or.java b/src/main/java/com/android/tools/r8/ir/code/Or.java
index 0cc297a..8fd53b3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Or.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Or.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.dex.code.DexOrLong2Addr;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 import java.util.Set;
 
 public class Or extends LogicalBinop {
@@ -103,37 +104,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (left.isZero()) {
-      return right;
-    }
-    if (right.isZero()) {
-      return left;
-    }
-    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
-      int result =
-          foldIntegers(
-              left.asSingleNumberValue().getIntValue(), right.asSingleNumberValue().getIntValue());
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()
-        && right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              foldIntegers(left.getDefinitelySetIntBits(), right.getDefinitelySetIntBits()),
-              left.getDefinitelyUnsetIntBits() & right.getDefinitelyUnsetIntBits());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(left.getDefinitelySetIntBits(), 0);
-    }
-    if (right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(right.getDefinitelySetIntBits(), 0);
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.orIntegers(appView, left, right);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Shl.java b/src/main/java/com/android/tools/r8/ir/code/Shl.java
index a9d9ee4..73c84e8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Shl.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Shl.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 
 public class Shl extends LogicalBinop {
 
@@ -98,28 +99,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (!right.isSingleNumberValue()) {
-      return AbstractValue.unknown();
-    }
-    int rightConst = right.asSingleNumberValue().getIntValue();
-    if (rightConst == 0) {
-      return left;
-    }
-    if (left.isSingleNumberValue()) {
-      int result = foldIntegers(left.asSingleNumberValue().getIntValue(), rightConst);
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation() && rightConst > 0) {
-      // Shift the known bits and add that we now know that the lowermost n bits are definitely
-      // unset. Note that when rightConst is 31, 1 << rightConst is Integer.MIN_VALUE. When
-      // subtracting 1 we overflow and get 0111...111, as desired.
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              foldIntegers(left.getDefinitelySetIntBits(), rightConst),
-              foldIntegers(left.getDefinitelyUnsetIntBits(), rightConst) | ((1 << rightConst) - 1));
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.shlIntegers(appView, left, right);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Shr.java b/src/main/java/com/android/tools/r8/ir/code/Shr.java
index bc59c79..52c7cb0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Shr.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Shr.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 
 public class Shr extends LogicalBinop {
 
@@ -98,25 +99,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (!right.isSingleNumberValue()) {
-      return AbstractValue.unknown();
-    }
-    int rightConst = right.asSingleNumberValue().getIntValue();
-    if (rightConst == 0) {
-      return left;
-    }
-    if (left.isSingleNumberValue()) {
-      int result = foldIntegers(left.asSingleNumberValue().getIntValue(), rightConst);
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              foldIntegers(left.getDefinitelySetIntBits(), rightConst),
-              foldIntegers(left.getDefinitelyUnsetIntBits(), rightConst));
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.shrIntegers(appView, left, right);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Ushr.java b/src/main/java/com/android/tools/r8/ir/code/Ushr.java
index 104b90d..c80f280 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Ushr.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Ushr.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.utils.BitUtils;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 
 public class Ushr extends LogicalBinop {
 
@@ -99,28 +99,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (!right.isSingleNumberValue()) {
-      return AbstractValue.unknown();
-    }
-    int rightConst = right.asSingleNumberValue().getIntValue();
-    if (rightConst == 0) {
-      return left;
-    }
-    if (left.isSingleNumberValue()) {
-      int result = foldIntegers(left.asSingleNumberValue().getIntValue(), rightConst);
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation() && rightConst > 0) {
-      // Shift the known bits information and add that we now know that the uppermost n bits are
-      // definitely unset.
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              foldIntegers(left.getDefinitelySetIntBits(), rightConst),
-              foldIntegers(left.getDefinitelyUnsetIntBits(), rightConst)
-                  | (BitUtils.ONLY_SIGN_BIT_SET_MASK >> (rightConst - 1)));
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.ushrIntegers(appView, left, right);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Xor.java b/src/main/java/com/android/tools/r8/ir/code/Xor.java
index 5116990..bddba7e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Xor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Xor.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.dex.code.DexXorLong2Addr;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 import java.util.Set;
 
 public class Xor extends LogicalBinop {
@@ -98,23 +99,7 @@
 
   @Override
   AbstractValue foldIntegers(AbstractValue left, AbstractValue right, AppView<?> appView) {
-    if (left.isSingleNumberValue() && right.isSingleNumberValue()) {
-      int result =
-          foldIntegers(
-              left.asSingleNumberValue().getIntValue(), right.asSingleNumberValue().getIntValue());
-      return appView.abstractValueFactory().createSingleNumberValue(result, getOutType());
-    }
-    if (left.hasDefinitelySetAndUnsetBitsInformation()
-        && right.hasDefinitelySetAndUnsetBitsInformation()) {
-      return appView
-          .abstractValueFactory()
-          .createDefiniteBitsNumberValue(
-              (left.getDefinitelySetIntBits() & right.getDefinitelyUnsetIntBits())
-                  | (left.getDefinitelyUnsetIntBits() & right.getDefinitelySetIntBits()),
-              (left.getDefinitelySetIntBits() & right.getDefinitelySetIntBits())
-                  | (left.getDefinitelyUnsetIntBits() & right.getDefinitelyUnsetIntBits()));
-    }
-    return AbstractValue.unknown();
+    return AbstractCalculator.xorIntegers(appView, left, right);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 7de5536..035690f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -73,7 +73,7 @@
     numberUnboxer.prepareForPrimaryOptimizationPass(timing, executorService);
     outliner.prepareForPrimaryOptimizationPass(graphLensForPrimaryOptimizationPass);
 
-    if (fieldAccessAnalysis != null) {
+    if (fieldAccessAnalysis != null && fieldAccessAnalysis.fieldAssignmentTracker() != null) {
       fieldAccessAnalysis.fieldAssignmentTracker().initialize();
     }
 
@@ -252,11 +252,12 @@
     onWaveDoneActions = Collections.synchronizedList(new ArrayList<>());
   }
 
-  public void waveDone(ProgramMethodSet wave, ExecutorService executorService)
-      throws ExecutionException {
+  public void waveDone(ProgramMethodSet wave, ExecutorService executorService) {
     delayedOptimizationFeedback.refineAppInfoWithLiveness(appView.appInfo().withLiveness());
     delayedOptimizationFeedback.updateVisibleOptimizationInfo();
-    fieldAccessAnalysis.fieldAssignmentTracker().waveDone(wave, delayedOptimizationFeedback);
+    if (fieldAccessAnalysis.fieldAssignmentTracker() != null) {
+      fieldAccessAnalysis.fieldAssignmentTracker().waveDone(wave, delayedOptimizationFeedback);
+    }
     appView.withArgumentPropagator(ArgumentPropagator::publishDelayedReprocessingCriteria);
     if (appView.options().protoShrinking().enableRemoveProtoEnumSwitchMap()) {
       appView.protoShrinker().protoEnumSwitchMapRemover.updateVisibleStaticFieldValues();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsList.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsList.java
index c050445..906b124 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsList.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsList.java
@@ -70,12 +70,6 @@
     SupportedClasses supportedMethods =
         new SupportedClassesGenerator(options, androidJar, minApi, androidPlatformBuild, true)
             .run(desugaredLibraryImplementation, desugaredLibrarySpecificationResource);
-    System.out.println(
-        "Generating lint files for "
-            + getDebugIdentifier()
-            + " (compile API "
-            + compilationLevel
-            + ")");
     writeLintFiles(compilationLevel, minApi, supportedMethods);
     return compilationLevel;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java
index 77d819f..b496cf8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java
@@ -14,7 +14,7 @@
   public static boolean isArrayUsedOnlyForHashCode(
       NewArrayFilled newArrayFilled, DexItemFactory factory) {
     Value array = newArrayFilled.outValue();
-    if (!array.hasSingleUniqueUser() || array.hasPhiUsers()) {
+    if (array == null || !array.hasSingleUniqueUser() || array.hasPhiUsers()) {
       return false;
     }
     InvokeStatic invoke = array.singleUniqueUser().asInvokeStatic();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 9987a33..3c46cf1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -698,7 +698,14 @@
   }
 
   private void setAbstractReturnValue(AbstractValue value) {
-    assert !abstractReturnValue.isSingleValue() || abstractReturnValue.equals(value)
+    assert !abstractReturnValue.isSingleValue()
+            || abstractReturnValue.equals(value)
+            || (abstractReturnValue.isSingleStatelessFieldValue()
+                && value.isSingleStatefulFieldValue()
+                && abstractReturnValue
+                    .asSingleFieldValue()
+                    .getField()
+                    .isIdenticalTo(value.asSingleFieldValue().getField()))
         : "return single value changed from " + abstractReturnValue + " to " + value;
     abstractReturnValue = value;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 97ead9d..b73ba99 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
@@ -88,9 +89,12 @@
         classes -> {
           // Disable argument propagation for methods that should not be optimized by setting their
           // method state to unknown.
-          new ArgumentPropagatorUnoptimizableMethods(
-                  appView, immediateSubtypingInfo, codeScanner.getMethodStates())
-              .initializeUnoptimizableMethodStates(classes);
+          new ArgumentPropagatorUnoptimizableFieldsAndMethods(
+                  appView,
+                  immediateSubtypingInfo,
+                  codeScanner.getFieldStates(),
+                  codeScanner.getMethodStates())
+              .run(classes);
 
           // Compute the mapping from virtual methods to their root virtual method and the set of
           // monomorphic virtual methods.
@@ -190,7 +194,7 @@
 
     // Find all the code objects that need reprocessing.
     new ArgumentPropagatorMethodReprocessingEnqueuer(appView, reprocessingCriteriaCollection)
-        .enqueueMethodForReprocessing(
+        .enqueueAndPrepareMethodsForReprocessing(
             graphLens, postMethodProcessorBuilder, executorService, timing);
     reprocessingCriteriaCollection = null;
 
@@ -222,8 +226,9 @@
       throws ExecutionException {
     // Unset the scanner since all code objects have been scanned at this point.
     assert appView.isAllCodeProcessed();
-    MethodStateCollectionByReference codeScannerResult = codeScanner.getMethodStates();
-    appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(codeScannerResult);
+    FieldStateCollection fieldStates = codeScanner.getFieldStates();
+    MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+    appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(methodStates);
     codeScanner = null;
 
     postMethodProcessorBuilder.rewrittenWithLens(appView);
@@ -233,12 +238,14 @@
             appView,
             converter,
             immediateSubtypingInfo,
-            codeScannerResult,
+            fieldStates,
+            methodStates,
             stronglyConnectedProgramComponents,
             interfaceDispatchOutsideProgram)
         .propagateOptimizationInfo(executorService, timing);
+    // TODO(b/296030319): Also publish the computed optimization information for fields.
     new ArgumentPropagatorOptimizationInfoPopulator(
-            appView, converter, codeScannerResult, postMethodProcessorBuilder)
+            appView, converter, fieldStates, methodStates, postMethodProcessorBuilder)
         .populateOptimizationInfo(executorService, timing);
     timing.end();
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 0eae64d..8df07a0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -4,29 +4,37 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
+import static com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow.asBaseInFlowOrNull;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
+import com.android.tools.r8.ir.analysis.value.objectstate.ObjectStateAnalysis;
 import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.AliasedValueConfiguration;
 import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
+import com.android.tools.r8.ir.code.FieldGet;
+import com.android.tools.r8.ir.code.FieldPut;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeCustom;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
@@ -36,10 +44,17 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodStateOrBottom;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteReceiverValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValueFactory;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InstanceFieldReadAbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ArgumentPropagatorReprocessingCriteriaCollection;
@@ -47,6 +62,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ParameterReprocessingCriteria;
 import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -54,6 +70,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
 /**
  * Analyzes each {@link IRCode} during the primary optimization to collect information about the
@@ -61,6 +78,7 @@
  *
  * <p>State pruning is applied on-the-fly to avoid storing redundant information.
  */
+// TODO(b/330130322): Consider extending the flow graph with method-return nodes.
 public class ArgumentPropagatorCodeScanner {
 
   private static AliasedValueConfiguration aliasedValueConfiguration =
@@ -70,6 +88,8 @@
 
   private final ArgumentPropagatorCodeScannerModeling modeling;
 
+  private final FieldValueFactory fieldValueFactory = new FieldValueFactory();
+
   private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
 
   private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
@@ -84,6 +104,12 @@
   private final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
 
   /**
+   * The abstract program state for this optimization. Intuitively maps each field to its abstract
+   * value and dynamic type.
+   */
+  private final FieldStateCollection fieldStates = FieldStateCollection.createConcurrent();
+
+  /**
    * The abstract program state for this optimization. Intuitively maps each parameter to its
    * abstract value and dynamic type.
    */
@@ -110,6 +136,10 @@
     virtualRootMethods.putAll(extension);
   }
 
+  public FieldStateCollection getFieldStates() {
+    return fieldStates;
+  }
+
   public MethodStateCollectionByReference getMethodStates() {
     return methodStates;
   }
@@ -118,8 +148,19 @@
     return virtualRootMethods.get(method.getReference());
   }
 
+  // TODO(b/296030319): Allow lookups in the FieldStateCollection using DexField keys to avoid the
+  //  need for definitionFor here.
+  private boolean isFieldValueAlreadyUnknown(DexField field) {
+    return isFieldValueAlreadyUnknown(appView.definitionFor(field).asProgramField());
+  }
+
+  private boolean isFieldValueAlreadyUnknown(ProgramField field) {
+    return fieldStates.get(field).isUnknown();
+  }
+
   protected boolean isMethodParameterAlreadyUnknown(
       MethodParameter methodParameter, ProgramMethod method) {
+    assert methodParameter.getMethod().isIdenticalTo(method.getReference());
     MethodState methodState =
         methodStates.get(
             method.getDefinition().belongsToDirectPool() || isMonomorphicVirtualMethod(method)
@@ -153,17 +194,198 @@
       AbstractValueSupplier abstractValueSupplier,
       Timing timing) {
     timing.begin("Argument propagation scanner");
-    for (Invoke invoke : code.<Invoke>instructions(Instruction::isInvoke)) {
-      if (invoke.isInvokeMethod()) {
-        scan(invoke.asInvokeMethod(), abstractValueSupplier, method, timing);
-      } else if (invoke.isInvokeCustom()) {
-        scan(invoke.asInvokeCustom());
+    for (Instruction instruction : code.instructions()) {
+      if (instruction.isFieldPut()) {
+        scan(instruction.asFieldPut(), abstractValueSupplier, method, timing);
+      } else if (instruction.isInvokeMethod()) {
+        scan(instruction.asInvokeMethod(), abstractValueSupplier, method, timing);
+      } else if (instruction.isInvokeCustom()) {
+        scan(instruction.asInvokeCustom());
       }
     }
     timing.end();
   }
 
   private void scan(
+      FieldPut fieldPut,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
+    ProgramField field = fieldPut.resolveField(appView, context).getProgramField();
+    if (field == null) {
+      // Nothing to propagate.
+      return;
+    }
+    addTemporaryFieldState(fieldPut, field, abstractValueSupplier, context, timing);
+  }
+
+  private void addTemporaryFieldState(
+      FieldPut fieldPut,
+      ProgramField field,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
+    timing.begin("Add field state");
+    fieldStates.addTemporaryFieldState(
+        field,
+        () -> computeFieldState(fieldPut, field, abstractValueSupplier, context, timing),
+        timing,
+        (existingFieldState, fieldStateToAdd) -> {
+          NonEmptyValueState newFieldState =
+              existingFieldState.mutableJoin(
+                  appView,
+                  fieldStateToAdd,
+                  field.getType(),
+                  StateCloner.getCloner(),
+                  Action.empty());
+          return narrowFieldState(field, newFieldState);
+        });
+    timing.end();
+  }
+
+  private NonEmptyValueState computeFieldState(
+      FieldPut fieldPut,
+      ProgramField resolvedField,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context,
+      Timing timing) {
+    timing.begin("Compute field state for field-put");
+    NonEmptyValueState result =
+        computeFieldState(fieldPut, resolvedField, abstractValueSupplier, context);
+    timing.end();
+    return result;
+  }
+
+  private NonEmptyValueState computeFieldState(
+      FieldPut fieldPut,
+      ProgramField field,
+      AbstractValueSupplier abstractValueSupplier,
+      ProgramMethod context) {
+    NonEmptyValueState inFlowState = computeInFlowState(field.getType(), fieldPut.value(), context);
+    if (inFlowState != null) {
+      return inFlowState;
+    }
+
+    if (field.getType().isArrayType()) {
+      Nullability nullability = fieldPut.value().getType().nullability();
+      return ConcreteArrayTypeValueState.create(nullability);
+    }
+
+    AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value());
+    if (abstractValue.isUnknown()) {
+      abstractValue =
+          getFallbackAbstractValueForField(
+              field,
+              () -> ObjectStateAnalysis.computeObjectState(fieldPut.value(), appView, context));
+    }
+    if (field.getType().isClassType()) {
+      DynamicType dynamicType =
+          WideningUtils.widenDynamicNonReceiverType(
+              appView, fieldPut.value().getDynamicType(appView), field.getType());
+      return ConcreteClassTypeValueState.create(abstractValue, dynamicType);
+    } else {
+      assert field.getType().isPrimitiveType();
+      return ConcretePrimitiveTypeValueState.create(abstractValue);
+    }
+  }
+
+  // If the value is an argument of the enclosing method or defined by a field-get, then clearly we
+  // have no information about its abstract value (yet). Instead of treating this as having an
+  // unknown runtime value, we instead record a flow constraint.
+  private InFlow computeInFlow(Value value, ProgramMethod context) {
+    Value valueRoot = value.getAliasedValue(aliasedValueConfiguration);
+    if (valueRoot.isArgument()) {
+      MethodParameter inParameter =
+          methodParameterFactory.create(context, valueRoot.getDefinition().asArgument().getIndex());
+      return widenBaseInFlow(inParameter, context);
+    } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+      FieldGet fieldGet = valueRoot.getDefinition().asFieldGet();
+      ProgramField field = fieldGet.resolveField(appView, context).getProgramField();
+      if (field == null) {
+        return null;
+      }
+      if (fieldGet.isInstanceGet()) {
+        Value receiverValue = fieldGet.asInstanceGet().object();
+        BaseInFlow receiverInFlow = asBaseInFlowOrNull(computeInFlow(receiverValue, context));
+        if (receiverInFlow != null
+            && receiverInFlow.equals(widenBaseInFlow(receiverInFlow, context))) {
+          return new InstanceFieldReadAbstractFunction(receiverInFlow, field.getReference());
+        }
+      }
+      return widenBaseInFlow(fieldValueFactory.create(field), context);
+    }
+    return null;
+  }
+
+  private InFlow widenBaseInFlow(BaseInFlow inFlow, ProgramMethod context) {
+    if (inFlow.isFieldValue()) {
+      if (isFieldValueAlreadyUnknown(inFlow.asFieldValue().getField())) {
+        return AbstractFunction.unknown();
+      }
+    } else {
+      assert inFlow.isMethodParameter();
+      if (isMethodParameterAlreadyUnknown(inFlow.asMethodParameter(), context)) {
+        return AbstractFunction.unknown();
+      }
+    }
+    return inFlow;
+  }
+
+  private NonEmptyValueState computeInFlowState(
+      DexType staticType, Value value, ProgramMethod context) {
+    InFlow inFlow = computeInFlow(value, context);
+    if (inFlow == null) {
+      return null;
+    }
+    if (inFlow.isUnknownAbstractFunction()) {
+      return ValueState.unknown();
+    }
+    assert inFlow.isBaseInFlow() || inFlow.isInstanceFieldReadAbstractFunction();
+    return ConcreteValueState.create(staticType, inFlow);
+  }
+
+  // Strengthens the abstract value of static final fields to a (self-)SingleFieldValue when the
+  // abstract value is unknown. The soundness of this is based on the fact that static final fields
+  // will never have their value changed after the <clinit> finishes, so value in a static final
+  // field can always be rematerialized by reading the field.
+  private NonEmptyValueState narrowFieldState(ProgramField field, NonEmptyValueState fieldState) {
+    AbstractValue fallbackAbstractValue =
+        getFallbackAbstractValueForField(field, ObjectState::empty);
+    if (!fallbackAbstractValue.isUnknown()) {
+      AbstractValue abstractValue = fieldState.getAbstractValue(appView);
+      if (!abstractValue.isUnknown()) {
+        return fieldState;
+      }
+      if (field.getType().isArrayType()) {
+        // We do not track an abstract value for array types.
+        return fieldState;
+      }
+      if (field.getType().isClassType()) {
+        DynamicType dynamicType =
+            fieldState.isReferenceState()
+                ? fieldState.asReferenceState().getDynamicType()
+                : DynamicType.unknown();
+        return new ConcreteClassTypeValueState(fallbackAbstractValue, dynamicType);
+      } else {
+        assert field.getType().isPrimitiveType();
+        return new ConcretePrimitiveTypeValueState(fallbackAbstractValue);
+      }
+    }
+    return fieldState;
+  }
+
+  // TODO(b/296030319): Also handle effectively final fields.
+  private AbstractValue getFallbackAbstractValueForField(
+      ProgramField field, Supplier<ObjectState> objectStateSupplier) {
+    if (field.getAccessFlags().isFinal() && field.getAccessFlags().isStatic()) {
+      return appView
+          .abstractValueFactory()
+          .createSingleFieldValue(field.getReference(), objectStateSupplier.get());
+    }
+    return AbstractValue.unknown();
+  }
+
+  private void scan(
       InvokeMethod invoke,
       AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
@@ -182,10 +404,7 @@
     }
 
     SingleResolutionResult<?> resolutionResult =
-        appView
-            .appInfo()
-            .unsafeResolveMethodDueToDexFormatLegacy(invokedMethod)
-            .asSingleResolution();
+        invoke.resolveMethod(appView, context).asSingleResolution();
     if (resolutionResult == null) {
       // Nothing to propagate; the invoke instruction fails.
       return;
@@ -309,7 +528,6 @@
     return result;
   }
 
-  @SuppressWarnings("UnusedVariable")
   // TODO(b/190154391): Add a strategy that widens the dynamic receiver type to allow easily
   //  experimenting with the performance/size trade-off between precise/imprecise handling of
   //  dynamic dispatch.
@@ -507,60 +725,41 @@
       return ValueState.unknown();
     }
 
-    Value argumentRoot = argument.getAliasedValue(aliasedValueConfiguration);
     DexType parameterType =
         invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic());
-    TypeElement parameterTypeElement = parameterType.toTypeElement(appView);
 
     // If the value is an argument of the enclosing method, then clearly we have no information
     // about its abstract value. Instead of treating this as having an unknown runtime value, we
     // instead record a flow constraint that specifies that all values that flow into the parameter
     // of this enclosing method also flows into the corresponding parameter of the methods
     // potentially called from this invoke instruction.
-    if (argumentRoot.isArgument()) {
-      MethodParameter forwardedParameter =
-          methodParameterFactory.create(
-              context, argumentRoot.getDefinition().asArgument().getIndex());
-      if (isMethodParameterAlreadyUnknown(forwardedParameter, context)) {
-        return ValueState.unknown();
-      }
-      if (parameterTypeElement.isClassType()) {
-        return new ConcreteClassTypeValueState(forwardedParameter);
-      } else if (parameterTypeElement.isArrayType()) {
-        return new ConcreteArrayTypeValueState(forwardedParameter);
-      } else {
-        assert parameterTypeElement.isPrimitiveType();
-        return new ConcretePrimitiveTypeValueState(forwardedParameter);
-      }
+    NonEmptyValueState inFlowState = computeInFlowState(parameterType, argument, context);
+    if (inFlowState != null) {
+      return inFlowState;
     }
 
     // Only track the nullability for array types.
-    if (parameterTypeElement.isArrayType()) {
+    if (parameterType.isArrayType()) {
       Nullability nullability = argument.getType().nullability();
-      return nullability.isMaybeNull()
-          ? ValueState.unknown()
-          : new ConcreteArrayTypeValueState(nullability);
+      return ConcreteArrayTypeValueState.create(nullability);
     }
 
     AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(argument);
 
     // For class types, we track both the abstract value and the dynamic type. If both are unknown,
     // then use UnknownParameterState.
-    if (parameterTypeElement.isClassType()) {
+    if (parameterType.isClassType()) {
       DynamicType dynamicType = argument.getDynamicType(appView);
       DynamicType widenedDynamicType =
           WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
-      return abstractValue.isUnknown() && widenedDynamicType.isUnknown()
-          ? ValueState.unknown()
-          : new ConcreteClassTypeValueState(abstractValue, widenedDynamicType);
+      return ConcreteClassTypeValueState.create(abstractValue, widenedDynamicType);
+    } else {
+      // For primitive types, we only track the abstract value, thus if the abstract value is
+      // unknown,
+      // we use UnknownParameterState.
+      assert parameterType.isPrimitiveType();
+      return ConcretePrimitiveTypeValueState.create(abstractValue);
     }
-
-    // For primitive types, we only track the abstract value, thus if the abstract value is unknown,
-    // we use UnknownParameterState.
-    assert parameterTypeElement.isPrimitiveType();
-    return abstractValue.isUnknown()
-        ? ValueState.unknown()
-        : new ConcretePrimitiveTypeValueState(abstractValue);
   }
 
   @SuppressWarnings("ReferenceEquality")
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
index 3e5e433..b4f5f95 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
@@ -17,7 +17,10 @@
 
   ArgumentPropagatorCodeScannerModeling(AppView<AppInfoWithLiveness> appView) {
     this.composeModeling =
-        appView.testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions
+        appView
+                .options()
+                .getJetpackComposeOptions()
+                .isModelingChangedArgumentsToComposableFunctions()
             ? new ArgumentPropagatorComposeModeling(appView)
             : null;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
index b857d7d..5056ee3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.optimize.argumentpropagation;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -14,17 +15,31 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.IRToLirFinalizer;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.optimize.argumentpropagation.reprocessingcriteria.ArgumentPropagatorReprocessingCriteriaCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.ObjectUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -45,7 +60,7 @@
    * Called indirectly from {@link IRConverter} to add all methods that require reprocessing to
    * {@param postMethodProcessorBuilder}.
    */
-  public void enqueueMethodForReprocessing(
+  public void enqueueAndPrepareMethodsForReprocessing(
       ArgumentPropagatorGraphLens graphLens,
       PostMethodProcessor.Builder postMethodProcessorBuilder,
       ExecutorService executorService,
@@ -69,6 +84,10 @@
     }
     timing.end();
 
+    timing.begin("Eliminate dead field accesses");
+    eliminateDeadFieldAccesses(executorService);
+    timing.end();
+
     timing.end();
   }
 
@@ -144,6 +163,77 @@
             postMethodProcessorBuilder.addAll(methodsToReprocessInClass, currentGraphLens));
   }
 
+  private void eliminateDeadFieldAccesses(ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz -> {
+          clazz.forEachProgramMethodMatching(
+              m -> m.hasCode() && !m.getCode().isSharedCodeObject(),
+              this::eliminateDeadFieldAccesses);
+        },
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private void eliminateDeadFieldAccesses(ProgramMethod method) {
+    Code code = method.getDefinition().getCode();
+    if (!code.isLirCode()) {
+      assert appView.isCfByteCodePassThrough(method.getDefinition());
+      return;
+    }
+    LirCode<Integer> lirCode = code.asLirCode();
+    LirCode<Integer> rewrittenLirCode = eliminateDeadFieldAccesses(method, lirCode);
+    if (ObjectUtils.notIdentical(lirCode, rewrittenLirCode)) {
+      method.setCode(rewrittenLirCode, appView);
+    }
+  }
+
+  private LirCode<Integer> eliminateDeadFieldAccesses(
+      ProgramMethod method, LirCode<Integer> lirCode) {
+    if (ArrayUtils.none(
+        lirCode.getConstantPool(), constant -> isDeadFieldAccess(constant, method))) {
+      return lirCode;
+    }
+    IRCode irCode = lirCode.buildIR(method, appView);
+    AffectedValues affectedValues = new AffectedValues();
+    BasicBlockIterator blocks = irCode.listIterator();
+    Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      if (blocksToRemove.contains(block)) {
+        continue;
+      }
+      InstructionListIterator instructionIterator = block.listIterator(irCode);
+      while (instructionIterator.hasNext()) {
+        FieldInstruction fieldInstruction = instructionIterator.next().asFieldInstruction();
+        if (fieldInstruction == null || !isDeadFieldAccess(fieldInstruction.getField(), method)) {
+          continue;
+        }
+        instructionIterator.replaceCurrentInstructionWithThrowNull(
+            appView, irCode, blocks, blocksToRemove, affectedValues);
+      }
+    }
+    irCode.removeBlocks(blocksToRemove);
+    affectedValues.narrowingWithAssumeRemoval(appView, irCode);
+    irCode.removeRedundantBlocks();
+    return new IRToLirFinalizer(appView)
+        .finalizeCode(irCode, BytecodeMetadataProvider.empty(), Timing.empty());
+  }
+
+  private boolean isDeadFieldAccess(LirConstant constant, ProgramMethod context) {
+    if (!(constant instanceof DexField)) {
+      return false;
+    }
+    DexField fieldReference = (DexField) constant;
+    return isDeadFieldAccess(fieldReference, context);
+  }
+
+  private boolean isDeadFieldAccess(DexField fieldReference, ProgramMethod context) {
+    ProgramField field = appView.appInfo().resolveField(fieldReference, context).getProgramField();
+    return field != null && field.getOptimizationInfo().getAbstractValue().isBottom();
+  }
+
   static class AffectedMethodUseRegistry
       extends DefaultUseRegistryWithResult<Boolean, ProgramMethod> {
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index e4181f2..2b14cf3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -17,11 +18,11 @@
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
-import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
@@ -47,6 +48,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final PrimaryR8IRConverter converter;
+  private final FieldStateCollection fieldStates;
   private final MethodStateCollectionByReference methodStates;
   private final InternalOptions options;
   private final PostMethodProcessor.Builder postMethodProcessorBuilder;
@@ -54,10 +56,12 @@
   public ArgumentPropagatorOptimizationInfoPopulator(
       AppView<AppInfoWithLiveness> appView,
       PrimaryR8IRConverter converter,
+      FieldStateCollection fieldStates,
       MethodStateCollectionByReference methodStates,
       PostMethodProcessor.Builder postMethodProcessorBuilder) {
     this.appView = appView;
     this.converter = converter;
+    this.fieldStates = fieldStates;
     this.methodStates = methodStates;
     this.options = appView.options();
     this.postMethodProcessorBuilder = postMethodProcessorBuilder;
@@ -94,11 +98,21 @@
 
   private ProgramMethodSet setOptimizationInfo(DexProgramClass clazz) {
     ProgramMethodSet prunedMethods = ProgramMethodSet.create();
+    clazz.forEachProgramField(this::setOptimizationInfo);
     clazz.forEachProgramMethod(method -> setOptimizationInfo(method, prunedMethods));
     clazz.getMethodCollection().removeMethods(prunedMethods.toDefinitionSet());
     return prunedMethods;
   }
 
+  public void setOptimizationInfo(ProgramField field) {
+    ValueState state = fieldStates.remove(field);
+    // TODO(b/296030319): Also publish non-bottom field states.
+    if (state.isBottom()) {
+      getSimpleFeedback()
+          .recordFieldHasAbstractValue(field.getDefinition(), appView, AbstractValue.bottom());
+    }
+  }
+
   public void setOptimizationInfo(ProgramMethod method, ProgramMethodSet prunedMethods) {
     setOptimizationInfo(method, prunedMethods, methodStates.remove(method));
   }
@@ -184,7 +198,7 @@
     if (optimizationInfo.returnsArgument()) {
       ValueState returnedArgumentState =
           monomorphicMethodState.getParameterState(optimizationInfo.getReturnedArgument());
-      OptimizationFeedback.getSimple()
+      getSimpleFeedback()
           .methodReturnsAbstractValue(
               method.getDefinition(), appView, returnedArgumentState.getAbstractValue(appView));
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
index 26a2167..97903a9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
@@ -31,6 +32,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final PrimaryR8IRConverter converter;
+  private final FieldStateCollection fieldStates;
   private final MethodStateCollectionByReference methodStates;
 
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
@@ -43,12 +45,14 @@
       AppView<AppInfoWithLiveness> appView,
       PrimaryR8IRConverter converter,
       ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      FieldStateCollection fieldStates,
       MethodStateCollectionByReference methodStates,
       List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
       BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram) {
     this.appView = appView;
     this.converter = converter;
     this.immediateSubtypingInfo = immediateSubtypingInfo;
+    this.fieldStates = fieldStates;
     this.methodStates = methodStates;
     this.stronglyConnectedProgramComponents = stronglyConnectedProgramComponents;
     this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
@@ -67,7 +71,7 @@
 
     // Solve the parameter flow constraints.
     timing.begin("Solve flow constraints");
-    new InFlowPropagator(appView, converter, methodStates).run(executorService);
+    new InFlowPropagator(appView, converter, fieldStates, methodStates).run(executorService);
     timing.end();
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
new file mode 100644
index 0000000..315c9f7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Collection;
+
+public class ArgumentPropagatorUnoptimizableFieldsAndMethods {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
+  private final FieldStateCollection fieldStates;
+  private final MethodStateCollectionByReference methodStates;
+
+  public ArgumentPropagatorUnoptimizableFieldsAndMethods(
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      FieldStateCollection fieldStates,
+      MethodStateCollectionByReference methodStates) {
+    this.appView = appView;
+    this.immediateSubtypingInfo = immediateSubtypingInfo;
+    this.fieldStates = fieldStates;
+    this.methodStates = methodStates;
+  }
+
+  public void run(Collection<DexProgramClass> stronglyConnectedComponent) {
+    initializeUnoptimizableFieldStates(stronglyConnectedComponent);
+    initializeUnoptimizableMethodStates(stronglyConnectedComponent);
+  }
+
+  private void initializeUnoptimizableFieldStates(
+      Collection<DexProgramClass> stronglyConnectedComponent) {
+    for (DexProgramClass clazz : stronglyConnectedComponent) {
+      clazz.forEachProgramField(
+          field -> {
+            if (isUnoptimizableField(field)) {
+              disableValuePropagationForField(field);
+            }
+          });
+    }
+  }
+
+  // TODO(b/190154391): Consider if we should bail out for classes that inherit from a missing
+  //  class.
+  private void initializeUnoptimizableMethodStates(
+      Collection<DexProgramClass> stronglyConnectedComponent) {
+    ProgramMethodSet unoptimizableVirtualMethods =
+        MethodOverridesCollector.findAllMethodsAndOverridesThatMatches(
+            appView,
+            immediateSubtypingInfo,
+            stronglyConnectedComponent,
+            method -> {
+              if (isUnoptimizableMethod(method)) {
+                if (method.getDefinition().belongsToVirtualPool()
+                    && !method.getHolder().isFinal()
+                    && !method.getAccessFlags().isFinal()) {
+                  return true;
+                } else {
+                  disableValuePropagationForMethodParameters(method);
+                }
+              }
+              return false;
+            });
+    unoptimizableVirtualMethods.forEach(this::disableValuePropagationForMethodParameters);
+  }
+
+  private void disableValuePropagationForField(ProgramField field) {
+    fieldStates.set(field, ValueState.unknown());
+  }
+
+  private void disableValuePropagationForMethodParameters(ProgramMethod method) {
+    methodStates.set(method, UnknownMethodState.get());
+  }
+
+  private boolean isUnoptimizableField(ProgramField field) {
+    KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+    InternalOptions options = appView.options();
+    return !keepInfo.isFieldPropagationAllowed(options);
+  }
+
+  private boolean isUnoptimizableMethod(ProgramMethod method) {
+    assert !method.getDefinition().belongsToVirtualPool()
+            || !method.getDefinition().isLibraryMethodOverride().isUnknown()
+        : "Unexpected virtual method without library method override information: "
+            + method.toSourceString();
+    InternalOptions options = appView.options();
+    return method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
+        || !appView.getKeepInfo(method).isArgumentPropagationAllowed(options);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
deleted file mode 100644
index 28aa563..0000000
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.optimize.argumentpropagation;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import java.util.Collection;
-
-public class ArgumentPropagatorUnoptimizableMethods {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
-  private final MethodStateCollectionByReference methodStates;
-
-  public ArgumentPropagatorUnoptimizableMethods(
-      AppView<AppInfoWithLiveness> appView,
-      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
-      MethodStateCollectionByReference methodStates) {
-    this.appView = appView;
-    this.immediateSubtypingInfo = immediateSubtypingInfo;
-    this.methodStates = methodStates;
-  }
-
-  // TODO(b/190154391): Consider if we should bail out for classes that inherit from a missing
-  //  class.
-  public void initializeUnoptimizableMethodStates(
-      Collection<DexProgramClass> stronglyConnectedComponent) {
-    ProgramMethodSet unoptimizableVirtualMethods =
-        MethodOverridesCollector.findAllMethodsAndOverridesThatMatches(
-            appView,
-            immediateSubtypingInfo,
-            stronglyConnectedComponent,
-            method -> {
-              if (isUnoptimizableMethod(method)) {
-                if (method.getDefinition().belongsToVirtualPool()
-                    && !method.getHolder().isFinal()
-                    && !method.getAccessFlags().isFinal()) {
-                  return true;
-                } else {
-                  disableArgumentPropagationForMethod(method);
-                }
-              }
-              return false;
-            });
-    unoptimizableVirtualMethods.forEach(this::disableArgumentPropagationForMethod);
-  }
-
-  private void disableArgumentPropagationForMethod(ProgramMethod method) {
-    methodStates.set(method, UnknownMethodState.get());
-  }
-
-  private boolean isUnoptimizableMethod(ProgramMethod method) {
-    assert !method.getDefinition().belongsToVirtualPool()
-            || !method.getDefinition().isLibraryMethodOverride().isUnknown()
-        : "Unexpected virtual method without library method override information: "
-            + method.toSourceString();
-    AppInfoWithLiveness appInfo = appView.appInfo();
-    InternalOptions options = appView.options();
-    return method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
-        || !appInfo.getKeepInfo().getMethodInfo(method).isArgumentPropagationAllowed(options);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
index da529db..7b390d6 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/AbstractFunction.java
@@ -3,9 +3,56 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
 public interface AbstractFunction extends InFlow {
 
+  static IdentityAbstractFunction identity() {
+    return IdentityAbstractFunction.get();
+  }
+
   static UnknownAbstractFunction unknown() {
     return UnknownAbstractFunction.get();
   }
+
+  /**
+   * Applies the current abstract function to its declared inputs (in {@link #getBaseInFlow()}).
+   *
+   * <p>It is guaranteed by the caller that the given {@param state} is the abstract state for the
+   * field or parameter that caused this function to be reevaluated. If this abstract function takes
+   * a single input, then {@param state} is guaranteed to be the state for the node returned by
+   * {@link #getBaseInFlow()}, and {@param flowGraphStateProvider} should never be used.
+   *
+   * <p>Abstract functions that depend on multiple inputs can lookup the state for each input in
+   * {@param flowGraphStateProvider}. Attempting to lookup the state of a non-declared input is an
+   * error.
+   */
+  ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState inState);
+
+  /** Returns true if the given {@param inFlow} is a declared input of this abstract function. */
+  boolean containsBaseInFlow(BaseInFlow inFlow);
+
+  /**
+   * Returns the program field or parameter graph nodes that this function depends on. Upon any
+   * change to the abstract state of any of these nodes this abstract function must be re-evaluated.
+   */
+  Iterable<BaseInFlow> getBaseInFlow();
+
+  @Override
+  default boolean isAbstractFunction() {
+    return true;
+  }
+
+  @Override
+  default AbstractFunction asAbstractFunction() {
+    return this;
+  }
+
+  default boolean isIdentity() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BaseInFlow.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BaseInFlow.java
new file mode 100644
index 0000000..ef35ee9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BaseInFlow.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+public interface BaseInFlow extends InFlow {
+
+  static BaseInFlow asBaseInFlowOrNull(InFlow inFlow) {
+    return inFlow != null ? inFlow.asBaseInFlow() : null;
+  }
+
+  @Override
+  default boolean isBaseInFlow() {
+    return true;
+  }
+
+  @Override
+  default BaseInFlow asBaseInFlow() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
index f325f4a..567a559 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
@@ -27,7 +27,7 @@
       StateCloner cloner,
       Action onChangedAction) {
     if (state.isBottom()) {
-      assert state == bottomPrimitiveTypeParameter();
+      assert state == bottomPrimitiveTypeState();
       return this;
     }
     if (state.isUnknown()) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
index 46a26d7..d973ed8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
@@ -40,18 +40,6 @@
   }
 
   @Override
-  public ValueState clearInFlow() {
-    if (hasInFlow()) {
-      if (nullability.isBottom()) {
-        return bottomArrayTypeParameter();
-      }
-      internalClearInFlow();
-    }
-    assert !isEffectivelyBottom();
-    return this;
-  }
-
-  @Override
   public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     if (getNullability().isDefinitelyNull()) {
       return appView.abstractValueFactory().createUncheckedNullValue();
@@ -65,6 +53,11 @@
   }
 
   @Override
+  public BottomValueState getCorrespondingBottom() {
+    return bottomArrayTypeState();
+  }
+
+  @Override
   public ConcreteParameterStateKind getKind() {
     return ConcreteParameterStateKind.ARRAY;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
index bf3d1f9..4653731 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
@@ -46,21 +46,8 @@
   }
 
   @Override
-  public ValueState clearInFlow() {
-    if (hasInFlow()) {
-      if (abstractValue.isBottom()) {
-        assert dynamicType.isBottom();
-        return bottomClassTypeParameter();
-      }
-      internalClearInFlow();
-    }
-    assert !isEffectivelyBottom();
-    return this;
-  }
-
-  @Override
   public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
-    if (getDynamicType().getNullability().isDefinitelyNull()) {
+    if (getNullability().isDefinitelyNull()) {
       assert abstractValue.isNull() || abstractValue.isUnknown();
       return appView.abstractValueFactory().createUncheckedNullValue();
     }
@@ -68,6 +55,11 @@
   }
 
   @Override
+  public BottomValueState getCorrespondingBottom() {
+    return bottomClassTypeState();
+  }
+
+  @Override
   public DynamicType getDynamicType() {
     return dynamicType;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
index 2d53523..816e303 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
@@ -29,30 +29,22 @@
     assert !isEffectivelyUnknown() : "Must use UnknownParameterState instead";
   }
 
-  public static NonEmptyValueState create(AbstractValue abstractValue) {
-    return abstractValue.isUnknown()
-        ? ValueState.unknown()
-        : new ConcretePrimitiveTypeValueState(abstractValue);
-  }
-
   public ConcretePrimitiveTypeValueState(InFlow inFlow) {
     this(AbstractValue.bottom(), SetUtils.newHashSet(inFlow));
   }
 
-  @Override
-  public ValueState clearInFlow() {
-    if (hasInFlow()) {
-      if (abstractValue.isBottom()) {
-        return bottomPrimitiveTypeParameter();
-      }
-      internalClearInFlow();
-    }
-    assert !isEffectivelyBottom();
-    return this;
+  public static NonEmptyValueState create(AbstractValue abstractValue) {
+    return create(abstractValue, Collections.emptySet());
+  }
+
+  public static NonEmptyValueState create(AbstractValue abstractValue, Set<InFlow> inFlow) {
+    return abstractValue.isUnknown()
+        ? ValueState.unknown()
+        : new ConcretePrimitiveTypeValueState(abstractValue, inFlow);
   }
 
   @Override
-  public ValueState mutableCopy() {
+  public ConcretePrimitiveTypeValueState mutableCopy() {
     return new ConcretePrimitiveTypeValueState(abstractValue, copyInFlow());
   }
 
@@ -106,6 +98,11 @@
   }
 
   @Override
+  public BottomValueState getCorrespondingBottom() {
+    return bottomPrimitiveTypeState();
+  }
+
+  @Override
   public ConcreteParameterStateKind getKind() {
     return ConcreteParameterStateKind.PRIMITIVE;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
index 186dae0..6169d4a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
@@ -31,20 +31,13 @@
   }
 
   @Override
-  public ValueState clearInFlow() {
-    if (hasInFlow()) {
-      if (dynamicType.isBottom()) {
-        return bottomReceiverParameter();
-      }
-      internalClearInFlow();
-    }
-    assert !isEffectivelyBottom();
-    return this;
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
+    return AbstractValue.unknown();
   }
 
   @Override
-  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
-    return AbstractValue.unknown();
+  public BottomValueState getCorrespondingBottom() {
+    return bottomReceiverParameter();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
index f05f9e6..e32324a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
@@ -6,12 +6,13 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.function.Function;
 
 public abstract class ConcreteValueState extends NonEmptyValueState {
 
@@ -28,7 +29,38 @@
     this.inFlow = inFlow;
   }
 
-  public abstract ValueState clearInFlow();
+  public static NonEmptyValueState create(DexType staticType, AbstractValue abstractValue) {
+    if (staticType.isArrayType()) {
+      return unknown();
+    } else if (staticType.isClassType()) {
+      return ConcreteClassTypeValueState.create(abstractValue, DynamicType.unknown());
+    } else {
+      assert staticType.isPrimitiveType();
+      return ConcretePrimitiveTypeValueState.create(abstractValue);
+    }
+  }
+
+  public static ConcreteValueState create(DexType staticType, InFlow inFlow) {
+    if (staticType.isArrayType()) {
+      return new ConcreteArrayTypeValueState(inFlow);
+    } else if (staticType.isClassType()) {
+      return new ConcreteClassTypeValueState(inFlow);
+    } else {
+      assert staticType.isPrimitiveType();
+      return new ConcretePrimitiveTypeValueState(inFlow);
+    }
+  }
+
+  public ValueState clearInFlow() {
+    if (hasInFlow()) {
+      internalClearInFlow();
+      if (isEffectivelyBottom()) {
+        return getCorrespondingBottom();
+      }
+    }
+    assert !isEffectivelyBottom();
+    return this;
+  }
 
   void internalClearInFlow() {
     inFlow = Collections.emptySet();
@@ -51,6 +83,8 @@
     return inFlow;
   }
 
+  public abstract BottomValueState getCorrespondingBottom();
+
   public abstract ConcreteParameterStateKind getKind();
 
   public abstract boolean isEffectivelyBottom();
@@ -68,16 +102,6 @@
   }
 
   @Override
-  public NonEmptyValueState mutableJoin(
-      AppView<AppInfoWithLiveness> appView,
-      Function<ValueState, NonEmptyValueState> stateSupplier,
-      DexType staticType,
-      StateCloner cloner,
-      Action onChangedAction) {
-    return mutableJoin(appView, stateSupplier.apply(this), staticType, cloner, onChangedAction);
-  }
-
-  @Override
   public final NonEmptyValueState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       ValueState state,
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java
new file mode 100644
index 0000000..544c365
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramFieldMap;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+public class FieldStateCollection {
+
+  private final ProgramFieldMap<NonEmptyValueState> fieldStates;
+
+  private FieldStateCollection(ProgramFieldMap<NonEmptyValueState> fieldStates) {
+    this.fieldStates = fieldStates;
+  }
+
+  public static FieldStateCollection createConcurrent() {
+    return new FieldStateCollection(ProgramFieldMap.createConcurrent());
+  }
+
+  public NonEmptyValueState addTemporaryFieldState(
+      AppView<AppInfoWithLiveness> appView,
+      ProgramField field,
+      Supplier<NonEmptyValueState> fieldStateSupplier,
+      Timing timing) {
+    return addTemporaryFieldState(
+        field,
+        fieldStateSupplier,
+        timing,
+        (existingFieldState, fieldStateToAdd) ->
+            existingFieldState.mutableJoin(
+                appView,
+                fieldStateToAdd,
+                field.getType(),
+                StateCloner.getCloner(),
+                Action.empty()));
+  }
+
+  /**
+   * This intentionally takes a {@link Supplier<NonEmptyValueState>} to avoid computing the field
+   * state for a given field put when nothing is known about the value of the field.
+   */
+  public NonEmptyValueState addTemporaryFieldState(
+      ProgramField field,
+      Supplier<NonEmptyValueState> fieldStateSupplier,
+      Timing timing,
+      BiFunction<ConcreteValueState, ConcreteValueState, NonEmptyValueState> joiner) {
+    ValueState joinState =
+        fieldStates.compute(
+            field,
+            (f, existingFieldState) -> {
+              if (existingFieldState == null) {
+                return fieldStateSupplier.get();
+              }
+              assert !existingFieldState.isBottom();
+              if (existingFieldState.isUnknown()) {
+                return existingFieldState;
+              }
+              NonEmptyValueState fieldStateToAdd = fieldStateSupplier.get();
+              if (fieldStateToAdd.isUnknown()) {
+                return fieldStateToAdd;
+              }
+              timing.begin("Join temporary field state");
+              ConcreteValueState existingConcreteFieldState = existingFieldState.asConcrete();
+              ConcreteValueState concreteFieldStateToAdd = fieldStateToAdd.asConcrete();
+              NonEmptyValueState joinResult =
+                  joiner.apply(existingConcreteFieldState, concreteFieldStateToAdd);
+              timing.end();
+              return joinResult;
+            });
+    assert joinState.isNonEmpty();
+    return joinState.asNonEmpty();
+  }
+
+  public void forEach(BiConsumer<ProgramField, ValueState> consumer) {
+    fieldStates.forEach(consumer);
+  }
+
+  public ValueState get(ProgramField field) {
+    NonEmptyValueState fieldState = fieldStates.get(field);
+    return fieldState != null ? fieldState : ValueState.bottom(field);
+  }
+
+  public ValueState remove(ProgramField field) {
+    ValueState removed = fieldStates.remove(field);
+    return removed != null ? removed : ValueState.bottom(field);
+  }
+
+  public ValueState set(ProgramField field, ValueState state) {
+    if (state.isNonEmpty()) {
+      return set(field, state.asNonEmpty());
+    }
+    return remove(field);
+  }
+
+  public ValueState set(ProgramField field, NonEmptyValueState state) {
+    NonEmptyValueState previous = fieldStates.put(field, state);
+    return previous != null ? previous : ValueState.bottom(field);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
new file mode 100644
index 0000000..f7bd0ec
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.DexField;
+
+// TODO(b/296030319): Change DexField to implement InFlow and use DexField in all places instead of
+//  FieldValue to avoid wrappers? This would also remove the need for the FieldValueFactory.
+public class FieldValue implements BaseInFlow {
+
+  private final DexField field;
+
+  public FieldValue(DexField field) {
+    this.field = field;
+  }
+
+  public DexField getField() {
+    return field;
+  }
+
+  @Override
+  public boolean isFieldValue() {
+    return true;
+  }
+
+  @Override
+  public boolean isFieldValue(DexField field) {
+    return this.field.isIdenticalTo(field);
+  }
+
+  @Override
+  public FieldValue asFieldValue() {
+    return this;
+  }
+
+  @Override
+  @SuppressWarnings("EqualsGetClass")
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    FieldValue fieldValue = (FieldValue) obj;
+    return field.isIdenticalTo(fieldValue.field);
+  }
+
+  @Override
+  public int hashCode() {
+    return field.hashCode();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java
new file mode 100644
index 0000000..2ae1d1b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValueFactory.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.ProgramField;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class FieldValueFactory {
+
+  private final Map<DexField, FieldValue> fieldValues = new ConcurrentHashMap<>();
+
+  public FieldValue create(ProgramField field) {
+    return fieldValues.computeIfAbsent(field.getReference(), FieldValue::new);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FlowGraphStateProvider.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FlowGraphStateProvider.java
new file mode 100644
index 0000000..c207a11
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FlowGraphStateProvider.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.FlowGraph;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.function.Supplier;
+
+public interface FlowGraphStateProvider {
+
+  static FlowGraphStateProvider create(FlowGraph flowGraph, AbstractFunction abstractFunction) {
+    if (!InternalOptions.assertionsEnabled()) {
+      return flowGraph;
+    }
+    // If the abstract function is a canonical function, or the abstract function has a single
+    // declared input, we should never perform any state lookups.
+    if (abstractFunction.isIdentity()
+        || abstractFunction.isUnknownAbstractFunction()
+        || abstractFunction.isUpdateChangedFlagsAbstractFunction()) {
+      return new FlowGraphStateProvider() {
+
+        @Override
+        public ValueState getState(DexField field) {
+          throw new Unreachable();
+        }
+
+        @Override
+        public ValueState getState(BaseInFlow inFlow, Supplier<ValueState> defaultStateProvider) {
+          throw new Unreachable();
+        }
+      };
+    }
+    // Otherwise, restrict state lookups to the declared base in flow. This is required for arriving
+    // at the correct fix point.
+    assert abstractFunction.isInstanceFieldReadAbstractFunction();
+    return new FlowGraphStateProvider() {
+
+      @Override
+      public ValueState getState(DexField field) {
+        assert abstractFunction.containsBaseInFlow(new FieldValue(field));
+        return flowGraph.getState(field);
+      }
+
+      @Override
+      public ValueState getState(BaseInFlow inFlow, Supplier<ValueState> defaultStateProvider) {
+        assert abstractFunction.containsBaseInFlow(inFlow);
+        return flowGraph.getState(inFlow, defaultStateProvider);
+      }
+    };
+  }
+
+  ValueState getState(DexField field);
+
+  ValueState getState(BaseInFlow inFlow, Supplier<ValueState> defaultStateProvider);
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
new file mode 100644
index 0000000..1545a75
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class IdentityAbstractFunction implements AbstractFunction {
+
+  private static final IdentityAbstractFunction INSTANCE = new IdentityAbstractFunction();
+
+  private IdentityAbstractFunction() {}
+
+  static IdentityAbstractFunction get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState inState) {
+    return inState;
+  }
+
+  @Override
+  public boolean containsBaseInFlow(BaseInFlow inFlow) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public Iterable<BaseInFlow> getBaseInFlow() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isIdentity() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
index b651045..1f28543 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
@@ -3,8 +3,47 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.optimize.compose.UpdateChangedFlagsAbstractFunction;
+
 public interface InFlow {
 
+  default boolean isAbstractFunction() {
+    return false;
+  }
+
+  default AbstractFunction asAbstractFunction() {
+    return null;
+  }
+
+  default boolean isBaseInFlow() {
+    return false;
+  }
+
+  default BaseInFlow asBaseInFlow() {
+    return null;
+  }
+
+  default boolean isFieldValue() {
+    return false;
+  }
+
+  default boolean isFieldValue(DexField field) {
+    return false;
+  }
+
+  default FieldValue asFieldValue() {
+    return null;
+  }
+
+  default boolean isInstanceFieldReadAbstractFunction() {
+    return false;
+  }
+
+  default InstanceFieldReadAbstractFunction asInstanceFieldReadAbstractFunction() {
+    return null;
+  }
+
   default boolean isMethodParameter() {
     return false;
   }
@@ -12,4 +51,16 @@
   default MethodParameter asMethodParameter() {
     return null;
   }
+
+  default boolean isUnknownAbstractFunction() {
+    return false;
+  }
+
+  default boolean isUpdateChangedFlagsAbstractFunction() {
+    return false;
+  }
+
+  default UpdateChangedFlagsAbstractFunction asUpdateChangedFlagsAbstractFunction() {
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java
new file mode 100644
index 0000000..bd97c0b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.google.common.collect.Lists;
+
+public class InstanceFieldReadAbstractFunction implements AbstractFunction {
+
+  private final BaseInFlow receiver;
+  private final DexField field;
+
+  public InstanceFieldReadAbstractFunction(BaseInFlow receiver, DexField field) {
+    this.receiver = receiver;
+    this.field = field;
+  }
+
+  @Override
+  public ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState predecessorState) {
+    ValueState state = flowGraphStateProvider.getState(receiver, () -> ValueState.bottom(field));
+    if (state.isBottom()) {
+      return ValueState.bottom(field);
+    }
+    if (!state.isClassState()) {
+      return getFallbackState(flowGraphStateProvider);
+    }
+    ConcreteClassTypeValueState classState = state.asClassState();
+    if (classState.getNullability().isDefinitelyNull()) {
+      return ValueState.bottom(field);
+    }
+    AbstractValue abstractValue = state.getAbstractValue(null);
+    if (!abstractValue.hasObjectState()) {
+      return getFallbackState(flowGraphStateProvider);
+    }
+    AbstractValue fieldValue = abstractValue.getObjectState().getAbstractFieldValue(field);
+    if (fieldValue.isUnknown()) {
+      return getFallbackState(flowGraphStateProvider);
+    }
+    return ConcreteValueState.create(field.getType(), fieldValue);
+  }
+
+  @Override
+  public boolean containsBaseInFlow(BaseInFlow inFlow) {
+    return inFlow.equals(receiver) || inFlow.isFieldValue(field);
+  }
+
+  @Override
+  public Iterable<BaseInFlow> getBaseInFlow() {
+    return Lists.newArrayList(receiver, new FieldValue(field));
+  }
+
+  private ValueState getFallbackState(FlowGraphStateProvider flowGraphStateProvider) {
+    ValueState valueState = flowGraphStateProvider.getState(new FieldValue(field), null);
+    assert !valueState.isConcrete() || !valueState.asConcrete().hasInFlow();
+    return valueState;
+  }
+
+  @Override
+  public boolean isInstanceFieldReadAbstractFunction() {
+    return true;
+  }
+
+  @Override
+  public InstanceFieldReadAbstractFunction asInstanceFieldReadAbstractFunction() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
index 9bfeab7..2fe1a2a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import java.util.Objects;
 
-public class MethodParameter implements InFlow {
+public class MethodParameter implements BaseInFlow {
 
   private final DexMethod method;
   private final int index;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
index a715205..97d0a2f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.ProgramMethod;
-import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -17,10 +16,6 @@
     super(methodStates);
   }
 
-  public static MethodStateCollectionByReference create() {
-    return new MethodStateCollectionByReference(new IdentityHashMap<>());
-  }
-
   public static MethodStateCollectionByReference createConcurrent() {
     return new MethodStateCollectionByReference(new ConcurrentHashMap<>());
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
index a244616..029a696 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 public class MethodStateCollectionBySignature extends MethodStateCollection<DexMethodSignature> {
 
@@ -20,10 +19,6 @@
     return new MethodStateCollectionBySignature(new HashMap<>());
   }
 
-  public static MethodStateCollectionBySignature createConcurrent() {
-    return new MethodStateCollectionBySignature(new ConcurrentHashMap<>());
-  }
-
   @Override
   DexMethodSignature getKey(ProgramMethod method) {
     return method.getMethodSignature();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
index 19bc95a..fd4186b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyValueState.java
@@ -4,12 +4,6 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.Action;
-import java.util.function.Function;
-
 public abstract class NonEmptyValueState extends ValueState {
 
   @Override
@@ -21,19 +15,4 @@
   public NonEmptyValueState asNonEmpty() {
     return this;
   }
-
-  public final NonEmptyValueState mutableJoin(
-      AppView<AppInfoWithLiveness> appView,
-      Function<ValueState, NonEmptyValueState> stateSupplier,
-      DexType staticType,
-      StateCloner cloner) {
-    return mutableJoin(appView, stateSupplier, staticType, cloner, Action.empty());
-  }
-
-  public abstract NonEmptyValueState mutableJoin(
-      AppView<AppInfoWithLiveness> appView,
-      Function<ValueState, NonEmptyValueState> stateSupplier,
-      DexType staticType,
-      StateCloner cloner,
-      Action onChangedAction);
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
new file mode 100644
index 0000000..ca1223f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IterableUtils;
+import java.util.Objects;
+
+/**
+ * Encodes the `x | const` abstract function. This is currently used as part of the modeling of
+ * updateChangedFlags, since the updateChangedFlags function is invoked with `changedFlags | 1` as
+ * an argument.
+ */
+public class OrAbstractFunction implements AbstractFunction {
+
+  public final BaseInFlow inFlow;
+  public final SingleNumberValue constant;
+
+  public OrAbstractFunction(BaseInFlow inFlow, SingleNumberValue constant) {
+    this.inFlow = inFlow;
+    this.constant = constant;
+  }
+
+  @Override
+  public ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState inState) {
+    ConcretePrimitiveTypeValueState inPrimitiveState = inState.asPrimitiveState();
+    AbstractValue result =
+        AbstractCalculator.orIntegers(appView, inPrimitiveState.getAbstractValue(), constant);
+    return ConcretePrimitiveTypeValueState.create(result, inPrimitiveState.copyInFlow());
+  }
+
+  @Override
+  public boolean containsBaseInFlow(BaseInFlow otherInFlow) {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().containsBaseInFlow(otherInFlow);
+    }
+    assert inFlow.isBaseInFlow();
+    return inFlow.equals(otherInFlow);
+  }
+
+  @Override
+  public Iterable<BaseInFlow> getBaseInFlow() {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().getBaseInFlow();
+    }
+    return IterableUtils.singleton(inFlow);
+  }
+
+  @Override
+  @SuppressWarnings("EqualsGetClass")
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    OrAbstractFunction fn = (OrAbstractFunction) obj;
+    return inFlow.equals(fn.inFlow) && constant.equals(fn.constant);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), inFlow, constant);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
index 07a87b5..f9ffd98 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
 public class UnknownAbstractFunction implements AbstractFunction {
 
   private static final UnknownAbstractFunction INSTANCE = new UnknownAbstractFunction();
@@ -12,4 +16,27 @@
   static UnknownAbstractFunction get() {
     return INSTANCE;
   }
+
+  @Override
+  public ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState inState) {
+    return ValueState.unknown();
+  }
+
+  @Override
+  public boolean containsBaseInFlow(BaseInFlow inFlow) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public Iterable<BaseInFlow> getBaseInFlow() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public boolean isUnknownAbstractFunction() {
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
index f9e7a1d..6e3601b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
@@ -7,9 +7,9 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.UnknownValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
-import java.util.function.Function;
 
 public class UnknownValueState extends NonEmptyValueState {
 
@@ -22,7 +22,7 @@
   }
 
   @Override
-  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
+  public UnknownValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     return AbstractValue.unknown();
   }
 
@@ -32,24 +32,14 @@
   }
 
   @Override
-  public ValueState mutableCopy() {
+  public UnknownValueState mutableCopy() {
     return this;
   }
 
   @Override
-  public ValueState mutableJoin(
+  public UnknownValueState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
-      ValueState parameterState,
-      DexType parameterType,
-      StateCloner cloner,
-      Action onChangedAction) {
-    return this;
-  }
-
-  @Override
-  public NonEmptyValueState mutableJoin(
-      AppView<AppInfoWithLiveness> appView,
-      Function<ValueState, NonEmptyValueState> stateSupplier,
+      ValueState state,
       DexType staticType,
       StateCloner cloner,
       Action onChangedAction) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
index 4cb4c2b..96adb2c 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -14,30 +15,34 @@
 public abstract class ValueState {
 
   public static BottomValueState bottom(ProgramField field) {
+    return bottom(field.getReference());
+  }
+
+  public static BottomValueState bottom(DexField field) {
     DexType fieldType = field.getType();
     if (fieldType.isArrayType()) {
-      return bottomArrayTypeParameter();
+      return bottomArrayTypeState();
     } else if (fieldType.isClassType()) {
-      return bottomClassTypeParameter();
+      return bottomClassTypeState();
     } else {
       assert fieldType.isPrimitiveType();
-      return bottomPrimitiveTypeParameter();
+      return bottomPrimitiveTypeState();
     }
   }
 
-  public static BottomValueState bottomArrayTypeParameter() {
+  public static BottomArrayTypeValueState bottomArrayTypeState() {
     return BottomArrayTypeValueState.get();
   }
 
-  public static BottomValueState bottomClassTypeParameter() {
+  public static BottomClassTypeValueState bottomClassTypeState() {
     return BottomClassTypeValueState.get();
   }
 
-  public static BottomValueState bottomPrimitiveTypeParameter() {
+  public static BottomPrimitiveTypeValueState bottomPrimitiveTypeState() {
     return BottomPrimitiveTypeValueState.get();
   }
 
-  public static BottomValueState bottomReceiverParameter() {
+  public static BottomReceiverValueState bottomReceiverParameter() {
     return BottomReceiverValueState.get();
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
new file mode 100644
index 0000000..294eb8e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -0,0 +1,292 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.MapUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.ProgramFieldSet;
+import com.google.common.collect.Lists;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+public class DefaultFieldValueJoiner {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final FieldStateCollection fieldStates;
+  private final List<FlowGraph> flowGraphs;
+
+  public DefaultFieldValueJoiner(
+      AppView<AppInfoWithLiveness> appView,
+      FieldStateCollection fieldStates,
+      List<FlowGraph> flowGraphs) {
+    this.appView = appView;
+    this.fieldStates = fieldStates;
+    this.flowGraphs = flowGraphs;
+  }
+
+  public Map<FlowGraph, Deque<FlowGraphNode>> joinDefaultFieldValuesForFieldsWithReadBeforeWrite(
+      ExecutorService executorService) throws ExecutionException {
+    // Find all the fields where we need to determine if each field read is guaranteed to be
+    // dominated by a write.
+    Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = getFieldsOfInterest();
+
+    // If constructor inlining is disabled, then we focus on whether each instance initializer
+    // definitely assigns the given field before it is read. We do the same for final and static
+    // fields.
+    Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields =
+        removeFieldsNotSubjectToInitializerAnalysis(fieldsOfInterest);
+    ProgramFieldSet fieldsWithLiveDefaultValue = ProgramFieldSet.createConcurrent();
+    analyzeInitializers(fieldsOfInterest, fieldsWithLiveDefaultValue::add, executorService);
+
+    // For non-final fields where writes in instance initializers may have been subject to
+    // constructor inlining, we find all new-instance instructions (including subtype allocations)
+    // and check if the field is written on each allocation before it is possibly read.
+    analyzeNewInstanceInstructions(nonFinalInstanceFields, fieldsWithLiveDefaultValue::add);
+
+    return updateFlowGraphs(fieldsWithLiveDefaultValue, executorService);
+  }
+
+  private Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
+    Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = new IdentityHashMap<>();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      clazz.forEachProgramField(
+          field -> {
+            // We only need to include the fields where including the default value would make a
+            // difference. Then we can assert below in updateFlowGraphs() that adding the default
+            // value changes the field state.
+            // TODO(b/296030319): Implement this for primitive fields.
+            ValueState state = fieldStates.get(field);
+            if (state.isUnknown()) {
+              return;
+            }
+            if (state.isReferenceState()
+                && state.asReferenceState().getNullability().isNullable()) {
+              return;
+            }
+            fieldsOfInterest
+                .computeIfAbsent(field.getHolder(), ignoreKey(ArrayList::new))
+                .add(field);
+          });
+    }
+    return fieldsOfInterest;
+  }
+
+  private Map<DexProgramClass, List<ProgramField>> removeFieldsNotSubjectToInitializerAnalysis(
+      Map<DexProgramClass, List<ProgramField>> fieldsOfInterest) {
+    // When constructor inlining is disabled, we only analyze the initializers of each field holder.
+    if (!appView.options().canInitNewInstanceUsingSuperclassConstructor()) {
+      return Collections.emptyMap();
+    }
+
+    // When constructor inlining is enabled, we can still limit the analysis to the instance
+    // initializers for final fields. We can do the same for static fields as <clinit> is not
+    // subject to inlining.
+    Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields = new IdentityHashMap<>();
+    MapUtils.removeIf(
+        fieldsOfInterest,
+        (holder, fields) -> {
+          fields.removeIf(
+              field -> {
+                if (!field.getAccessFlags().isFinal() && !field.getAccessFlags().isStatic()) {
+                  nonFinalInstanceFields
+                      .computeIfAbsent(holder, ignoreKey(ArrayList::new))
+                      .add(field);
+                }
+                return false;
+              });
+          return fields.isEmpty();
+        });
+    return nonFinalInstanceFields;
+  }
+
+  private void analyzeInitializers(
+      Map<DexProgramClass, List<ProgramField>> fieldsOfInterest,
+      Consumer<ProgramField> concurrentLiveDefaultValueConsumer,
+      ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processMap(
+        fieldsOfInterest,
+        (clazz, fields) -> {
+          ProgramFieldSet instanceFieldsWithLiveDefaultValue = ProgramFieldSet.create();
+          ProgramFieldSet staticFieldsWithLiveDefaultValue = ProgramFieldSet.create();
+          partitionFields(
+              fields, instanceFieldsWithLiveDefaultValue, staticFieldsWithLiveDefaultValue);
+          analyzeClassInitializerAssignments(
+              clazz, staticFieldsWithLiveDefaultValue, concurrentLiveDefaultValueConsumer);
+          analyzeInstanceInitializerAssignments(
+              clazz, instanceFieldsWithLiveDefaultValue, concurrentLiveDefaultValueConsumer);
+        },
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+
+  private void partitionFields(
+      Collection<ProgramField> fields,
+      ProgramFieldSet instanceFieldsWithLiveDefaultValue,
+      ProgramFieldSet staticFieldsWithLiveDefaultValue) {
+    for (ProgramField field : fields) {
+      if (field.getAccessFlags().isStatic()) {
+        staticFieldsWithLiveDefaultValue.add(field);
+      } else {
+        instanceFieldsWithLiveDefaultValue.add(field);
+      }
+    }
+  }
+
+  private void analyzeClassInitializerAssignments(
+      DexProgramClass clazz,
+      ProgramFieldSet staticFieldsWithLiveDefaultValue,
+      Consumer<ProgramField> liveDefaultValueConsumer) {
+    if (staticFieldsWithLiveDefaultValue.isEmpty()) {
+      return;
+    }
+    if (clazz.hasClassInitializer()) {
+      IRCode code =
+          clazz
+              .getProgramClassInitializer()
+              .buildIR(appView, MethodConversionOptions.nonConverting());
+      FieldReadBeforeWriteAnalysis analysis = new FieldReadBeforeWriteAnalysis(appView, code);
+      staticFieldsWithLiveDefaultValue.removeIf(analysis::isStaticFieldNeverReadBeforeWrite);
+    }
+    staticFieldsWithLiveDefaultValue.forEach(liveDefaultValueConsumer);
+  }
+
+  private void analyzeInstanceInitializerAssignments(
+      DexProgramClass clazz,
+      ProgramFieldSet instanceFieldsWithLiveDefaultValue,
+      Consumer<ProgramField> liveDefaultValueConsumer) {
+    if (instanceFieldsWithLiveDefaultValue.isEmpty()) {
+      return;
+    }
+    List<ProgramMethod> instanceInitializers =
+        Lists.newArrayList(clazz.programInstanceInitializers());
+    // TODO(b/296030319): Handle multiple instance initializers.
+    if (instanceInitializers.size() == 1) {
+      ProgramMethod instanceInitializer = ListUtils.first(instanceInitializers);
+      IRCode code = instanceInitializer.buildIR(appView, MethodConversionOptions.nonConverting());
+      FieldReadBeforeWriteAnalysis analysis = new FieldReadBeforeWriteAnalysis(appView, code);
+      instanceFieldsWithLiveDefaultValue.removeIf(analysis::isInstanceFieldNeverReadBeforeWrite);
+    }
+    instanceFieldsWithLiveDefaultValue.forEach(liveDefaultValueConsumer);
+  }
+
+  private void analyzeNewInstanceInstructions(
+      Map<DexProgramClass, List<ProgramField>> nonFinalInstanceFields,
+      Consumer<ProgramField> liveDefaultValueConsumer) {
+    // Conservatively treat all fields as maybe read before written.
+    // TODO(b/296030319): Implement analysis by building IR for all methods that instantiate the
+    //  relevant classes and analyzing the puts to the newly created instances.
+    for (ProgramField field : IterableUtils.flatten(nonFinalInstanceFields.values())) {
+      liveDefaultValueConsumer.accept(field);
+    }
+  }
+
+  private Map<FlowGraph, Deque<FlowGraphNode>> updateFlowGraphs(
+      ProgramFieldSet fieldsWithLiveDefaultValue, ExecutorService executorService)
+      throws ExecutionException {
+    Collection<Pair<FlowGraph, Deque<FlowGraphNode>>> worklists =
+        ThreadUtils.processItemsWithResultsThatMatches(
+            flowGraphs,
+            flowGraph -> {
+              Deque<FlowGraphNode> worklist = new ArrayDeque<>();
+              flowGraph.forEachFieldNode(
+                  node -> {
+                    ProgramField field = node.getField();
+                    if (fieldsWithLiveDefaultValue.remove(field)) {
+                      node.addState(
+                          appView,
+                          getDefaultValueState(field),
+                          () -> {
+                            if (node.isUnknown()) {
+                              node.clearPredecessors();
+                            }
+                            node.addToWorkList(worklist);
+                          });
+                    }
+                  });
+              return new Pair<>(flowGraph, worklist);
+            },
+            pair -> !pair.getSecond().isEmpty(),
+            appView.options().getThreadingModule(),
+            executorService);
+    // Unseen fields are not added to any flow graphs, since they are not needed for flow
+    // propagation. Update these fields directly in the field state collection.
+    for (ProgramField field : fieldsWithLiveDefaultValue) {
+      fieldStates.addTemporaryFieldState(
+          appView, field, () -> getDefaultValueState(field), Timing.empty());
+    }
+    return MapUtils.newIdentityHashMap(
+        builder -> worklists.forEach(pair -> builder.put(pair.getFirst(), pair.getSecond())));
+  }
+
+  private ConcreteValueState getDefaultValueState(ProgramField field) {
+    AbstractValueFactory abstractValueFactory = appView.abstractValueFactory();
+    AbstractValue defaultValue;
+    if (field.getAccessFlags().isStatic() && field.getDefinition().hasExplicitStaticValue()) {
+      defaultValue = field.getDefinition().getStaticValue().toAbstractValue(abstractValueFactory);
+    } else if (field.getType().isPrimitiveType()) {
+      defaultValue = abstractValueFactory.createZeroValue();
+    } else {
+      defaultValue = abstractValueFactory.createUncheckedNullValue();
+    }
+    NonEmptyValueState fieldStateToAdd;
+    if (field.getType().isArrayType()) {
+      Nullability defaultNullability = Nullability.definitelyNull();
+      fieldStateToAdd = ConcreteArrayTypeValueState.create(defaultNullability);
+    } else if (field.getType().isClassType()) {
+      assert defaultValue.isNull()
+          || defaultValue.isSingleStringValue()
+          || defaultValue.isSingleDexItemBasedStringValue();
+      DynamicType dynamicType =
+          defaultValue.isNull()
+              ? DynamicType.definitelyNull()
+              : DynamicType.createExact(
+                  TypeElement.stringClassType(appView, Nullability.definitelyNotNull()));
+      fieldStateToAdd = ConcreteClassTypeValueState.create(defaultValue, dynamicType);
+    } else {
+      assert field.getType().isPrimitiveType();
+      fieldStateToAdd = ConcretePrimitiveTypeValueState.create(defaultValue);
+    }
+    // We should always be able to map static field values to an unknown abstract value.
+    if (fieldStateToAdd.isUnknown()) {
+      throw new Unreachable();
+    }
+    return fieldStateToAdd.asConcrete();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java
new file mode 100644
index 0000000..f11d945
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteAnalysis.java
@@ -0,0 +1,237 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.ConcreteMutableFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.KnownFieldSet;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.DequeUtils;
+import com.android.tools.r8.utils.LazyBox;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+class FieldReadBeforeWriteAnalysis {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final IRCode code;
+  private final ProgramMethod context;
+  private final LazyBox<DominatorTree> lazyDominatorTree;
+  private final List<BasicBlock> returnBlocks;
+
+  private Map<BasicBlock, AbstractFieldSet> fieldsMaybeReadBeforeBlockInclusiveCache;
+
+  public FieldReadBeforeWriteAnalysis(AppView<AppInfoWithLiveness> appView, IRCode code) {
+    this.appView = appView;
+    this.code = code;
+    this.context = code.context();
+    this.lazyDominatorTree = new LazyBox<>(() -> new DominatorTree(code));
+    this.returnBlocks = code.computeNormalExitBlocks();
+  }
+
+  public boolean isInstanceFieldNeverReadBeforeWrite(ProgramField field) {
+    assert field.getHolder() == context.getHolder();
+    InstancePut instancePut = null;
+    for (InstancePut candidate :
+        code.getThis().<InstancePut>uniqueUsers(Instruction::isInstancePut)) {
+      if (candidate.getField().isIdenticalTo(field.getReference())) {
+        if (instancePut != null) {
+          // In the somewhat unusual case (?) that the same constructor assigns the same field
+          // multiple times, we simply bail out and conservatively report that the field is maybe
+          // read before it is written.
+          return false;
+        }
+        instancePut = candidate;
+      }
+    }
+    // TODO(b/296030319): Improve precision using escape analysis for receiver.
+    return instancePut != null
+        && !isFieldMaybeReadBeforeInstructionInInitializer(field, instancePut)
+        && lazyDominatorTree.computeIfAbsent().dominatesAllOf(instancePut.getBlock(), returnBlocks);
+  }
+
+  public boolean isStaticFieldNeverReadBeforeWrite(ProgramField field) {
+    assert field.getHolder() == context.getHolder();
+    StaticPut staticPut = null;
+    for (StaticPut candidate : code.<StaticPut>instructions(Instruction::isStaticPut)) {
+      if (candidate.getField().isIdenticalTo(field.getReference())) {
+        if (staticPut != null) {
+          // In the somewhat unusual case (?) that the same constructor assigns the same field
+          // multiple times, we simply bail out and conservatively report that the field is maybe
+          // read before it is written.
+          return false;
+        }
+        staticPut = candidate;
+      }
+    }
+    return staticPut != null
+        && !isFieldMaybeReadBeforeInstructionInInitializer(field, staticPut)
+        && lazyDominatorTree.computeIfAbsent().dominatesAllOf(staticPut.getBlock(), returnBlocks);
+  }
+
+  private boolean isFieldMaybeReadBeforeInstructionInInitializer(
+      DexClassAndField field, Instruction instruction) {
+    BasicBlock block = instruction.getBlock();
+
+    // First check if the field may be read in any of the (transitive) predecessor blocks.
+    if (fieldMaybeReadBeforeBlock(field, block)) {
+      return true;
+    }
+
+    // Then check if any of the instructions that precede the given instruction in the current block
+    // may read the field.
+    InstructionIterator instructionIterator = block.iterator();
+    while (instructionIterator.hasNext()) {
+      Instruction current = instructionIterator.next();
+      if (current == instruction) {
+        break;
+      }
+      if (current.readSet(appView, context).contains(field)) {
+        return true;
+      }
+    }
+
+    // Otherwise, the field is not read prior to the given instruction.
+    return false;
+  }
+
+  private boolean fieldMaybeReadBeforeBlock(DexClassAndField field, BasicBlock block) {
+    for (BasicBlock predecessor : block.getPredecessors()) {
+      if (fieldMaybeReadBeforeBlockInclusive(field, predecessor)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean fieldMaybeReadBeforeBlockInclusive(DexClassAndField field, BasicBlock block) {
+    return getOrCreateFieldsMaybeReadBeforeBlockInclusive().get(block).contains(field);
+  }
+
+  private Map<BasicBlock, AbstractFieldSet> getOrCreateFieldsMaybeReadBeforeBlockInclusive() {
+    if (fieldsMaybeReadBeforeBlockInclusiveCache == null) {
+      fieldsMaybeReadBeforeBlockInclusiveCache = createFieldsMaybeReadBeforeBlockInclusive();
+    }
+    return fieldsMaybeReadBeforeBlockInclusiveCache;
+  }
+
+  /**
+   * Eagerly creates a mapping from each block to the set of fields that may be read in that block
+   * and its transitive predecessors.
+   */
+  private Map<BasicBlock, AbstractFieldSet> createFieldsMaybeReadBeforeBlockInclusive() {
+    Map<BasicBlock, AbstractFieldSet> result = new IdentityHashMap<>();
+    Deque<BasicBlock> worklist = DequeUtils.newArrayDeque(code.entryBlock());
+    while (!worklist.isEmpty()) {
+      BasicBlock block = worklist.removeFirst();
+      boolean seenBefore = result.containsKey(block);
+      AbstractFieldSet readSet =
+          result.computeIfAbsent(block, ignore -> EmptyFieldSet.getInstance());
+      if (readSet.isTop()) {
+        // We already have unknown information for this block.
+        continue;
+      }
+
+      assert readSet.isKnownFieldSet();
+      KnownFieldSet knownReadSet = readSet.asKnownFieldSet();
+      int oldSize = seenBefore ? knownReadSet.size() : -1;
+
+      // Everything that is read in the predecessor blocks should also be included in the read set
+      // for the current block, so here we join the information from the predecessor blocks into the
+      // current read set.
+      boolean blockOrPredecessorMaybeReadAnyField = false;
+      for (BasicBlock predecessor : block.getPredecessors()) {
+        AbstractFieldSet predecessorReadSet =
+            result.getOrDefault(predecessor, EmptyFieldSet.getInstance());
+        if (predecessorReadSet.isBottom()) {
+          continue;
+        }
+        if (predecessorReadSet.isTop()) {
+          blockOrPredecessorMaybeReadAnyField = true;
+          break;
+        }
+        assert predecessorReadSet.isConcreteFieldSet();
+        if (!knownReadSet.isConcreteFieldSet()) {
+          knownReadSet = new ConcreteMutableFieldSet();
+        }
+        knownReadSet.asConcreteFieldSet().addAll(predecessorReadSet.asConcreteFieldSet());
+      }
+
+      if (!blockOrPredecessorMaybeReadAnyField) {
+        // Finally, we update the read set with the fields that are read by the instructions in the
+        // current block. This can be skipped if the block has already been processed.
+        if (seenBefore) {
+          assert verifyFieldSetContainsAllFieldReadsInBlock(knownReadSet, block, context);
+        } else {
+          for (Instruction instruction : block.getInstructions()) {
+            AbstractFieldSet instructionReadSet = instruction.readSet(appView, context);
+            if (instructionReadSet.isBottom()) {
+              continue;
+            }
+            if (instructionReadSet.isTop()) {
+              blockOrPredecessorMaybeReadAnyField = true;
+              break;
+            }
+            if (!knownReadSet.isConcreteFieldSet()) {
+              knownReadSet = new ConcreteMutableFieldSet();
+            }
+            knownReadSet.asConcreteFieldSet().addAll(instructionReadSet.asConcreteFieldSet());
+          }
+        }
+      }
+
+      boolean changed = false;
+      if (blockOrPredecessorMaybeReadAnyField) {
+        // Record that this block reads all fields.
+        result.put(block, UnknownFieldSet.getInstance());
+        changed = true;
+      } else {
+        if (knownReadSet != readSet) {
+          result.put(block, knownReadSet.asConcreteFieldSet());
+        }
+        if (knownReadSet.size() != oldSize) {
+          assert knownReadSet.size() > oldSize;
+          changed = true;
+        }
+      }
+
+      if (changed) {
+        // Rerun the analysis for all successors because the state of the current block changed.
+        worklist.addAll(block.getSuccessors());
+      }
+    }
+    return result;
+  }
+
+  private boolean verifyFieldSetContainsAllFieldReadsInBlock(
+      KnownFieldSet readSet, BasicBlock block, ProgramMethod context) {
+    for (Instruction instruction : block.getInstructions()) {
+      AbstractFieldSet instructionReadSet = instruction.readSet(appView, context);
+      assert !instructionReadSet.isTop();
+      if (instructionReadSet.isBottom()) {
+        continue;
+      }
+      for (DexEncodedField field : instructionReadSet.asConcreteFieldSet().getFields()) {
+        assert readSet.contains(field);
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java
new file mode 100644
index 0000000..01a65d2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.optimize.argumentpropagation.utils.BidirectedGraph;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class FlowGraph extends BidirectedGraph<FlowGraphNode> implements FlowGraphStateProvider {
+
+  private final Map<DexField, FlowGraphFieldNode> fieldNodes;
+  private final Map<DexMethod, Int2ReferenceMap<FlowGraphParameterNode>> parameterNodes;
+
+  public FlowGraph(
+      Map<DexField, FlowGraphFieldNode> fieldNodes,
+      Map<DexMethod, Int2ReferenceMap<FlowGraphParameterNode>> parameterNodes) {
+    this.fieldNodes = fieldNodes;
+    this.parameterNodes = parameterNodes;
+  }
+
+  public FlowGraph(Collection<FlowGraphNode> nodes) {
+    this(new IdentityHashMap<>(), new IdentityHashMap<>());
+    for (FlowGraphNode node : nodes) {
+      if (node.isFieldNode()) {
+        FlowGraphFieldNode fieldNode = node.asFieldNode();
+        fieldNodes.put(fieldNode.getField().getReference(), fieldNode);
+      } else {
+        FlowGraphParameterNode parameterNode = node.asParameterNode();
+        parameterNodes
+            .computeIfAbsent(
+                parameterNode.getMethod().getReference(), ignoreKey(Int2ReferenceOpenHashMap::new))
+            .put(parameterNode.getParameterIndex(), parameterNode);
+      }
+    }
+  }
+
+  public static FlowGraphBuilder builder(
+      AppView<AppInfoWithLiveness> appView,
+      IRConverter converter,
+      FieldStateCollection fieldStates,
+      MethodStateCollectionByReference methodStates) {
+    return new FlowGraphBuilder(appView, converter, fieldStates, methodStates);
+  }
+
+  public void forEachFieldNode(Consumer<? super FlowGraphFieldNode> consumer) {
+    fieldNodes.values().forEach(consumer);
+  }
+
+  @Override
+  public void forEachNeighbor(FlowGraphNode node, Consumer<? super FlowGraphNode> consumer) {
+    node.getPredecessors().forEach(consumer);
+    node.getSuccessors().forEach(consumer);
+  }
+
+  @Override
+  public void forEachNode(Consumer<? super FlowGraphNode> consumer) {
+    forEachFieldNode(consumer);
+    parameterNodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
+  }
+
+  @Override
+  public ValueState getState(DexField field) {
+    return fieldNodes.get(field).getState();
+  }
+
+  @Override
+  public ValueState getState(BaseInFlow inFlow, Supplier<ValueState> defaultStateProvider) {
+    if (inFlow.isFieldValue()) {
+      FieldValue fieldValue = inFlow.asFieldValue();
+      return getState(fieldValue.getField());
+    } else {
+      assert inFlow.isMethodParameter();
+      MethodParameter methodParameter = inFlow.asMethodParameter();
+      FlowGraphParameterNode parameterNode =
+          parameterNodes
+              .getOrDefault(methodParameter.getMethod(), Int2ReferenceMaps.emptyMap())
+              .get(methodParameter.getIndex());
+      if (parameterNode != null) {
+        return parameterNode.getState();
+      }
+      return defaultStateProvider.get();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java
new file mode 100644
index 0000000..00d36e6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java
@@ -0,0 +1,300 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.graph.ProgramField.asProgramFieldOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.TraversalContinuation;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FlowGraphBuilder {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final IRConverter converter;
+  private final FieldStateCollection fieldStates;
+  private final MethodStateCollectionByReference methodStates;
+
+  private final Map<DexField, FlowGraphFieldNode> fieldNodes = new IdentityHashMap<>();
+  private final Map<DexMethod, Int2ReferenceMap<FlowGraphParameterNode>> parameterNodes =
+      new IdentityHashMap<>();
+
+  public FlowGraphBuilder(
+      AppView<AppInfoWithLiveness> appView,
+      IRConverter converter,
+      FieldStateCollection fieldStates,
+      MethodStateCollectionByReference methodStates) {
+    this.appView = appView;
+    this.converter = converter;
+    this.fieldStates = fieldStates;
+    this.methodStates = methodStates;
+  }
+
+  public FlowGraphBuilder addClasses(Collection<DexProgramClass> classes) {
+    for (DexProgramClass clazz : classes) {
+      add(clazz);
+    }
+    return this;
+  }
+
+  public FlowGraph build() {
+    return new FlowGraph(fieldNodes, parameterNodes);
+  }
+
+  private void add(DexProgramClass clazz) {
+    clazz.forEachProgramField(this::addField);
+    clazz.forEachProgramMethod(this::addMethodParameters);
+  }
+
+  private void addField(ProgramField field) {
+    ValueState fieldState = fieldStates.get(field);
+
+    // No need to create nodes for fields with no in-flow or no useful information.
+    if (fieldState.isBottom() || fieldState.isUnknown()) {
+      return;
+    }
+
+    ConcreteValueState concreteFieldState = fieldState.asConcrete();
+
+    // No need to create a node for a field that doesn't depend on any other nodes, unless some
+    // other node depends on this field, in which case that other node will lead to creation of a
+    // node for the current field.
+    if (!concreteFieldState.hasInFlow()) {
+      return;
+    }
+
+    FlowGraphFieldNode node = getOrCreateFieldNode(field, concreteFieldState);
+    for (InFlow inFlow : concreteFieldState.getInFlow()) {
+      if (addInFlow(inFlow, node).shouldBreak()) {
+        assert node.isUnknown();
+        break;
+      }
+    }
+
+    ValueState concreteFieldStateOrBottom = concreteFieldState.clearInFlow();
+    if (concreteFieldStateOrBottom.isBottom()) {
+      fieldStates.remove(field);
+      if (!node.getState().isUnknown()) {
+        assert node.getState() == concreteFieldState;
+        node.setState(concreteFieldStateOrBottom);
+      }
+    }
+  }
+
+  private void addMethodParameters(ProgramMethod method) {
+    MethodState methodState = methodStates.get(method);
+
+    // No need to create nodes for parameters with no in-flow or no useful information.
+    if (methodState.isBottom() || methodState.isUnknown()) {
+      return;
+    }
+
+    // Add nodes for the parameters for which we have non-trivial information.
+    ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+    List<ValueState> parameterStates = monomorphicMethodState.getParameterStates();
+    for (int parameterIndex = 0; parameterIndex < parameterStates.size(); parameterIndex++) {
+      ValueState parameterState = parameterStates.get(parameterIndex);
+      addMethodParameter(method, parameterIndex, monomorphicMethodState, parameterState);
+    }
+  }
+
+  private void addMethodParameter(
+      ProgramMethod method,
+      int parameterIndex,
+      ConcreteMonomorphicMethodState methodState,
+      ValueState parameterState) {
+    // No need to create nodes for parameters with no in-parameters and parameters we don't know
+    // anything about.
+    if (parameterState.isBottom() || parameterState.isUnknown()) {
+      return;
+    }
+
+    ConcreteValueState concreteParameterState = parameterState.asConcrete();
+
+    // No need to create a node for a parameter that doesn't depend on any other parameters,
+    // unless some other node depends on this parameter, in which case that other node will lead
+    // to the creation of a node for the current parameter.
+    if (!concreteParameterState.hasInFlow()) {
+      return;
+    }
+
+    FlowGraphParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
+    for (InFlow inFlow : concreteParameterState.getInFlow()) {
+      if (addInFlow(inFlow, node).shouldBreak()) {
+        assert node.isUnknown();
+        break;
+      }
+    }
+
+    if (!node.getState().isUnknown()) {
+      assert node.getState() == concreteParameterState;
+      node.setState(concreteParameterState.clearInFlow());
+    }
+  }
+
+  // Returns BREAK if the current node has been set to unknown.
+  private TraversalContinuation<?, ?> addInFlow(InFlow inFlow, FlowGraphNode node) {
+    if (inFlow.isAbstractFunction()) {
+      return addInFlow(inFlow.asAbstractFunction(), node);
+    } else if (inFlow.isFieldValue()) {
+      return addInFlow(inFlow.asFieldValue(), node);
+    } else if (inFlow.isMethodParameter()) {
+      return addInFlow(inFlow.asMethodParameter(), node);
+    } else {
+      throw new Unreachable(inFlow.getClass().getTypeName());
+    }
+  }
+
+  private TraversalContinuation<?, ?> addInFlow(AbstractFunction inFlow, FlowGraphNode node) {
+    for (BaseInFlow baseInFlow : inFlow.getBaseInFlow()) {
+      TraversalContinuation<?, ?> traversalContinuation;
+      if (baseInFlow.isFieldValue()) {
+        traversalContinuation = addInFlow(baseInFlow.asFieldValue(), node, inFlow);
+      } else {
+        assert baseInFlow.isMethodParameter();
+        traversalContinuation = addInFlow(baseInFlow.asMethodParameter(), node, inFlow);
+      }
+      if (traversalContinuation.shouldBreak()) {
+        return traversalContinuation;
+      }
+    }
+    return TraversalContinuation.doContinue();
+  }
+
+  private TraversalContinuation<?, ?> addInFlow(FieldValue inFlow, FlowGraphNode node) {
+    return addInFlow(inFlow, node, AbstractFunction.identity());
+  }
+
+  private TraversalContinuation<?, ?> addInFlow(
+      FieldValue inFlow, FlowGraphNode node, AbstractFunction transferFunction) {
+    assert !node.isUnknown();
+
+    ProgramField field = asProgramFieldOrNull(appView.definitionFor(inFlow.getField()));
+    if (field == null) {
+      assert false;
+      return TraversalContinuation.doContinue();
+    }
+
+    ValueState fieldState = getFieldState(field, fieldStates);
+    if (fieldState.isUnknown()) {
+      // The current node depends on a field for which we don't know anything.
+      node.clearPredecessors();
+      node.setStateToUnknown();
+      return TraversalContinuation.doBreak();
+    }
+
+    FlowGraphFieldNode fieldNode = getOrCreateFieldNode(field, fieldState);
+    node.addPredecessor(fieldNode, transferFunction);
+    return TraversalContinuation.doContinue();
+  }
+
+  private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, FlowGraphNode node) {
+    return addInFlow(inFlow, node, AbstractFunction.identity());
+  }
+
+  private TraversalContinuation<?, ?> addInFlow(
+      MethodParameter inFlow, FlowGraphNode node, AbstractFunction transferFunction) {
+    ProgramMethod enclosingMethod = getEnclosingMethod(inFlow);
+    if (enclosingMethod == null) {
+      // This is a parameter of a single caller inlined method. Since this method has been
+      // pruned, the call from inside the method no longer exists, and we can therefore safely
+      // skip it.
+      assert converter.getInliner().verifyIsPrunedDueToSingleCallerInlining(inFlow.getMethod());
+      return TraversalContinuation.doContinue();
+    }
+
+    MethodState enclosingMethodState = getMethodState(enclosingMethod, methodStates);
+    if (enclosingMethodState.isBottom()) {
+      // The current node takes a value from a dead method; no need to propagate any information
+      // from the dead assignment.
+      return TraversalContinuation.doContinue();
+    }
+
+    if (enclosingMethodState.isUnknown()) {
+      // The current node depends on a parameter for which we don't know anything.
+      node.clearPredecessors();
+      node.setStateToUnknown();
+      return TraversalContinuation.doBreak();
+    }
+
+    assert enclosingMethodState.isConcrete();
+    assert enclosingMethodState.asConcrete().isMonomorphic();
+
+    FlowGraphParameterNode predecessor =
+        getOrCreateParameterNode(
+            enclosingMethod, inFlow.getIndex(), enclosingMethodState.asConcrete().asMonomorphic());
+    node.addPredecessor(predecessor, transferFunction);
+    return TraversalContinuation.doContinue();
+  }
+
+  private FlowGraphFieldNode getOrCreateFieldNode(ProgramField field, ValueState fieldState) {
+    return fieldNodes.computeIfAbsent(
+        field.getReference(), ignoreKey(() -> new FlowGraphFieldNode(field, fieldState)));
+  }
+
+  private FlowGraphParameterNode getOrCreateParameterNode(
+      ProgramMethod method, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
+    Int2ReferenceMap<FlowGraphParameterNode> parameterNodesForMethod =
+        parameterNodes.computeIfAbsent(
+            method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
+    return parameterNodesForMethod.compute(
+        parameterIndex,
+        (ignore, parameterNode) ->
+            parameterNode != null
+                ? parameterNode
+                : new FlowGraphParameterNode(
+                    method, methodState, parameterIndex, method.getArgumentType(parameterIndex)));
+  }
+
+  private ProgramMethod getEnclosingMethod(MethodParameter methodParameter) {
+    DexMethod methodReference = methodParameter.getMethod();
+    return methodReference.lookupOnProgramClass(
+        asProgramClassOrNull(appView.definitionFor(methodParameter.getMethod().getHolderType())));
+  }
+
+  private ValueState getFieldState(ProgramField field, FieldStateCollection fieldStates) {
+    if (field == null) {
+      // Conservatively return unknown if for some reason we can't find the field.
+      assert false;
+      return ValueState.unknown();
+    }
+    return fieldStates.get(field);
+  }
+
+  private MethodState getMethodState(
+      ProgramMethod method, MethodStateCollectionByReference methodStates) {
+    if (method == null) {
+      // Conservatively return unknown if for some reason we can't find the method.
+      assert false;
+      return MethodState.unknown();
+    }
+    return methodStates.get(method);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphFieldNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphFieldNode.java
new file mode 100644
index 0000000..c25ed35
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphFieldNode.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+
+public class FlowGraphFieldNode extends FlowGraphNode {
+
+  private final ProgramField field;
+  private ValueState fieldState;
+
+  FlowGraphFieldNode(ProgramField field, ValueState fieldState) {
+    this.field = field;
+    this.fieldState = fieldState;
+  }
+
+  public ProgramField getField() {
+    return field;
+  }
+
+  @Override
+  DexType getStaticType() {
+    return field.getType();
+  }
+
+  void addDefaultValue(AppView<AppInfoWithLiveness> appView, Action onChangedAction) {
+    AbstractValueFactory abstractValueFactory = appView.abstractValueFactory();
+    AbstractValue defaultValue;
+    if (field.getAccessFlags().isStatic() && field.getDefinition().hasExplicitStaticValue()) {
+      defaultValue = field.getDefinition().getStaticValue().toAbstractValue(abstractValueFactory);
+    } else if (field.getType().isPrimitiveType()) {
+      defaultValue = abstractValueFactory.createZeroValue();
+    } else {
+      defaultValue = abstractValueFactory.createUncheckedNullValue();
+    }
+    NonEmptyValueState fieldStateToAdd;
+    if (field.getType().isArrayType()) {
+      Nullability defaultNullability = Nullability.definitelyNull();
+      fieldStateToAdd = ConcreteArrayTypeValueState.create(defaultNullability);
+    } else if (field.getType().isClassType()) {
+      assert defaultValue.isNull() || defaultValue.isSingleStringValue();
+      DynamicType dynamicType =
+          defaultValue.isNull()
+              ? DynamicType.definitelyNull()
+              : DynamicType.createExact(
+                  TypeElement.stringClassType(appView, Nullability.definitelyNotNull()));
+      fieldStateToAdd = ConcreteClassTypeValueState.create(defaultValue, dynamicType);
+    } else {
+      assert field.getType().isPrimitiveType();
+      fieldStateToAdd = ConcretePrimitiveTypeValueState.create(defaultValue);
+    }
+    if (fieldStateToAdd.isConcrete()) {
+      addState(appView, fieldStateToAdd.asConcrete(), onChangedAction);
+    } else {
+      // We should always be able to map static field values to an unknown abstract value.
+      assert false;
+      setStateToUnknown();
+      onChangedAction.execute();
+    }
+  }
+
+  @Override
+  ValueState getState() {
+    return fieldState;
+  }
+
+  @Override
+  void setState(ValueState fieldState) {
+    this.fieldState = fieldState;
+  }
+
+  @Override
+  boolean isFieldNode() {
+    return true;
+  }
+
+  @Override
+  FlowGraphFieldNode asFieldNode() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java
new file mode 100644
index 0000000..4075465
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+import com.google.common.collect.Sets;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
+
+public abstract class FlowGraphNode {
+
+  private final Set<FlowGraphNode> predecessors = Sets.newIdentityHashSet();
+  private final Map<FlowGraphNode, Set<AbstractFunction>> successors = new IdentityHashMap<>();
+
+  private boolean inWorklist = true;
+
+  void addState(
+      AppView<AppInfoWithLiveness> appView, ConcreteValueState stateToAdd, Action onChangedAction) {
+    ValueState oldState = getState();
+    ValueState newState =
+        oldState.mutableJoin(
+            appView, stateToAdd, getStaticType(), StateCloner.getCloner(), onChangedAction);
+    if (newState != oldState) {
+      setState(newState);
+      onChangedAction.execute();
+    }
+  }
+
+  abstract ValueState getState();
+
+  abstract DexType getStaticType();
+
+  abstract void setState(ValueState valueState);
+
+  void setStateToUnknown() {
+    setState(ValueState.unknown());
+  }
+
+  void addPredecessor(FlowGraphNode predecessor, AbstractFunction abstractFunction) {
+    predecessor.successors.computeIfAbsent(this, ignoreKey(HashSet::new)).add(abstractFunction);
+    predecessors.add(predecessor);
+  }
+
+  void clearPredecessors() {
+    for (FlowGraphNode predecessor : predecessors) {
+      predecessor.successors.remove(this);
+    }
+    predecessors.clear();
+  }
+
+  void clearPredecessors(FlowGraphNode cause) {
+    for (FlowGraphNode predecessor : predecessors) {
+      if (predecessor != cause) {
+        predecessor.successors.remove(this);
+      }
+    }
+    predecessors.clear();
+  }
+
+  Set<FlowGraphNode> getPredecessors() {
+    return predecessors;
+  }
+
+  boolean hasPredecessors() {
+    return !predecessors.isEmpty();
+  }
+
+  void clearDanglingSuccessors() {
+    successors.clear();
+  }
+
+  Set<FlowGraphNode> getSuccessors() {
+    return successors.keySet();
+  }
+
+  public void forEachSuccessor(BiConsumer<FlowGraphNode, Set<AbstractFunction>> consumer) {
+    successors.forEach(consumer);
+  }
+
+  public void removeSuccessorIf(BiPredicate<FlowGraphNode, Set<AbstractFunction>> predicate) {
+    successors.entrySet().removeIf(entry -> predicate.test(entry.getKey(), entry.getValue()));
+  }
+
+  boolean hasSuccessors() {
+    return !successors.isEmpty();
+  }
+
+  boolean isBottom() {
+    return getState().isBottom();
+  }
+
+  boolean isFieldNode() {
+    return false;
+  }
+
+  FlowGraphFieldNode asFieldNode() {
+    return null;
+  }
+
+  boolean isParameterNode() {
+    return false;
+  }
+
+  FlowGraphParameterNode asParameterNode() {
+    return null;
+  }
+
+  boolean isEffectivelyUnknown() {
+    return getState().isConcrete() && getState().asConcrete().isEffectivelyUnknown();
+  }
+
+  boolean isUnknown() {
+    return getState().isUnknown();
+  }
+
+  // No need to enqueue the affected node if it is already in the worklist or if it does not have
+  // any successors (i.e., the successor is a leaf).
+  void addToWorkList(Deque<FlowGraphNode> worklist) {
+    if (!inWorklist && hasSuccessors()) {
+      worklist.add(this);
+      inWorklist = true;
+    }
+  }
+
+  void unsetInWorklist() {
+    assert inWorklist;
+    inWorklist = false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphParameterNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphParameterNode.java
new file mode 100644
index 0000000..b82cecf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphParameterNode.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+
+class FlowGraphParameterNode extends FlowGraphNode {
+
+  private final ProgramMethod method;
+  private final ConcreteMonomorphicMethodState methodState;
+  private final int parameterIndex;
+  private final DexType parameterType;
+
+  FlowGraphParameterNode(
+      ProgramMethod method,
+      ConcreteMonomorphicMethodState methodState,
+      int parameterIndex,
+      DexType parameterType) {
+    this.method = method;
+    this.methodState = methodState;
+    this.parameterIndex = parameterIndex;
+    this.parameterType = parameterType;
+  }
+
+  ProgramMethod getMethod() {
+    return method;
+  }
+
+  int getParameterIndex() {
+    return parameterIndex;
+  }
+
+  @Override
+  DexType getStaticType() {
+    return parameterType;
+  }
+
+  @Override
+  ValueState getState() {
+    return methodState.getParameterState(parameterIndex);
+  }
+
+  @Override
+  void setState(ValueState parameterState) {
+    methodState.setParameterState(parameterIndex, parameterState);
+  }
+
+  @Override
+  boolean isParameterNode() {
+    return true;
+  }
+
+  @Override
+  FlowGraphParameterNode asParameterNode() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
new file mode 100644
index 0000000..8f60f27
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation.propagation;
+
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.PrintStream;
+
+// A simple (unused) writer to aid debugging of field and parameter flow propagation graphs.
+@VisibleForTesting
+public class FlowGraphWriter {
+
+  private final FlowGraph flowGraph;
+
+  public FlowGraphWriter(FlowGraph flowGraph) {
+    this.flowGraph = flowGraph;
+  }
+
+  public void write(PrintStream out) {
+    out.println("digraph {");
+    out.println("  stylesheet = \"/frameworks/g3doc/includes/graphviz-style.css\"");
+    flowGraph.forEachNode(node -> writeNode(out, node));
+    out.println("}");
+  }
+
+  private void writeEdge(PrintStream out) {
+    out.println(" -> ");
+  }
+
+  private void writeNode(PrintStream out, FlowGraphNode node) {
+    if (!node.hasSuccessors()) {
+      writeNodeLabel(out, node);
+      return;
+    }
+    node.forEachSuccessor(
+        (successor, transferFunctions) -> {
+          writeNodeLabel(out, node);
+          writeEdge(out);
+          writeNodeLabel(out, successor);
+        });
+  }
+
+  private void writeNodeLabel(PrintStream out, FlowGraphNode node) {
+    if (node.isFieldNode()) {
+      writeFieldNodeLabel(out, node.asFieldNode());
+    } else {
+      assert node.isParameterNode();
+      writeParameterNodeLabel(out, node.asParameterNode());
+    }
+  }
+
+  private void writeFieldNodeLabel(PrintStream out, FlowGraphFieldNode node) {
+    out.print("\"");
+    ProgramField field = node.getField();
+    out.print(field.getHolderType().getSimpleName());
+    out.print(".");
+    out.print(field.getName().toSourceString());
+    out.print("\"");
+  }
+
+  private void writeParameterNodeLabel(PrintStream out, FlowGraphParameterNode node) {
+    out.print("\"");
+    ProgramMethod method = node.getMethod();
+    out.print(method.getHolderType().getSimpleName());
+    out.print(".");
+    out.print(method.getName().toSourceString());
+    out.print("(");
+    out.print(node.getParameterIndex());
+    out.print(")\"");
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
index add2a26..3cd35c2 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
@@ -1,86 +1,121 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-
 package com.android.tools.r8.optimize.argumentpropagation.propagation;
 
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.android.tools.r8.utils.MapUtils.ignoreKey;
-
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
-import com.android.tools.r8.optimize.argumentpropagation.codescanner.StateCloner;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
-import com.android.tools.r8.optimize.argumentpropagation.utils.BidirectedGraph;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayDeque;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Deque;
-import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
 
 public class InFlowPropagator {
 
   final AppView<AppInfoWithLiveness> appView;
   final IRConverter converter;
+  final FieldStateCollection fieldStates;
   final MethodStateCollectionByReference methodStates;
 
   public InFlowPropagator(
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
+      FieldStateCollection fieldStates,
       MethodStateCollectionByReference methodStates) {
     this.appView = appView;
     this.converter = converter;
+    this.fieldStates = fieldStates;
     this.methodStates = methodStates;
   }
 
   public void run(ExecutorService executorService) throws ExecutionException {
-    // Build a graph with an edge from parameter p -> parameter p' if all argument information for p
-    // must be included in the argument information for p'.
-    FlowGraph flowGraph = new FlowGraph(appView.appInfo().classes());
+    // Compute strongly connected components so that we can compute the fixpoint of multiple flow
+    // graphs in parallel.
+    List<FlowGraph> flowGraphs = computeStronglyConnectedFlowGraphs();
+    processFlowGraphs(flowGraphs, executorService);
 
-    List<Set<ParameterNode>> stronglyConnectedComponents =
-        flowGraph.computeStronglyConnectedComponents();
-    ThreadUtils.processItems(
-        stronglyConnectedComponents,
-        this::process,
-        appView.options().getThreadingModule(),
-        executorService);
+    // Account for the fact that fields that are read before they are written also needs to include
+    // the default value in the field state. We only need to analyze if a given field is read before
+    // it is written if the field has a non-trivial state in the flow graph. Therefore, we only
+    // perform this analysis after having computed the initial fixpoint(s). The hypothesis is that
+    // many fields will have reached the unknown state after the initial fixpoint, meaning there is
+    // fewer fields to analyze.
+    updateFieldStates(fieldStates, flowGraphs);
+    Map<FlowGraph, Deque<FlowGraphNode>> worklists =
+        includeDefaultValuesInFieldStates(fieldStates, flowGraphs, executorService);
+
+    // Since the inclusion of default values changes the flow graphs, we need to repeat the
+    // fixpoint.
+    processWorklists(worklists, executorService);
 
     // The algorithm only changes the parameter states of each monomorphic method state. In case any
     // of these method states have effectively become unknown, we replace them by the canonicalized
     // unknown method state.
     postProcessMethodStates(executorService);
+
+    // Copy the result of the flow graph propagation back to the field state collection.
+    updateFieldStates(fieldStates, flowGraphs);
   }
 
-  private void process(Set<ParameterNode> stronglyConnectedComponent) {
-    // Build a worklist containing all the parameter nodes.
-    Deque<ParameterNode> worklist = new ArrayDeque<>(stronglyConnectedComponent);
+  private List<FlowGraph> computeStronglyConnectedFlowGraphs() {
+    // Build a graph with an edge from parameter p -> parameter p' if all argument information for p
+    // must be included in the argument information for p'.
+    FlowGraph flowGraph =
+        FlowGraph.builder(appView, converter, fieldStates, methodStates)
+            .addClasses(appView.appInfo().classes())
+            .build();
+    List<Set<FlowGraphNode>> stronglyConnectedComponents =
+        flowGraph.computeStronglyConnectedComponents();
+    return ListUtils.map(stronglyConnectedComponents, FlowGraph::new);
+  }
 
+  private Map<FlowGraph, Deque<FlowGraphNode>> includeDefaultValuesInFieldStates(
+      FieldStateCollection fieldStates, List<FlowGraph> flowGraphs, ExecutorService executorService)
+      throws ExecutionException {
+    DefaultFieldValueJoiner joiner = new DefaultFieldValueJoiner(appView, fieldStates, flowGraphs);
+    return joiner.joinDefaultFieldValuesForFieldsWithReadBeforeWrite(executorService);
+  }
+
+  private void processFlowGraphs(List<FlowGraph> flowGraphs, ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(
+        flowGraphs, this::process, appView.options().getThreadingModule(), executorService);
+  }
+
+  private void processWorklists(
+      Map<FlowGraph, Deque<FlowGraphNode>> worklists, ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processMap(
+        worklists, this::process, appView.options().getThreadingModule(), executorService);
+  }
+
+  private void process(FlowGraph flowGraph) {
+    // Build a worklist containing all the nodes.
+    Deque<FlowGraphNode> worklist = new ArrayDeque<>();
+    flowGraph.forEachNode(worklist::add);
+    process(flowGraph, worklist);
+  }
+
+  private void process(FlowGraph flowGraph, Deque<FlowGraphNode> worklist) {
     // Repeatedly propagate argument information through edges in the flow graph until there are no
     // more changes.
     // TODO(b/190154391): Consider a path p1 -> p2 -> p3 in the graph. If we process p2 first, then
@@ -88,41 +123,62 @@
     //  need to reprocess p2 and then p3. If we always process leaves in the graph first, we would
     //  process p1, then p2, then p3, and then be done.
     while (!worklist.isEmpty()) {
-      ParameterNode parameterNode = worklist.removeLast();
-      parameterNode.unsetPending();
-      propagate(
-          parameterNode,
-          affectedNode -> {
-            // No need to enqueue the affected node if it is already in the worklist or if it does
-            // not have any successors (i.e., the successor is a leaf).
-            if (!affectedNode.isPending() && affectedNode.hasSuccessors()) {
-              worklist.add(affectedNode);
-              affectedNode.setPending();
-            }
-          });
+      FlowGraphNode node = worklist.removeLast();
+      node.unsetInWorklist();
+      propagate(flowGraph, node, worklist);
     }
   }
 
-  private void propagate(
-      ParameterNode parameterNode, Consumer<ParameterNode> affectedNodeConsumer) {
-    ValueState parameterState = parameterNode.getState();
-    if (parameterState.isBottom()) {
+  private void propagate(FlowGraph flowGraph, FlowGraphNode node, Deque<FlowGraphNode> worklist) {
+    if (node.isBottom()) {
       return;
     }
-    List<ParameterNode> newlyUnknownParameterNodes = new ArrayList<>();
-    for (ParameterNode successorNode : parameterNode.getSuccessors()) {
-      ValueState newParameterState =
-          successorNode.addState(
-              appView,
-              parameterState.asNonEmpty(),
-              () -> affectedNodeConsumer.accept(successorNode));
-      if (newParameterState.isUnknown()) {
-        newlyUnknownParameterNodes.add(successorNode);
+    if (node.isUnknown()) {
+      assert !node.hasPredecessors();
+      for (FlowGraphNode successorNode : node.getSuccessors()) {
+        assert !successorNode.isUnknown();
+        successorNode.clearPredecessors(node);
+        successorNode.setStateToUnknown();
+        successorNode.addToWorkList(worklist);
       }
+      node.clearDanglingSuccessors();
+    } else {
+      propagateNode(flowGraph, node, worklist);
     }
-    for (ParameterNode newlyUnknownParameterNode : newlyUnknownParameterNodes) {
-      newlyUnknownParameterNode.clearPredecessors();
-    }
+  }
+
+  private void propagateNode(
+      FlowGraph flowGraph, FlowGraphNode node, Deque<FlowGraphNode> worklist) {
+    ConcreteValueState state = node.getState().asConcrete();
+    node.removeSuccessorIf(
+        (successorNode, transferFunctions) -> {
+          assert !successorNode.isUnknown();
+          for (AbstractFunction transferFunction : transferFunctions) {
+            FlowGraphStateProvider flowGraphStateProvider =
+                FlowGraphStateProvider.create(flowGraph, transferFunction);
+            ValueState transferState =
+                transferFunction.apply(appView, flowGraphStateProvider, state);
+            if (transferState.isBottom()) {
+              // Nothing to propagate.
+            } else if (transferState.isUnknown()) {
+              successorNode.setStateToUnknown();
+              successorNode.addToWorkList(worklist);
+            } else {
+              ConcreteValueState concreteTransferState = transferState.asConcrete();
+              successorNode.addState(
+                  appView, concreteTransferState, () -> successorNode.addToWorkList(worklist));
+            }
+            // If this successor has become unknown, there is no point in continuing to propagate
+            // flow to it from any of its predecessors. We therefore clear the predecessors to
+            // improve performance of the fixpoint computation.
+            if (successorNode.isUnknown()) {
+              successorNode.clearPredecessors(node);
+              return true;
+            }
+            assert !successorNode.isEffectivelyUnknown();
+          }
+          return false;
+        });
   }
 
   private void postProcessMethodStates(ExecutorService executorService) throws ExecutionException {
@@ -151,228 +207,19 @@
     }
   }
 
-  public class FlowGraph extends BidirectedGraph<ParameterNode> {
-
-    private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> nodes = new IdentityHashMap<>();
-
-    public FlowGraph(Iterable<DexProgramClass> classes) {
-      classes.forEach(this::add);
-    }
-
-    @Override
-    public void forEachNeighbor(ParameterNode node, Consumer<? super ParameterNode> consumer) {
-      node.getPredecessors().forEach(consumer);
-      node.getSuccessors().forEach(consumer);
-    }
-
-    @Override
-    public void forEachNode(Consumer<? super ParameterNode> consumer) {
-      nodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
-    }
-
-    private void add(DexProgramClass clazz) {
-      clazz.forEachProgramMethod(this::add);
-    }
-
-    private void add(ProgramMethod method) {
-      MethodState methodState = methodStates.get(method);
-
-      // No need to create nodes for parameters with no in-flow or no useful information.
-      if (methodState.isBottom() || methodState.isUnknown()) {
-        return;
-      }
-
-      // Add nodes for the parameters for which we have non-trivial information.
-      ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
-      List<ValueState> parameterStates = monomorphicMethodState.getParameterStates();
-      for (int parameterIndex = 0; parameterIndex < parameterStates.size(); parameterIndex++) {
-        ValueState parameterState = parameterStates.get(parameterIndex);
-        add(method, parameterIndex, monomorphicMethodState, parameterState);
-      }
-    }
-
-    private void add(
-        ProgramMethod method,
-        int parameterIndex,
-        ConcreteMonomorphicMethodState methodState,
-        ValueState parameterState) {
-      // No need to create nodes for parameters with no in-parameters and parameters we don't know
-      // anything about.
-      if (parameterState.isBottom() || parameterState.isUnknown()) {
-        return;
-      }
-
-      ConcreteValueState concreteParameterState = parameterState.asConcrete();
-
-      // No need to create a node for a parameter that doesn't depend on any other parameters
-      // (unless some other parameter depends on this parameter).
-      if (!concreteParameterState.hasInFlow()) {
-        return;
-      }
-
-      ParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
-      for (InFlow inFlow : concreteParameterState.getInFlow()) {
-        if (inFlow.isMethodParameter()) {
-          if (addInFlow(inFlow.asMethodParameter(), node).shouldBreak()) {
-            break;
-          }
-        } else {
-          throw new Unreachable();
-        }
-      }
-
-      if (!node.getState().isUnknown()) {
-        assert node.getState() == concreteParameterState;
-        node.setState(concreteParameterState.clearInFlow());
-      }
-    }
-
-    private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, ParameterNode node) {
-      ProgramMethod enclosingMethod = getEnclosingMethod(inFlow);
-      if (enclosingMethod == null) {
-        // This is a parameter of a single caller inlined method. Since this method has been
-        // pruned, the call from inside the method no longer exists, and we can therefore safely
-        // skip it.
-        assert converter.getInliner().verifyIsPrunedDueToSingleCallerInlining(inFlow.getMethod());
-        return TraversalContinuation.doContinue();
-      }
-
-      MethodState enclosingMethodState = getMethodState(enclosingMethod);
-      if (enclosingMethodState.isBottom()) {
-        // The current method is called from a dead method; no need to propagate any information
-        // from the dead call site.
-        return TraversalContinuation.doContinue();
-      }
-
-      if (enclosingMethodState.isUnknown()) {
-        // The parameter depends on another parameter for which we don't know anything.
-        node.clearPredecessors();
-        node.setState(ValueState.unknown());
-        return TraversalContinuation.doBreak();
-      }
-
-      assert enclosingMethodState.isConcrete();
-      assert enclosingMethodState.asConcrete().isMonomorphic();
-
-      ParameterNode predecessor =
-          getOrCreateParameterNode(
-              enclosingMethod,
-              inFlow.getIndex(),
-              enclosingMethodState.asConcrete().asMonomorphic());
-      node.addPredecessor(predecessor);
-      return TraversalContinuation.doContinue();
-    }
-
-    private ParameterNode getOrCreateParameterNode(
-        ProgramMethod method, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
-      Int2ReferenceMap<ParameterNode> parameterNodesForMethod =
-          nodes.computeIfAbsent(method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
-      return parameterNodesForMethod.compute(
-          parameterIndex,
-          (ignore, parameterNode) ->
-              parameterNode != null
-                  ? parameterNode
-                  : new ParameterNode(
-                      methodState, parameterIndex, method.getArgumentType(parameterIndex)));
-    }
-
-    private ProgramMethod getEnclosingMethod(MethodParameter methodParameter) {
-      DexMethod methodReference = methodParameter.getMethod();
-      return methodReference.lookupOnProgramClass(
-          asProgramClassOrNull(appView.definitionFor(methodParameter.getMethod().getHolderType())));
-    }
-
-    private MethodState getMethodState(ProgramMethod method) {
-      if (method == null) {
-        // Conservatively return unknown if for some reason we can't find the method.
-        assert false;
-        return MethodState.unknown();
-      }
-      return methodStates.get(method);
-    }
-  }
-
-  static class ParameterNode {
-
-    private final ConcreteMonomorphicMethodState methodState;
-    private final int parameterIndex;
-    private final DexType parameterType;
-
-    private final Set<ParameterNode> predecessors = Sets.newIdentityHashSet();
-    private final Set<ParameterNode> successors = Sets.newIdentityHashSet();
-
-    private boolean pending = true;
-
-    ParameterNode(
-        ConcreteMonomorphicMethodState methodState, int parameterIndex, DexType parameterType) {
-      this.methodState = methodState;
-      this.parameterIndex = parameterIndex;
-      this.parameterType = parameterType;
-    }
-
-    void addPredecessor(ParameterNode predecessor) {
-      predecessor.successors.add(this);
-      predecessors.add(predecessor);
-    }
-
-    void clearPredecessors() {
-      for (ParameterNode predecessor : predecessors) {
-        predecessor.successors.remove(this);
-      }
-      predecessors.clear();
-    }
-
-    Set<ParameterNode> getPredecessors() {
-      return predecessors;
-    }
-
-    ValueState getState() {
-      return methodState.getParameterState(parameterIndex);
-    }
-
-    Set<ParameterNode> getSuccessors() {
-      return successors;
-    }
-
-    boolean hasSuccessors() {
-      return !successors.isEmpty();
-    }
-
-    boolean isPending() {
-      return pending;
-    }
-
-    ValueState addState(
-        AppView<AppInfoWithLiveness> appView,
-        NonEmptyValueState parameterStateToAdd,
-        Action onChangedAction) {
-      ValueState oldParameterState = getState();
-      ValueState newParameterState =
-          oldParameterState.mutableJoin(
-              appView,
-              parameterStateToAdd,
-              parameterType,
-              StateCloner.getCloner(),
-              onChangedAction);
-      if (newParameterState != oldParameterState) {
-        setState(newParameterState);
-        onChangedAction.execute();
-      }
-      return newParameterState;
-    }
-
-    void setPending() {
-      assert !isPending();
-      pending = true;
-    }
-
-    void setState(ValueState parameterState) {
-      methodState.setParameterState(parameterIndex, parameterState);
-    }
-
-    void unsetPending() {
-      assert pending;
-      pending = false;
+  private void updateFieldStates(
+      FieldStateCollection fieldStates, Collection<FlowGraph> flowGraphs) {
+    for (FlowGraph flowGraph : flowGraphs) {
+      flowGraph.forEachFieldNode(
+          node -> {
+            ProgramField field = node.getField();
+            ValueState state = node.getState();
+            ValueState previousState = fieldStates.set(field, state);
+            assert state.isUnknown()
+                    || state == previousState
+                    || (state.isConcrete() && previousState.isBottom())
+                : "Expected current state to be >= previous state";
+          });
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
index 2a54b56..6890b1c 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
@@ -4,12 +4,17 @@
 package com.android.tools.r8.optimize.compose;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
 import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -17,9 +22,10 @@
 import com.android.tools.r8.ir.code.Or;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.OrAbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.BitUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.google.common.collect.Iterables;
 
@@ -32,7 +38,10 @@
   private final DexString invokeName;
 
   public ArgumentPropagatorComposeModeling(AppView<AppInfoWithLiveness> appView) {
-    assert appView.testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
+    assert appView
+        .options()
+        .getJetpackComposeOptions()
+        .isModelingChangedArgumentsToComposableFunctions();
     this.appView = appView;
     this.rewrittenComposeReferences =
         appView
@@ -139,69 +148,83 @@
       return null;
     }
 
-    // We only model the two last arguments ($$changed and $$default) to the @Composable function.
-    // If this argument is not on of these two int arguments, then don't apply any modeling.
-    if (argumentIndex <= composerParameterIndex) {
+    // We only model the $$changed argument to the @Composable function.
+    if (argumentIndex != composerParameterIndex + 1) {
       return null;
     }
 
     assert argument.getType().isInt();
 
-    DexString expectedFieldName;
-    ValueState state = ValueState.bottomPrimitiveTypeParameter();
-    if (!hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2) {
-      // We are looking at an argument to the $$changed parameter of the @Composable function.
-      // We generally expect this argument to be defined by a call to updateChangedFlags().
-      if (argument.isDefinedByInstructionSatisfying(Instruction::isInvokeStatic)) {
-        InvokeStatic invokeStatic = argument.getDefinition().asInvokeStatic();
-        DexMethod maybeUpdateChangedFlagsMethod = invokeStatic.getInvokedMethod();
-        if (!maybeUpdateChangedFlagsMethod.isIdenticalTo(
-            rewrittenComposeReferences.updatedChangedFlagsMethod)) {
-          return null;
-        }
-        // Assume the call does not impact the $$changed capture and strip the call.
-        argument = invokeStatic.getFirstArgument();
+    DexField changedField =
+        appView
+            .dexItemFactory()
+            .createField(
+                context.getHolderType(),
+                appView.dexItemFactory().intType,
+                rewrittenComposeReferences.changedFieldName);
+
+    UpdateChangedFlagsAbstractFunction inFlow = null;
+    // We are looking at an argument to the $$changed parameter of the @Composable function.
+    // We generally expect this argument to be defined by a call to updateChangedFlags().
+    if (argument.isDefinedByInstructionSatisfying(Instruction::isInvokeStatic)) {
+      InvokeStatic invokeStatic = argument.getDefinition().asInvokeStatic();
+      SingleResolutionResult<?> resolutionResult =
+          invokeStatic.resolveMethod(appView, context).asSingleResolution();
+      if (resolutionResult == null) {
+        return null;
       }
-      // Allow the argument to be defined by `this.$$changed | 1`.
-      if (argument.isDefinedByInstructionSatisfying(Instruction::isOr)) {
-        Or or = argument.getDefinition().asOr();
-        Value maybeNumberOperand =
-            or.leftValue().isConstNumber() ? or.leftValue() : or.rightValue();
-        Value otherOperand = or.getOperand(1 - or.inValues().indexOf(maybeNumberOperand));
-        if (!maybeNumberOperand.isConstNumber(1)) {
-          return null;
-        }
-        // Strip the OR instruction.
-        argument = otherOperand;
-        // Update the model from bottom to a special value that effectively throws away any known
-        // information about the lowermost bit of $$changed.
-        state =
-            new ConcretePrimitiveTypeValueState(
-                appView
-                    .abstractValueFactory()
-                    .createDefiniteBitsNumberValue(
-                        BitUtils.ALL_BITS_SET_MASK, BitUtils.ALL_BITS_SET_MASK << 1));
+      DexClassAndMethod invokeSingleTarget =
+          resolutionResult
+              .lookupDispatchTarget(appView, invokeStatic, context)
+              .getSingleDispatchTarget();
+      if (invokeSingleTarget == null) {
+        return null;
       }
-      expectedFieldName = rewrittenComposeReferences.changedFieldName;
+      inFlow =
+          invokeSingleTarget
+              .getOptimizationInfo()
+              .getAbstractFunction()
+              .asUpdateChangedFlagsAbstractFunction();
+      if (inFlow == null) {
+        return null;
+      }
+      // By accounting for the abstract function we can safely strip the call.
+      argument = invokeStatic.getFirstArgument();
+    }
+    // Allow the argument to be defined by `this.$$changed | 1`.
+    if (argument.isDefinedByInstructionSatisfying(Instruction::isOr)) {
+      Or or = argument.getDefinition().asOr();
+      Value maybeNumberOperand = or.leftValue().isConstNumber() ? or.leftValue() : or.rightValue();
+      Value otherOperand = or.getOperand(1 - or.inValues().indexOf(maybeNumberOperand));
+      if (!maybeNumberOperand.isConstNumber(1)) {
+        return null;
+      }
+      // Strip the OR instruction.
+      argument = otherOperand;
+      // Update the model from bottom to a special value that effectively throws away any known
+      // information about the lowermost bit of $$changed.
+      SingleNumberValue one =
+          appView.abstractValueFactory().createSingleNumberValue(1, TypeElement.getInt());
+      inFlow =
+          new UpdateChangedFlagsAbstractFunction(
+              new OrAbstractFunction(new FieldValue(changedField), one));
     } else {
-      // We are looking at an argument to the $$default parameter of the @Composable function.
-      expectedFieldName = rewrittenComposeReferences.defaultFieldName;
+      inFlow = new UpdateChangedFlagsAbstractFunction(new FieldValue(changedField));
     }
 
-    // At this point we expect that the restart lambda is reading either this.$$changed or
-    // this.$$default using an instance-get.
+    // At this point we expect that the restart lambda is reading this.$$changed using an
+    // instance-get.
     if (!argument.isDefinedByInstructionSatisfying(Instruction::isInstanceGet)) {
       return null;
     }
 
     // Check that the instance-get is reading the capture field that we expect it to.
     InstanceGet instanceGet = argument.getDefinition().asInstanceGet();
-    if (!instanceGet.getField().getName().isIdenticalTo(expectedFieldName)) {
+    if (!instanceGet.getField().isIdenticalTo(changedField)) {
       return null;
     }
 
-    // Return the argument model. Note that, for the $$default field, this is always bottom, which
-    // is equivalent to modeling that this call does not contribute any new argument information.
-    return state;
+    // Return the argument model.
+    return new ConcretePrimitiveTypeValueState(inFlow);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java
index 1c23e06..c2890d8 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposableOptimizationPass.java
@@ -6,8 +6,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Iterables;
@@ -35,18 +33,11 @@
       ExecutorService executorService,
       Timing timing)
       throws ExecutionException {
-    InternalOptions options = appView.options();
-    if (!options.isOptimizing() || !options.isShrinking()) {
-      return;
+    if (appView.options().getJetpackComposeOptions().isComposableOptimizationPassEnabled()) {
+      timing.time(
+          "ComposableOptimizationPass",
+          () -> new ComposableOptimizationPass(appView, converter).processWaves(executorService));
     }
-    TestingOptions testingOptions = options.getTestingOptions();
-    if (!testingOptions.enableComposableOptimizationPass
-        || !testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions) {
-      return;
-    }
-    timing.time(
-        "ComposableOptimizationPass",
-        () -> new ComposableOptimizationPass(appView, converter).processWaves(executorService));
   }
 
   void processWaves(ExecutorService executorService) throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
index 258b69b..846dcbe 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
@@ -3,9 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.compose;
 
+import static com.android.tools.r8.graph.ProgramField.asProgramFieldOrNull;
+
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -20,12 +24,20 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorOptimizationInfoPopulator;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.LazyBox;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Iterables;
@@ -52,12 +64,12 @@
     this.converter = converter;
   }
 
-  // TODO(b/302483644): Process wave concurrently.
   public Set<ComposableCallGraphNode> processWave(
       Set<ComposableCallGraphNode> wave, ExecutorService executorService)
       throws ExecutionException {
     ProcessorContext processorContext = appView.createProcessorContext();
-    wave.forEach(
+    ThreadUtils.processItems(
+        wave,
         node -> {
           assert !processed.contains(node);
           converter.processDesugaredMethod(
@@ -66,17 +78,25 @@
               this,
               processorContext.createMethodProcessingContext(node.getMethod()),
               MethodConversionOptions.forLirPhase(appView));
-        });
+        },
+        appView.options().getThreadingModule(),
+        executorService);
     processed.addAll(wave);
     return optimizeComposableFunctionsCalledFromWave(wave, executorService);
   }
 
-  @SuppressWarnings("UnusedVariable")
   private Set<ComposableCallGraphNode> optimizeComposableFunctionsCalledFromWave(
       Set<ComposableCallGraphNode> wave, ExecutorService executorService)
       throws ExecutionException {
+    prepareForInFlowPropagator();
+
+    InFlowPropagator inFlowPropagator =
+        new InFlowPropagator(
+            appView, converter, codeScanner.getFieldStates(), codeScanner.getMethodStates());
+    inFlowPropagator.run(executorService);
+
     ArgumentPropagatorOptimizationInfoPopulator optimizationInfoPopulator =
-        new ArgumentPropagatorOptimizationInfoPopulator(appView, null, null, null);
+        new ArgumentPropagatorOptimizationInfoPopulator(appView, null, null, null, null);
     Set<ComposableCallGraphNode> optimizedComposableFunctions = Sets.newIdentityHashSet();
     wave.forEach(
         node ->
@@ -92,36 +112,87 @@
     return optimizedComposableFunctions;
   }
 
-  private MethodState getMethodState(ComposableCallGraphNode node) {
-    assert processed.containsAll(node.getCallers());
-    MethodState methodState = codeScanner.getMethodStates().get(node.getMethod());
-    return widenMethodState(methodState);
+  private void prepareForInFlowPropagator() {
+    FieldStateCollection fieldStates = codeScanner.getFieldStates();
+
+    // Set all field states to unknown since we are not guaranteed to have processes all field
+    // writes.
+    fieldStates.forEach(
+        (field, fieldState) ->
+            fieldStates.addTemporaryFieldState(
+                appView, field, ValueState::unknown, Timing.empty()));
+
+    // Widen all parameter states that have in-flow to unknown, except when the in-flow is an
+    // update-changed-flags abstract function.
+    MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+    methodStates.forEach(
+        (method, methodState) -> {
+          if (!methodState.isMonomorphic()) {
+            assert methodState.isUnknown();
+            return;
+          }
+          ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
+          for (int parameterIndex = 0;
+              parameterIndex < monomorphicMethodState.size();
+              parameterIndex++) {
+            ValueState parameterState = monomorphicMethodState.getParameterState(parameterIndex);
+            if (parameterState.isConcrete()) {
+              ConcreteValueState concreteParameterState = parameterState.asConcrete();
+              prepareParameterStateForInFlowPropagator(
+                  method, monomorphicMethodState, parameterIndex, concreteParameterState);
+            }
+          }
+        });
   }
 
-  /**
-   * If a parameter state of the current method state encodes that it is greater than (lattice wise)
-   * than another parameter in the program, then widen the parameter state to unknown. This is
-   * needed since we are not guaranteed to have seen all possible call sites of the callers of this
-   * method.
-   */
-  private MethodState widenMethodState(MethodState methodState) {
-    assert !methodState.isBottom();
-    assert !methodState.isPolymorphic();
-    if (methodState.isMonomorphic()) {
-      ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
-      for (int i = 0; i < monomorphicMethodState.size(); i++) {
-        if (monomorphicMethodState.getParameterState(i).isConcrete()) {
-          ConcreteValueState concreteParameterState =
-              monomorphicMethodState.getParameterState(i).asConcrete();
-          if (concreteParameterState.hasInFlow()) {
-            monomorphicMethodState.setParameterState(i, ValueState.unknown());
-          }
-        }
-      }
-    } else {
-      assert methodState.isUnknown();
+  private void prepareParameterStateForInFlowPropagator(
+      DexMethod method,
+      ConcreteMonomorphicMethodState methodState,
+      int parameterIndex,
+      ConcreteValueState parameterState) {
+    if (!parameterState.hasInFlow()) {
+      return;
     }
-    return methodState;
+
+    UpdateChangedFlagsAbstractFunction transferFunction = null;
+    if (parameterState.getInFlow().size() == 1) {
+      transferFunction =
+          Iterables.getOnlyElement(parameterState.getInFlow())
+              .asUpdateChangedFlagsAbstractFunction();
+    }
+    if (transferFunction == null) {
+      methodState.setParameterState(parameterIndex, ValueState.unknown());
+      return;
+    }
+
+    // This is a call to a composable function from a restart function.
+    Iterable<BaseInFlow> baseInFlow = transferFunction.getBaseInFlow();
+    assert Iterables.size(baseInFlow) == 1;
+    BaseInFlow singleBaseInFlow = IterableUtils.first(baseInFlow);
+    assert singleBaseInFlow.isFieldValue();
+
+    ProgramField field =
+        asProgramFieldOrNull(appView.definitionFor(singleBaseInFlow.asFieldValue().getField()));
+    assert field != null;
+
+    // If the only input to the $$changed parameter of the Composable function is in-flow then skip.
+    if (methodState.getParameterState(parameterIndex).getAbstractValue(appView).isBottom()) {
+      methodState.setParameterState(parameterIndex, ValueState.unknown());
+      return;
+    }
+
+    codeScanner
+        .getFieldStates()
+        .addTemporaryFieldState(
+            appView,
+            field,
+            () -> new ConcretePrimitiveTypeValueState(new MethodParameter(method, parameterIndex)),
+            Timing.empty());
+  }
+
+  private MethodState getMethodState(ComposableCallGraphNode node) {
+    assert processed.containsAll(node.getCallers());
+    return codeScanner.getMethodStates().get(node.getMethod());
   }
 
   public void scan(ProgramMethod method, IRCode code, Timing timing) {
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
index e441188..e38f9a4 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
@@ -12,7 +12,6 @@
 public class ComposeReferences {
 
   public final DexString changedFieldName;
-  public final DexString defaultFieldName;
 
   public final DexType composableType;
   public final DexType composerType;
@@ -21,7 +20,6 @@
 
   public ComposeReferences(DexItemFactory factory) {
     changedFieldName = factory.createString("$$changed");
-    defaultFieldName = factory.createString("$$default");
 
     composableType = factory.createType("Landroidx/compose/runtime/Composable;");
     composerType = factory.createType("Landroidx/compose/runtime/Composer;");
@@ -35,12 +33,10 @@
 
   public ComposeReferences(
       DexString changedFieldName,
-      DexString defaultFieldName,
       DexType composableType,
       DexType composerType,
       DexMethod updatedChangedFlagsMethod) {
     this.changedFieldName = changedFieldName;
-    this.defaultFieldName = defaultFieldName;
     this.composableType = composableType;
     this.composerType = composerType;
     this.updatedChangedFlagsMethod = updatedChangedFlagsMethod;
@@ -49,7 +45,6 @@
   public ComposeReferences rewrittenWithLens(GraphLens graphLens, GraphLens codeLens) {
     return new ComposeReferences(
         changedFieldName,
-        defaultFieldName,
         graphLens.lookupClassType(composableType, codeLens),
         graphLens.lookupClassType(composerType, codeLens),
         graphLens.getRenamedMethodSignature(updatedChangedFlagsMethod, codeLens));
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/JetpackComposeOptions.java b/src/main/java/com/android/tools/r8/optimize/compose/JetpackComposeOptions.java
new file mode 100644
index 0000000..69ec5e6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/JetpackComposeOptions.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.compose;
+
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SystemPropertyUtils;
+
+public class JetpackComposeOptions {
+
+  private final InternalOptions options;
+
+  public boolean enableComposableOptimizationPass =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.jetpackcompose.enableComposableOptimizationPass", false);
+
+  public boolean enableModelingOfChangedArguments =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.jetpackcompose.enableModelingOfChangedArguments", false);
+
+  public JetpackComposeOptions(InternalOptions options) {
+    this.options = options;
+  }
+
+  public void enableAllOptimizations(boolean enable) {
+    enableComposableOptimizationPass = enable;
+    enableModelingOfChangedArguments = enable;
+  }
+
+  public boolean isAnyOptimizationsEnabled() {
+    return isComposableOptimizationPassEnabled()
+        || isModelingChangedArgumentsToComposableFunctions();
+  }
+
+  public boolean isComposableOptimizationPassEnabled() {
+    return isModelingChangedArgumentsToComposableFunctions() && enableComposableOptimizationPass;
+  }
+
+  public boolean isModelingChangedArgumentsToComposableFunctions() {
+    return options.isOptimizing() && options.isShrinking() && enableModelingOfChangedArguments;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
index 5ae767a..2d940e9 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
@@ -3,15 +3,150 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.compose;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.SingleNumberValue;
+import com.android.tools.r8.ir.analysis.value.arithmetic.AbstractCalculator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.OrAbstractFunction;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.IterableUtils;
+import java.util.Objects;
 
 public class UpdateChangedFlagsAbstractFunction implements AbstractFunction {
 
-  @SuppressWarnings("UnusedVariable")
+  private static final int changedLowBitMask = 0b001_001_001_001_001_001_001_001_001_001_0;
+  private static final int changedHighBitMask = changedLowBitMask << 1;
+  private static final int changedMask = ~(changedLowBitMask | changedHighBitMask);
+
   private final InFlow inFlow;
 
   public UpdateChangedFlagsAbstractFunction(InFlow inFlow) {
     this.inFlow = inFlow;
   }
+
+  @Override
+  public ValueState apply(
+      AppView<AppInfoWithLiveness> appView,
+      FlowGraphStateProvider flowGraphStateProvider,
+      ConcreteValueState baseInState) {
+    ValueState inState;
+    if (inFlow.isAbstractFunction()) {
+      AbstractFunction orFunction = inFlow.asAbstractFunction();
+      assert orFunction instanceof OrAbstractFunction;
+      inState = orFunction.apply(appView, flowGraphStateProvider, baseInState);
+    } else {
+      inState = baseInState;
+    }
+    if (!inState.isPrimitiveState()) {
+      assert inState.isBottom() || inState.isUnknown();
+      return inState;
+    }
+    AbstractValue result = apply(appView, inState.asPrimitiveState().getAbstractValue());
+    return ConcretePrimitiveTypeValueState.create(result);
+  }
+
+  /**
+   * Applies the following function to the given {@param abstractValue}.
+   *
+   * <pre>
+   * private const val changedLowBitMask = 0b001_001_001_001_001_001_001_001_001_001_0
+   * private const val changedHighBitMask = changedLowBitMask shl 1
+   * private const val changedMask = (changedLowBitMask or changedHighBitMask).inv()
+   *
+   * internal fun updateChangedFlags(flags: Int): Int {
+   *     val lowBits = flags and changedLowBitMask
+   *     val highBits = flags and changedHighBitMask
+   *     return ((flags and changedMask) or
+   *         (lowBits or (highBits shr 1)) or ((lowBits shl 1) and highBits))
+   * }
+   * </pre>
+   */
+  private AbstractValue apply(AppView<AppInfoWithLiveness> appView, AbstractValue flagsValue) {
+    if (flagsValue.isSingleNumberValue()) {
+      return apply(appView, flagsValue.asSingleNumberValue().getIntValue());
+    }
+    AbstractValueFactory factory = appView.abstractValueFactory();
+    // Load constants.
+    AbstractValue changedLowBitMaskValue =
+        factory.createUncheckedSingleNumberValue(changedLowBitMask);
+    AbstractValue changedHighBitMaskValue =
+        factory.createUncheckedSingleNumberValue(changedHighBitMask);
+    AbstractValue changedMaskValue = factory.createUncheckedSingleNumberValue(changedMask);
+    // Evaluate expression.
+    AbstractValue lowBitsValue =
+        AbstractCalculator.andIntegers(appView, flagsValue, changedLowBitMaskValue);
+    AbstractValue highBitsValue =
+        AbstractCalculator.andIntegers(appView, flagsValue, changedHighBitMaskValue);
+    AbstractValue changedBitsValue =
+        AbstractCalculator.andIntegers(appView, flagsValue, changedMaskValue);
+    return AbstractCalculator.orIntegers(
+        appView,
+        changedBitsValue,
+        lowBitsValue,
+        AbstractCalculator.shrIntegers(appView, highBitsValue, 1),
+        AbstractCalculator.andIntegers(
+            appView, AbstractCalculator.shlIntegers(appView, lowBitsValue, 1), highBitsValue));
+  }
+
+  private SingleNumberValue apply(AppView<AppInfoWithLiveness> appView, int flags) {
+    int lowBits = flags & changedLowBitMask;
+    int highBits = flags & changedHighBitMask;
+    int changedBits = flags & changedMask;
+    int result = changedBits | lowBits | (highBits >> 1) | ((lowBits << 1) & highBits);
+    return appView.abstractValueFactory().createUncheckedSingleNumberValue(result);
+  }
+
+  @Override
+  public boolean containsBaseInFlow(BaseInFlow otherInFlow) {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().containsBaseInFlow(otherInFlow);
+    }
+    assert inFlow.isBaseInFlow();
+    return inFlow.equals(otherInFlow);
+  }
+
+  @Override
+  public Iterable<BaseInFlow> getBaseInFlow() {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().getBaseInFlow();
+    }
+    assert inFlow.isBaseInFlow();
+    return IterableUtils.singleton(inFlow.asBaseInFlow());
+  }
+
+  @Override
+  public boolean isUpdateChangedFlagsAbstractFunction() {
+    return true;
+  }
+
+  @Override
+  public UpdateChangedFlagsAbstractFunction asUpdateChangedFlagsAbstractFunction() {
+    return this;
+  }
+
+  @Override
+  @SuppressWarnings("EqualsGetClass")
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    UpdateChangedFlagsAbstractFunction fn = (UpdateChangedFlagsAbstractFunction) obj;
+    return inFlow.equals(fn.inFlow);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), inFlow);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 40e2beb..0fa843d 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -334,7 +334,7 @@
       AnnotatedKind kind,
       Mode mode,
       InternalOptions options) {
-    return options.testing.modelUnknownChangedAndDefaultArgumentsToComposableFunctions
+    return options.getJetpackComposeOptions().isAnyOptimizationsEnabled()
         && mode.isInitialTreeShaking()
         && kind.isMethod()
         && annotation
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index db53845..5981161 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -40,6 +40,10 @@
     return new Builder(this);
   }
 
+  public boolean isFieldPropagationAllowed(GlobalKeepInfoConfiguration configuration) {
+    return isOptimizationAllowed(configuration) && isShrinkingAllowed(configuration);
+  }
+
   public boolean isFieldTypeStrengtheningAllowed(GlobalKeepInfoConfiguration configuration) {
     return internalIsFieldTypeStrengtheningAllowed();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index b8b3ddf..28f377b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -82,6 +82,7 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
+import com.android.tools.r8.optimize.compose.JetpackComposeOptions;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemovalOptions;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
@@ -401,6 +402,8 @@
   // Optimization-related flags. These should conform to -dontoptimize and disableAllOptimizations.
   public boolean enableFieldBitAccessAnalysis =
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
+  public boolean enableFieldAssignmentTracker = true;
+  public boolean enableFieldValueAnalysis = true;
   public boolean enableUnusedInterfaceRemoval = true;
   public boolean enableDevirtualization = true;
   public boolean enableEnumUnboxing = true;
@@ -929,6 +932,7 @@
   private final CfCodeAnalysisOptions cfCodeAnalysisOptions = new CfCodeAnalysisOptions();
   private final ClassInlinerOptions classInlinerOptions = new ClassInlinerOptions();
   private final InlinerOptions inlinerOptions = new InlinerOptions(this);
+  private final JetpackComposeOptions jetpackComposeOptions = new JetpackComposeOptions(this);
   private final HorizontalClassMergerOptions horizontalClassMergerOptions =
       new HorizontalClassMergerOptions();
   private final VerticalClassMergerOptions verticalClassMergerOptions =
@@ -982,6 +986,10 @@
     return horizontalClassMergerOptions;
   }
 
+  public JetpackComposeOptions getJetpackComposeOptions() {
+    return jetpackComposeOptions;
+  }
+
   public VerticalClassMergerOptions getVerticalClassMergerOptions() {
     return verticalClassMergerOptions;
   }
@@ -2387,13 +2395,6 @@
         System.getProperty("com.android.tools.r8.disableMarkingClassesFinal") != null;
     public boolean testEnableTestAssertions = false;
     public boolean keepMetadataInR8IfNotRewritten = true;
-    public boolean enableComposableOptimizationPass =
-        SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault(
-            "com.android.tools.r8.enableComposableOptimizationPass", false);
-    public boolean modelUnknownChangedAndDefaultArgumentsToComposableFunctions =
-        SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault(
-            "com.android.tools.r8.modelUnknownChangedAndDefaultArgumentsToComposableFunctions",
-            false);
 
     // Flag to allow processing of resources in D8. A data resource consumer still needs to be
     // specified.
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
index 2952b30..c778df3 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramFieldSet.java
@@ -88,6 +88,10 @@
     return remove(field.getReference());
   }
 
+  public boolean remove(ProgramField field) {
+    return remove(field.getReference());
+  }
+
   public boolean removeIf(Predicate<? super ProgramField> predicate) {
     return backing.values().removeIf(predicate);
   }
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index d22f197..208d106 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.AssertionsConfiguration.AssertionTransformationScope;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
@@ -31,6 +32,7 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
@@ -46,6 +48,7 @@
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
@@ -78,6 +81,18 @@
     return jar;
   }
 
+  private Path getTestResources() throws Exception {
+    Path resourceOutput = temp.newFile("base_resources.ap_").toPath();
+    AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput);
+    return resourceOutput;
+  }
+
+  private Path getFeatureTestResources(TemporaryFolder temp) throws Exception {
+    Path resourceOutput = temp.newFile("feature_resources.ap_").toPath();
+    AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput);
+    return resourceOutput;
+  }
+
   @Test(expected = CompilationFailedException.class)
   public void emptyBuilder() throws Throwable {
     // The builder must have a program consumer.
@@ -178,6 +193,68 @@
   }
 
   @Test
+  public void passAndroidResources() throws Throwable {
+    Path working = temp.getRoot().toPath();
+    Path input = getJarWithA();
+    Path library = ToolHelper.getDefaultAndroidJar();
+    Path output = working.resolve("classes.dex");
+    Path resourceInput = getTestResources();
+    Path resourceOutput = working.resolve("resources_out.ap_");
+    assertFalse(Files.exists(output));
+    assertFalse(Files.exists(resourceOutput));
+    ProcessResult result =
+        ToolHelper.forkR8(
+            working,
+            input.toAbsolutePath().toString(),
+            "--lib",
+            library.toAbsolutePath().toString(),
+            "--android-resources",
+            resourceInput.toAbsolutePath().toString(),
+            resourceOutput.toAbsolutePath().toString(),
+            "--no-tree-shaking");
+    assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+    assertTrue(Files.exists(output));
+    System.out.println(result.stdout);
+    assertTrue(Files.exists(resourceOutput));
+  }
+
+  @Test
+  public void passFeatureResources() throws Throwable {
+    Path working = temp.getRoot().toPath();
+    Path input = getJarWithA();
+    Path inputFeature = getJarWithB();
+    Path library = ToolHelper.getDefaultAndroidJar();
+    Path output = working.resolve("classes.dex");
+    Path featureOutput = working.resolve("feature.zip");
+    Path resourceInput = getTestResources();
+    Path resourceOutput = working.resolve("resources_out.ap_");
+    TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest();
+    featureSplitTemp.create();
+    Path featureReasourceInput = getFeatureTestResources(featureSplitTemp);
+    Path featureResourceOutput = working.resolve("feature_resources_out.ap_");
+    assertFalse(Files.exists(output));
+    assertFalse(Files.exists(featureOutput));
+    String pathSeparator = File.pathSeparator;
+    ProcessResult result =
+        ToolHelper.forkR8(
+            working,
+            input.toAbsolutePath().toString(),
+            "--lib",
+            library.toAbsolutePath().toString(),
+            "--android-resources",
+            resourceInput.toAbsolutePath().toString(),
+            resourceOutput.toAbsolutePath().toString(),
+            "--feature",
+            inputFeature.toAbsolutePath() + pathSeparator + featureReasourceInput.toAbsolutePath(),
+            featureOutput.toAbsolutePath() + pathSeparator + featureResourceOutput.toAbsolutePath(),
+            "--no-tree-shaking");
+    assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+    assertTrue(Files.exists(output));
+    assertTrue(Files.exists(featureOutput));
+    assertTrue(Files.exists(resourceOutput));
+  }
+
+  @Test
   public void featureOnlyOneArgument() throws Throwable {
     Path working = temp.getRoot().toPath();
     Path input = getJarWithA();
diff --git a/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java b/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java
index d056f65..7c40462 100644
--- a/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java
+++ b/src/test/java/com/android/tools/r8/compose/NestedComposableArgumentPropagationTest.java
@@ -116,9 +116,9 @@
             .addOptionsModification(
                 options -> {
                   options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces();
-                  options.testing.enableComposableOptimizationPass = enableComposeOptimizations;
-                  options.testing.modelUnknownChangedAndDefaultArgumentsToComposableFunctions =
-                      enableComposeOptimizations;
+                  options
+                      .getJetpackComposeOptions()
+                      .enableAllOptimizations(enableComposeOptimizations);
                 })
             .setMinApi(AndroidApiLevel.N)
             .allowDiagnosticMessages()
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
index 556a914..4bab75f 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
@@ -130,7 +130,7 @@
   public void testCombineRemoveRewritten() {
     InternalOptions options = new InternalOptions();
     DexItemFactory factory = options.dexItemFactory();
-    AbstractValueFactory abstractValueFactory = new AbstractValueFactory(options);
+    AbstractValueFactory abstractValueFactory = new AbstractValueFactory();
 
     ArgumentInfoCollection.Builder builder1 = ArgumentInfoCollection.builder();
     builder1.addArgumentInfo(
diff --git a/src/test/java/com/android/tools/r8/graph/invokestatic/InvokeStaticOnInterfaceTest.java b/src/test/java/com/android/tools/r8/graph/invokestatic/InvokeStaticOnInterfaceTest.java
index aaadb51..a9686e1 100644
--- a/src/test/java/com/android/tools/r8/graph/invokestatic/InvokeStaticOnInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/graph/invokestatic/InvokeStaticOnInterfaceTest.java
@@ -83,6 +83,11 @@
             .addOptionsModification(o -> o.testing.allowInvokeErrors = true)
             .addDontObfuscate()
             .addKeepMainRule(Main.class)
+            // Without a keep rule, we conclude that I.m() is not called and remove it. That is not
+            // true, however, since there is an invalid invoke to I.m(). To preserve the error we
+            // could consider rewriting invoke to `throw new ICCE()`, as this would also ensure that
+            // the compiled program runs on all runtimes with the same behavior.
+            .addKeepClassAndMembersRules(I.class)
             .run(parameters.getRuntime(), Main.class);
     if (parameters.getRuntime().asCf().isNewerThan(CfVm.JDK8)) {
       runResult.assertFailureWithErrorThatMatches(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java
new file mode 100644
index 0000000..e1074d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/RestartLambdaPropagationTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.callsites;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 RestartLambdaPropagationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject restartableMethodSubject =
+                  mainClassSubject.uniqueMethodWithOriginalName("restartableMethod");
+              assertThat(restartableMethodSubject, isPresent());
+              assertEquals(
+                  parameters.isDexRuntime() ? 1 : 2,
+                  restartableMethodSubject.getParameters().size());
+              assertEquals(
+                  parameters.isDexRuntime(),
+                  restartableMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstNumber(42)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Postponing!", "Restarting!", "42", "Stopping!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Runnable restarter = restartableMethod(true, 42);
+      restarter.run();
+    }
+
+    @NeverInline
+    static Runnable restartableMethod(boolean doRestart, int flags) {
+      if (doRestart) {
+        System.out.println("Postponing!");
+        return () -> {
+          System.out.println("Restarting!");
+          Runnable restarter = restartableMethod(false, flags);
+          if (restarter == null) {
+            System.out.println("Stopping!");
+          } else {
+            throw new RuntimeException();
+          }
+        };
+      }
+      System.out.println(flags);
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SwitchWithSimpleCasesInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SwitchWithSimpleCasesInliningTest.java
new file mode 100644
index 0000000..44eb259
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/SwitchWithSimpleCasesInliningTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.inliner.conditionalsimpleinlining;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 SwitchWithSimpleCasesInliningTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject mainMethodSubject = mainClassSubject.mainMethod();
+              assertThat(mainMethodSubject, isPresent());
+              // TODO(b/331337747): Should be true.
+              assertFalse(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .filter(InstructionSubject::isConstString)
+                      .allMatch(i -> i.isConstString("O")));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "true", "false", "true", "true", "false", "true", "false", "false", "false", "false",
+            "true", "true", "false", "true", "false");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // Known.
+      System.out.println(isEnabled("A"));
+      System.out.println(isEnabled("B"));
+      System.out.println(isEnabled("C"));
+      System.out.println(isEnabled("D"));
+      System.out.println(isEnabled("E"));
+      System.out.println(isEnabled("F"));
+      System.out.println(isEnabled("G"));
+      System.out.println(isEnabled("H"));
+      System.out.println(isEnabled("I"));
+      System.out.println(isEnabled("J"));
+      System.out.println(isEnabled("K"));
+      System.out.println(isEnabled("L"));
+      System.out.println(isEnabled("M"));
+      System.out.println(isEnabled("N"));
+      // Unknown.
+      System.out.println(isEnabled("O"));
+    }
+
+    public static boolean isEnabled(String feature) {
+      switch (feature) {
+        case "A":
+          return true;
+        case "B":
+          return false;
+        case "C":
+          return true;
+        case "D":
+          return true;
+        case "E":
+          return false;
+        case "F":
+          return true;
+        case "G":
+          return false;
+        case "H":
+          return false;
+        case "I":
+          return false;
+        case "J":
+          return false;
+        case "K":
+          return true;
+        case "L":
+          return true;
+        case "M":
+          return false;
+        case "N":
+          return true;
+        default:
+          return hasProperty(feature);
+      }
+    }
+
+    @NeverInline
+    public static boolean hasProperty(String property) {
+      return System.getProperty(property) != null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/BottomFieldStatePropagationTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/BottomFieldStatePropagationTest.java
new file mode 100644
index 0000000..d484c87
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/BottomFieldStatePropagationTest.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import 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 BottomFieldStatePropagationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableConstantArgumentAnnotations()
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Builder builder = alwaysFalse() ? new Builder(args.length) : null;
+      test(builder);
+    }
+
+    static boolean alwaysFalse() {
+      return false;
+    }
+
+    @KeepConstantArguments
+    @NeverInline
+    static void test(Builder builder) {
+      // Argument propagation should prove that the field access is unreachable due to the field
+      // value being bottom.
+      unreachable(builder.f);
+    }
+
+    @KeepConstantArguments
+    @NeverInline
+    static void unreachable(int i) {
+      System.out.println(i);
+    }
+  }
+
+  static class Builder {
+
+    int f;
+
+    Builder(int f) {
+      this.f = f;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/FieldStateArgumentPropagationTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/FieldStateArgumentPropagationTest.java
new file mode 100644
index 0000000..6272631
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/FieldStateArgumentPropagationTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 FieldStateArgumentPropagationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              options.enableFieldAssignmentTracker = false;
+              options.enableFieldValueAnalysis = false;
+            })
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject printMethodSubject =
+                  mainClassSubject.uniqueMethodWithOriginalName("print");
+              assertThat(printMethodSubject, isPresent());
+              assertEquals(0, printMethodSubject.getProgramMethod().getArity());
+
+              MethodSubject printlnMethodSubject =
+                  mainClassSubject.uniqueMethodWithOriginalName("println");
+              assertThat(printlnMethodSubject, isPresent());
+              assertEquals(0, printlnMethodSubject.getProgramMethod().getArity());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      print(Greetings.HELLO.f);
+      println(Greetings.WORLD.f);
+    }
+
+    @NeverInline
+    static void print(String message) {
+      System.out.print(message);
+    }
+
+    @NeverInline
+    static void println(String message) {
+      System.out.println(message);
+    }
+  }
+
+  static class Greetings {
+
+    static final Greetings HELLO;
+    static final Greetings WORLD;
+
+    static {
+      HELLO = new Greetings("Hello");
+      WORLD = new Greetings(", world!");
+    }
+
+    final String f;
+
+    Greetings(String f) {
+      this.f = f;
+    }
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index c74d4d5..eb5b8e2 100644
--- a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -31,6 +31,7 @@
 import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -41,6 +42,8 @@
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
@@ -103,6 +106,20 @@
     return dollarIndex > 0 && name.charAt(dollarIndex - 1) == 'R';
   }
 
+  public static void writePrecompiledManifestAndResourcePB(Path resourceOutput) {
+    try (ZipOutputStream out =
+        new ZipOutputStream(
+            Files.newOutputStream(
+                resourceOutput, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
+      ZipUtils.writeToZipStream(
+          out, "AndroidManifest.xml", CompiledProto.SIMPLE_MANIFEST, ZipEntry.STORED);
+      ZipUtils.writeToZipStream(
+          out, "resources.pb", CompiledProto.SIMPLE_RESOURCE_TABLE, ZipEntry.STORED);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   public static String SIMPLE_MANIFEST_WITH_APP_NAME =
       "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
           + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
@@ -368,7 +385,7 @@
       return this;
     }
 
-    AndroidTestResourceBuilder setPackageId(int packageId) {
+    public AndroidTestResourceBuilder setPackageId(int packageId) {
       this.packageId = packageId;
       return this;
     }
diff --git a/src/test/testbase/java/com/android/tools/r8/androidresources/CompiledProto.java b/src/test/testbase/java/com/android/tools/r8/androidresources/CompiledProto.java
new file mode 100644
index 0000000..9cda6e0
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/androidresources/CompiledProto.java
@@ -0,0 +1,814 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+public class CompiledProto {
+
+  // A simple resource table with just the string app_name referenced from the manifest below
+  public static final byte[] SIMPLE_RESOURCE_TABLE =
+      new byte[] {
+        (byte) 10,
+        (byte) 98,
+        (byte) 10,
+        (byte) 96,
+        (byte) 1,
+        (byte) 0,
+        (byte) 28,
+        (byte) 0,
+        (byte) 96,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 2,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 1,
+        (byte) 0,
+        (byte) 0,
+        (byte) 36,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 3,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 52,
+        (byte) 52,
+        (byte) 47,
+        (byte) 116,
+        (byte) 109,
+        (byte) 112,
+        (byte) 47,
+        (byte) 106,
+        (byte) 117,
+        (byte) 110,
+        (byte) 105,
+        (byte) 116,
+        (byte) 53,
+        (byte) 57,
+        (byte) 53,
+        (byte) 50,
+        (byte) 50,
+        (byte) 51,
+        (byte) 52,
+        (byte) 52,
+        (byte) 55,
+        (byte) 54,
+        (byte) 56,
+        (byte) 51,
+        (byte) 57,
+        (byte) 49,
+        (byte) 56,
+        (byte) 53,
+        (byte) 53,
+        (byte) 53,
+        (byte) 53,
+        (byte) 47,
+        (byte) 114,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 118,
+        (byte) 97,
+        (byte) 108,
+        (byte) 117,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 115,
+        (byte) 116,
+        (byte) 114,
+        (byte) 105,
+        (byte) 110,
+        (byte) 103,
+        (byte) 115,
+        (byte) 46,
+        (byte) 120,
+        (byte) 109,
+        (byte) 108,
+        (byte) 0,
+        (byte) 0,
+        (byte) 0,
+        (byte) 18,
+        (byte) 102,
+        (byte) 10,
+        (byte) 2,
+        (byte) 8,
+        (byte) 127,
+        (byte) 18,
+        (byte) 20,
+        (byte) 116,
+        (byte) 104,
+        (byte) 101,
+        (byte) 112,
+        (byte) 97,
+        (byte) 99,
+        (byte) 107,
+        (byte) 97,
+        (byte) 103,
+        (byte) 101,
+        (byte) 49,
+        (byte) 50,
+        (byte) 55,
+        (byte) 46,
+        (byte) 102,
+        (byte) 111,
+        (byte) 111,
+        (byte) 98,
+        (byte) 97,
+        (byte) 114,
+        (byte) 26,
+        (byte) 74,
+        (byte) 10,
+        (byte) 2,
+        (byte) 8,
+        (byte) 1,
+        (byte) 18,
+        (byte) 6,
+        (byte) 115,
+        (byte) 116,
+        (byte) 114,
+        (byte) 105,
+        (byte) 110,
+        (byte) 103,
+        (byte) 26,
+        (byte) 60,
+        (byte) 10,
+        (byte) 0,
+        (byte) 18,
+        (byte) 8,
+        (byte) 97,
+        (byte) 112,
+        (byte) 112,
+        (byte) 95,
+        (byte) 110,
+        (byte) 97,
+        (byte) 109,
+        (byte) 101,
+        (byte) 26,
+        (byte) 2,
+        (byte) 18,
+        (byte) 0,
+        (byte) 50,
+        (byte) 42,
+        (byte) 10,
+        (byte) 0,
+        (byte) 18,
+        (byte) 38,
+        (byte) 10,
+        (byte) 6,
+        (byte) 8,
+        (byte) 1,
+        (byte) 18,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2,
+        (byte) 34,
+        (byte) 28,
+        (byte) 18,
+        (byte) 26,
+        (byte) 10,
+        (byte) 24,
+        (byte) 77,
+        (byte) 111,
+        (byte) 115,
+        (byte) 116,
+        (byte) 32,
+        (byte) 105,
+        (byte) 109,
+        (byte) 112,
+        (byte) 111,
+        (byte) 114,
+        (byte) 116,
+        (byte) 97,
+        (byte) 110,
+        (byte) 116,
+        (byte) 32,
+        (byte) 97,
+        (byte) 112,
+        (byte) 112,
+        (byte) 32,
+        (byte) 101,
+        (byte) 118,
+        (byte) 101,
+        (byte) 114,
+        (byte) 46,
+        (byte) 34,
+        (byte) 51,
+        (byte) 10,
+        (byte) 35,
+        (byte) 65,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 32,
+        (byte) 65,
+        (byte) 115,
+        (byte) 115,
+        (byte) 101,
+        (byte) 116,
+        (byte) 32,
+        (byte) 80,
+        (byte) 97,
+        (byte) 99,
+        (byte) 107,
+        (byte) 97,
+        (byte) 103,
+        (byte) 105,
+        (byte) 110,
+        (byte) 103,
+        (byte) 32,
+        (byte) 84,
+        (byte) 111,
+        (byte) 111,
+        (byte) 108,
+        (byte) 32,
+        (byte) 40,
+        (byte) 97,
+        (byte) 97,
+        (byte) 112,
+        (byte) 116,
+        (byte) 41,
+        (byte) 18,
+        (byte) 12,
+        (byte) 50,
+        (byte) 46,
+        (byte) 49,
+        (byte) 57,
+        (byte) 45,
+        (byte) 57,
+        (byte) 50,
+        (byte) 56,
+        (byte) 57,
+        (byte) 51,
+        (byte) 53,
+        (byte) 56
+      };
+
+  // A simple manifest with just the app name embedded (referenced through the resource table)
+  // "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+  //    "<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  //         package="com.android.tools.r8">
+  //        <application android:label="@string/app_name">
+  //        </application>
+  //    "</manifest>"
+  public static final byte[] SIMPLE_MANIFEST =
+      new byte[] {
+        (byte) 10,
+        (byte) -113,
+        (byte) 4,
+        (byte) 10,
+        (byte) 57,
+        (byte) 10,
+        (byte) 7,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 18,
+        (byte) 42,
+        (byte) 104,
+        (byte) 116,
+        (byte) 116,
+        (byte) 112,
+        (byte) 58,
+        (byte) 47,
+        (byte) 47,
+        (byte) 115,
+        (byte) 99,
+        (byte) 104,
+        (byte) 101,
+        (byte) 109,
+        (byte) 97,
+        (byte) 115,
+        (byte) 46,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 46,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 107,
+        (byte) 47,
+        (byte) 114,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 26,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2,
+        (byte) 26,
+        (byte) 8,
+        (byte) 109,
+        (byte) 97,
+        (byte) 110,
+        (byte) 105,
+        (byte) 102,
+        (byte) 101,
+        (byte) 115,
+        (byte) 116,
+        (byte) 34,
+        (byte) 31,
+        (byte) 18,
+        (byte) 7,
+        (byte) 112,
+        (byte) 97,
+        (byte) 99,
+        (byte) 107,
+        (byte) 97,
+        (byte) 103,
+        (byte) 101,
+        (byte) 26,
+        (byte) 20,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 46,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 46,
+        (byte) 116,
+        (byte) 111,
+        (byte) 111,
+        (byte) 108,
+        (byte) 115,
+        (byte) 46,
+        (byte) 114,
+        (byte) 56,
+        (byte) 34,
+        (byte) 82,
+        (byte) 10,
+        (byte) 42,
+        (byte) 104,
+        (byte) 116,
+        (byte) 116,
+        (byte) 112,
+        (byte) 58,
+        (byte) 47,
+        (byte) 47,
+        (byte) 115,
+        (byte) 99,
+        (byte) 104,
+        (byte) 101,
+        (byte) 109,
+        (byte) 97,
+        (byte) 115,
+        (byte) 46,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 46,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 107,
+        (byte) 47,
+        (byte) 114,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 18,
+        (byte) 17,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 112,
+        (byte) 105,
+        (byte) 108,
+        (byte) 101,
+        (byte) 83,
+        (byte) 100,
+        (byte) 107,
+        (byte) 86,
+        (byte) 101,
+        (byte) 114,
+        (byte) 115,
+        (byte) 105,
+        (byte) 111,
+        (byte) 110,
+        (byte) 26,
+        (byte) 2,
+        (byte) 51,
+        (byte) 49,
+        (byte) 34,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2,
+        (byte) 40,
+        (byte) -14,
+        (byte) -118,
+        (byte) -124,
+        (byte) 8,
+        (byte) 50,
+        (byte) 4,
+        (byte) 58,
+        (byte) 2,
+        (byte) 48,
+        (byte) 31,
+        (byte) 34,
+        (byte) 40,
+        (byte) 18,
+        (byte) 24,
+        (byte) 112,
+        (byte) 108,
+        (byte) 97,
+        (byte) 116,
+        (byte) 102,
+        (byte) 111,
+        (byte) 114,
+        (byte) 109,
+        (byte) 66,
+        (byte) 117,
+        (byte) 105,
+        (byte) 108,
+        (byte) 100,
+        (byte) 86,
+        (byte) 101,
+        (byte) 114,
+        (byte) 115,
+        (byte) 105,
+        (byte) 111,
+        (byte) 110,
+        (byte) 67,
+        (byte) 111,
+        (byte) 100,
+        (byte) 101,
+        (byte) 26,
+        (byte) 2,
+        (byte) 51,
+        (byte) 49,
+        (byte) 34,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2,
+        (byte) 50,
+        (byte) 4,
+        (byte) 58,
+        (byte) 2,
+        (byte) 48,
+        (byte) 31,
+        (byte) 34,
+        (byte) 80,
+        (byte) 10,
+        (byte) 42,
+        (byte) 104,
+        (byte) 116,
+        (byte) 116,
+        (byte) 112,
+        (byte) 58,
+        (byte) 47,
+        (byte) 47,
+        (byte) 115,
+        (byte) 99,
+        (byte) 104,
+        (byte) 101,
+        (byte) 109,
+        (byte) 97,
+        (byte) 115,
+        (byte) 46,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 46,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 107,
+        (byte) 47,
+        (byte) 114,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 18,
+        (byte) 25,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 112,
+        (byte) 105,
+        (byte) 108,
+        (byte) 101,
+        (byte) 83,
+        (byte) 100,
+        (byte) 107,
+        (byte) 86,
+        (byte) 101,
+        (byte) 114,
+        (byte) 115,
+        (byte) 105,
+        (byte) 111,
+        (byte) 110,
+        (byte) 67,
+        (byte) 111,
+        (byte) 100,
+        (byte) 101,
+        (byte) 110,
+        (byte) 97,
+        (byte) 109,
+        (byte) 101,
+        (byte) 26,
+        (byte) 2,
+        (byte) 49,
+        (byte) 50,
+        (byte) 40,
+        (byte) -13,
+        (byte) -118,
+        (byte) -124,
+        (byte) 8,
+        (byte) 34,
+        (byte) 40,
+        (byte) 18,
+        (byte) 24,
+        (byte) 112,
+        (byte) 108,
+        (byte) 97,
+        (byte) 116,
+        (byte) 102,
+        (byte) 111,
+        (byte) 114,
+        (byte) 109,
+        (byte) 66,
+        (byte) 117,
+        (byte) 105,
+        (byte) 108,
+        (byte) 100,
+        (byte) 86,
+        (byte) 101,
+        (byte) 114,
+        (byte) 115,
+        (byte) 105,
+        (byte) 111,
+        (byte) 110,
+        (byte) 78,
+        (byte) 97,
+        (byte) 109,
+        (byte) 101,
+        (byte) 26,
+        (byte) 2,
+        (byte) 49,
+        (byte) 50,
+        (byte) 34,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2,
+        (byte) 50,
+        (byte) 4,
+        (byte) 58,
+        (byte) 2,
+        (byte) 48,
+        (byte) 12,
+        (byte) 42,
+        (byte) 13,
+        (byte) 18,
+        (byte) 5,
+        (byte) 10,
+        (byte) 32,
+        (byte) 32,
+        (byte) 32,
+        (byte) 32,
+        (byte) 26,
+        (byte) 4,
+        (byte) 8,
+        (byte) 3,
+        (byte) 16,
+        (byte) 41,
+        (byte) 42,
+        (byte) -110,
+        (byte) 1,
+        (byte) 10,
+        (byte) -119,
+        (byte) 1,
+        (byte) 26,
+        (byte) 11,
+        (byte) 97,
+        (byte) 112,
+        (byte) 112,
+        (byte) 108,
+        (byte) 105,
+        (byte) 99,
+        (byte) 97,
+        (byte) 116,
+        (byte) 105,
+        (byte) 111,
+        (byte) 110,
+        (byte) 34,
+        (byte) 107,
+        (byte) 10,
+        (byte) 42,
+        (byte) 104,
+        (byte) 116,
+        (byte) 116,
+        (byte) 112,
+        (byte) 58,
+        (byte) 47,
+        (byte) 47,
+        (byte) 115,
+        (byte) 99,
+        (byte) 104,
+        (byte) 101,
+        (byte) 109,
+        (byte) 97,
+        (byte) 115,
+        (byte) 46,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 46,
+        (byte) 99,
+        (byte) 111,
+        (byte) 109,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 107,
+        (byte) 47,
+        (byte) 114,
+        (byte) 101,
+        (byte) 115,
+        (byte) 47,
+        (byte) 97,
+        (byte) 110,
+        (byte) 100,
+        (byte) 114,
+        (byte) 111,
+        (byte) 105,
+        (byte) 100,
+        (byte) 18,
+        (byte) 5,
+        (byte) 108,
+        (byte) 97,
+        (byte) 98,
+        (byte) 101,
+        (byte) 108,
+        (byte) 26,
+        (byte) 16,
+        (byte) 64,
+        (byte) 115,
+        (byte) 116,
+        (byte) 114,
+        (byte) 105,
+        (byte) 110,
+        (byte) 103,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 112,
+        (byte) 95,
+        (byte) 110,
+        (byte) 97,
+        (byte) 109,
+        (byte) 101,
+        (byte) 34,
+        (byte) 2,
+        (byte) 8,
+        (byte) 4,
+        (byte) 40,
+        (byte) -127,
+        (byte) -128,
+        (byte) -124,
+        (byte) 8,
+        (byte) 50,
+        (byte) 27,
+        (byte) 10,
+        (byte) 25,
+        (byte) 16,
+        (byte) -128,
+        (byte) -128,
+        (byte) -124,
+        (byte) -8,
+        (byte) 7,
+        (byte) 26,
+        (byte) 15,
+        (byte) 115,
+        (byte) 116,
+        (byte) 114,
+        (byte) 105,
+        (byte) 110,
+        (byte) 103,
+        (byte) 47,
+        (byte) 97,
+        (byte) 112,
+        (byte) 112,
+        (byte) 95,
+        (byte) 110,
+        (byte) 97,
+        (byte) 109,
+        (byte) 101,
+        (byte) 48,
+        (byte) 3,
+        (byte) 42,
+        (byte) 13,
+        (byte) 18,
+        (byte) 5,
+        (byte) 10,
+        (byte) 32,
+        (byte) 32,
+        (byte) 32,
+        (byte) 32,
+        (byte) 26,
+        (byte) 4,
+        (byte) 8,
+        (byte) 4,
+        (byte) 16,
+        (byte) 50,
+        (byte) 26,
+        (byte) 4,
+        (byte) 8,
+        (byte) 4,
+        (byte) 16,
+        (byte) 4,
+        (byte) 42,
+        (byte) 9,
+        (byte) 18,
+        (byte) 1,
+        (byte) 10,
+        (byte) 26,
+        (byte) 4,
+        (byte) 8,
+        (byte) 5,
+        (byte) 16,
+        (byte) 18,
+        (byte) 26,
+        (byte) 2,
+        (byte) 8,
+        (byte) 2
+      };
+}
diff --git a/tools/test.py b/tools/test.py
index 4ffed63..9805e8e 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -516,6 +516,9 @@
             ))
     rotate_test_reports()
 
+    if options.print_times:
+        gradle_args.append('-Pprint_times=true')
+
     # Now run tests on selected runtime(s).
     if options.runtimes:
         if options.dex_vm != 'default':
@@ -548,8 +551,6 @@
     # Legacy testing populates the runtimes based on dex_vm.
     vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
 
-    if options.print_times:
-        gradle_args.append('-Pprint_times=true')
     for art_vm in vms_to_test:
         vm_suffix = "_" + options.dex_vm_kind if art_vm != "default" else ""
         runtimes = ['dex-' + art_vm]