Initial parser and writer for a light-weight IR.
Bug: b/225838009
Change-Id: I6c9b002488d62ccd26cf2fa1bda7dc2d699ad9fd
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteArrayIterator.java b/src/main/java/com/android/tools/r8/lightir/ByteArrayIterator.java
new file mode 100644
index 0000000..c1f72dc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/ByteArrayIterator.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2022, 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.lightir;
+
+import it.unimi.dsi.fastutil.bytes.ByteIterator;
+
+/** Simple implementation of an iterator over a primitive byte array. */
+public class ByteArrayIterator implements ByteIterator {
+
+ private final int size;
+ private final byte[] buffer;
+ private int index = 0;
+
+ public ByteArrayIterator(byte[] bytes) {
+ size = bytes.length;
+ buffer = bytes;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < size;
+ }
+
+ @Override
+ public byte nextByte() {
+ return buffer[index++];
+ }
+
+ @Override
+ public Byte next() {
+ return nextByte();
+ }
+
+ @Override
+ public int skip(int i) {
+ int actual = index + i <= size ? i : size - index;
+ index += actual;
+ return actual;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteArrayWriter.java b/src/main/java/com/android/tools/r8/lightir/ByteArrayWriter.java
new file mode 100644
index 0000000..7636ed4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/ByteArrayWriter.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2022, 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.lightir;
+
+import java.io.ByteArrayOutputStream;
+
+/** Simple implementation to construct a primitive byte array. */
+public class ByteArrayWriter implements ByteWriter {
+
+ // Backing is just the default capacity reallocating java byte array.
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ @Override
+ public void put(int u1) {
+ assert ByteUtils.isU1(u1);
+ buffer.write(u1);
+ }
+
+ public byte[] toByteArray() {
+ return buffer.toByteArray();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteUtils.java b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
new file mode 100644
index 0000000..40a80d2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2022, 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.lightir;
+
+/** Simple utilities for byte encodings. */
+public class ByteUtils {
+
+ public static boolean isU1(int value) {
+ return 0 <= value && value <= 0xFF;
+ }
+
+ // Lossy truncation of an integer value to its lowest byte.
+ private static int truncateToU1(int value) {
+ return value & 0xFF;
+ }
+
+ public static int ensureU1(int value) {
+ assert isU1(value);
+ return truncateToU1(value);
+ }
+
+ public static int fromU1(byte value) {
+ return value & 0xFF;
+ }
+
+ public static int intEncodingSize(int value) {
+ return 4;
+ }
+
+ public static void writeEncodedInt(int value, ByteWriter writer) {
+ assert 4 == intEncodingSize(value);
+ writer.put(truncateToU1(value >> 24));
+ writer.put(truncateToU1(value >> 16));
+ writer.put(truncateToU1(value >> 8));
+ writer.put(truncateToU1(value));
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteWriter.java b/src/main/java/com/android/tools/r8/lightir/ByteWriter.java
new file mode 100644
index 0000000..0a2a75e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/ByteWriter.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2022, 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.lightir;
+
+/** Most primitive interface for providing consumer to the LIRWriter. */
+public interface ByteWriter {
+
+ /** Put a byte value, must represent an unsigned byte (int between 0 and 255). */
+ void put(int u1);
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java
new file mode 100644
index 0000000..a08c036
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2022, 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.lightir;
+
+public interface LIRBasicInstructionCallback {
+
+ /**
+ * Most basic callback for interpreting LIR.
+ *
+ * @param opcode The opcode of the instruction (See {@code LIROpcodes} for values).
+ * @param operandsOffsetInBytes The offset into the byte stream at which the instruction's payload
+ * starts.
+ * @param operandsSizeInBytes The total size of the instruction's payload (excluding the opcode
+ * itself an any payload size encoding).
+ */
+ void onInstruction(int opcode, int operandsOffsetInBytes, int operandsSizeInBytes);
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
new file mode 100644
index 0000000..7ced9ef
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2022, 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.lightir;
+
+import com.android.tools.r8.graph.DexItem;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+
+public class LIRBuilder {
+
+ private final ByteArrayWriter byteWriter = new ByteArrayWriter();
+ private final LIRWriter writer = new LIRWriter(byteWriter);
+ private final Reference2IntMap<DexItem> constants;
+
+ public LIRBuilder() {
+ constants = new Reference2IntOpenHashMap<>();
+ }
+
+ private int getConstantIndex(DexItem item) {
+ int nextIndex = constants.size();
+ Integer oldIndex = constants.putIfAbsent(item, nextIndex);
+ return oldIndex != null ? oldIndex : nextIndex;
+ }
+
+ public LIRBuilder addNop() {
+ writer.writeOneByteInstruction(LIROpcodes.NOP);
+ return this;
+ }
+
+ public LIRBuilder addConstNull() {
+ writer.writeOneByteInstruction(LIROpcodes.ACONST_NULL);
+ return this;
+ }
+
+ public LIRBuilder addConstInt(int value) {
+ if (0 <= value && value <= 5) {
+ writer.writeOneByteInstruction(LIROpcodes.ICONST_0 + value);
+ } else {
+ writer.writeInstruction(LIROpcodes.ICONST, ByteUtils.intEncodingSize(value));
+ ByteUtils.writeEncodedInt(value, writer::writeOperand);
+ }
+ return this;
+ }
+
+ public LIRCode build() {
+ int constantsCount = constants.size();
+ DexItem[] constantTable = new DexItem[constantsCount];
+ constants.forEach((item, index) -> constantTable[index] = item);
+ return new LIRCode(constantTable, byteWriter.toByteArray());
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRCode.java b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
new file mode 100644
index 0000000..c91dbd9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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.lightir;
+
+import com.android.tools.r8.graph.DexItem;
+
+public class LIRCode implements Iterable<LIRInstructionView> {
+
+ private final DexItem[] constants;
+ private final byte[] instructions;
+
+ public static LIRBuilder builder() {
+ return new LIRBuilder();
+ }
+
+ // Should be constructed using LIRBuilder.
+ LIRCode(DexItem[] constants, byte[] instructions) {
+ this.constants = constants;
+ this.instructions = instructions;
+ }
+
+ @Override
+ public LIRIterator iterator() {
+ return new LIRIterator(new ByteArrayIterator(instructions));
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java b/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
new file mode 100644
index 0000000..59051e3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2022, 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.lightir;
+
+/**
+ * Abstract view of a LIR instruction.
+ *
+ * <p>The view should not be considered a representation of an instruction as the underlying data
+ * can change. The view callbacks allow interpreting the instruction at different levels of
+ * abstraction depending on need.
+ */
+public interface LIRInstructionView {
+
+ void accept(LIRBasicInstructionCallback eventCallback);
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRIterator.java b/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
new file mode 100644
index 0000000..e05d521
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2022, 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.lightir;
+
+import it.unimi.dsi.fastutil.bytes.ByteIterator;
+import java.util.Iterator;
+
+/**
+ * Basic iterator over the light IR.
+ *
+ * <p>This iterator is internally a zero-allocation parser with the "elements" as a view onto the
+ * current state.
+ */
+public class LIRIterator implements Iterator<LIRInstructionView>, LIRInstructionView {
+
+ private final ByteIterator iterator;
+
+ private int currentByteIndex = 0;
+ private int currentOpcode = -1;
+ private int currentOperandSize = 0;
+
+ public LIRIterator(ByteIterator iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public LIRInstructionView next() {
+ currentOpcode = u1();
+ if (LIROpcodes.isOneByteInstruction(currentOpcode)) {
+ currentOperandSize = 0;
+ } else {
+ // Any instruction that is not a single byte has a two-byte header. The second byte is the
+ // size of the variable width operand payload.
+ currentOperandSize = u1();
+ skip(currentOperandSize);
+ }
+ return this;
+ }
+
+ @Override
+ public void accept(LIRBasicInstructionCallback eventCallback) {
+ int operandsOffset = currentByteIndex - currentOperandSize;
+ eventCallback.onInstruction(currentOpcode, operandsOffset, currentOperandSize);
+ }
+
+ private void skip(int i) {
+ currentByteIndex += i;
+ iterator.skip(i);
+ }
+
+ private int u1() {
+ ++currentByteIndex;
+ return ByteUtils.fromU1(iterator.nextByte());
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
new file mode 100644
index 0000000..85cc9f1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
@@ -0,0 +1,182 @@
+// Copyright (c) 2022, 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.lightir;
+
+/**
+ * Constants related to LIR.
+ *
+ * <p>The constants generally follow the bytecode values as defined by the classfile format.
+ */
+public interface LIROpcodes {
+
+ static boolean isOneByteInstruction(int opcode) {
+ assert opcode >= NOP;
+ return opcode <= DCONST_1;
+ }
+
+ // Instructions maintaining the same opcode as defined in CF.
+ int NOP = 0;
+ int ACONST_NULL = 1;
+ int ICONST_M1 = 2;
+ int ICONST_0 = 3;
+ int ICONST_1 = 4;
+ int ICONST_2 = 5;
+ int ICONST_3 = 6;
+ int ICONST_4 = 7;
+ int ICONST_5 = 8;
+ int LCONST_0 = 9;
+ int LCONST_1 = 10;
+ int FCONST_0 = 11;
+ int FCONST_1 = 12;
+ int FCONST_2 = 13;
+ int DCONST_0 = 14;
+ int DCONST_1 = 15;
+ // int BIPUSH = 16;
+ // int SIPUSH = 17;
+ int LDC = 18;
+ // int ILOAD = 21;
+ // int LLOAD = 22;
+ // int FLOAD = 23;
+ // int DLOAD = 24;
+ // int ALOAD = 25;
+ int IALOAD = 46;
+ int LALOAD = 47;
+ int FALOAD = 48;
+ int DALOAD = 49;
+ int AALOAD = 50;
+ int BALOAD = 51;
+ int CALOAD = 52;
+ int SALOAD = 53;
+ // int ISTORE = 54;
+ // int LSTORE = 55;
+ // int FSTORE = 56;
+ // int DSTORE = 57;
+ // int ASTORE = 58;
+ int IASTORE = 79;
+ int LASTORE = 80;
+ int FASTORE = 81;
+ int DASTORE = 82;
+ int AASTORE = 83;
+ int BASTORE = 84;
+ int CASTORE = 85;
+ int SASTORE = 86;
+ // int POP = 87;
+ // int POP2 = 88;
+ // int DUP = 89;
+ // int DUP_X1 = 90;
+ // int DUP_X2 = 91;
+ // int DUP2 = 92;
+ // int DUP2_X1 = 93;
+ // int DUP2_X2 = 94;
+ // int SWAP = 95;
+ int IADD = 96;
+ int LADD = 97;
+ int FADD = 98;
+ int DADD = 99;
+ int ISUB = 100;
+ int LSUB = 101;
+ int FSUB = 102;
+ int DSUB = 103;
+ int IMUL = 104;
+ int LMUL = 105;
+ int FMUL = 106;
+ int DMUL = 107;
+ int IDIV = 108;
+ int LDIV = 109;
+ int FDIV = 110;
+ int DDIV = 111;
+ int IREM = 112;
+ int LREM = 113;
+ int FREM = 114;
+ int DREM = 115;
+ int INEG = 116;
+ int LNEG = 117;
+ int FNEG = 118;
+ int DNEG = 119;
+ int ISHL = 120;
+ int LSHL = 121;
+ int ISHR = 122;
+ int LSHR = 123;
+ int IUSHR = 124;
+ int LUSHR = 125;
+ int IAND = 126;
+ int LAND = 127;
+ int IOR = 128;
+ int LOR = 129;
+ int IXOR = 130;
+ int LXOR = 131;
+ // int IINC = 132;
+ int I2L = 133;
+ int I2F = 134;
+ int I2D = 135;
+ int L2I = 136;
+ int L2F = 137;
+ int L2D = 138;
+ int F2I = 139;
+ int F2L = 140;
+ int F2D = 141;
+ int D2I = 142;
+ int D2L = 143;
+ int D2F = 144;
+ int I2B = 145;
+ int I2C = 146;
+ int I2S = 147;
+ int LCMP = 148;
+ int FCMPL = 149;
+ int FCMPG = 150;
+ int DCMPL = 151;
+ int DCMPG = 152;
+ int IFEQ = 153;
+ int IFNE = 154;
+ int IFLT = 155;
+ int IFGE = 156;
+ int IFGT = 157;
+ int IFLE = 158;
+ int IF_ICMPEQ = 159;
+ int IF_ICMPNE = 160;
+ int IF_ICMPLT = 161;
+ int IF_ICMPGE = 162;
+ int IF_ICMPGT = 163;
+ int IF_ICMPLE = 164;
+ int IF_ACMPEQ = 165;
+ int IF_ACMPNE = 166;
+ int GOTO = 167;
+ // int JSR = 168;
+ // int RET = 169;
+ int TABLESWITCH = 170;
+ int LOOKUPSWITCH = 171;
+ int IRETURN = 172;
+ int LRETURN = 173;
+ int FRETURN = 174;
+ int DRETURN = 175;
+ int ARETURN = 176;
+ int RETURN = 177;
+ int GETSTATIC = 178;
+ int PUTSTATIC = 179;
+ int GETFIELD = 180;
+ int PUTFIELD = 181;
+ int INVOKEVIRTUAL = 182;
+ int INVOKESPECIAL = 183;
+ int INVOKESTATIC = 184;
+ int INVOKEINTERFACE = 185;
+ int INVOKEDYNAMIC = 186;
+ int NEW = 187;
+ int NEWARRAY = 188;
+ int ANEWARRAY = 189;
+ int ARRAYLENGTH = 190;
+ int ATHROW = 191;
+ int CHECKCAST = 192;
+ int INSTANCEOF = 193;
+ int MONITORENTER = 194;
+ int MONITOREXIT = 195;
+ int MULTIANEWARRAY = 197;
+ int IFNULL = 198;
+ int IFNONNULL = 199;
+
+ // Non-CF instructions.
+ int ICONST = 200;
+ int LCONST = 201;
+ int FCONST = 202;
+ int DCONST = 203;
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRWriter.java b/src/main/java/com/android/tools/r8/lightir/LIRWriter.java
new file mode 100644
index 0000000..074726d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRWriter.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2022, 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.lightir;
+
+/**
+ * Lowest level writer for constructing LIR encoded data.
+ *
+ * <p>This writer deals with just the instruction and operand encodings. For higher level structure,
+ * such as the constant pool, see LIRBuilder.
+ */
+public class LIRWriter {
+
+ private final ByteWriter writer;
+ private int pendingOperandBytes = 0;
+
+ public LIRWriter(ByteWriter writer) {
+ this.writer = writer;
+ }
+
+ public void writeOneByteInstruction(int opcode) {
+ assert LIROpcodes.isOneByteInstruction(opcode);
+ assert pendingOperandBytes == 0;
+ writer.put(ByteUtils.ensureU1(opcode));
+ }
+
+ public void writeInstruction(int opcode, int operandsSizeInBytes) {
+ assert pendingOperandBytes == 0;
+ writer.put(ByteUtils.ensureU1(opcode));
+ writer.put(ByteUtils.ensureU1(operandsSizeInBytes));
+ pendingOperandBytes = operandsSizeInBytes;
+ }
+
+ public void writeOperand(int u1) {
+ assert pendingOperandBytes > 0;
+ pendingOperandBytes--;
+ writer.put(ByteUtils.ensureU1(u1));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java b/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
new file mode 100644
index 0000000..0602eb3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
@@ -0,0 +1,79 @@
+// Copyright (c) 2022, 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.lightir;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.IntBox;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LIRBasicCallbackTest extends TestBase {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public LIRBasicCallbackTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void test() throws Exception {
+ LIRCode code = LIRCode.builder().addConstNull().addConstInt(42).build();
+
+ // State to keep track of position in the byte array as we don't expose this in the iterator.
+ IntBox offset = new IntBox(0);
+
+ LIRIterator it = code.iterator();
+
+ // The iterator and the elements are the same object providing a view on the byte stream.
+ assertTrue(it.hasNext());
+ LIRInstructionView next = it.next();
+ assertSame(it, next);
+
+ it.accept(
+ (int opcode, int operandOffset, int operandSize) -> {
+ int headerSize = 1;
+ assertEquals(LIROpcodes.ACONST_NULL, opcode);
+ assertEquals(offset.get() + headerSize, operandOffset);
+ assertEquals(0, operandSize);
+ offset.increment(headerSize + operandSize);
+ });
+
+ assertTrue(it.hasNext());
+ it.next();
+ it.accept(
+ (int opcode, int operandOffset, int operandSize) -> {
+ int headerSize = 2; // opcode + payload-size
+ assertEquals(LIROpcodes.ICONST, opcode);
+ assertEquals(offset.get() + headerSize, operandOffset);
+ assertEquals(4, operandSize);
+ offset.increment(headerSize + operandSize);
+ });
+ assertFalse(it.hasNext());
+
+ // The iterator can also be use in a normal java for-each loop.
+ // However, the item is not an actual item just a current view, so it can't be cached!
+ LIRInstructionView oldView = null;
+ for (LIRInstructionView view : code) {
+ if (oldView == null) {
+ oldView = view;
+ view.accept((opcode, ignore1, ignore2) -> assertEquals(LIROpcodes.ACONST_NULL, opcode));
+ } else {
+ assertSame(oldView, view);
+ view.accept((opcode, ignore1, ignore2) -> assertEquals(LIROpcodes.ICONST, opcode));
+ }
+ }
+ }
+}