Support translation between LIR and IR.

This adds support for just the parts needed to translate hello world.

Bug: b/225838009
Change-Id: I4833c3d2d793e274bc3978441be31f9033b3adb6
diff --git a/src/main/java/com/android/tools/r8/ir/code/Argument.java b/src/main/java/com/android/tools/r8/ir/code/Argument.java
index edd3468..76e9d15 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Argument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Argument.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LIRBuilder;
 import java.util.Set;
 
 /**
@@ -174,4 +175,9 @@
       return this;
     }
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value> builder) {
+    builder.addArgument(index, knownToBeBoolean);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index ca71070..afb3203 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
+import com.android.tools.r8.lightir.LIRBuilder;
 import java.io.UTFDataFormatException;
 
 public class ConstString extends ConstInstruction {
@@ -179,4 +180,9 @@
     assert getOutType().equals(expectedType);
     return true;
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value> builder) {
+    builder.addConstString(value);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 7cb6f50..55d96c5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
+import com.android.tools.r8.lightir.LIRBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
@@ -1556,6 +1557,10 @@
     return false;
   }
 
+  public void buildLIR(LIRBuilder<Value> builder) {
+    throw new Unimplemented("Missing impl for " + getClass().getSimpleName());
+  }
+
   public static class SideEffectAssumption {
 
     public static final SideEffectAssumption NONE = new SideEffectAssumption();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index fed5f58..f44d128 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LIRBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.List;
 
@@ -175,4 +176,9 @@
       return this;
     }
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value> builder) {
+    builder.addInvokeVirtual(getInvokedMethod(), arguments());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 752b30c..935e9df 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LIRBuilder;
 
 public class Return extends JumpInstruction {
 
@@ -150,4 +151,13 @@
       return this;
     }
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value> builder) {
+    if (hasReturnValue()) {
+      builder.addReturn(returnValue());
+    } else {
+      builder.addReturnVoid();
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
index cdec1d9..19b9b27 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.lightir.LIRBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Set;
 
@@ -290,4 +291,9 @@
       return this;
     }
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value> builder) {
+    builder.addStaticGet(getField());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteUtils.java b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
index a4a4bdf..5c51959 100644
--- a/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
+++ b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
@@ -3,6 +3,8 @@
 // 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 utilities for byte encodings. */
 public class ByteUtils {
 
@@ -36,6 +38,15 @@
     writer.put(truncateToU1(value));
   }
 
+  public static int readEncodedInt(ByteIterator iterator) {
+    assert 4 == intEncodingSize(0);
+    int value = ensureU1(iterator.nextByte()) << 24;
+    value |= ensureU1(iterator.nextByte()) << 16;
+    value |= ensureU1(iterator.nextByte()) << 8;
+    value |= ensureU1(iterator.nextByte());
+    return value;
+  }
+
   public static boolean isU2(int value) {
     return (value >= 0) && (value <= 0xFFFF);
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIR2IRBuilder.java b/src/main/java/com/android/tools/r8/lightir/LIR2IRBuilder.java
new file mode 100644
index 0000000..76f1040
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIR2IRBuilder.java
@@ -0,0 +1,198 @@
+// 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.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Argument;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.ConstString;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NumberGenerator;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import it.unimi.dsi.fastutil.ints.IntList;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class LIR2IRBuilder {
+
+  private final AppView<?> appView;
+
+  public LIR2IRBuilder(AppView<?> appView) {
+    this.appView = appView;
+  }
+
+  public IRCode translate(ProgramMethod method, LIRCode lirCode) {
+    Parser parser = new Parser(lirCode, appView);
+    parser.parseArguments(method);
+    lirCode.forEach(view -> view.accept(parser));
+    return parser.getIRCode(method);
+  }
+
+  /**
+   * When building IR the structured LIR parser is used to obtain the decoded operand indexes. The
+   * below parser subclass handles translation of indexes to SSA values.
+   */
+  private static class Parser extends LIRParsedInstructionCallback {
+
+    private final AppView<?> appView;
+    private final LIRCode code;
+    private final NumberGenerator valueNumberGenerator = new NumberGenerator();
+    private final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
+
+    private final Value[] values;
+    private final LinkedList<BasicBlock> blocks = new LinkedList<>();
+
+    private BasicBlock currentBlock;
+    private int nextInstructionIndex = 0;
+
+    public Parser(LIRCode code, AppView<?> appView) {
+      super(code);
+      this.appView = appView;
+      this.code = code;
+      this.values = new Value[code.getArgumentCount() + code.getInstructionCount()];
+    }
+
+    public void parseArguments(ProgramMethod method) {
+      currentBlock = new BasicBlock();
+      currentBlock.setNumber(basicBlockNumberGenerator.next());
+      boolean hasReceiverArgument = !method.getDefinition().isStatic();
+      assert code.getArgumentCount()
+          == method.getParameters().size() + (hasReceiverArgument ? 1 : 0);
+      if (hasReceiverArgument) {
+        addThisArgument(method.getHolderType());
+      }
+      method.getParameters().forEach(this::addArgument);
+    }
+
+    public IRCode getIRCode(ProgramMethod method) {
+      // TODO(b/225838009): Support control flow.
+      currentBlock.setFilled();
+      blocks.add(currentBlock);
+      return new IRCode(
+          appView.options(),
+          method,
+          Position.syntheticNone(),
+          blocks,
+          valueNumberGenerator,
+          basicBlockNumberGenerator,
+          code.getMetadata(),
+          method.getOrigin(),
+          new MutableMethodConversionOptions(appView.options()));
+    }
+
+    public Value getSsaValue(int index) {
+      Value value = values[index];
+      if (value == null) {
+        value = new Value(valueNumberGenerator.next(), TypeElement.getBottom(), null);
+        values[index] = value;
+      }
+      return value;
+    }
+
+    public List<Value> getSsaValues(IntList indices) {
+      List<Value> arguments = new ArrayList<>(indices.size());
+      for (int i = 0; i < indices.size(); i++) {
+        arguments.add(getSsaValue(indices.getInt(i)));
+      }
+      return arguments;
+    }
+
+    public int peekNextInstructionIndex() {
+      return nextInstructionIndex;
+    }
+
+    public Value getOutValueForNextInstruction(TypeElement type) {
+      // TODO(b/225838009): Support debug locals.
+      DebugLocalInfo localInfo = null;
+      int index = peekNextInstructionIndex();
+      Value value = values[index];
+      if (value == null) {
+        value = new Value(valueNumberGenerator.next(), type, localInfo);
+        values[index] = value;
+      } else {
+        value.setType(type);
+        if (localInfo != null) {
+          value.setLocalInfo(localInfo);
+        }
+      }
+      return value;
+    }
+
+    private void addInstruction(Instruction instruction) {
+      // TODO(b/225838009): Add correct position info.
+      instruction.setPosition(Position.syntheticNone());
+      currentBlock.getInstructions().add(instruction);
+      instruction.setBlock(currentBlock);
+      ++nextInstructionIndex;
+    }
+
+    private void addThisArgument(DexType type) {
+      Argument argument = addArgument(type);
+      argument.outValue().markAsThis();
+    }
+
+    private Argument addArgument(DexType type) {
+      Argument instruction =
+          new Argument(
+              getOutValueForNextInstruction(type.toTypeElement(appView)),
+              peekNextInstructionIndex(),
+              type.isBooleanType());
+      addInstruction(instruction);
+      return instruction;
+    }
+
+    @Override
+    public void onConstNull() {
+      Value dest = getOutValueForNextInstruction(TypeElement.getNull());
+      addInstruction(new ConstNumber(dest, 0));
+    }
+
+    @Override
+    public void onConstString(DexString string) {
+      Value dest = getOutValueForNextInstruction(TypeElement.stringClassType(appView));
+      addInstruction(new ConstString(dest, string));
+    }
+
+    @Override
+    public void onInvokeVirtual(DexMethod target, IntList arguments) {
+      // TODO(b/225838009): Maintain is-interface bit.
+      Value dest = getInvokeInstructionOutputValue(target);
+      List<Value> ssaArgumentValues = getSsaValues(arguments);
+      InvokeVirtual instruction = new InvokeVirtual(target, dest, ssaArgumentValues);
+      addInstruction(instruction);
+    }
+
+    private Value getInvokeInstructionOutputValue(DexMethod target) {
+      return target.getReturnType().isVoidType()
+          ? null
+          : getOutValueForNextInstruction(target.getReturnType().toTypeElement(appView));
+    }
+
+    @Override
+    public void onStaticGet(DexField field) {
+      Value dest = getOutValueForNextInstruction(field.getTypeElement(appView));
+      addInstruction(new StaticGet(dest, field));
+    }
+
+    @Override
+    public void onReturnVoid() {
+      addInstruction(new Return());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java
deleted file mode 100644
index a08c036..0000000
--- a/src/main/java/com/android/tools/r8/lightir/LIRBasicInstructionCallback.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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
index 7ced9ef..06e642b 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
@@ -3,18 +3,38 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.code.IRMetadata;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.List;
 
-public class LIRBuilder {
+/**
+ * Builder for constructing LIR code from IR.
+ *
+ * @param <V> Type of SSA values. This is abstract to ensure that value internals are not used in
+ *     building.
+ */
+public class LIRBuilder<V> {
+
+  public interface ValueIndexGetter<V> {
+    int getValueIndex(V value);
+  }
 
   private final ByteArrayWriter byteWriter = new ByteArrayWriter();
   private final LIRWriter writer = new LIRWriter(byteWriter);
   private final Reference2IntMap<DexItem> constants;
+  private final ValueIndexGetter<V> valueIndexGetter;
+  private int argumentCount = 0;
+  private int instructionCount = 0;
+  private IRMetadata metadata = null;
 
-  public LIRBuilder() {
+  public LIRBuilder(ValueIndexGetter<V> valueIndexGetter) {
     constants = new Reference2IntOpenHashMap<>();
+    this.valueIndexGetter = valueIndexGetter;
   }
 
   private int getConstantIndex(DexItem item) {
@@ -23,17 +43,59 @@
     return oldIndex != null ? oldIndex : nextIndex;
   }
 
-  public LIRBuilder addNop() {
+  private int constantIndexSize(DexItem item) {
+    return 4;
+  }
+
+  private void writeConstantIndex(DexItem item) {
+    int index = getConstantIndex(item);
+    ByteUtils.writeEncodedInt(index, writer::writeOperand);
+  }
+
+  private int getValueIndex(V value) {
+    return valueIndexGetter.getValueIndex(value);
+  }
+
+  private int valueIndexSize(int index) {
+    return ByteUtils.intEncodingSize(index);
+  }
+
+  private void writeValueIndex(int index) {
+    ByteUtils.writeEncodedInt(index, writer::writeOperand);
+  }
+
+  public LIRBuilder<V> setMetadata(IRMetadata metadata) {
+    this.metadata = metadata;
+    return this;
+  }
+
+  public LIRBuilder<V> writeConstantReferencingInstruction(int opcode, DexItem item) {
+    writer.writeInstruction(opcode, constantIndexSize(item));
+    writeConstantIndex(item);
+    return this;
+  }
+
+  public LIRBuilder<V> addArgument(int index, boolean knownToBeBoolean) {
+    // Arguments are implicitly given by method descriptor and not an actual instruction.
+    assert argumentCount == index;
+    argumentCount++;
+    return this;
+  }
+
+  public LIRBuilder<V> addNop() {
+    instructionCount++;
     writer.writeOneByteInstruction(LIROpcodes.NOP);
     return this;
   }
 
-  public LIRBuilder addConstNull() {
+  public LIRBuilder<V> addConstNull() {
+    instructionCount++;
     writer.writeOneByteInstruction(LIROpcodes.ACONST_NULL);
     return this;
   }
 
-  public LIRBuilder addConstInt(int value) {
+  public LIRBuilder<V> addConstInt(int value) {
+    instructionCount++;
     if (0 <= value && value <= 5) {
       writer.writeOneByteInstruction(LIROpcodes.ICONST_0 + value);
     } else {
@@ -43,10 +105,50 @@
     return this;
   }
 
+  public LIRBuilder<V> addConstString(DexString string) {
+    instructionCount++;
+    return writeConstantReferencingInstruction(LIROpcodes.LDC, string);
+  }
+
+  public LIRBuilder<V> addStaticGet(DexField field) {
+    instructionCount++;
+    return writeConstantReferencingInstruction(LIROpcodes.GETSTATIC, field);
+  }
+
+  public LIRBuilder<V> addInvokeVirtual(DexMethod method, List<V> arguments) {
+    instructionCount++;
+    int argumentOprandSize = constantIndexSize(method);
+    int[] argumentIndexes = new int[arguments.size()];
+    int i = 0;
+    for (V argument : arguments) {
+      int argumentIndex = getValueIndex(argument);
+      argumentIndexes[i++] = argumentIndex;
+      argumentOprandSize += valueIndexSize(argumentIndex);
+    }
+    writer.writeInstruction(LIROpcodes.INVOKEVIRTUAL, argumentOprandSize);
+    writeConstantIndex(method);
+    for (int argumentIndex : argumentIndexes) {
+      writeValueIndex(argumentIndex);
+    }
+    return this;
+  }
+
+  public LIRBuilder<V> addReturn(V value) {
+    return this;
+  }
+
+  public LIRBuilder<V> addReturnVoid() {
+    instructionCount++;
+    writer.writeInstruction(LIROpcodes.RETURN, 0);
+    return this;
+  }
+
   public LIRCode build() {
+    assert metadata != null;
     int constantsCount = constants.size();
     DexItem[] constantTable = new DexItem[constantsCount];
     constants.forEach((item, index) -> constantTable[index] = item);
-    return new LIRCode(constantTable, byteWriter.toByteArray());
+    return new LIRCode(
+        metadata, constantTable, argumentCount, byteWriter.toByteArray(), instructionCount);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRCode.java b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
index c91dbd9..d20c4d6 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
@@ -4,24 +4,93 @@
 package com.android.tools.r8.lightir;
 
 import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.lightir.LIRBuilder.ValueIndexGetter;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
 
 public class LIRCode implements Iterable<LIRInstructionView> {
 
+  private final IRMetadata metadata;
+
+  /** Constant pool of items. */
   private final DexItem[] constants;
+
+  /** Full number of arguments (including receiver for non-static methods). */
+  private final int argumentCount;
+
+  /** Byte encoding of the instructions (including phis). */
   private final byte[] instructions;
 
-  public static LIRBuilder builder() {
-    return new LIRBuilder();
+  /** Cached value for the number of logical instructions (including phis). */
+  private final int instructionCount;
+
+  public static <V> LIRBuilder<V> builder(ValueIndexGetter<V> valueIndexGetter) {
+    return new LIRBuilder<V>(valueIndexGetter);
   }
 
   // Should be constructed using LIRBuilder.
-  LIRCode(DexItem[] constants, byte[] instructions) {
+  LIRCode(
+      IRMetadata metadata,
+      DexItem[] constants,
+      int argumentCount,
+      byte[] instructions,
+      int instructionCount) {
+    this.metadata = metadata;
     this.constants = constants;
+    this.argumentCount = argumentCount;
     this.instructions = instructions;
+    this.instructionCount = instructionCount;
+  }
+
+  public int getArgumentCount() {
+    return argumentCount;
+  }
+
+  public byte[] getInstructionBytes() {
+    return instructions;
+  }
+
+  public int getInstructionCount() {
+    return instructionCount;
+  }
+
+  public IRMetadata getMetadata() {
+    return metadata;
+  }
+
+  public DexItem getConstantItem(int index) {
+    return constants[index];
   }
 
   @Override
   public LIRIterator iterator() {
     return new LIRIterator(new ByteArrayIterator(instructions));
   }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder("LIRCode{");
+    builder.append("constants:");
+    StringUtils.append(builder, Arrays.asList(constants));
+    builder
+        .append(", arguments:")
+        .append(argumentCount)
+        .append(", instructions(size:")
+        .append(instructions.length)
+        .append("):{");
+    int index = 0;
+    for (LIRInstructionView view : this) {
+      builder
+          .append(LIROpcodes.toString(view.getOpcode()))
+          .append("(size:")
+          .append(1 + view.getRemainingOperandSizeInBytes())
+          .append(")");
+      if (index++ < instructionCount) {
+        builder.append(",");
+      }
+    }
+    builder.append("}}");
+    return builder.toString();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LIRInstructionCallback.java
new file mode 100644
index 0000000..42a0955
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRInstructionCallback.java
@@ -0,0 +1,10 @@
+// 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;
+
+/** Convenience interface to accept a LIR instruction view. */
+public interface LIRInstructionCallback {
+
+  void onInstructionView(LIRInstructionView view);
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java b/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
index 59051e3..f42abdb 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
@@ -12,5 +12,21 @@
  */
 public interface LIRInstructionView {
 
-  void accept(LIRBasicInstructionCallback eventCallback);
+  /** Convenience method to forward control to a callback. */
+  void accept(LIRInstructionCallback eventCallback);
+
+  /** The opcode of the instruction (See {@code LIROpcodes} for values). */
+  int getOpcode();
+
+  /** The remaining size of the instruction's payload. */
+  int getRemainingOperandSizeInBytes();
+
+  /** True if the instruction has any operands that have not yet been parsed. */
+  boolean hasMoreOperands();
+
+  /** Get the next operand as a constant-pool index. */
+  int getNextConstantOperand();
+
+  /** Get the next operand as an SSA value index. */
+  int getNextValueOperand();
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRIterator.java b/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
index e05d521..9659aa5 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
@@ -18,35 +18,69 @@
 
   private int currentByteIndex = 0;
   private int currentOpcode = -1;
-  private int currentOperandSize = 0;
+  private int endOfCurrentInstruction = 0;
 
   public LIRIterator(ByteIterator iterator) {
     this.iterator = iterator;
   }
 
+  private void skipRemainingOperands() {
+    if (hasMoreOperands()) {
+      skip(getRemainingOperandSizeInBytes());
+    }
+  }
+
   @Override
   public boolean hasNext() {
+    skipRemainingOperands();
     return iterator.hasNext();
   }
 
   @Override
   public LIRInstructionView next() {
+    skipRemainingOperands();
     currentOpcode = u1();
     if (LIROpcodes.isOneByteInstruction(currentOpcode)) {
-      currentOperandSize = 0;
+      endOfCurrentInstruction = currentByteIndex;
     } 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);
+      int operandSize = u1();
+      endOfCurrentInstruction = currentByteIndex + operandSize;
     }
     return this;
   }
 
   @Override
-  public void accept(LIRBasicInstructionCallback eventCallback) {
-    int operandsOffset = currentByteIndex - currentOperandSize;
-    eventCallback.onInstruction(currentOpcode, operandsOffset, currentOperandSize);
+  public void accept(LIRInstructionCallback eventCallback) {
+    eventCallback.onInstructionView(this);
+  }
+
+  @Override
+  public int getOpcode() {
+    return currentOpcode;
+  }
+
+  @Override
+  public int getRemainingOperandSizeInBytes() {
+    return endOfCurrentInstruction - currentByteIndex;
+  }
+
+  @Override
+  public boolean hasMoreOperands() {
+    return currentByteIndex < endOfCurrentInstruction;
+  }
+
+  @Override
+  public int getNextConstantOperand() {
+    assert hasMoreOperands();
+    return u4();
+  }
+
+  @Override
+  public int getNextValueOperand() {
+    assert hasMoreOperands();
+    return u4();
   }
 
   private void skip(int i) {
@@ -58,4 +92,9 @@
     ++currentByteIndex;
     return ByteUtils.fromU1(iterator.nextByte());
   }
+
+  private int u4() {
+    currentByteIndex += 4;
+    return ByteUtils.readEncodedInt(iterator);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
index 85cc9f1..2141611 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.errors.Unreachable;
+
 /**
  * Constants related to LIR.
  *
@@ -179,4 +181,312 @@
   int LCONST = 201;
   int FCONST = 202;
   int DCONST = 203;
+
+  static String toString(int opcode) {
+    switch (opcode) {
+      case NOP:
+        return "NOP";
+      case ACONST_NULL:
+        return "ACONST_NULL";
+      case ICONST_M1:
+        return "ICONST_M1";
+      case ICONST_0:
+        return "ICONST_0";
+      case ICONST_1:
+        return "ICONST_1";
+      case ICONST_2:
+        return "ICONST_2";
+      case ICONST_3:
+        return "ICONST_3";
+      case ICONST_4:
+        return "ICONST_4";
+      case ICONST_5:
+        return "ICONST_5";
+      case LCONST_0:
+        return "LCONST_0";
+      case LCONST_1:
+        return "LCONST_1";
+      case FCONST_0:
+        return "FCONST_0";
+      case FCONST_1:
+        return "FCONST_1";
+      case FCONST_2:
+        return "FCONST_2";
+      case DCONST_0:
+        return "DCONST_0";
+      case DCONST_1:
+        return "DCONST_1";
+        // case BIPUSH: return "BIPUSH";
+        // case SIPUSH: return "SIPUSH";
+      case LDC:
+        return "LDC";
+        // case ILOAD: return "ILOAD";
+        // case LLOAD: return "LLOAD";
+        // case FLOAD: return "FLOAD";
+        // case DLOAD: return "DLOAD";
+        // case ALOAD: return "ALOAD";
+      case IALOAD:
+        return "IALOAD";
+      case LALOAD:
+        return "LALOAD";
+      case FALOAD:
+        return "FALOAD";
+      case DALOAD:
+        return "DALOAD";
+      case AALOAD:
+        return "AALOAD";
+      case BALOAD:
+        return "BALOAD";
+      case CALOAD:
+        return "CALOAD";
+      case SALOAD:
+        return "SALOAD";
+        // case ISTORE: return "ISTORE";
+        // case LSTORE: return "LSTORE";
+        // case FSTORE: return "FSTORE";
+        // case DSTORE: return "DSTORE";
+        // case ASTORE: return "ASTORE";
+      case IASTORE:
+        return "IASTORE";
+      case LASTORE:
+        return "LASTORE";
+      case FASTORE:
+        return "FASTORE";
+      case DASTORE:
+        return "DASTORE";
+      case AASTORE:
+        return "AASTORE";
+      case BASTORE:
+        return "BASTORE";
+      case CASTORE:
+        return "CASTORE";
+      case SASTORE:
+        return "SASTORE";
+        // case POP: return "POP";
+        // case POP2: return "POP2";
+        // case DUP: return "DUP";
+        // case DUP_X1: return "DUP_X1";
+        // case DUP_X2: return "DUP_X2";
+        // case DUP2: return "DUP2";
+        // case DUP2_X1: return "DUP2_X1";
+        // case DUP2_X2: return "DUP2_X2";
+        // case SWAP: return "SWAP";
+      case IADD:
+        return "IADD";
+      case LADD:
+        return "LADD";
+      case FADD:
+        return "FADD";
+      case DADD:
+        return "DADD";
+      case ISUB:
+        return "ISUB";
+      case LSUB:
+        return "LSUB";
+      case FSUB:
+        return "FSUB";
+      case DSUB:
+        return "DSUB";
+      case IMUL:
+        return "IMUL";
+      case LMUL:
+        return "LMUL";
+      case FMUL:
+        return "FMUL";
+      case DMUL:
+        return "DMUL";
+      case IDIV:
+        return "IDIV";
+      case LDIV:
+        return "LDIV";
+      case FDIV:
+        return "FDIV";
+      case DDIV:
+        return "DDIV";
+      case IREM:
+        return "IREM";
+      case LREM:
+        return "LREM";
+      case FREM:
+        return "FREM";
+      case DREM:
+        return "DREM";
+      case INEG:
+        return "INEG";
+      case LNEG:
+        return "LNEG";
+      case FNEG:
+        return "FNEG";
+      case DNEG:
+        return "DNEG";
+      case ISHL:
+        return "ISHL";
+      case LSHL:
+        return "LSHL";
+      case ISHR:
+        return "ISHR";
+      case LSHR:
+        return "LSHR";
+      case IUSHR:
+        return "IUSHR";
+      case LUSHR:
+        return "LUSHR";
+      case IAND:
+        return "IAND";
+      case LAND:
+        return "LAND";
+      case IOR:
+        return "IOR";
+      case LOR:
+        return "LOR";
+      case IXOR:
+        return "IXOR";
+      case LXOR:
+        return "LXOR";
+        // case IINC: return "IINC";
+      case I2L:
+        return "I2L";
+      case I2F:
+        return "I2F";
+      case I2D:
+        return "I2D";
+      case L2I:
+        return "L2I";
+      case L2F:
+        return "L2F";
+      case L2D:
+        return "L2D";
+      case F2I:
+        return "F2I";
+      case F2L:
+        return "F2L";
+      case F2D:
+        return "F2D";
+      case D2I:
+        return "D2I";
+      case D2L:
+        return "D2L";
+      case D2F:
+        return "D2F";
+      case I2B:
+        return "I2B";
+      case I2C:
+        return "I2C";
+      case I2S:
+        return "I2S";
+      case LCMP:
+        return "LCMP";
+      case FCMPL:
+        return "FCMPL";
+      case FCMPG:
+        return "FCMPG";
+      case DCMPL:
+        return "DCMPL";
+      case DCMPG:
+        return "DCMPG";
+      case IFEQ:
+        return "IFEQ";
+      case IFNE:
+        return "IFNE";
+      case IFLT:
+        return "IFLT";
+      case IFGE:
+        return "IFGE";
+      case IFGT:
+        return "IFGT";
+      case IFLE:
+        return "IFLE";
+      case IF_ICMPEQ:
+        return "IF_ICMPEQ";
+      case IF_ICMPNE:
+        return "IF_ICMPNE";
+      case IF_ICMPLT:
+        return "IF_ICMPLT";
+      case IF_ICMPGE:
+        return "IF_ICMPGE";
+      case IF_ICMPGT:
+        return "IF_ICMPGT";
+      case IF_ICMPLE:
+        return "IF_ICMPLE";
+      case IF_ACMPEQ:
+        return "IF_ACMPEQ";
+      case IF_ACMPNE:
+        return "IF_ACMPNE";
+      case GOTO:
+        return "GOTO";
+        // case JSR: return "JSR";
+        // case RET: return "RET";
+      case TABLESWITCH:
+        return "TABLESWITCH";
+      case LOOKUPSWITCH:
+        return "LOOKUPSWITCH";
+      case IRETURN:
+        return "IRETURN";
+      case LRETURN:
+        return "LRETURN";
+      case FRETURN:
+        return "FRETURN";
+      case DRETURN:
+        return "DRETURN";
+      case ARETURN:
+        return "ARETURN";
+      case RETURN:
+        return "RETURN";
+      case GETSTATIC:
+        return "GETSTATIC";
+      case PUTSTATIC:
+        return "PUTSTATIC";
+      case GETFIELD:
+        return "GETFIELD";
+      case PUTFIELD:
+        return "PUTFIELD";
+      case INVOKEVIRTUAL:
+        return "INVOKEVIRTUAL";
+      case INVOKESPECIAL:
+        return "INVOKESPECIAL";
+      case INVOKESTATIC:
+        return "INVOKESTATIC";
+      case INVOKEINTERFACE:
+        return "INVOKEINTERFACE";
+      case INVOKEDYNAMIC:
+        return "INVOKEDYNAMIC";
+      case NEW:
+        return "NEW";
+      case NEWARRAY:
+        return "NEWARRAY";
+      case ANEWARRAY:
+        return "ANEWARRAY";
+      case ARRAYLENGTH:
+        return "ARRAYLENGTH";
+      case ATHROW:
+        return "ATHROW";
+      case CHECKCAST:
+        return "CHECKCAST";
+      case INSTANCEOF:
+        return "INSTANCEOF";
+      case MONITORENTER:
+        return "MONITORENTER";
+      case MONITOREXIT:
+        return "MONITOREXIT";
+      case MULTIANEWARRAY:
+        return "MULTIANEWARRAY";
+      case IFNULL:
+        return "IFNULL";
+      case IFNONNULL:
+        return "IFNONNULL";
+
+        // Non-CF instructions.
+      case ICONST:
+        return "ICONST";
+      case LCONST:
+        return "LCONST";
+      case FCONST:
+        return "FCONST";
+      case DCONST:
+        return "DCONST";
+
+      default:
+        throw new Unreachable("Unexpected LIR opcode: " + opcode);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
new file mode 100644
index 0000000..8ffc285
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
@@ -0,0 +1,98 @@
+// 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.errors.Unimplemented;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+
+/**
+ * Structured callbacks for interpreting LIR.
+ *
+ * <p>This callback parses the actual instructions and dispatches to instruction specific methods
+ * where the parsed data is provided as arguments. Instructions that are part of a family of
+ * instructions have a default implementation that will call the "instruction family" methods (e.g.,
+ * onInvokeVirtual will default dispatch to onInvokedMethodInstruction).
+ *
+ * <p>Due to the parsing of the individual instructions, this parser has a higher overhead than
+ * using the basic {@code LIRInstructionView}.
+ */
+public class LIRParsedInstructionCallback implements LIRInstructionCallback {
+
+  private final LIRCode code;
+
+  public LIRParsedInstructionCallback(LIRCode code) {
+    this.code = code;
+  }
+
+  public void onConstNull() {}
+
+  public void onConstString(DexString string) {}
+
+  public void onInvokeMethodInstruction(DexMethod method, IntList arguments) {}
+
+  public void onInvokeVirtual(DexMethod method, IntList arguments) {
+    onInvokeMethodInstruction(method, arguments);
+  }
+
+  public void onFieldInstruction(DexField field) {
+    onFieldInstruction(field);
+  }
+
+  public void onStaticGet(DexField field) {
+    onFieldInstruction(field);
+  }
+
+  public void onReturnVoid() {}
+
+  private DexItem getConstantItem(int index) {
+    return code.getConstantItem(index);
+  }
+
+  @Override
+  public final void onInstructionView(LIRInstructionView view) {
+    switch (view.getOpcode()) {
+      case LIROpcodes.ACONST_NULL:
+        {
+          onConstNull();
+          break;
+        }
+      case LIROpcodes.LDC:
+        {
+          DexItem item = getConstantItem(view.getNextConstantOperand());
+          if (item instanceof DexString) {
+            onConstString((DexString) item);
+          }
+          break;
+        }
+      case LIROpcodes.INVOKEVIRTUAL:
+        {
+          DexMethod target = (DexMethod) getConstantItem(view.getNextConstantOperand());
+          IntList arguments = new IntArrayList();
+          while (view.hasMoreOperands()) {
+            arguments.add(view.getNextValueOperand());
+          }
+          onInvokeVirtual(target, arguments);
+          break;
+        }
+      case LIROpcodes.GETSTATIC:
+        {
+          DexField field = (DexField) getConstantItem(view.getNextConstantOperand());
+          onStaticGet(field);
+          break;
+        }
+      case LIROpcodes.RETURN:
+        {
+          onReturnVoid();
+          break;
+        }
+      default:
+        throw new Unimplemented("No dispatch for opcode " + LIROpcodes.toString(view.getOpcode()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java b/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
index 0602eb3..302f82f 100644
--- a/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
+++ b/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
@@ -11,7 +11,8 @@
 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 com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.code.IRMetadata;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -30,10 +31,15 @@
 
   @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);
+    LIRCode code =
+        LIRCode.builder(
+                v -> {
+                  throw new Unreachable();
+                })
+            .setMetadata(IRMetadata.unknown())
+            .addConstNull()
+            .addConstInt(42)
+            .build();
 
     LIRIterator it = code.iterator();
 
@@ -43,23 +49,17 @@
     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);
+        insn -> {
+          assertEquals(LIROpcodes.ACONST_NULL, insn.getOpcode());
+          assertEquals(0, insn.getRemainingOperandSizeInBytes());
         });
 
     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);
+        insn -> {
+          assertEquals(LIROpcodes.ICONST, insn.getOpcode());
+          assertEquals(4, insn.getRemainingOperandSizeInBytes());
         });
     assertFalse(it.hasNext());
 
@@ -69,10 +69,10 @@
     for (LIRInstructionView view : code) {
       if (oldView == null) {
         oldView = view;
-        view.accept((opcode, ignore1, ignore2) -> assertEquals(LIROpcodes.ACONST_NULL, opcode));
+        view.accept(insn -> assertEquals(LIROpcodes.ACONST_NULL, insn.getOpcode()));
       } else {
         assertSame(oldView, view);
-        view.accept((opcode, ignore1, ignore2) -> assertEquals(LIROpcodes.ICONST, opcode));
+        view.accept(insn -> assertEquals(LIROpcodes.ICONST, insn.getOpcode()));
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
new file mode 100644
index 0000000..6bf8f98
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
@@ -0,0 +1,171 @@
+// 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.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Marker;
+import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader;
+import com.android.tools.r8.graph.LazyLoadedDexApplication.Builder;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.ir.optimize.CodeRewriter;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.jar.CfApplicationWriter;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.Timing;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.nio.file.Path;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LIRRoundtripTest extends TestBase {
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultCfRuntime().build();
+  }
+
+  private final TestParameters parameters;
+  private AppView<?> appView;
+  private ProgramMethod method;
+
+  public LIRRoundtripTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    DexItemFactory factory = new DexItemFactory();
+    InternalOptions options = new InternalOptions(factory, new Reporter());
+    options.programConsumer = ClassFileConsumer.emptyConsumer();
+    Builder builder = DexApplication.builder(options, Timing.empty());
+    JarClassFileReader<DexProgramClass> reader =
+        new JarClassFileReader<>(
+            new JarApplicationReader(options),
+            clazz -> {
+              builder.addProgramClass(clazz);
+              clazz
+                  .programMethods()
+                  .forEach(
+                      m -> {
+                        if (m.getReference().qualifiedName().endsWith("main")) {
+                          method = m;
+                        }
+                      });
+            },
+            ClassKind.PROGRAM);
+    reader.read(Origin.unknown(), ToolHelper.getClassAsBytes(TestClass.class));
+    appView =
+        AppView.createForD8(
+            AppInfo.createInitialAppInfo(
+                builder.build(), GlobalSyntheticsStrategy.forNonSynthesizing()));
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForJvm()
+        .addProgramClasses(TestClass.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Test
+  public void testRoundtrip() throws Exception {
+    IRCode irCode1 = translateCf2IR();
+    LIRCode lirCode = translateIR2LIR(irCode1);
+    IRCode irCode2 = translateLIR2IR(lirCode);
+    translateIR2Cf(irCode2);
+    Path out = writeToFile();
+    testForJvm()
+        .addProgramFiles(out)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private Path writeToFile() {
+    Path out = temp.getRoot().toPath().resolve("out.jar");
+    Marker fakeMarker = new Marker(Tool.D8);
+    ArchiveConsumer consumer = new ArchiveConsumer(out);
+    new CfApplicationWriter(appView, fakeMarker).write(consumer);
+    consumer.finished(appView.reporter());
+    return out;
+  }
+
+  private IRCode translateCf2IR() {
+    CfCode cfCode = method.getDefinition().getCode().asCfCode();
+    return cfCode.buildIR(method, appView, Origin.unknown());
+  }
+
+  private LIRCode translateIR2LIR(IRCode irCode) {
+    Reference2IntMap<Value> values = new Reference2IntOpenHashMap<>();
+    int index = 0;
+    for (Instruction instruction : irCode.instructions()) {
+      if (instruction.hasOutValue()) {
+        values.put(instruction.outValue(), index);
+      }
+      index++;
+    }
+    LIRBuilder<Value> builder =
+        new LIRBuilder<Value>(values::getInt).setMetadata(irCode.metadata());
+    BasicBlockIterator blockIt = irCode.listIterator();
+    while (blockIt.hasNext()) {
+      BasicBlock block = blockIt.next();
+      // TODO(b/225838009): Support control flow.
+      assert !block.hasPhis();
+      InstructionIterator it = block.iterator();
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+        instruction.buildLIR(builder);
+      }
+    }
+    return builder.build();
+  }
+
+  private IRCode translateLIR2IR(LIRCode lirCode) {
+    return new LIR2IRBuilder(appView).translate(method, lirCode);
+  }
+
+  private void translateIR2Cf(IRCode irCode) {
+    CodeRewriter codeRewriter = new CodeRewriter(appView);
+    DeadCodeRemover deadCodeRemover = new DeadCodeRemover(appView, codeRewriter);
+    CfCode cfCode =
+        new CfBuilder(appView, method, irCode, BytecodeMetadataProvider.empty())
+            .build(deadCodeRemover);
+    method.setCode(cfCode, appView);
+  }
+}