Support basic control flow in LIR.

Bug: b/225838009
Change-Id: Id2f6efa8a76f3fbcf7b82b67d674a29509de817a
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 76e9d15..182b9b4 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
@@ -177,7 +177,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     builder.addArgument(index, knownToBeBoolean);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
index 14837ee..5715e54 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
@@ -17,6 +17,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;
 
 public class ArrayLength extends Instruction {
 
@@ -151,4 +152,9 @@
   public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
     return false;
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
+    builder.addArrayLength(array());
+  }
 }
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 afb3203..900fe91 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
@@ -182,7 +182,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     builder.addConstString(value);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugPosition.java b/src/main/java/com/android/tools/r8/ir/code/DebugPosition.java
index 1f87da5..651fb35 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugPosition.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugPosition.java
@@ -101,7 +101,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     builder.addDebugPosition(getPosition());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 025dec0..b953a68 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfGoto;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.lightir.LIRBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
 import java.util.List;
 import java.util.ListIterator;
@@ -126,6 +127,11 @@
     return true;
   }
 
+  @Override
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
+    builder.addGoto(getTarget());
+  }
+
   public static class Builder extends BuilderBase<Builder, Goto> {
 
     private BasicBlock target;
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java
index 9269104..12089ec 100644
--- a/src/main/java/com/android/tools/r8/ir/code/If.java
+++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.lightir.LIRBuilder;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOutputMode;
@@ -289,4 +290,16 @@
     assert inValues.get(0).outType() == inValues.get(1).outType();
     builder.add(new CfIfCmp(type, ifType, builder.getLabel(getTrueTarget())), this);
   }
+
+  @Override
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
+    ValueType ifType = inValues.get(0).outType();
+    if (inValues.size() == 1) {
+      builder.addIf(type, ifType, inValues.get(0), getTrueTarget());
+      return;
+    }
+    assert inValues.size() == 2;
+    assert inValues.get(0).outType() == inValues.get(1).outType();
+    builder.addIfCmp(type, ifType, inValues, getTrueTarget());
+  }
 }
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 7aba8bd..c99ee11 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
@@ -1559,7 +1559,7 @@
     return false;
   }
 
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     throw new Unimplemented("Missing impl for " + getClass().getSimpleName());
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 09f6e36..9203e93 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -218,7 +218,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     builder.addInvokeDirect(getInvokedMethod(), arguments());
   }
 
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 1af5563..0cbf370 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
@@ -184,7 +184,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> 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 935e9df..d8882e4 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
@@ -153,7 +153,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     if (hasReturnValue()) {
       builder.addReturn(returnValue());
     } else {
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 19986d0..afad1ff 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
@@ -300,7 +300,7 @@
   }
 
   @Override
-  public void buildLIR(LIRBuilder<Value> builder) {
+  public void buildLIR(LIRBuilder<Value, BasicBlock> builder) {
     builder.addStaticGet(getField());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 9967ae3..ab044e3 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -1656,10 +1656,14 @@
       code = roundtripThroughLIR(code, feedback, bytecodeMetadataProvider, timing);
     }
     if (options.isGeneratingClassFiles()) {
+      timing.begin("IR->CF");
       finalizeToCf(code, feedback, bytecodeMetadataProvider, timing);
+      timing.end();
     } else {
       assert options.isGeneratingDex();
+      timing.begin("IR->DEX");
       finalizeToDex(code, feedback, bytecodeMetadataProvider, timing);
+      timing.end();
     }
   }
 
@@ -1668,8 +1672,12 @@
       OptimizationFeedback feedback,
       BytecodeMetadataProvider bytecodeMetadataProvider,
       Timing timing) {
-    LIRCode lirCode = IR2LIRConverter.translate(code);
+    timing.begin("IR->LIR");
+    LIRCode lirCode = IR2LIRConverter.translate(code, appView.dexItemFactory());
+    timing.end();
+    timing.begin("LIR->IR");
     IRCode irCode = LIR2IRConverter.translate(code.context(), lirCode, appView);
+    timing.end();
     return irCode;
   }
 
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
index e91fd8a..9908098 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LIRConverter.java
@@ -3,36 +3,70 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.graph.DexItemFactory;
 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.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.lightir.LIRBuilder.BlockIndexGetter;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
 
 public class IR2LIRConverter {
 
   private IR2LIRConverter() {}
 
-  public static LIRCode translate(IRCode irCode) {
+  public static LIRCode translate(IRCode irCode, DexItemFactory factory) {
+    irCode.traceBlocks();
+    Reference2IntMap<BasicBlock> blocks = new Reference2IntOpenHashMap<>();
     Reference2IntMap<Value> values = new Reference2IntOpenHashMap<>();
-    int index = 0;
-    for (Instruction instruction : irCode.instructions()) {
-      if (instruction.hasOutValue()) {
-        values.put(instruction.outValue(), index);
+    int instructionIndex = 0;
+    int valueIndex = 0;
+    for (BasicBlock block : irCode.blocks) {
+      blocks.put(block, instructionIndex);
+      for (Phi phi : block.getPhis()) {
+        values.put(phi, valueIndex);
+        valueIndex++;
+        instructionIndex++;
       }
-      index++;
+      for (Instruction instruction : block.getInstructions()) {
+        if (instruction.hasOutValue()) {
+          values.put(instruction.outValue(), valueIndex);
+        }
+        valueIndex++;
+        if (!instruction.isArgument()) {
+          instructionIndex++;
+        }
+      }
     }
-    LIRBuilder<Value> builder =
-        new LIRBuilder<Value>(irCode.context().getReference(), values::getInt)
+    LIRBuilder<Value, BasicBlock> builder =
+        new LIRBuilder<Value, BasicBlock>(
+                irCode.context().getReference(), values::getInt, blocks::getInt, factory)
             .setMetadata(irCode.metadata());
     BasicBlockIterator blockIt = irCode.listIterator();
     while (blockIt.hasNext()) {
       BasicBlock block = blockIt.next();
-      // TODO(b/225838009): Support control flow.
-      assert !block.hasPhis();
+      if (block.hasPhis()) {
+        // The block order of the predecessors may change, since the LIR does not encode the
+        // direct links, the block order is used to determine predecessor order.
+        int[] permutation = computePermutation(block.getPredecessors(), blocks::getInt);
+        Value[] operands = new Value[block.getPredecessors().size()];
+        for (Phi phi : block.getPhis()) {
+          permuteOperands(phi.getOperands(), permutation, operands);
+          builder.addPhi(phi.getType(), Arrays.asList(operands));
+        }
+      }
       InstructionIterator it = block.iterator();
       while (it.hasNext()) {
         Instruction instruction = it.next();
@@ -42,4 +76,49 @@
     }
     return builder.build();
   }
+
+  private static void permuteOperands(List<Value> operands, int[] permutation, Value[] output) {
+    for (int i = 0; i < operands.size(); i++) {
+      Value operand = operands.get(i);
+      output[permutation[i]] = operand;
+    }
+  }
+
+  private static int[] computePermutation(
+      List<BasicBlock> originalPredecessors, BlockIndexGetter<BasicBlock> blockIndexGetter) {
+    int predecessorCount = originalPredecessors.size();
+    // The final predecessor list is sorted by block order.
+    List<BasicBlock> sortedPredecessors = new ArrayList<>(originalPredecessors);
+    sortedPredecessors.sort(Comparator.comparingInt(blockIndexGetter::getBlockIndex));
+    // Since predecessors are not unique, build a map from each unique block to its set of indices.
+    Reference2ReferenceMap<BasicBlock, IntList> mapping =
+        new Reference2ReferenceOpenHashMap<>(predecessorCount);
+    for (int originalIndex = 0; originalIndex < predecessorCount; originalIndex++) {
+      BasicBlock predecessor = originalPredecessors.get(originalIndex);
+      mapping.computeIfAbsent(predecessor, k -> new IntArrayList(1)).add(originalIndex);
+    }
+    // Assign an original index to each sorted index.
+    int[] permutation = new int[predecessorCount];
+    for (int sortedIndex = 0; sortedIndex < predecessorCount; ) {
+      BasicBlock predecessor = sortedPredecessors.get(sortedIndex);
+      IntList originalIndices = mapping.get(predecessor);
+      assert verifySameBlock(sortedPredecessors, sortedIndex, originalIndices.size());
+      for (int originalIndex : originalIndices) {
+        permutation[originalIndex] = sortedIndex++;
+      }
+    }
+    return permutation;
+  }
+
+  private static boolean verifySameBlock(List<BasicBlock> predecessors, int startIndex, int size) {
+    if (size == 1) {
+      return true;
+    }
+    BasicBlock block = predecessors.get(startIndex);
+    for (int i = startIndex + 1; i < startIndex + size; i++) {
+      BasicBlock other = predecessors.get(i);
+      assert block == other;
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
index 75a372b..efccdb9 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIR2IRConverter.java
@@ -12,15 +12,21 @@
 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.ArrayLength;
 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.DebugPosition;
+import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.NumberGenerator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.ir.code.Return;
@@ -28,6 +34,9 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
 import com.android.tools.r8.lightir.LIRCode.PositionEntry;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -39,6 +48,7 @@
 
   public static IRCode translate(ProgramMethod method, LIRCode lirCode, AppView<?> appView) {
     Parser parser = new Parser(lirCode, method.getReference(), appView);
+    parser.computeControlFlowGraph();
     parser.parseArguments(method);
     lirCode.forEach(view -> view.accept(parser));
     return parser.getIRCode(method);
@@ -50,13 +60,15 @@
    */
   private static class Parser extends LIRParsedInstructionCallback {
 
+    private static final int ENTRY_BLOCK_INDEX = -1;
+
     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 final Int2ReferenceMap<BasicBlock> blocks = new Int2ReferenceOpenHashMap<>();
 
     private BasicBlock currentBlock = null;
     private int nextInstructionIndex = 0;
@@ -76,9 +88,22 @@
       currentPosition = SyntheticPosition.builder().setLine(0).setMethod(method).build();
     }
 
+    private void closeCurrentBlock() {
+      currentBlock = null;
+    }
+
+    private void ensureCurrentBlock() {
+      BasicBlock nextBlock = blocks.get(nextInstructionIndex);
+      if (nextBlock != null) {
+        assert currentBlock != nextBlock;
+        currentBlock = nextBlock;
+      }
+      assert currentBlock != null;
+    }
+
     private void ensureCurrentPosition() {
       if (nextPositionEntry != null
-          && nextPositionEntry.fromInstructionIndex < nextInstructionIndex) {
+          && nextPositionEntry.fromInstructionIndex <= nextInstructionIndex) {
         currentPosition = nextPositionEntry.position;
         advanceNextPositionEntry();
       }
@@ -91,16 +116,44 @@
               : null;
     }
 
+    public void computeControlFlowGraph() {
+      // TODO(b/225838009): Since each basic block has a terminal instruction we can compute block
+      //  structure lazily while iterating the instructions and avoid this additional pass.
+      // Always create an entry block as it cannot be targeted from code.
+      blocks.put(ENTRY_BLOCK_INDEX, createBlock());
+      for (LIRInstructionView view : code) {
+        int opcode = view.getOpcode();
+        if (LIROpcodes.isControlFlowInstruction(opcode)) {
+          // TODO(b/225838009): Deal with multi-target instructions.
+          int target = view.getNextBlockOperand();
+          blocks.computeIfAbsent(target, k -> createBlock());
+          if (LIROpcodes.isControlFlowInstructionWithFallthrough(opcode)) {
+            blocks.computeIfAbsent(view.getInstructionIndex() + 1, k -> createBlock());
+          }
+        }
+      }
+    }
+
+    public BasicBlock createBlock() {
+      BasicBlock block = new BasicBlock();
+      block.setNumber(basicBlockNumberGenerator.next());
+      // The LIR is in SSA with accurate phis. The blocks are thus filled by construction.
+      block.setFilled();
+      return block;
+    }
+
     public void parseArguments(ProgramMethod method) {
-      currentBlock = new BasicBlock();
-      currentBlock.setNumber(basicBlockNumberGenerator.next());
+      currentBlock = blocks.get(ENTRY_BLOCK_INDEX);
       boolean hasReceiverArgument = !method.getDefinition().isStatic();
       assert code.getArgumentCount()
           == method.getParameters().size() + (hasReceiverArgument ? 1 : 0);
       if (hasReceiverArgument) {
         addThisArgument(method.getHolderType());
       }
-      method.getParameters().forEach(this::addArgument);
+      int index = hasReceiverArgument ? 1 : 0;
+      for (DexType parameter : method.getParameters()) {
+        addArgument(parameter, index++);
+      }
       // Set up position state after adding arguments.
       advanceNextPositionEntry();
     }
@@ -108,12 +161,17 @@
     public IRCode getIRCode(ProgramMethod method) {
       // TODO(b/225838009): Support control flow.
       currentBlock.setFilled();
-      blocks.add(currentBlock);
+      LinkedList<BasicBlock> blockList = new LinkedList<>();
+      IntList blockIndices = new IntArrayList(blocks.keySet());
+      blockIndices.sort(Integer::compare);
+      for (int i = 0; i < blockIndices.size(); i++) {
+        blockList.add(blocks.get(blockIndices.getInt(i)));
+      }
       return new IRCode(
           appView.options(),
           method,
           Position.syntheticNone(),
-          blocks,
+          blockList,
           valueNumberGenerator,
           basicBlockNumberGenerator,
           code.getMetadata(),
@@ -121,7 +179,13 @@
           new MutableMethodConversionOptions(appView.options()));
     }
 
-    public Value getSsaValue(int index) {
+    public BasicBlock getBasicBlock(int instructionIndex) {
+      BasicBlock block = blocks.get(instructionIndex);
+      assert block != null;
+      return block;
+    }
+
+    public Value getValue(int index) {
       Value value = values[index];
       if (value == null) {
         value = new Value(valueNumberGenerator.next(), TypeElement.getBottom(), null);
@@ -130,10 +194,10 @@
       return value;
     }
 
-    public List<Value> getSsaValues(IntList indices) {
+    public List<Value> getValues(IntList indices) {
       List<Value> arguments = new ArrayList<>(indices.size());
       for (int i = 0; i < indices.size(); i++) {
-        arguments.add(getSsaValue(indices.getInt(i)));
+        arguments.add(getValue(indices.getInt(i)));
       }
       return arguments;
     }
@@ -145,7 +209,7 @@
     public Value getOutValueForNextInstruction(TypeElement type) {
       // TODO(b/225838009): Support debug locals.
       DebugLocalInfo localInfo = null;
-      int index = peekNextInstructionIndex();
+      int index = peekNextInstructionIndex() + code.getArgumentCount();
       Value value = values[index];
       if (value == null) {
         value = new Value(valueNumberGenerator.next(), type, localInfo);
@@ -159,27 +223,59 @@
       return value;
     }
 
-    private void addInstruction(Instruction instruction) {
+    public Phi getPhiForNextInstructionAndAdvanceState(TypeElement type) {
+      int index = peekNextInstructionIndex() + code.getArgumentCount();
+      // TODO(b/225838009): The phi constructor implicitly adds to the block, so we need to ensure
+      //  the block. However, we must grab the index above. Find a way to clean this up so it is
+      //  uniform with instructions.
+      advanceInstructionState();
+      // Creating the phi implicitly adds it to currentBlock.
+      DebugLocalInfo localInfo = null;
+      Phi phi =
+          new Phi(
+              valueNumberGenerator.next(), currentBlock, type, localInfo, RegisterReadType.NORMAL);
+      Value value = values[index];
+      if (value != null) {
+        // A fake ssa value has already been created, replace the users by the actual phi.
+        // TODO(b/225838009): We could consider encoding the value type as a bit in the value index
+        //  and avoid the overhead of replacing users at phi-definition time.
+        assert !value.isPhi();
+        value.replaceUsers(phi);
+      }
+      values[index] = phi;
+      return phi;
+    }
+
+    private void advanceInstructionState() {
+      ensureCurrentBlock();
       ensureCurrentPosition();
-      instruction.setPosition(currentPosition);
-      currentBlock.getInstructions().add(instruction);
-      instruction.setBlock(currentBlock);
       ++nextInstructionIndex;
     }
 
+    private void addInstruction(Instruction instruction) {
+      advanceInstructionState();
+      instruction.setPosition(currentPosition);
+      currentBlock.getInstructions().add(instruction);
+      instruction.setBlock(currentBlock);
+    }
+
     private void addThisArgument(DexType type) {
-      Argument argument = addArgument(type);
+      Argument argument = addArgument(type, 0);
       argument.outValue().markAsThis();
     }
 
-    private Argument addArgument(DexType type) {
-      Argument instruction =
-          new Argument(
-              getOutValueForNextInstruction(type.toTypeElement(appView)),
-              peekNextInstructionIndex(),
-              type.isBooleanType());
-      addInstruction(instruction);
-      return instruction;
+    private Argument addArgument(DexType type, int index) {
+      // Arguments are not included in the "instructions" so this does not call "addInstruction"
+      // which would otherwise advance the state.
+      Value dest = getValue(index);
+      dest.setType(type.toTypeElement(appView));
+      Argument argument = new Argument(dest, index, type.isBooleanType());
+      assert currentBlock != null;
+      assert currentPosition.isSyntheticPosition();
+      argument.setPosition(currentPosition);
+      currentBlock.getInstructions().add(argument);
+      argument.setBlock(currentBlock);
+      return argument;
     }
 
     @Override
@@ -195,10 +291,28 @@
     }
 
     @Override
+    public void onIf(Type ifKind, int blockIndex, int valueIndex) {
+      BasicBlock targetBlock = getBasicBlock(blockIndex);
+      Value value = getValue(valueIndex);
+      addInstruction(new If(ifKind, value));
+      currentBlock.link(targetBlock);
+      currentBlock.link(getBasicBlock(nextInstructionIndex));
+      closeCurrentBlock();
+    }
+
+    @Override
+    public void onGoto(int blockIndex) {
+      BasicBlock targetBlock = getBasicBlock(blockIndex);
+      addInstruction(new Goto());
+      currentBlock.link(targetBlock);
+      closeCurrentBlock();
+    }
+
+    @Override
     public void onInvokeDirect(DexMethod target, IntList arguments) {
       // TODO(b/225838009): Maintain is-interface bit.
       Value dest = getInvokeInstructionOutputValue(target);
-      List<Value> ssaArgumentValues = getSsaValues(arguments);
+      List<Value> ssaArgumentValues = getValues(arguments);
       InvokeDirect instruction = new InvokeDirect(target, dest, ssaArgumentValues);
       addInstruction(instruction);
     }
@@ -207,7 +321,7 @@
     public void onInvokeVirtual(DexMethod target, IntList arguments) {
       // TODO(b/225838009): Maintain is-interface bit.
       Value dest = getInvokeInstructionOutputValue(target);
-      List<Value> ssaArgumentValues = getSsaValues(arguments);
+      List<Value> ssaArgumentValues = getValues(arguments);
       InvokeVirtual instruction = new InvokeVirtual(target, dest, ssaArgumentValues);
       addInstruction(instruction);
     }
@@ -230,8 +344,25 @@
     }
 
     @Override
+    public void onArrayLength(int arrayIndex) {
+      Value dest = getOutValueForNextInstruction(TypeElement.getInt());
+      Value arrayValue = getValue(arrayIndex);
+      addInstruction(new ArrayLength(dest, arrayValue));
+    }
+
+    @Override
     public void onDebugPosition() {
       addInstruction(new DebugPosition());
     }
+
+    @Override
+    public void onPhi(DexType type, IntList operands) {
+      Phi phi = getPhiForNextInstructionAndAdvanceState(type.toTypeElement(appView));
+      List<Value> values = new ArrayList<>(operands.size());
+      for (int i = 0; i < operands.size(); i++) {
+        values.add(getValue(operands.getInt(i)));
+      }
+      phi.addOperands(values);
+    }
   }
 }
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 fe9e5b2..409f02e 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRBuilder.java
@@ -4,18 +4,25 @@
 package com.android.tools.r8.lightir;
 
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexItemFactory;
 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.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.lightir.LIRCode.PositionEntry;
 import com.android.tools.r8.utils.ListUtils;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -23,17 +30,27 @@
  *
  * @param <V> Type of SSA values. This is abstract to ensure that value internals are not used in
  *     building.
+ * @param <B> Type of basic blocks. This is abstract to ensure that basic block internals are not
+ *     used in building.
  */
-public class LIRBuilder<V> {
+public class LIRBuilder<V, B> {
 
+  // Abstraction for the only accessible properties of an SSA value.
   public interface ValueIndexGetter<V> {
     int getValueIndex(V value);
   }
 
+  // Abstraction for the only accessible properties of a basic block.
+  public interface BlockIndexGetter<B> {
+    int getBlockIndex(B block);
+  }
+
+  private final DexItemFactory factory;
   private final ByteArrayWriter byteWriter = new ByteArrayWriter();
   private final LIRWriter writer = new LIRWriter(byteWriter);
   private final Reference2IntMap<DexItem> constants;
   private final ValueIndexGetter<V> valueIndexGetter;
+  private final BlockIndexGetter<B> blockIndexGetter;
   private final List<PositionEntry> positionTable;
   private int argumentCount = 0;
   private int instructionCount = 0;
@@ -42,15 +59,26 @@
   private Position currentPosition;
   private Position flushedPosition;
 
-  public LIRBuilder(DexMethod method, ValueIndexGetter<V> valueIndexGetter) {
+  // TODO(b/225838009): Reconsider this fixed space as the operand count for phis is much larger.
+  // Pre-allocated space for caching value indexes when writing instructions.
+  private static final int MAX_VALUE_COUNT = 10;
+  private int[] valueIndexBuffer = new int[MAX_VALUE_COUNT];
+
+  public LIRBuilder(
+      DexMethod method,
+      ValueIndexGetter<V> valueIndexGetter,
+      BlockIndexGetter<B> blockIndexGetter,
+      DexItemFactory factory) {
+    this.factory = factory;
     constants = new Reference2IntOpenHashMap<>();
     positionTable = new ArrayList<>();
     this.valueIndexGetter = valueIndexGetter;
+    this.blockIndexGetter = blockIndexGetter;
     currentPosition = SyntheticPosition.builder().setLine(0).setMethod(method).build();
     flushedPosition = currentPosition;
   }
 
-  public LIRBuilder<V> setCurrentPosition(Position position) {
+  public LIRBuilder<V, B> setCurrentPosition(Position position) {
     assert position != null;
     assert position != Position.none();
     currentPosition = position;
@@ -75,6 +103,7 @@
 
   private void writeConstantIndex(DexItem item) {
     int index = getConstantIndex(item);
+    assert constantIndexSize(item) == ByteUtils.intEncodingSize(index);
     ByteUtils.writeEncodedInt(index, writer::writeOperand);
   }
 
@@ -90,25 +119,31 @@
     ByteUtils.writeEncodedInt(index, writer::writeOperand);
   }
 
-  public LIRBuilder<V> setMetadata(IRMetadata metadata) {
+  private int getBlockIndex(B block) {
+    return blockIndexGetter.getBlockIndex(block);
+  }
+
+  private int blockIndexSize(int index) {
+    return ByteUtils.intEncodingSize(index);
+  }
+
+  private void writeBlockIndex(int index) {
+    ByteUtils.writeEncodedInt(index, writer::writeOperand);
+  }
+
+  public LIRBuilder<V, B> 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) {
+  public LIRBuilder<V, B> addArgument(int index, boolean knownToBeBoolean) {
     // Arguments are implicitly given by method descriptor and not an actual instruction.
     assert argumentCount == index;
     argumentCount++;
     return this;
   }
 
-  private void addInstruction() {
+  private void advanceInstructionState() {
     if (!currentPosition.equals(flushedPosition)) {
       setPositionIndex(instructionCount, currentPosition);
       flushedPosition = currentPosition;
@@ -116,76 +151,185 @@
     ++instructionCount;
   }
 
-  public LIRBuilder<V> addConstNull() {
-    addInstruction();
-    writer.writeOneByteInstruction(LIROpcodes.ACONST_NULL);
+  private LIRBuilder<V, B> addNoOperandInstruction(int opcode) {
+    advanceInstructionState();
+    writer.writeOneByteInstruction(opcode);
     return this;
   }
 
-  public LIRBuilder<V> addConstInt(int value) {
-    addInstruction();
+  private LIRBuilder<V, B> addOneItemInstruction(int opcode, DexItem item) {
+    return addInstructionTemplate(opcode, Collections.singletonList(item), Collections.emptyList());
+  }
+
+  private LIRBuilder<V, B> addOneValueInstruction(int opcode, V value) {
+    return addInstructionTemplate(
+        opcode, Collections.emptyList(), Collections.singletonList(value));
+  }
+
+  private LIRBuilder<V, B> addInstructionTemplate(int opcode, List<DexItem> items, List<V> values) {
+    assert values.size() < MAX_VALUE_COUNT;
+    advanceInstructionState();
+    int operandSize = 0;
+    for (DexItem item : items) {
+      operandSize += constantIndexSize(item);
+    }
+    for (int i = 0; i < values.size(); i++) {
+      V value = values.get(i);
+      int valueIndex = getValueIndex(value);
+      operandSize += valueIndexSize(valueIndex);
+      valueIndexBuffer[i] = valueIndex;
+    }
+    writer.writeInstruction(opcode, operandSize);
+    for (DexItem item : items) {
+      writeConstantIndex(item);
+    }
+    for (int i = 0; i < values.size(); i++) {
+      writeValueIndex(valueIndexBuffer[i]);
+    }
+    return this;
+  }
+
+  public LIRBuilder<V, B> addConstNull() {
+    return addNoOperandInstruction(LIROpcodes.ACONST_NULL);
+  }
+
+  public LIRBuilder<V, B> addConstInt(int value) {
     if (0 <= value && value <= 5) {
-      writer.writeOneByteInstruction(LIROpcodes.ICONST_0 + value);
+      addNoOperandInstruction(LIROpcodes.ICONST_0 + value);
     } else {
+      advanceInstructionState();
       writer.writeInstruction(LIROpcodes.ICONST, ByteUtils.intEncodingSize(value));
       ByteUtils.writeEncodedInt(value, writer::writeOperand);
     }
     return this;
   }
 
-  public LIRBuilder<V> addConstString(DexString string) {
-    addInstruction();
-    return writeConstantReferencingInstruction(LIROpcodes.LDC, string);
+  public LIRBuilder<V, B> addConstString(DexString string) {
+    return addOneItemInstruction(LIROpcodes.LDC, string);
   }
 
-  public LIRBuilder<V> addStaticGet(DexField field) {
-    addInstruction();
-    return writeConstantReferencingInstruction(LIROpcodes.GETSTATIC, field);
+  public LIRBuilder<V, B> addArrayLength(V array) {
+    return addOneValueInstruction(LIROpcodes.ARRAYLENGTH, array);
   }
 
-  public LIRBuilder<V> addInvokeInstruction(int opcode, DexMethod method, List<V> arguments) {
-    addInstruction();
-    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(opcode, argumentOprandSize);
-    writeConstantIndex(method);
-    for (int argumentIndex : argumentIndexes) {
-      writeValueIndex(argumentIndex);
-    }
-    return this;
+  public LIRBuilder<V, B> addStaticGet(DexField field) {
+    return addOneItemInstruction(LIROpcodes.GETSTATIC, field);
   }
 
-  public LIRBuilder<V> addInvokeDirect(DexMethod method, List<V> arguments) {
+  public LIRBuilder<V, B> addInvokeInstruction(int opcode, DexMethod method, List<V> arguments) {
+    return addInstructionTemplate(opcode, Collections.singletonList(method), arguments);
+  }
+
+  public LIRBuilder<V, B> addInvokeDirect(DexMethod method, List<V> arguments) {
     return addInvokeInstruction(LIROpcodes.INVOKEDIRECT, method, arguments);
   }
 
-  public LIRBuilder<V> addInvokeVirtual(DexMethod method, List<V> arguments) {
+  public LIRBuilder<V, B> addInvokeVirtual(DexMethod method, List<V> arguments) {
     return addInvokeInstruction(LIROpcodes.INVOKEVIRTUAL, method, arguments);
   }
 
-  public LIRBuilder<V> addReturn(V value) {
+  public LIRBuilder<V, B> addReturn(V value) {
     throw new Unimplemented();
   }
 
-  public LIRBuilder<V> addReturnVoid() {
-    addInstruction();
-    writer.writeOneByteInstruction(LIROpcodes.RETURN);
+  public LIRBuilder<V, B> addReturnVoid() {
+    return addNoOperandInstruction(LIROpcodes.RETURN);
+  }
+
+  public LIRBuilder<V, B> addDebugPosition(Position position) {
+    assert currentPosition == position;
+    return addNoOperandInstruction(LIROpcodes.DEBUGPOS);
+  }
+
+  public LIRBuilder<V, B> addGoto(B target) {
+    int targetIndex = getBlockIndex(target);
+    int operandSize = blockIndexSize(targetIndex);
+    advanceInstructionState();
+    writer.writeInstruction(LIROpcodes.GOTO, operandSize);
+    writeBlockIndex(targetIndex);
     return this;
   }
 
-  public LIRBuilder<V> addDebugPosition(Position position) {
-    assert currentPosition == position;
-    addInstruction();
-    writer.writeOneByteInstruction(LIROpcodes.DEBUGPOS);
+  public LIRBuilder<V, B> addIf(Type ifKind, ValueType valueType, V value, B trueTarget) {
+    int opcode;
+    switch (ifKind) {
+      case EQ:
+        opcode = valueType.isObject() ? LIROpcodes.IFNULL : LIROpcodes.IFEQ;
+        break;
+      case GE:
+        opcode = LIROpcodes.IFGE;
+        break;
+      case GT:
+        opcode = LIROpcodes.IFGT;
+        break;
+      case LE:
+        opcode = LIROpcodes.IFLE;
+        break;
+      case LT:
+        opcode = LIROpcodes.IFLT;
+        break;
+      case NE:
+        opcode = valueType.isObject() ? LIROpcodes.IFNONNULL : LIROpcodes.IFNE;
+        break;
+      default:
+        throw new Unreachable("Unexpected if kind: " + ifKind);
+    }
+    int targetIndex = getBlockIndex(trueTarget);
+    int valueIndex = getValueIndex(value);
+    int operandSize = blockIndexSize(targetIndex) + valueIndexSize(valueIndex);
+    advanceInstructionState();
+    writer.writeInstruction(opcode, operandSize);
+    writeBlockIndex(targetIndex);
+    writeValueIndex(valueIndex);
     return this;
   }
 
+  public LIRBuilder<V, B> addIfCmp(
+      Type ifKind, ValueType valueType, List<V> inValues, B trueTarget) {
+    int opcode;
+    switch (ifKind) {
+      case EQ:
+        opcode = valueType.isObject() ? LIROpcodes.IF_ACMPEQ : LIROpcodes.IF_ICMPEQ;
+        break;
+      case GE:
+        opcode = LIROpcodes.IF_ICMPGE;
+        break;
+      case GT:
+        opcode = LIROpcodes.IF_ICMPGT;
+        break;
+      case LE:
+        opcode = LIROpcodes.IF_ICMPLE;
+        break;
+      case LT:
+        opcode = LIROpcodes.IF_ICMPLT;
+        break;
+      case NE:
+        opcode = valueType.isObject() ? LIROpcodes.IF_ACMPNE : LIROpcodes.IF_ICMPNE;
+        break;
+      default:
+        throw new Unreachable("Unexpected if kind " + ifKind);
+    }
+    int targetIndex = getBlockIndex(trueTarget);
+    int valueOneIndex = getValueIndex(inValues.get(0));
+    int valueTwoIndex = getValueIndex(inValues.get(1));
+    int operandSize =
+        blockIndexSize(targetIndex) + valueIndexSize(valueOneIndex) + valueIndexSize(valueTwoIndex);
+    advanceInstructionState();
+    writer.writeInstruction(opcode, operandSize);
+    writeBlockIndex(targetIndex);
+    writeValueIndex(valueOneIndex);
+    writeValueIndex(valueTwoIndex);
+    return this;
+  }
+
+  public LIRBuilder<V, B> addPhi(TypeElement type, List<V> operands) {
+    DexType dexType =
+        type.isPrimitiveType()
+            ? type.asPrimitiveType().toDexType(factory)
+            : type.asReferenceType().toDexType(factory);
+    return addInstructionTemplate(LIROpcodes.PHI, Collections.singletonList(dexType), operands);
+  }
+
   public LIRCode build() {
     assert metadata != null;
     int constantsCount = constants.size();
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 04dffd4..858de2a 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRCode.java
@@ -4,9 +4,11 @@
 package com.android.tools.r8.lightir;
 
 import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.lightir.LIRBuilder.BlockIndexGetter;
 import com.android.tools.r8.lightir.LIRBuilder.ValueIndexGetter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
@@ -34,14 +36,18 @@
   /** Full number of arguments (including receiver for non-static methods). */
   private final int argumentCount;
 
-  /** Byte encoding of the instructions (including phis). */
+  /** Byte encoding of the instructions (excludes arguments, includes phis). */
   private final byte[] instructions;
 
-  /** Cached value for the number of logical instructions (including phis). */
+  /** Cached value for the number of logical instructions (excludes arguments, includes phis). */
   private final int instructionCount;
 
-  public static <V> LIRBuilder<V> builder(DexMethod method, ValueIndexGetter<V> valueIndexGetter) {
-    return new LIRBuilder<V>(method, valueIndexGetter);
+  public static <V, B> LIRBuilder<V, B> builder(
+      DexMethod method,
+      ValueIndexGetter<V> valueIndexGetter,
+      BlockIndexGetter<B> blockIndexGetter,
+      DexItemFactory factory) {
+    return new LIRBuilder<V, B>(method, valueIndexGetter, blockIndexGetter, factory);
   }
 
   // Should be constructed using LIRBuilder.
@@ -102,6 +108,7 @@
         .append("):{");
     int index = 0;
     for (LIRInstructionView view : this) {
+      builder.append(index).append(':');
       builder.append(LIROpcodes.toString(view.getOpcode()));
       if (view.getRemainingOperandSizeInBytes() > 0) {
         builder.append("(size:").append(1 + view.getRemainingOperandSizeInBytes()).append(")");
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 f42abdb..e67e6db 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRInstructionView.java
@@ -15,6 +15,9 @@
   /** Convenience method to forward control to a callback. */
   void accept(LIRInstructionCallback eventCallback);
 
+  /** Get the instruction index. */
+  int getInstructionIndex();
+
   /** The opcode of the instruction (See {@code LIROpcodes} for values). */
   int getOpcode();
 
@@ -29,4 +32,7 @@
 
   /** Get the next operand as an SSA value index. */
   int getNextValueOperand();
+
+  /** Get the next operand as a basic-block index. */
+  int getNextBlockOperand();
 }
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 9659aa5..fabe52e 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRIterator.java
@@ -16,10 +16,14 @@
 
   private final ByteIterator iterator;
 
+  // State of the byte offsets into the iterator.
   private int currentByteIndex = 0;
-  private int currentOpcode = -1;
   private int endOfCurrentInstruction = 0;
 
+  // State of the instruction interpretation.
+  private int currentInstructionIndex = -1;
+  private int currentOpcode = -1;
+
   public LIRIterator(ByteIterator iterator) {
     this.iterator = iterator;
   }
@@ -39,6 +43,7 @@
   @Override
   public LIRInstructionView next() {
     skipRemainingOperands();
+    ++currentInstructionIndex;
     currentOpcode = u1();
     if (LIROpcodes.isOneByteInstruction(currentOpcode)) {
       endOfCurrentInstruction = currentByteIndex;
@@ -57,6 +62,11 @@
   }
 
   @Override
+  public int getInstructionIndex() {
+    return currentInstructionIndex;
+  }
+
+  @Override
   public int getOpcode() {
     return currentOpcode;
   }
@@ -83,6 +93,12 @@
     return u4();
   }
 
+  @Override
+  public int getNextBlockOperand() {
+    assert hasMoreOperands();
+    return u4();
+  }
+
   private void skip(int i) {
     currentByteIndex += i;
     iterator.skip(i);
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 089469f..7e0c520 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIROpcodes.java
@@ -17,6 +17,25 @@
     return opcode <= DCONST_1 || opcode == RETURN || opcode == DEBUGPOS;
   }
 
+  static boolean isControlFlowInstruction(int opcode) {
+    return opcode == GOTO || isControlFlowInstructionWithFallthrough(opcode);
+  }
+
+  static boolean isControlFlowInstructionWithFallthrough(int opcode) {
+    switch (opcode) {
+      case IFEQ:
+      case IFNE:
+      case IFLT:
+      case IFGE:
+      case IFGT:
+      case IFLE:
+        // TODO(b/225838009): put in the rest!
+        return true;
+      default:
+        return false;
+    }
+  }
+
   // Instructions maintaining the same opcode as defined in CF.
   // int NOP = 0;
   int ACONST_NULL = 1;
@@ -183,6 +202,7 @@
   int DCONST = 203;
   int INVOKEDIRECT = 204;
   int DEBUGPOS = 205;
+  int PHI = 206;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -489,6 +509,8 @@
         return "INVOKEDIRECT";
       case DEBUGPOS:
         return "DEBUGPOS";
+      case PHI:
+        return "PHI";
 
       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
index 317eeee..8f32cf5 100644
--- a/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LIRParsedInstructionCallback.java
@@ -8,6 +8,8 @@
 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.graph.DexType;
+import com.android.tools.r8.ir.code.If;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 
@@ -34,6 +36,10 @@
 
   public void onConstString(DexString string) {}
 
+  public void onIf(If.Type ifKind, int blockIndex, int valueIndex) {}
+
+  public void onGoto(int blockIndex) {}
+
   public void onInvokeMethodInstruction(DexMethod method, IntList arguments) {}
 
   public void onInvokeDirect(DexMethod method, IntList arguments) {
@@ -54,8 +60,12 @@
 
   public void onReturnVoid() {}
 
+  public void onArrayLength(int arrayIndex) {}
+
   public void onDebugPosition() {}
 
+  public void onPhi(DexType type, IntList operands) {}
+
   private DexItem getConstantItem(int index) {
     return code.getConstantItem(index);
   }
@@ -76,6 +86,19 @@
           }
           break;
         }
+      case LIROpcodes.IFNE:
+        {
+          int blockIndex = view.getNextBlockOperand();
+          int valueIndex = view.getNextValueOperand();
+          onIf(If.Type.NE, blockIndex, valueIndex);
+          break;
+        }
+      case LIROpcodes.GOTO:
+        {
+          int blockIndex = view.getNextBlockOperand();
+          onGoto(blockIndex);
+          break;
+        }
       case LIROpcodes.INVOKEDIRECT:
         {
           DexMethod target = getInvokeInstructionTarget(view);
@@ -101,11 +124,26 @@
           onReturnVoid();
           break;
         }
+      case LIROpcodes.ARRAYLENGTH:
+        {
+          onArrayLength(view.getNextValueOperand());
+          break;
+        }
       case LIROpcodes.DEBUGPOS:
         {
           onDebugPosition();
           break;
         }
+      case LIROpcodes.PHI:
+        {
+          DexType type = (DexType) getConstantItem(view.getNextConstantOperand());
+          IntList operands = new IntArrayList();
+          while (view.hasMoreOperands()) {
+            operands.add(view.getNextValueOperand());
+          }
+          onPhi(type, operands);
+          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 1273bfd..c8e9e8a 100644
--- a/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
+++ b/src/test/java/com/android/tools/r8/lightir/LIRBasicCallbackTest.java
@@ -41,7 +41,11 @@
                 method,
                 v -> {
                   throw new Unreachable();
-                })
+                },
+                b -> {
+                  throw new Unreachable();
+                },
+                factory)
             .setMetadata(IRMetadata.unknown())
             .addConstNull()
             .addConstInt(42)
diff --git a/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
index a169bfc..3d0b390 100644
--- a/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
+++ b/src/test/java/com/android/tools/r8/lightir/LIRRoundtripTest.java
@@ -18,7 +18,7 @@
 
   static class TestClass {
     public static void main(String[] args) {
-      System.out.println("Hello, world!");
+      System.out.println(args.length == 0 ? "Hello, world!" : "Oh no!");
     }
   }