Add int_to_byte conversion to value propagation.

Bug: b/465094702
Change-Id: Iab9c4229e189b3d469436b4a044377e95adb544a
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 124b3f7..d969c86 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -82,6 +82,11 @@
     return !isZero();
   }
 
+  public byte getByteValue() {
+    assert outType() == ValueType.INT;
+    return (byte) value;
+  }
+
   public int getIntValue() {
     assert outType() == ValueType.INT
         || outType() == ValueType.OBJECT; // Used for is-null conditionals.
diff --git a/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java b/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
index a5a44d9..9d199e7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NumberConversion.java
@@ -22,8 +22,12 @@
 import com.android.tools.r8.dex.code.DexLongToInt;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
 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.analysis.value.SingleNumberValue;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.lightir.LirBuilder;
@@ -147,4 +151,24 @@
   public boolean outTypeKnownToBeBoolean(Set<Phi> seen) {
     return type.getTo() == NumericType.BYTE && source().knownToBeBoolean(seen);
   }
+
+  @Override
+  public AbstractValue getAbstractValue(
+      AppView<?> appView, ProgramMethod context, AbstractValueSupplier abstractValueSupplier) {
+    AbstractValue val = abstractValueSupplier.getAbstractValue(getFirstOperand(), appView, context);
+    if (!val.isSingleNumberValue()) {
+      return super.getAbstractValue(appView, context, abstractValueSupplier);
+    }
+    SingleNumberValue num = val.asSingleNumberValue();
+    AbstractValueFactory valueFactory = appView.abstractValueFactory();
+    PrimitiveTypeElement typeElement = PrimitiveTypeElement.fromNumericType(type.getTo());
+
+    switch (type) {
+      case INT_TO_BYTE:
+        long rawValue = num.getIntValue();
+        return valueFactory.createSingleNumberValue(rawValue, typeElement);
+      default:
+        return super.getAbstractValue(appView, context, abstractValueSupplier);
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/numberconversion/IntToByteTest.java b/src/test/java/com/android/tools/r8/optimize/numberconversion/IntToByteTest.java
new file mode 100644
index 0000000..991c787
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/numberconversion/IntToByteTest.java
@@ -0,0 +1,114 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.numberconversion;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.timing.Timing;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class IntToByteTest extends TestBase {
+
+  @Parameterized.Parameter(0)
+  public int input;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static Integer[] data() {
+    return new Integer[] {
+      128, 65408, -65408, 42, -32, 0,
+    };
+  }
+
+  @Test
+  public void test() throws Exception {
+    byte[] testClass = TestDump.dump(input, (byte) input);
+    AndroidApp androidApp = AndroidApp.builder().addClassProgramData(testClass).build();
+    AppView<AppInfoWithClassHierarchy> appView = computeAppViewWithClassHierarchy(androidApp);
+    IRCode ir = new CodeInspector(androidApp).clazz(TestDump.CLASS_NAME).mainMethod().buildIR();
+    assertTrue(ir.streamInstructions().anyMatch(Instruction::isNumberConversion));
+
+    // Run optimization and dead code elimination.
+    CodeRewriterResult result =
+        new SparseConditionalConstantPropagation(appView).run(ir, null, null, Timing.empty());
+    assertEquals(OptionalBool.TRUE, result.hasChanged());
+    new DeadCodeRemover(appView).run(ir, Timing.empty());
+
+    // Assertions.
+    ImmutableList<ConstNumber> constNumbers =
+        ImmutableList.copyOf(
+            ir.streamInstructions()
+                .filter(Instruction::isConstNumber)
+                .map(Instruction::asConstNumber)
+                .iterator());
+    assertEquals(1, constNumbers.size());
+    ConstNumber cst = constNumbers.get(0);
+    assertEquals(ValueType.INT, cst.outType());
+    assertEquals((byte) 0, cst.getByteValue());
+  }
+
+  public static class TestDump implements Opcodes {
+
+    public static final String CLASS_NAME = "TestClass";
+
+    /**
+     * Constructs the below method where {@code [..]} describes meta values.
+     *
+     * <pre>{@code
+     * public class [CLASS_NAME] {
+     *     public static void main(String[] args) {
+     *         System.out.println(((byte) [input]) - [expected]);
+     *     }
+     * }
+     * }</pre>
+     *
+     * Subtraction is used to verify the internal state of constants rather than just the output
+     * instructions. This avoids the situation where the input is not truncated during optimization,
+     * but is still truncated in the final instructions because of typed extraction.
+     */
+    public static byte[] dump(int input, byte expected) throws Exception {
+
+      ClassWriter classWriter = new ClassWriter(0);
+
+      classWriter.visit(V1_8, ACC_PUBLIC, CLASS_NAME, null, "java/lang/Object", null);
+      MethodVisitor methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      methodVisitor.visitCode();
+      methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      methodVisitor.visitLdcInsn(input);
+      methodVisitor.visitInsn(I2B);
+      methodVisitor.visitIntInsn(BIPUSH, expected);
+      methodVisitor.visitInsn(ISUB);
+      methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(3, 1);
+      methodVisitor.visitEnd();
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+  }
+}