Skip arguments to Composable functions for optimizing bitwise operations

Change-Id: I6438072788e3d76abc66c6c9fc682a4d1f292bc4
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index a5fe760..af882b2 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -38,6 +38,7 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
+import com.android.tools.r8.optimize.compose.ComposeReferences;
 import com.android.tools.r8.optimize.interfaces.collection.OpenClosedInterfacesCollection;
 import com.android.tools.r8.profile.art.ArtProfileCollection;
 import com.android.tools.r8.profile.startup.profile.StartupProfile;
@@ -105,7 +106,9 @@
   // Currently however the liveness may be downgraded thus loosing the computed keep info.
   private KeepInfoCollection keepInfo = null;
 
-  private final AbstractValueFactory abstractValueFactory = new AbstractValueFactory();
+  private ComposeReferences composeReferences = null;
+
+  private final AbstractValueFactory abstractValueFactory;
   private final AbstractValueConstantPropagationJoiner abstractValueConstantPropagationJoiner;
   private final AbstractValueFieldJoiner abstractValueFieldJoiner;
   private final AbstractValueParameterJoiner abstractValueParameterJoiner;
@@ -180,6 +183,7 @@
         timing.time(
             "Compilation context", () -> CompilationContext.createInitialContext(options()));
     this.wholeProgramOptimizations = wholeProgramOptimizations;
+    abstractValueFactory = new AbstractValueFactory(options());
     abstractValueConstantPropagationJoiner = new AbstractValueConstantPropagationJoiner(this);
     if (enableWholeProgramOptimizations()) {
       abstractValueFieldJoiner = new AbstractValueFieldJoiner(withClassHierarchy());
@@ -510,6 +514,14 @@
     return appInfo.dexItemFactory();
   }
 
+  public ComposeReferences getComposeReferences() {
+    assert testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
+    if (composeReferences == null) {
+      composeReferences = new ComposeReferences(dexItemFactory());
+    }
+    return composeReferences;
+  }
+
   public boolean enableWholeProgramOptimizations() {
     return wholeProgramOptimizations == WholeProgramOptimizations.ON;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index 78beee1..efe3ead 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -51,6 +51,10 @@
       return reference.apply(type -> TYPE, field -> FIELD, method -> METHOD);
     }
 
+    public boolean isMethod() {
+      return this == METHOD;
+    }
+
     public boolean isParameter() {
       return this == PARAMETER;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
index 2dbb047..79eca14 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
@@ -14,17 +14,26 @@
 import com.android.tools.r8.ir.analysis.value.objectstate.KnownLengthArrayState;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class AbstractValueFactory {
 
-  private ConcurrentHashMap<DexType, SingleConstClassValue> singleConstClassValues =
+  private final TestingOptions testingOptions;
+
+  private final ConcurrentHashMap<DexType, SingleConstClassValue> singleConstClassValues =
       new ConcurrentHashMap<>();
-  private ConcurrentHashMap<Long, SingleNumberValue> singleNumberValues = new ConcurrentHashMap<>();
-  private ConcurrentHashMap<DexString, SingleStringValue> singleStringValues =
+  private final ConcurrentHashMap<Long, SingleNumberValue> singleNumberValues =
       new ConcurrentHashMap<>();
-  private ConcurrentHashMap<Integer, KnownLengthArrayState> knownArrayLengthStates =
+  private final ConcurrentHashMap<DexString, SingleStringValue> singleStringValues =
       new ConcurrentHashMap<>();
+  private final ConcurrentHashMap<Integer, KnownLengthArrayState> knownArrayLengthStates =
+      new ConcurrentHashMap<>();
+
+  public AbstractValueFactory(InternalOptions options) {
+    testingOptions = options.testing;
+  }
 
   public SingleBoxedBooleanValue createBoxedBooleanFalse() {
     return SingleBoxedBooleanValue.getFalseInstance();
@@ -71,10 +80,20 @@
       int definitelySetBits, int definitelyUnsetBits) {
     if (definitelySetBits != 0 || definitelyUnsetBits != 0) {
       // If all bits are known, then create a single number value.
-      if ((definitelySetBits | definitelyUnsetBits) == ALL_BITS_SET_MASK) {
+      boolean allBitsSet = (definitelySetBits | definitelyUnsetBits) == ALL_BITS_SET_MASK;
+      // Account for the temporary hack in the Compose modeling where we create a
+      // DefiniteBitsNumberValue with set bits=0b1^32 and unset bits = 0b1^(31)0. This value is used
+      // to simulate the effect of `x | 1` in joins.
+      if (testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions) {
+        boolean overlappingSetAndUnsetBits = (definitelySetBits & definitelyUnsetBits) != 0;
+        if (overlappingSetAndUnsetBits) {
+          allBitsSet = false;
+        }
+      }
+      if (allBitsSet) {
         return createUncheckedSingleNumberValue(definitelySetBits);
       }
-      return new DefiniteBitsNumberValue(definitelySetBits, definitelyUnsetBits);
+      return new DefiniteBitsNumberValue(definitelySetBits, definitelyUnsetBits, testingOptions);
     }
     return AbstractValue.unknown();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
index 778e61b..a18281e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/DefiniteBitsNumberValue.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions.TestingOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import java.util.Objects;
 
@@ -16,8 +17,10 @@
   private final int definitelySetBits;
   private final int definitelyUnsetBits;
 
-  public DefiniteBitsNumberValue(int definitelySetBits, int definitelyUnsetBits) {
-    assert (definitelySetBits & definitelyUnsetBits) == 0;
+  public DefiniteBitsNumberValue(
+      int definitelySetBits, int definitelyUnsetBits, TestingOptions testingOptions) {
+    assert (definitelySetBits & definitelyUnsetBits) == 0
+        || testingOptions.modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
     this.definitelySetBits = definitelySetBits;
     this.definitelyUnsetBits = definitelyUnsetBits;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 003b119..6011eb0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -788,10 +788,26 @@
     boolean isConstant = definition != null && definition.isConstNumber();
     if (isConstant || hasLocalInfo()) {
       builder.append("(");
-      if (isConstant && definition.asConstNumber().outValue != null) {
+      if (isConstant && definition.asConstNumber().hasOutValue()) {
         ConstNumber constNumber = definition.asConstNumber();
         if (constNumber.getOutType().isSinglePrimitive()) {
-          builder.append((int) constNumber.getRawValue());
+          Value constNumberValue = constNumber.outValue();
+          int intValue = (int) constNumber.getRawValue();
+          boolean useBinaryRepresentation =
+              !constNumberValue.hasPhiUsers()
+                  && constNumberValue.uniqueUsers().stream()
+                      .allMatch(
+                          user ->
+                              user.isAnd()
+                                  || user.isOr()
+                                  || ((user.isShl() || user.isShr() || user.isUshr())
+                                      && constNumberValue == user.asShl().getFirstOperand())
+                                  || user.isXor());
+          if (useBinaryRepresentation) {
+            builder.append("0b").append(Integer.toBinaryString(intValue));
+          } else {
+            builder.append(intValue);
+          }
         } else {
           builder.append(constNumber.getRawValue());
         }
@@ -823,6 +839,12 @@
     return isConstant() && getConstInstruction().isConstNumber();
   }
 
+  public boolean isConstNumber(long rawValue) {
+    return isConstant()
+        && getConstInstruction().isConstNumber()
+        && getConstInstruction().asConstNumber().getRawValue() == rawValue;
+  }
+
   public boolean isConstBoolean(boolean value) {
     return isConstNumber()
         && definition.asConstNumber().getRawValue() == BooleanUtils.longValue(value);
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 3a6e882..49d6400 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
@@ -67,6 +67,8 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
+  private final ArgumentPropagatorCodeScannerModeling modeling;
+
   private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
 
   private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
@@ -91,6 +93,7 @@
       AppView<AppInfoWithLiveness> appView,
       ArgumentPropagatorReprocessingCriteriaCollection reprocessingCriteriaCollection) {
     this.appView = appView;
+    this.modeling = new ArgumentPropagatorCodeScannerModeling(appView);
     this.reprocessingCriteriaCollection = reprocessingCriteriaCollection;
   }
 
@@ -404,11 +407,11 @@
       parameterStates.add(
           computeParameterStateForNonReceiver(
               invoke,
+              singleTarget,
               argumentIndex,
               invoke.getArgument(argumentIndex),
               context,
-              existingMethodState,
-              methodReprocessingCriteria.getParameterReprocessingCriteria(argumentIndex)));
+              existingMethodState));
     }
 
     // We simulate that the return value is used for methods with void return type. This ensures
@@ -448,11 +451,18 @@
   @SuppressWarnings("UnusedVariable")
   private ParameterState computeParameterStateForNonReceiver(
       InvokeMethod invoke,
+      ProgramMethod singleTarget,
       int argumentIndex,
       Value argument,
       ProgramMethod context,
-      ConcreteMonomorphicMethodStateOrBottom existingMethodState,
-      ParameterReprocessingCriteria parameterReprocessingCriteria) {
+      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
+    ParameterState modeledState =
+        modeling.modelParameterStateForArgumentToFunction(
+            invoke, singleTarget, argumentIndex, argument);
+    if (modeledState != null) {
+      return modeledState;
+    }
+
     // Don't compute a state for this parameter if the stored state is already unknown.
     if (existingMethodState.isMonomorphic()
         && existingMethodState.asMonomorphic().getParameterState(argumentIndex).isUnknown()) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
new file mode 100644
index 0000000..7ab506e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScannerModeling.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
+import com.android.tools.r8.optimize.compose.ArgumentPropagatorComposeModeling;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class ArgumentPropagatorCodeScannerModeling {
+
+  private final ArgumentPropagatorComposeModeling composeModeling;
+
+  ArgumentPropagatorCodeScannerModeling(AppView<AppInfoWithLiveness> appView) {
+    this.composeModeling =
+        appView.testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions
+            ? new ArgumentPropagatorComposeModeling(appView)
+            : null;
+  }
+
+  ParameterState modelParameterStateForArgumentToFunction(
+      InvokeMethod invoke, ProgramMethod singleTarget, int argumentIndex, Value argument) {
+    if (composeModeling != null) {
+      return composeModeling.modelParameterStateForChangedOrDefaultArgumentToComposableFunction(
+          invoke, singleTarget, argumentIndex, argument);
+    }
+    return null;
+  }
+}
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
new file mode 100644
index 0000000..4bd4242
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorComposeModeling.java
@@ -0,0 +1,180 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.compose;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Or;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeParameterState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.BitUtils;
+import com.android.tools.r8.utils.BooleanUtils;
+
+public class ArgumentPropagatorComposeModeling {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ComposeReferences composeReferences;
+
+  public ArgumentPropagatorComposeModeling(AppView<AppInfoWithLiveness> appView) {
+    assert appView.testing().modelUnknownChangedAndDefaultArgumentsToComposableFunctions;
+    this.appView = appView;
+    this.composeReferences = appView.getComposeReferences();
+  }
+
+  /**
+   * Models calls to @Composable functions from Compose restart lambdas.
+   *
+   * <p>The @Composable functions are static and should have one of the following two parameter
+   * lists:
+   *
+   * <ol>
+   *   <li>(..., Composable, int)
+   *   <li>(..., Composable, int, int)
+   * </ol>
+   *
+   * <p>The int argument after the Composable parameter is the $$changed parameter. The second int
+   * argument after the Composable parameter (if present) is the $$default parameter.
+   *
+   * <p>The call to a @Composable function from its restart lambda follows the following code
+   * pattern:
+   *
+   * <pre>
+   *   MyComposableFunction(
+   *       ..., this.composer, updateChangedFlags(this.$$changed) || 1, this.$$default)
+   * </pre>
+   *
+   * <p>The modeling performed by this method assumes that updateChangedFlags() does not have any
+   * impact on $$changed (see the current implementation below). The modeling also assumes that
+   * this.$$changed and this.$$default are captures of the $$changed and $$default parameters of
+   * the @Composable function.
+   *
+   * <pre>
+   *   internal fun updateChangedFlags(flags: Int): Int {
+   *     val lowBits = flags and changedLowBitMask
+   *     val highBits = flags and changedHighBitMask
+   *     return ((flags and changedMask) or
+   *         (lowBits or (highBits shr 1)) or ((lowBits shl 1) and highBits))
+   *   }
+   * </pre>
+   */
+  public ParameterState modelParameterStateForChangedOrDefaultArgumentToComposableFunction(
+      InvokeMethod invoke, ProgramMethod singleTarget, int argumentIndex, Value argument) {
+    // First check if this is an invoke to a @Composable function.
+    if (singleTarget == null
+        || singleTarget
+                .getDefinition()
+                .annotations()
+                .getFirstMatching(composeReferences.composableType)
+            == null) {
+      return null;
+    }
+
+    // The @Composable function is expected to be static and have >= 2 parameters.
+    if (!invoke.isInvokeStatic()) {
+      return null;
+    }
+
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (invokedMethod.getArity() < 2) {
+      return null;
+    }
+
+    // Check if the parameters list is one of (..., Composer, int) or (..., Composer, int, int).
+    if (!invokedMethod.getParameter(invokedMethod.getArity() - 1).isIntType()) {
+      return null;
+    }
+
+    boolean hasDefaultParameter =
+        invokedMethod.getParameter(invokedMethod.getArity() - 2).isIntType();
+    if (hasDefaultParameter && invokedMethod.getArity() < 3) {
+      return null;
+    }
+
+    int composerParameterIndex =
+        invokedMethod.getArity() - 2 - BooleanUtils.intValue(hasDefaultParameter);
+    if (!invokedMethod
+        .getParameter(composerParameterIndex)
+        .isIdenticalTo(composeReferences.composerType)) {
+      return null;
+    }
+
+    // We only model the two last arguments ($$changed and $$default) to the @Composable function.
+    // If this argument is not on of these two int arguments, then don't apply any modeling.
+    if (argumentIndex <= composerParameterIndex) {
+      return null;
+    }
+
+    assert argument.getType().isInt();
+
+    DexString expectedFieldName;
+    ParameterState state = ParameterState.bottomPrimitiveTypeParameter();
+    if (!hasDefaultParameter || argumentIndex == invokedMethod.getArity() - 2) {
+      // We are looking at an argument to the $$changed parameter of the @Composable function.
+      // We generally expect this argument to be defined by a call to updateChangedFlags().
+      if (argument.isDefinedByInstructionSatisfying(Instruction::isInvokeStatic)) {
+        InvokeStatic invokeStatic = argument.getDefinition().asInvokeStatic();
+        DexMethod maybeUpdateChangedFlagsMethod =
+            appView
+                .graphLens()
+                .getOriginalMethodSignature(
+                    invokeStatic.getInvokedMethod(), GraphLens.getIdentityLens());
+        if (!maybeUpdateChangedFlagsMethod.isIdenticalTo(
+            composeReferences.updatedChangedFlagsMethod)) {
+          return null;
+        }
+        // Assume the call does not impact the $$changed capture and strip the call.
+        argument = invokeStatic.getFirstArgument();
+      }
+      // Allow the argument to be defined by `this.$$changed | 1`.
+      if (argument.isDefinedByInstructionSatisfying(Instruction::isOr)) {
+        Or or = argument.getDefinition().asOr();
+        Value maybeNumberOperand =
+            or.leftValue().isConstNumber() ? or.leftValue() : or.rightValue();
+        Value otherOperand = or.getOperand(1 - or.inValues().indexOf(maybeNumberOperand));
+        if (!maybeNumberOperand.isConstNumber(1)) {
+          return null;
+        }
+        // Strip the OR instruction.
+        argument = otherOperand;
+        // Update the model from bottom to a special value that effectively throws away any known
+        // information about the lowermost bit of $$changed.
+        state =
+            new ConcretePrimitiveTypeParameterState(
+                appView
+                    .abstractValueFactory()
+                    .createDefiniteBitsNumberValue(
+                        BitUtils.ALL_BITS_SET_MASK, BitUtils.ALL_BITS_SET_MASK << 1));
+      }
+      expectedFieldName = composeReferences.changedFieldName;
+    } else {
+      // We are looking at an argument to the $$default parameter of the @Composable function.
+      expectedFieldName = composeReferences.defaultFieldName;
+    }
+
+    // At this point we expect that the restart lambda is reading either this.$$changed or
+    // this.$$default using an instance-get.
+    if (!argument.isDefinedByInstructionSatisfying(Instruction::isInstanceGet)) {
+      return null;
+    }
+
+    // Check that the instance-get is reading the capture field that we expect it to.
+    InstanceGet instanceGet = argument.getDefinition().asInstanceGet();
+    if (!instanceGet.getField().getName().isIdenticalTo(expectedFieldName)) {
+      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;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
new file mode 100644
index 0000000..7a489a7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeReferences.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.optimize.compose;
+
+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;
+
+public class ComposeReferences {
+
+  public final DexString changedFieldName;
+  public final DexString defaultFieldName;
+
+  public final DexType composableType;
+  public final DexType composerType;
+
+  public final DexMethod updatedChangedFlagsMethod;
+
+  public ComposeReferences(DexItemFactory factory) {
+    changedFieldName = factory.createString("$$changed");
+    defaultFieldName = factory.createString("$$default");
+
+    composableType = factory.createType("Landroidx/compose/runtime/Composable;");
+    composerType = factory.createType("Landroidx/compose/runtime/Composer;");
+
+    updatedChangedFlagsMethod =
+        factory.createMethod(
+            factory.createType("Landroidx/compose/runtime/RecomposeScopeImplKt;"),
+            factory.createProto(factory.intType, factory.intType),
+            "updateChangedFlags");
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysisHelper.java b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysisHelper.java
index e06916b..5416619 100644
--- a/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysisHelper.java
+++ b/src/main/java/com/android/tools/r8/optimize/interfaces/analysis/CfOpenClosedInterfacesAnalysisHelper.java
@@ -92,7 +92,7 @@
               assert false;
             }
           } else {
-            assert false;
+            assert array.isNullType();
           }
         },
         options);
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 6389e26..a02420c 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -62,7 +62,7 @@
       ProgramDefinition holder, DexAnnotation annotation, AnnotatedKind kind) {
     return annotationsToRetain.contains(annotation)
         || shouldKeepAnnotation(
-            appView, holder, annotation, isAnnotationTypeLive(annotation), kind);
+            appView, holder, annotation, isAnnotationTypeLive(annotation), kind, mode);
   }
 
   public static boolean shouldKeepAnnotation(
@@ -70,13 +70,14 @@
       ProgramDefinition holder,
       DexAnnotation annotation,
       boolean isAnnotationTypeLive,
-      AnnotatedKind kind) {
+      AnnotatedKind kind,
+      Mode mode) {
     // If we cannot run the AnnotationRemover we are keeping the annotation.
-    if (!appView.options().isShrinking()) {
+    InternalOptions options = appView.options();
+    if (!options.isShrinking()) {
       return true;
     }
 
-    InternalOptions options = appView.options();
     ProguardKeepAttributes config =
         options.getProguardConfiguration() != null
             ? options.getProguardConfiguration().getKeepAttributes()
@@ -145,6 +146,9 @@
             .startsWith(options.itemFactory.dalvikAnnotationOptimizationPrefix)) {
           return true;
         }
+        if (isComposableAnnotationToRetain(appView, annotation, kind, mode, options)) {
+          return true;
+        }
         if (kind.isParameter()) {
           if (!options.isKeepRuntimeInvisibleParameterAnnotationsEnabled()) {
             return false;
@@ -291,14 +295,10 @@
     boolean isAnnotation =
         definition.isProgramClass() && definition.asProgramClass().isAnnotation();
     if (keepInfo.isAnnotationRemovalAllowed(options)) {
-      if (isAnnotation) {
+      if (isAnnotation || mode.isInitialTreeShaking()) {
         definition.rewriteAllAnnotations(
-            (annotation, isParameterAnnotation) ->
-                shouldRetainAnnotationOnAnnotationClass(annotation) ? annotation : null);
-      } else if (mode.isInitialTreeShaking()) {
-        definition.rewriteAllAnnotations(
-            (annotation, isParameterAnnotation) ->
-                annotationsToRetain.contains(annotation) ? annotation : null);
+            (annotation, kind) ->
+                shouldRetainAnnotation(definition, annotation, kind) ? annotation : null);
       } else {
         definition.clearAllAnnotations();
       }
@@ -308,14 +308,34 @@
     }
   }
 
-  private boolean shouldRetainAnnotationOnAnnotationClass(DexAnnotation annotation) {
-    if (DexAnnotation.isAnnotationDefaultAnnotation(annotation, appView.dexItemFactory())) {
-      return shouldRetainAnnotationDefaultAnnotationOnAnnotationClass(annotation);
+  private boolean shouldRetainAnnotation(
+      ProgramDefinition definition, DexAnnotation annotation, AnnotatedKind kind) {
+    boolean isAnnotationOnAnnotationClass =
+        definition.isProgramClass() && definition.asProgramClass().isAnnotation();
+    if (isAnnotationOnAnnotationClass) {
+      if (DexAnnotation.isAnnotationDefaultAnnotation(annotation, appView.dexItemFactory())) {
+        return shouldRetainAnnotationDefaultAnnotationOnAnnotationClass(annotation);
+      }
+      if (DexAnnotation.isJavaLangRetentionAnnotation(annotation, appView.dexItemFactory())) {
+        return shouldRetainRetentionAnnotationOnAnnotationClass(annotation);
+      }
     }
-    if (DexAnnotation.isJavaLangRetentionAnnotation(annotation, appView.dexItemFactory())) {
-      return shouldRetainRetentionAnnotationOnAnnotationClass(annotation);
-    }
-    return annotationsToRetain.contains(annotation);
+    return annotationsToRetain.contains(annotation)
+        || isComposableAnnotationToRetain(appView, annotation, kind, mode, options);
+  }
+
+  private static boolean isComposableAnnotationToRetain(
+      AppView<?> appView,
+      DexAnnotation annotation,
+      AnnotatedKind kind,
+      Mode mode,
+      InternalOptions options) {
+    return options.testing.modelUnknownChangedAndDefaultArgumentsToComposableFunctions
+        && mode.isInitialTreeShaking()
+        && kind.isMethod()
+        && annotation
+            .getAnnotationType()
+            .isIdenticalTo(appView.getComposeReferences().composableType);
   }
 
   @SuppressWarnings("UnusedVariable")
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index d07235f..fda3ddf 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2347,7 +2347,7 @@
       return true;
     }
     return AnnotationRemover.shouldKeepAnnotation(
-        appView, annotatedItem, annotation, isLive, annotatedKind);
+        appView, annotatedItem, annotation, isLive, annotatedKind, mode);
   }
 
   private DexProgramClass resolveBaseType(DexType type, ProgramDefinition context) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 1367e23..51593eb 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2398,6 +2398,10 @@
         System.getProperty("com.android.tools.r8.disableMarkingClassesFinal") != null;
     public boolean testEnableTestAssertions = false;
     public boolean keepMetadataInR8IfNotRewritten = true;
+    public boolean modelUnknownChangedAndDefaultArgumentsToComposableFunctions =
+        SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault(
+            "com.android.tools.r8.modelUnknownChangedAndDefaultArgumentsToComposableFunctions",
+            false);
 
     // Flag to allow processing of resources in D8. A data resource consumer still needs to be
     // specified.
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
index 7cd720a..556a914 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ArgumentInfoCollectionTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.proto.RemovedArgumentInfo;
 import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.utils.InternalOptions;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -127,8 +128,9 @@
 
   @Test
   public void testCombineRemoveRewritten() {
-    DexItemFactory factory = new DexItemFactory();
-    AbstractValueFactory abstractValueFactory = new AbstractValueFactory();
+    InternalOptions options = new InternalOptions();
+    DexItemFactory factory = options.dexItemFactory();
+    AbstractValueFactory abstractValueFactory = new AbstractValueFactory(options);
 
     ArgumentInfoCollection.Builder builder1 = ArgumentInfoCollection.builder();
     builder1.addArgumentInfo(