Add support for pushing resource values into static field values

This was originally disabled, but in cases where we have keeps on R
classes this is increasing the code size of using non final fields

Bug: b/287398085
Change-Id: Ia20a45916d67b60d817ac32d25735a25a387d2c1
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index fe532fc..0bd18ff 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.Arrays;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import org.objectweb.asm.ConstantDynamic;
@@ -57,7 +58,8 @@
     ANNOTATION(0x1d),
     NULL(0x1e),
     BOOLEAN(0x1f),
-    CONST_DYNAMIC(-1);
+    CONST_DYNAMIC(-1),
+    RESOURCE_NUMBER(-2);
 
     public static DexValueKind fromId(int id) {
       switch (id) {
@@ -99,6 +101,8 @@
           return BOOLEAN;
         case -1:
           return CONST_DYNAMIC;
+        case -2:
+          return RESOURCE_NUMBER;
         default:
           throw new Unreachable();
       }
@@ -266,6 +270,14 @@
     return null;
   }
 
+  public boolean isDexValueResourceNumber() {
+    return false;
+  }
+
+  public DexValueResourceNumber asDexValueResourceNumber() {
+    return null;
+  }
+
   public boolean isDexValueLong() {
     return false;
   }
@@ -886,6 +898,96 @@
     }
   }
 
+  public static class DexValueResourceNumber extends DexValueNumber {
+    private final int value;
+
+    private DexValueResourceNumber(int value) {
+      this.value = value;
+    }
+
+    public static DexValueResourceNumber create(int value) {
+      return new DexValueResourceNumber(value);
+    }
+
+    public int getValue() {
+      return value;
+    }
+
+    @Override
+    public AbstractValue toAbstractValue(AbstractValueFactory factory) {
+      return factory.createSingleResourceNumberValue(getValue());
+    }
+
+    @Override
+    public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
+      writeIntegerTo(DexValueKind.INT, value, Integer.BYTES, dest);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(value, getValueKind());
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other == this) {
+        return true;
+      }
+      return other instanceof DexValueResourceNumber
+          && value == ((DexValueResourceNumber) other).value;
+    }
+
+    @Override
+    public String toString() {
+      return "ResourceNumber " + value;
+    }
+
+    @Override
+    public DexType getType(DexItemFactory factory) {
+      return factory.intType;
+    }
+
+    @Override
+    public long getRawValue() {
+      return value;
+    }
+
+    @Override
+    public Object getBoxedValue() {
+      return value;
+    }
+
+    @Override
+    public Object asAsmEncodedObject() {
+      return Integer.valueOf(value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
+    }
+
+    @Override
+    int internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      return visitor.visitInt(value, other.asDexValueResourceNumber().value);
+    }
+
+    @Override
+    public DexValueKind getValueKind() {
+      return DexValueKind.RESOURCE_NUMBER;
+    }
+
+    @Override
+    public boolean isDexValueResourceNumber() {
+      return true;
+    }
+
+    @Override
+    public DexValueResourceNumber asDexValueResourceNumber() {
+      return this;
+    }
+  }
+
   public static class DexValueLong extends DexValueNumber {
 
     public static final DexValueLong DEFAULT = new DexValueLong(0);
diff --git a/src/main/java/com/android/tools/r8/graph/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index b69adc0..986267c 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -277,7 +277,9 @@
               || arg.isDexValueLong()
               || arg.isDexValueFloat()
               || arg.isDexValueDouble()
-              || arg.isDexValueString();
+              || arg.isDexValueString()
+              || arg.isDexValueResourceNumber();
+          break;
       }
       if (continuation.shouldBreak()) {
         break;
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index fa4979c..3d1cb69 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
+import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.IRCode;
@@ -46,9 +47,26 @@
 
   public static void register(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
-    if (enabled(appView, enqueuer)) {
+    if (fieldAccessAnalysisEnabled(appView, enqueuer)) {
       enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
     }
+    if (liveFieldAnalysisEnabled(appView, enqueuer)) {
+      enqueuer.registerAnalysis(
+          new EnqueuerAnalysis() {
+            @Override
+            public void processNewlyLiveField(
+                ProgramField field, ProgramDefinition context, EnqueuerWorklist worklist) {
+              DexEncodedField definition = field.getDefinition();
+              if (field.getAccessFlags().isStatic()
+                  && definition.hasExplicitStaticValue()
+                  && definition.getStaticValue().isDexValueResourceNumber()) {
+                appView
+                    .getResourceShrinkerState()
+                    .trace(definition.getStaticValue().asDexValueResourceNumber().getValue());
+              }
+            }
+          });
+    }
   }
 
   @Override
@@ -56,7 +74,14 @@
     EnqueuerFieldAccessAnalysis.super.done(enqueuer);
   }
 
-  private static boolean enabled(
+  private static boolean liveFieldAnalysisEnabled(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
+    return appView.options().androidResourceProvider != null
+        && appView.options().isOptimizedResourceShrinking()
+        && enqueuer.getMode().isFinalTreeShaking();
+  }
+
+  private static boolean fieldAccessAnalysisEnabled(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     return appView.options().androidResourceProvider != null
         && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index afaf0c7..c6d8ac4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.DexValue.DexValueLong;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
+import com.android.tools.r8.graph.DexValue.DexValueResourceNumber;
 import com.android.tools.r8.graph.DexValue.DexValueShort;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.FieldResolutionResult;
@@ -188,6 +189,9 @@
               } else {
                 throw new Unreachable("Unexpected default value for field type " + fieldType + ".");
               }
+            } else if (value.isConstResourceNumber()) {
+              int resourceValue = value.getDefinition().asResourceConstNumber().getValue();
+              fieldsWithStaticValues.put(field, DexValueResourceNumber.create(resourceValue));
             } else {
               ConstNumber cnst = value.getConstInstruction().asConstNumber();
               if (fieldType == dexItemFactory.booleanType) {
@@ -442,7 +446,7 @@
                     isWrittenBefore.remove(fieldReference);
                   }
                   continue;
-                } else if ((fieldReference.type.isPrimitiveType() && !hasPutOfConstResource(put))
+                } else if (fieldReference.type.isPrimitiveType()
                     || fieldReference.type == dexItemFactory.stringType) {
                   finalFieldPuts.put(field, put);
                   unnecessaryStaticPuts.add(put);
@@ -509,10 +513,6 @@
     return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
   }
 
-  private boolean hasPutOfConstResource(StaticPut put) {
-    return put.value().isConstResourceNumber();
-  }
-
   private Map<DexEncodedField, StaticPut> validateFinalFieldPuts(
       Map<DexEncodedField, StaticPut> finalFieldPuts,
       Map<DexField, Set<StaticPut>> isWrittenBefore) {
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 4a8af9c..530ec46 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3333,6 +3333,11 @@
       // Already live.
       return;
     }
+
+    assert !field.getAccessFlags().isStatic()
+        || !field.getDefinition().hasExplicitStaticValue()
+        || !field.getDefinition().getStaticValue().isDexValueResourceNumber()
+        || mode.isFinalTreeShaking();
     addEffectivelyLiveOriginalField(field);
 
     // Mark the field as targeted.
diff --git a/src/test/java/com/android/tools/r8/androidresources/ConstResourceValueToStaticFieldTest.java b/src/test/java/com/android/tools/r8/androidresources/ConstResourceValueToStaticFieldTest.java
new file mode 100644
index 0000000..82459aa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ConstResourceValueToStaticFieldTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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 ConstResourceValueToStaticFieldTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class)
+        .build(temp);
+  }
+
+  private R8FullTestBuilder getSharedBuilder() throws Exception {
+    return testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .enableOptimizedShrinking();
+  }
+
+  @Test
+  public void testR8KeepAll() throws Exception {
+    getSharedBuilder()
+        .addKeepAllClassesRule()
+        .compile()
+        .inspect(
+            codeInspector -> {
+              ClassSubject rStringClass = codeInspector.clazz(R.string.class);
+              // Since we have the keep all we should still have the class and the init.
+              assertThat(rStringClass, isPresent());
+              assertThat(rStringClass.uniqueInstanceInitializer(), isPresent());
+              assertThat(rStringClass.field("int", "foo"), isPresent());
+              // Even with the keep all we should have removed the field write, and hence the
+              // clinit.
+              assertThat(rStringClass.clinit(), isAbsent());
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testR8NoKeep() throws Exception {
+    getSharedBuilder()
+        .compile()
+        .inspect(
+            codeInspector -> {
+              ClassSubject rStringClass = codeInspector.clazz(R.string.class);
+              assertThat(rStringClass, isAbsent());
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  public static class FooBar {
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.string.foo);
+      }
+    }
+  }
+
+  public static class R {
+    public static class string {
+      public static int foo = 0x7f110004;
+    }
+  }
+}