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();
+ }
+ }
+}