Interleave argument and field propagation

Bug: b/296030319
Change-Id: Ic0d3c149c5903bac51279ad4c0ca659b4430056f
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/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/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 135a011..058be03 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;
@@ -89,8 +90,11 @@
           // Disable argument propagation for methods that should not be optimized by setting their
           // method state to unknown.
           new ArgumentPropagatorUnoptimizableFieldsAndMethods(
-                  appView, immediateSubtypingInfo, codeScanner.getMethodStates())
-              .initializeUnoptimizableMethodStates(classes);
+                  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.
@@ -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, 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..7cfd03a 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
@@ -11,18 +11,21 @@
 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.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;
@@ -36,10 +39,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.FieldValue;
+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.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 +57,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;
@@ -61,6 +72,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 +82,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 +98,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 +130,10 @@
     virtualRootMethods.putAll(extension);
   }
 
+  public FieldStateCollection getFieldStates() {
+    return fieldStates;
+  }
+
   public MethodStateCollectionByReference getMethodStates() {
     return methodStates;
   }
@@ -118,6 +142,10 @@
     return virtualRootMethods.get(method.getReference());
   }
 
+  private boolean isFieldValueAlreadyUnknown(ProgramField field) {
+    return fieldStates.get(field).isUnknown();
+  }
+
   protected boolean isMethodParameterAlreadyUnknown(
       MethodParameter methodParameter, ProgramMethod method) {
     MethodState methodState =
@@ -153,17 +181,163 @@
       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) {
+    Value valueRoot = fieldPut.value().getAliasedValue(aliasedValueConfiguration);
+    InFlow inFlow = null;
+    if (valueRoot.isArgument()) {
+      // 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 current field.
+      MethodParameter inParameter =
+          methodParameterFactory.create(context, valueRoot.getDefinition().asArgument().getIndex());
+      if (isMethodParameterAlreadyUnknown(inParameter, context)) {
+        return ValueState.unknown();
+      }
+      inFlow = inParameter;
+    } else if (valueRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+      FieldGet fieldGet = valueRoot.getDefinition().asFieldGet();
+      ProgramField otherField = fieldGet.resolveField(appView, context).getProgramField();
+      if (otherField != null) {
+        FieldValue fieldValue = fieldValueFactory.create(otherField);
+        if (isFieldValueAlreadyUnknown(otherField)) {
+          return ValueState.unknown();
+        }
+        inFlow = fieldValue;
+      }
+    }
+
+    if (inFlow != null) {
+      return ConcreteValueState.create(field.getType(), inFlow);
+    }
+
+    if (field.getType().isArrayType()) {
+      Nullability nullability = fieldPut.value().getType().nullability();
+      return ConcreteArrayTypeValueState.create(nullability);
+    }
+
+    AbstractValue abstractValue = abstractValueSupplier.getAbstractValue(fieldPut.value());
+    if (abstractValue.isUnknown()) {
+      // TODO(b/296030319): Add the current object state to the computed fallback abstract value.
+      abstractValue = getFallbackAbstractValueForField(field);
+    }
+    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);
+    }
+  }
+
+  // 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);
+    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) {
+    if (field.getAccessFlags().isFinal() && field.getAccessFlags().isStatic()) {
+      return appView
+          .abstractValueFactory()
+          .createSingleFieldValue(field.getReference(), ObjectState.empty());
+    }
+    return AbstractValue.unknown();
+  }
+
+  private void scan(
       InvokeMethod invoke,
       AbstractValueSupplier abstractValueSupplier,
       ProgramMethod context,
@@ -182,10 +356,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 +480,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.
@@ -517,6 +687,7 @@
     // 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.
+    InFlow inFlow = null;
     if (argumentRoot.isArgument()) {
       MethodParameter forwardedParameter =
           methodParameterFactory.create(
@@ -524,43 +695,47 @@
       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);
+      inFlow = forwardedParameter;
+    } else if (argumentRoot.isDefinedByInstructionSatisfying(Instruction::isFieldGet)) {
+      // If the value is defined by a program field read, then record a flow constraint that
+      // specifies that all values that flow into the field also flows into the current parameter of
+      // this invoke instruction.
+      FieldGet fieldGet = argumentRoot.getDefinition().asFieldGet();
+      ProgramField field = fieldGet.resolveField(appView, context).getProgramField();
+      if (field != null) {
+        if (isFieldValueAlreadyUnknown(field)) {
+          return ValueState.unknown();
+        }
+        inFlow = fieldValueFactory.create(field);
       }
     }
 
+    if (inFlow != null) {
+      return ConcreteValueState.create(parameterType, inFlow);
+    }
+
     // 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/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
index eec2d32..315c9f7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableFieldsAndMethods.java
@@ -7,10 +7,14 @@
 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;
@@ -20,20 +24,40 @@
 
   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.
-  public void initializeUnoptimizableMethodStates(
+  private void initializeUnoptimizableMethodStates(
       Collection<DexProgramClass> stronglyConnectedComponent) {
     ProgramMethodSet unoptimizableVirtualMethods =
         MethodOverridesCollector.findAllMethodsAndOverridesThatMatches(
@@ -47,26 +71,35 @@
                     && !method.getAccessFlags().isFinal()) {
                   return true;
                 } else {
-                  disableArgumentPropagationForMethod(method);
+                  disableValuePropagationForMethodParameters(method);
                 }
               }
               return false;
             });
-    unoptimizableVirtualMethods.forEach(this::disableArgumentPropagationForMethod);
+    unoptimizableVirtualMethods.forEach(this::disableValuePropagationForMethodParameters);
   }
 
-  private void disableArgumentPropagationForMethod(ProgramMethod method) {
+  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();
-    AppInfoWithLiveness appInfo = appView.appInfo();
     InternalOptions options = appView.options();
     return method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
-        || !appInfo.getKeepInfo().getMethodInfo(method).isArgumentPropagationAllowed(options);
+        || !appView.getKeepInfo(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..5d353f9 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
@@ -5,7 +5,41 @@
 
 public interface AbstractFunction extends InFlow {
 
+  static IdentityAbstractFunction identity() {
+    return IdentityAbstractFunction.get();
+  }
+
   static UnknownAbstractFunction unknown() {
     return UnknownAbstractFunction.get();
   }
+
+  /**
+   * Applies the current abstract function to the given {@param state}.
+   *
+   * <p>It is guaranteed by the caller that the given {@param state} is the abstract state for the
+   * field or parameter this function depends on, i.e., the node returned by {@link
+   * #getBaseInFlow()}.
+   */
+  NonEmptyValueState apply(ConcreteValueState state);
+
+  /**
+   * Returns the (single) program field or parameter graph node that this function depends on. Upon
+   * any change to the abstract state of this graph node this abstract function must be
+   * re-evaluated.
+   */
+  InFlow 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/ConcreteValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteValueState.java
index f05f9e6..5bc8659 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
@@ -11,7 +11,6 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.function.Function;
 
 public abstract class ConcreteValueState extends NonEmptyValueState {
 
@@ -28,6 +27,17 @@
     this.inFlow = inFlow;
   }
 
+  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 abstract ValueState clearInFlow();
 
   void internalClearInFlow() {
@@ -68,16 +78,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..d323112
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldStateCollection.java
@@ -0,0 +1,94 @@
+// 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 void set(ProgramField field, NonEmptyValueState state) {
+    fieldStates.put(field, state);
+  }
+}
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..24ad150
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
@@ -0,0 +1,49 @@
+// 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 InFlow {
+
+  private final DexField field;
+
+  public FieldValue(DexField field) {
+    this.field = field;
+  }
+
+  public DexField getField() {
+    return field;
+  }
+
+  @Override
+  public boolean isFieldValue() {
+    return true;
+  }
+
+  @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/IdentityAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
new file mode 100644
index 0000000..de16de1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
@@ -0,0 +1,32 @@
+// 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;
+
+public class IdentityAbstractFunction implements AbstractFunction {
+
+  private static final IdentityAbstractFunction INSTANCE = new IdentityAbstractFunction();
+
+  private IdentityAbstractFunction() {}
+
+  static IdentityAbstractFunction get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public NonEmptyValueState apply(ConcreteValueState state) {
+    return state;
+  }
+
+  @Override
+  public InFlow 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..d858bef 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,26 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.optimize.compose.UpdateChangedFlagsAbstractFunction;
+
 public interface InFlow {
 
+  default boolean isAbstractFunction() {
+    return false;
+  }
+
+  default AbstractFunction asAbstractFunction() {
+    return null;
+  }
+
+  default boolean isFieldValue() {
+    return false;
+  }
+
+  default FieldValue asFieldValue() {
+    return null;
+  }
+
   default boolean isMethodParameter() {
     return false;
   }
@@ -12,4 +30,12 @@
   default MethodParameter asMethodParameter() {
     return null;
   }
+
+  default boolean isUpdateChangedFlagsAbstractFunction() {
+    return false;
+  }
+
+  default UpdateChangedFlagsAbstractFunction asUpdateChangedFlagsAbstractFunction() {
+    return null;
+  }
 }
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..e450a76
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
@@ -0,0 +1,56 @@
+// 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 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 InFlow inFlow;
+  public final long constant;
+
+  public OrAbstractFunction(InFlow inFlow, long constant) {
+    this.inFlow = inFlow;
+    this.constant = constant;
+  }
+
+  @Override
+  public NonEmptyValueState apply(ConcreteValueState state) {
+    // TODO(b/302483644): Implement this abstract function to allow correct value propagation of
+    //  updateChangedFlags(x | 1).
+    return state;
+  }
+
+  @Override
+  public InFlow getBaseInFlow() {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().getBaseInFlow();
+    }
+    assert inFlow.isFieldValue() || inFlow.isMethodParameter();
+    return 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 == 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..8ac1736 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,8 @@
 // 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;
+
 public class UnknownAbstractFunction implements AbstractFunction {
 
   private static final UnknownAbstractFunction INSTANCE = new UnknownAbstractFunction();
@@ -12,4 +14,14 @@
   static UnknownAbstractFunction get() {
     return INSTANCE;
   }
+
+  @Override
+  public NonEmptyValueState apply(ConcreteValueState state) {
+    return ValueState.unknown();
+  }
+
+  @Override
+  public InFlow getBaseInFlow() {
+    throw new Unreachable();
+  }
 }
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/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
new file mode 100644
index 0000000..542b17e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -0,0 +1,221 @@
+// 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.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FlowGraph;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.Node;
+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.ThreadUtils;
+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 List<FlowGraph> flowGraphs;
+
+  public DefaultFieldValueJoiner(AppView<AppInfoWithLiveness> appView, List<FlowGraph> flowGraphs) {
+    this.appView = appView;
+    this.flowGraphs = flowGraphs;
+  }
+
+  public Collection<Deque<Node>> 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();
+    analyzeInstanceInitializers(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 (FlowGraph flowGraph : flowGraphs) {
+      // TODO(b/296030319): 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.
+      flowGraph.forEachFieldNode(
+          node -> {
+            ProgramField field = node.getField();
+            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 analyzeInstanceInitializers(
+      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 Collection<Deque<Node>> updateFlowGraphs(
+      ProgramFieldSet fieldsWithLiveDefaultValue, ExecutorService executorService)
+      throws ExecutionException {
+    return ThreadUtils.processItemsWithResultsThatMatches(
+        flowGraphs,
+        flowGraph -> {
+          Deque<Node> worklist = new ArrayDeque<>();
+          flowGraph.forEachFieldNode(
+              node -> {
+                ProgramField field = node.getField();
+                if (fieldsWithLiveDefaultValue.contains(field)) {
+                  node.addDefaultValue(
+                      appView,
+                      () -> {
+                        if (node.isUnknown()) {
+                          node.clearPredecessors();
+                        }
+                        node.addToWorkList(worklist);
+                      });
+                }
+              });
+          return worklist;
+        },
+        worklist -> !worklist.isEmpty(),
+        appView.options().getThreadingModule(),
+        executorService);
+  }
+}
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/FlowGraphWriter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
new file mode 100644
index 0000000..7b438c4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphWriter.java
@@ -0,0 +1,77 @@
+// 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.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FieldNode;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.FlowGraph;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.Node;
+import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator.ParameterNode;
+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, Node 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, Node node) {
+    if (node.isFieldNode()) {
+      writeFieldNodeLabel(out, node.asFieldNode());
+    } else {
+      assert node.isParameterNode();
+      writeParameterNodeLabel(out, node.asParameterNode());
+    }
+  }
+
+  private void writeFieldNodeLabel(PrintStream out, FieldNode 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, ParameterNode 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..27fcb12 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,10 +1,10 @@
-// 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.graph.ProgramField.asProgramFieldOrNull;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.errors.Unreachable;
@@ -12,11 +12,23 @@
 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.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.conversion.IRConverter;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
+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.ConcreteMethodState;
 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.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;
@@ -27,49 +39,63 @@
 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.android.tools.r8.utils.collections.ProgramFieldMap;
 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.HashSet;
 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.BiConsumer;
+import java.util.function.BiPredicate;
 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.
+    Collection<Deque<Node>> worklists =
+        includeDefaultValuesInFieldStates(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
@@ -77,10 +103,40 @@
     postProcessMethodStates(executorService);
   }
 
-  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 = new FlowGraph(appView.appInfo().classes());
+    List<Set<Node>> stronglyConnectedComponents = flowGraph.computeStronglyConnectedComponents();
+    return ListUtils.map(stronglyConnectedComponents, FlowGraph::new);
+  }
 
+  private Collection<Deque<Node>> includeDefaultValuesInFieldStates(
+      List<FlowGraph> flowGraphs, ExecutorService executorService) throws ExecutionException {
+    DefaultFieldValueJoiner joiner = new DefaultFieldValueJoiner(appView, 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(Collection<Deque<Node>> worklists, ExecutorService executorService)
+      throws ExecutionException {
+    ThreadUtils.processItems(
+        worklists, this::process, appView.options().getThreadingModule(), executorService);
+  }
+
+  private void process(FlowGraph flowGraph) {
+    // Build a worklist containing all the nodes.
+    Deque<Node> worklist = new ArrayDeque<>();
+    flowGraph.forEachNode(worklist::add);
+    process(worklist);
+  }
+
+  private void process(Deque<Node> 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 +144,56 @@
     //  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();
-            }
-          });
+      Node node = worklist.removeLast();
+      node.unsetInWorklist();
+      propagate(node, worklist);
     }
   }
 
-  private void propagate(
-      ParameterNode parameterNode, Consumer<ParameterNode> affectedNodeConsumer) {
-    ValueState parameterState = parameterNode.getState();
-    if (parameterState.isBottom()) {
+  private void propagate(Node node, Deque<Node> 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 (Node successorNode : node.getSuccessors()) {
+        assert !successorNode.isUnknown();
+        successorNode.clearPredecessors(node);
+        successorNode.setStateToUnknown();
+        successorNode.addToWorkList(worklist);
       }
+      node.clearDanglingSuccessors();
+    } else {
+      propagateNode(node, worklist);
     }
-    for (ParameterNode newlyUnknownParameterNode : newlyUnknownParameterNodes) {
-      newlyUnknownParameterNode.clearPredecessors();
-    }
+  }
+
+  private void propagateNode(Node node, Deque<Node> worklist) {
+    ConcreteValueState state = node.getState().asConcrete();
+    node.removeSuccessorIf(
+        (successorNode, transferFunctions) -> {
+          assert !successorNode.isUnknown();
+          for (AbstractFunction transferFunction : transferFunctions) {
+            NonEmptyValueState transferState = transferFunction.apply(state);
+            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,30 +222,85 @@
     }
   }
 
-  public class FlowGraph extends BidirectedGraph<ParameterNode> {
+  public class FlowGraph extends BidirectedGraph<Node> {
 
-    private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> nodes = new IdentityHashMap<>();
+    private final ProgramFieldMap<FieldNode> fieldNodes = ProgramFieldMap.create();
+    private final Map<DexMethod, Int2ReferenceMap<ParameterNode>> parameterNodes =
+        new IdentityHashMap<>();
 
     public FlowGraph(Iterable<DexProgramClass> classes) {
       classes.forEach(this::add);
     }
 
+    public FlowGraph(Collection<Node> nodes) {
+      for (Node node : nodes) {
+        if (node.isFieldNode()) {
+          FieldNode fieldNode = node.asFieldNode();
+          fieldNodes.put(fieldNode.getField(), fieldNode);
+        } else {
+          ParameterNode parameterNode = node.asParameterNode();
+          parameterNodes
+              .computeIfAbsent(
+                  parameterNode.getMethod().getReference(),
+                  ignoreKey(Int2ReferenceOpenHashMap::new))
+              .put(parameterNode.getParameterIndex(), parameterNode);
+        }
+      }
+    }
+
+    public void forEachFieldNode(Consumer<? super FieldNode> consumer) {
+      fieldNodes.values().forEach(consumer);
+    }
+
     @Override
-    public void forEachNeighbor(ParameterNode node, Consumer<? super ParameterNode> consumer) {
+    public void forEachNeighbor(Node node, Consumer<? super Node> 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));
+    public void forEachNode(Consumer<? super Node> consumer) {
+      forEachFieldNode(consumer);
+      parameterNodes.values().forEach(nodesForMethod -> nodesForMethod.values().forEach(consumer));
     }
 
     private void add(DexProgramClass clazz) {
-      clazz.forEachProgramMethod(this::add);
+      clazz.forEachProgramField(this::addField);
+      clazz.forEachProgramMethod(this::addMethodParameters);
     }
 
-    private void add(ProgramMethod method) {
+    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;
+      }
+
+      FieldNode node = getOrCreateFieldNode(field, concreteFieldState);
+      for (InFlow inFlow : concreteFieldState.getInFlow()) {
+        if (addInFlow(inFlow, node).shouldBreak()) {
+          assert node.isUnknown();
+          break;
+        }
+      }
+
+      if (!node.getState().isUnknown()) {
+        assert node.getState() == concreteFieldState;
+        node.setState(concreteFieldState.clearInFlow());
+      }
+    }
+
+    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.
@@ -187,11 +313,11 @@
       List<ValueState> parameterStates = monomorphicMethodState.getParameterStates();
       for (int parameterIndex = 0; parameterIndex < parameterStates.size(); parameterIndex++) {
         ValueState parameterState = parameterStates.get(parameterIndex);
-        add(method, parameterIndex, monomorphicMethodState, parameterState);
+        addMethodParameter(method, parameterIndex, monomorphicMethodState, parameterState);
       }
     }
 
-    private void add(
+    private void addMethodParameter(
         ProgramMethod method,
         int parameterIndex,
         ConcreteMonomorphicMethodState methodState,
@@ -204,20 +330,18 @@
 
       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).
+      // 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;
       }
 
       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 (addInFlow(inFlow, node).shouldBreak()) {
+          assert node.isUnknown();
+          break;
         }
       }
 
@@ -227,7 +351,60 @@
       }
     }
 
-    private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, ParameterNode node) {
+    // Returns BREAK if the current node has been set to unknown.
+    private TraversalContinuation<?, ?> addInFlow(InFlow inFlow, Node 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, Node node) {
+      InFlow baseInFlow = inFlow.getBaseInFlow();
+      if (baseInFlow.isFieldValue()) {
+        return addInFlow(baseInFlow.asFieldValue(), node, inFlow);
+      } else {
+        assert baseInFlow.isMethodParameter();
+        return addInFlow(baseInFlow.asMethodParameter(), node, inFlow);
+      }
+    }
+
+    private TraversalContinuation<?, ?> addInFlow(FieldValue inFlow, Node node) {
+      return addInFlow(inFlow, node, AbstractFunction.identity());
+    }
+
+    private TraversalContinuation<?, ?> addInFlow(
+        FieldValue inFlow, Node node, AbstractFunction transferFunction) {
+      ProgramField field = asProgramFieldOrNull(appView.definitionFor(inFlow.getField()));
+      if (field == null) {
+        assert false;
+        return TraversalContinuation.doContinue();
+      }
+
+      ValueState fieldState = getFieldState(field);
+      if (fieldState.isUnknown()) {
+        // The current node depends on a field for which we don't know anything.
+        node.clearPredecessors();
+        node.setStateToUnknown();
+        return TraversalContinuation.doBreak();
+      }
+
+      FieldNode fieldNode = getOrCreateFieldNode(field, fieldState);
+      node.addPredecessor(fieldNode, transferFunction);
+      return TraversalContinuation.doContinue();
+    }
+
+    private TraversalContinuation<?, ?> addInFlow(MethodParameter inFlow, Node node) {
+      return addInFlow(inFlow, node, AbstractFunction.identity());
+    }
+
+    private TraversalContinuation<?, ?> addInFlow(
+        MethodParameter inFlow, Node 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
@@ -239,15 +416,15 @@
 
       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.
+        // 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 parameter depends on another parameter for which we don't know anything.
+        // The current node depends on a parameter for which we don't know anything.
         node.clearPredecessors();
-        node.setState(ValueState.unknown());
+        node.setStateToUnknown();
         return TraversalContinuation.doBreak();
       }
 
@@ -259,21 +436,26 @@
               enclosingMethod,
               inFlow.getIndex(),
               enclosingMethodState.asConcrete().asMonomorphic());
-      node.addPredecessor(predecessor);
+      node.addPredecessor(predecessor, transferFunction);
       return TraversalContinuation.doContinue();
     }
 
+    private FieldNode getOrCreateFieldNode(ProgramField field, ValueState fieldState) {
+      return fieldNodes.computeIfAbsent(field, f -> new FieldNode(f, fieldState));
+    }
+
     private ParameterNode getOrCreateParameterNode(
         ProgramMethod method, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
       Int2ReferenceMap<ParameterNode> parameterNodesForMethod =
-          nodes.computeIfAbsent(method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
+          parameterNodes.computeIfAbsent(
+              method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
       return parameterNodesForMethod.compute(
           parameterIndex,
           (ignore, parameterNode) ->
               parameterNode != null
                   ? parameterNode
                   : new ParameterNode(
-                      methodState, parameterIndex, method.getArgumentType(parameterIndex)));
+                      method, methodState, parameterIndex, method.getArgumentType(parameterIndex)));
     }
 
     private ProgramMethod getEnclosingMethod(MethodParameter methodParameter) {
@@ -282,6 +464,15 @@
           asProgramClassOrNull(appView.definitionFor(methodParameter.getMethod().getHolderType())));
     }
 
+    private ValueState getFieldState(ProgramField field) {
+      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) {
       if (method == null) {
         // Conservatively return unknown if for some reason we can't find the method.
@@ -292,87 +483,254 @@
     }
   }
 
-  static class ParameterNode {
+  public abstract static class Node {
 
-    private final ConcreteMonomorphicMethodState methodState;
-    private final int parameterIndex;
-    private final DexType parameterType;
+    private final Set<Node> predecessors = Sets.newIdentityHashSet();
+    private final Map<Node, Set<AbstractFunction>> successors = new IdentityHashMap<>();
 
-    private final Set<ParameterNode> predecessors = Sets.newIdentityHashSet();
-    private final Set<ParameterNode> successors = Sets.newIdentityHashSet();
+    private boolean inWorklist = true;
 
-    private boolean pending = true;
-
-    ParameterNode(
-        ConcreteMonomorphicMethodState methodState, int parameterIndex, DexType parameterType) {
-      this.methodState = methodState;
-      this.parameterIndex = parameterIndex;
-      this.parameterType = parameterType;
+    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();
+      }
     }
 
-    void addPredecessor(ParameterNode predecessor) {
-      predecessor.successors.add(this);
+    abstract ValueState getState();
+
+    abstract DexType getStaticType();
+
+    abstract void setState(ValueState valueState);
+
+    void setStateToUnknown() {
+      setState(ValueState.unknown());
+    }
+
+    void addPredecessor(Node predecessor, AbstractFunction abstractFunction) {
+      predecessor.successors.computeIfAbsent(this, ignoreKey(HashSet::new)).add(abstractFunction);
       predecessors.add(predecessor);
     }
 
     void clearPredecessors() {
-      for (ParameterNode predecessor : predecessors) {
+      for (Node predecessor : predecessors) {
         predecessor.successors.remove(this);
       }
       predecessors.clear();
     }
 
-    Set<ParameterNode> getPredecessors() {
+    void clearPredecessors(Node cause) {
+      for (Node predecessor : predecessors) {
+        if (predecessor != cause) {
+          predecessor.successors.remove(this);
+        }
+      }
+      predecessors.clear();
+    }
+
+    Set<Node> getPredecessors() {
       return predecessors;
     }
 
-    ValueState getState() {
-      return methodState.getParameterState(parameterIndex);
+    boolean hasPredecessors() {
+      return !predecessors.isEmpty();
     }
 
-    Set<ParameterNode> getSuccessors() {
-      return successors;
+    void clearDanglingSuccessors() {
+      successors.clear();
+    }
+
+    Set<Node> getSuccessors() {
+      return successors.keySet();
+    }
+
+    public void forEachSuccessor(BiConsumer<Node, Set<AbstractFunction>> consumer) {
+      successors.forEach(consumer);
+    }
+
+    public void removeSuccessorIf(BiPredicate<Node, Set<AbstractFunction>> predicate) {
+      successors.entrySet().removeIf(entry -> predicate.test(entry.getKey(), entry.getValue()));
     }
 
     boolean hasSuccessors() {
       return !successors.isEmpty();
     }
 
-    boolean isPending() {
-      return pending;
+    boolean isBottom() {
+      return getState().isBottom();
     }
 
-    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);
+    boolean isFieldNode() {
+      return false;
+    }
+
+    FieldNode asFieldNode() {
+      return null;
+    }
+
+    boolean isParameterNode() {
+      return false;
+    }
+
+    ParameterNode 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<Node> worklist) {
+      if (!inWorklist && hasSuccessors()) {
+        worklist.add(this);
+        inWorklist = true;
+      }
+    }
+
+    void unsetInWorklist() {
+      assert inWorklist;
+      inWorklist = false;
+    }
+  }
+
+  public static class FieldNode extends Node {
+
+    private final ProgramField field;
+    private ValueState fieldState;
+
+    FieldNode(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();
       }
-      return newParameterState;
     }
 
-    void setPending() {
-      assert !isPending();
-      pending = true;
+    @Override
+    ValueState getState() {
+      return fieldState;
     }
 
+    @Override
+    void setState(ValueState fieldState) {
+      this.fieldState = fieldState;
+    }
+
+    @Override
+    boolean isFieldNode() {
+      return true;
+    }
+
+    @Override
+    FieldNode asFieldNode() {
+      return this;
+    }
+  }
+
+  static class ParameterNode extends Node {
+
+    private final ProgramMethod method;
+    private final ConcreteMonomorphicMethodState methodState;
+    private final int parameterIndex;
+    private final DexType parameterType;
+
+    ParameterNode(
+        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);
     }
 
-    void unsetPending() {
-      assert pending;
-      pending = false;
+    @Override
+    boolean isParameterNode() {
+      return true;
+    }
+
+    @Override
+    ParameterNode asParameterNode() {
+      return this;
     }
   }
 }
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..ebba6d5 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,10 +4,13 @@
 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.code.InstanceGet;
@@ -17,9 +20,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;
 
@@ -147,19 +151,43 @@
 
     assert argument.getType().isInt();
 
-    DexString expectedFieldName;
-    ValueState state = ValueState.bottomPrimitiveTypeParameter();
-    if (!hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2) {
+    DexString expectedFieldName =
+        !hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2
+            ? rewrittenComposeReferences.changedFieldName
+            : rewrittenComposeReferences.defaultFieldName;
+    DexField expectedField =
+        appView
+            .dexItemFactory()
+            .createField(
+                context.getHolderType(), appView.dexItemFactory().intType, expectedFieldName);
+
+    UpdateChangedFlagsAbstractFunction inFlow = null;
+    if (expectedFieldName.isIdenticalTo(rewrittenComposeReferences.changedFieldName)) {
       // 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)) {
+        SingleResolutionResult<?> resolutionResult =
+            invokeStatic.resolveMethod(appView, context).asSingleResolution();
+        if (resolutionResult == null) {
           return null;
         }
-        // Assume the call does not impact the $$changed capture and strip the call.
+        DexClassAndMethod invokeSingleTarget =
+            resolutionResult
+                .lookupDispatchTarget(appView, invokeStatic, context)
+                .getSingleDispatchTarget();
+        if (invokeSingleTarget == null) {
+          return null;
+        }
+        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`.
@@ -175,17 +203,12 @@
         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));
+        inFlow =
+            new UpdateChangedFlagsAbstractFunction(
+                new OrAbstractFunction(new FieldValue(expectedField), 1));
+      } else {
+        inFlow = new UpdateChangedFlagsAbstractFunction(new FieldValue(expectedField));
       }
-      expectedFieldName = rewrittenComposeReferences.changedFieldName;
-    } else {
-      // We are looking at an argument to the $$default parameter of the @Composable function.
-      expectedFieldName = rewrittenComposeReferences.defaultFieldName;
     }
 
     // At this point we expect that the restart lambda is reading either this.$$changed or
@@ -196,12 +219,14 @@
 
     // 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(expectedField)) {
       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 inFlow != null
+        ? new ConcretePrimitiveTypeValueState(inFlow)
+        : ValueState.bottomPrimitiveTypeParameter();
   }
 }
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..1dc5061 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;
@@ -21,9 +25,15 @@
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorOptimizationInfoPopulator;
 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.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.optimize.argumentpropagation.propagation.InFlowPropagator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.LazyBox;
 import com.android.tools.r8.utils.Timing;
@@ -71,10 +81,16 @@
     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);
     Set<ComposableCallGraphNode> optimizedComposableFunctions = Sets.newIdentityHashSet();
@@ -92,36 +108,79 @@
     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.
+    InFlow baseInFlow = transferFunction.getBaseInFlow();
+    assert baseInFlow.isFieldValue();
+
+    ProgramField field =
+        asProgramFieldOrNull(appView.definitionFor(baseInFlow.asFieldValue().getField()));
+    assert field != null;
+
+    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/UpdateChangedFlagsAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
index 5ae767a..c3dbf97 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
@@ -4,14 +4,60 @@
 package com.android.tools.r8.optimize.compose;
 
 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.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
+import java.util.Objects;
 
 public class UpdateChangedFlagsAbstractFunction implements AbstractFunction {
 
-  @SuppressWarnings("UnusedVariable")
   private final InFlow inFlow;
 
   public UpdateChangedFlagsAbstractFunction(InFlow inFlow) {
     this.inFlow = inFlow;
   }
+
+  @Override
+  public NonEmptyValueState apply(ConcreteValueState state) {
+    // TODO(b/302483644): Implement this abstract function to allow correct value propagation of
+    //  updateChangedFlags(x | 1).
+    return state;
+  }
+
+  @Override
+  public InFlow getBaseInFlow() {
+    if (inFlow.isAbstractFunction()) {
+      return inFlow.asAbstractFunction().getBaseInFlow();
+    }
+    assert inFlow.isFieldValue() || inFlow.isMethodParameter();
+    return inFlow;
+  }
+
+  @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/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/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;
+    }
+  }
+}