Essential CFG IR instructions

Issue: https://github.com/dart-lang/sdk/issues/61635
Change-Id: I7de7c4a3bd7008c03c81243c73a71c2018ab7c70
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/459382
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/pkg/cfg/lib/ir/constant_value.dart b/pkg/cfg/lib/ir/constant_value.dart
new file mode 100644
index 0000000..2825223
--- /dev/null
+++ b/pkg/cfg/lib/ir/constant_value.dart
@@ -0,0 +1,251 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:cfg/ir/instructions.dart';
+import 'package:cfg/ir/global_context.dart';
+import 'package:cfg/ir/types.dart';
+import 'package:cfg/utils/misc.dart';
+import 'package:kernel/ast.dart' as ast;
+import 'package:kernel/src/printer.dart' as ast_printer show AstPrinter;
+import 'package:kernel/type_environment.dart' show StaticTypeContext;
+
+/// Represents an arbitrary constant value.
+///
+/// [ConstantValue] is a thin wrapper around [ast.Constant].
+/// Constants which do not have corresponding representation in AST
+/// (e.g. constant type arguments) have dedicated subclasses of
+/// [ast.AuxiliaryConstant].
+extension type ConstantValue(ast.Constant constant) {
+  factory ConstantValue.fromInt(int value) =>
+      ConstantValue(ast.IntConstant(value));
+  factory ConstantValue.fromDouble(double value) =>
+      ConstantValue(ast.DoubleConstant(value));
+  factory ConstantValue.fromBool(bool value) =>
+      ConstantValue(ast.BoolConstant(value));
+  factory ConstantValue.fromNull() => ConstantValue(ast.NullConstant());
+  factory ConstantValue.fromString(String value) =>
+      ConstantValue(ast.StringConstant(value));
+
+  int get intValue => (constant as ast.IntConstant).value;
+  double get doubleValue => (constant as ast.DoubleConstant).value;
+  bool get boolValue => (constant as ast.BoolConstant).value;
+  String get stringValue => (constant as ast.StringConstant).value;
+
+  bool get isInt => constant is ast.IntConstant;
+  bool get isDouble => constant is ast.DoubleConstant;
+  bool get isBool => constant is ast.BoolConstant;
+  bool get isNull => constant is ast.NullConstant;
+  bool get isString => constant is ast.StringConstant;
+
+  CType get type => switch (constant) {
+    ast.IntConstant() => const IntType(),
+    ast.DoubleConstant() => const DoubleType(),
+    ast.BoolConstant() => const BoolType(),
+    ast.NullConstant() => const NullType(),
+    ast.StringConstant() => const StringType(),
+    TypeArgumentsConstant() => const TypeArgumentsType(),
+    _ => StaticType(
+      constant.getType(GlobalContext.instance.staticTypeContextForConstants),
+    ),
+  };
+
+  bool get isZero => switch (constant) {
+    ast.IntConstant(:var value) => value == 0,
+    ast.DoubleConstant(:var value) => value == 0.0,
+    _ => false,
+  };
+
+  bool get isNegative => switch (constant) {
+    ast.IntConstant(:var value) => value < 0,
+    ast.DoubleConstant(:var value) => value.isNegative,
+    _ => false,
+  };
+
+  String valueToString() => switch (constant) {
+    ast.StringConstant(:var value) => '"${value}"',
+    ast.PrimitiveConstant(:var value) => value.toString(),
+    _ => constant.toString(),
+  };
+}
+
+/// Utility class to perform operations on constant values.
+///
+/// Methods of this class return `null` when constant folding
+/// cannot be performed (e.g. corresponding operation would
+/// throw an exception at runtime).
+class ConstantFolding {
+  const ConstantFolding();
+
+  ConstantValue comparison(
+    ComparisonOpcode op,
+    ConstantValue left,
+    ConstantValue right,
+  ) {
+    final result = switch (op) {
+      ComparisonOpcode.equal => left.constant == right.constant,
+      ComparisonOpcode.notEqual => left.constant != right.constant,
+      ComparisonOpcode.identical => left.constant == right.constant,
+      ComparisonOpcode.notIdentical => left.constant != right.constant,
+      ComparisonOpcode.intEqual => left.intValue == right.intValue,
+      ComparisonOpcode.intNotEqual => left.intValue != right.intValue,
+      ComparisonOpcode.intLess => left.intValue < right.intValue,
+      ComparisonOpcode.intLessOrEqual => left.intValue <= right.intValue,
+      ComparisonOpcode.intGreater => left.intValue > right.intValue,
+      ComparisonOpcode.intGreaterOrEqual => left.intValue >= right.intValue,
+      ComparisonOpcode.intTestIsZero => (left.intValue & right.intValue) == 0,
+      ComparisonOpcode.intTestIsNotZero =>
+        (left.intValue & right.intValue) != 0,
+      ComparisonOpcode.doubleEqual => left.doubleValue == right.doubleValue,
+      ComparisonOpcode.doubleNotEqual => left.doubleValue != right.doubleValue,
+      ComparisonOpcode.doubleLess => left.doubleValue < right.doubleValue,
+      ComparisonOpcode.doubleLessOrEqual =>
+        left.doubleValue <= right.doubleValue,
+      ComparisonOpcode.doubleGreater => left.doubleValue > right.doubleValue,
+      ComparisonOpcode.doubleGreaterOrEqual =>
+        left.doubleValue >= right.doubleValue,
+    };
+    return ConstantValue.fromBool(result);
+  }
+
+  ConstantValue? binaryIntOp(
+    BinaryIntOpcode op,
+    ConstantValue left,
+    ConstantValue right,
+  ) {
+    final a = left.intValue;
+    final b = right.intValue;
+    switch (op) {
+      case BinaryIntOpcode.add:
+        return ConstantValue.fromInt(a + b);
+      case BinaryIntOpcode.sub:
+        return ConstantValue.fromInt(a - b);
+      case BinaryIntOpcode.mul:
+        return ConstantValue.fromInt(a * b);
+      case BinaryIntOpcode.truncatingDiv:
+        return (b != 0) ? ConstantValue.fromInt(a ~/ b) : null;
+      case BinaryIntOpcode.mod:
+        return (b != 0) ? ConstantValue.fromInt(a % b) : null;
+      case BinaryIntOpcode.rem:
+        return (b != 0) ? ConstantValue.fromInt(a.remainder(b)) : null;
+      case BinaryIntOpcode.bitOr:
+        return ConstantValue.fromInt(a | b);
+      case BinaryIntOpcode.bitAnd:
+        return ConstantValue.fromInt(a & b);
+      case BinaryIntOpcode.bitXor:
+        return ConstantValue.fromInt(a ^ b);
+      case BinaryIntOpcode.shiftLeft:
+        return (b >= 0) ? ConstantValue.fromInt(a << b) : null;
+      case BinaryIntOpcode.shiftRight:
+        return (b >= 0) ? ConstantValue.fromInt(a >> b) : null;
+      case BinaryIntOpcode.unsignedShiftRight:
+        return (b >= 0) ? ConstantValue.fromInt(a >>> b) : null;
+    }
+  }
+
+  ConstantValue? unaryIntOp(UnaryIntOpcode op, ConstantValue operand) {
+    final x = operand.intValue;
+    switch (op) {
+      case UnaryIntOpcode.neg:
+        return ConstantValue.fromInt(-x);
+      case UnaryIntOpcode.bitNot:
+        return ConstantValue.fromInt(~x);
+      case UnaryIntOpcode.toDouble:
+        return ConstantValue.fromDouble(x.toDouble());
+      case UnaryIntOpcode.abs:
+        return ConstantValue.fromInt(x.abs());
+      case UnaryIntOpcode.sign:
+        return ConstantValue.fromInt(x.sign);
+    }
+  }
+
+  ConstantValue? binaryDoubleOp(
+    BinaryDoubleOpcode op,
+    ConstantValue left,
+    ConstantValue right,
+  ) {
+    final a = left.doubleValue;
+    final b = right.doubleValue;
+    switch (op) {
+      case BinaryDoubleOpcode.add:
+        return ConstantValue.fromDouble(a + b);
+      case BinaryDoubleOpcode.sub:
+        return ConstantValue.fromDouble(a - b);
+      case BinaryDoubleOpcode.mul:
+        return ConstantValue.fromDouble(a * b);
+      case BinaryDoubleOpcode.mod:
+        return ConstantValue.fromDouble(a % b);
+      case BinaryDoubleOpcode.rem:
+        return ConstantValue.fromDouble(a.remainder(b));
+      case BinaryDoubleOpcode.div:
+        return ConstantValue.fromDouble(a / b);
+      case BinaryDoubleOpcode.truncatingDiv:
+        final doubleResult = a / b;
+        return doubleResult.isFinite
+            ? ConstantValue.fromInt(doubleResult.truncate())
+            : null;
+    }
+  }
+
+  ConstantValue? unaryDoubleOp(UnaryDoubleOpcode op, ConstantValue operand) {
+    final x = operand.doubleValue;
+    switch (op) {
+      case UnaryDoubleOpcode.neg:
+        return ConstantValue.fromDouble(-x);
+      case UnaryDoubleOpcode.abs:
+        return ConstantValue.fromDouble(x.abs());
+      case UnaryDoubleOpcode.sign:
+        return ConstantValue.fromDouble(x.sign);
+      case UnaryDoubleOpcode.square:
+        return ConstantValue.fromDouble(x * x);
+      case UnaryDoubleOpcode.round:
+        return x.isFinite ? ConstantValue.fromInt(x.round()) : null;
+      case UnaryDoubleOpcode.floor:
+        return x.isFinite ? ConstantValue.fromInt(x.floor()) : null;
+      case UnaryDoubleOpcode.ceil:
+        return x.isFinite ? ConstantValue.fromInt(x.ceil()) : null;
+      case UnaryDoubleOpcode.truncate:
+        return x.isFinite ? ConstantValue.fromInt(x.truncate()) : null;
+      case UnaryDoubleOpcode.roundToDouble:
+        return ConstantValue.fromDouble(x.roundToDouble());
+      case UnaryDoubleOpcode.floorToDouble:
+        return ConstantValue.fromDouble(x.floorToDouble());
+      case UnaryDoubleOpcode.ceilToDouble:
+        return ConstantValue.fromDouble(x.ceilToDouble());
+      case UnaryDoubleOpcode.truncateToDouble:
+        return ConstantValue.fromDouble(x.truncateToDouble());
+    }
+  }
+}
+
+/// Constant type arguments.
+class TypeArgumentsConstant extends ast.AuxiliaryConstant {
+  final List<ast.DartType> types;
+
+  TypeArgumentsConstant(this.types);
+
+  @override
+  void visitChildren(ast.Visitor v) {
+    ast.visitList(types, v);
+  }
+
+  @override
+  void toTextInternal(ast_printer.AstPrinter printer) {
+    printer.writeTypeArguments(types);
+  }
+
+  @override
+  String toString() => toStringInternal();
+
+  @override
+  int get hashCode => listHashCode(types);
+
+  @override
+  bool operator ==(Object other) {
+    return other is TypeArgumentsConstant &&
+        listEquals(this.types, other.types);
+  }
+
+  @override
+  ast.DartType getType(StaticTypeContext context) => const ast.DynamicType();
+}
diff --git a/pkg/cfg/lib/ir/flow_graph.dart b/pkg/cfg/lib/ir/flow_graph.dart
new file mode 100644
index 0000000..995be0e
--- /dev/null
+++ b/pkg/cfg/lib/ir/flow_graph.dart
@@ -0,0 +1,152 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:cfg/ir/constant_value.dart';
+import 'package:cfg/ir/functions.dart';
+import 'package:cfg/ir/instructions.dart';
+import 'package:cfg/ir/local_variable.dart';
+import 'package:cfg/ir/use_lists.dart';
+import 'package:cfg/utils/arena.dart';
+
+/// Control-flow graph of instructions implementing a single function.
+///
+/// [FlowGraph] is also an arena to allocate use lists.
+class FlowGraph extends Uint32Arena {
+  final CFunction function;
+
+  /// Local variables used in this graph.
+  final List<LocalVariable> localVariables = [];
+
+  /// All instructions used in this graph.
+  /// [Instruction.id] is an index in this list.
+  final List<Instruction> instructions = [];
+
+  /// Empty array of uses. Used by all instructions with 0 inputs.
+  late final UsesArray emptyUsesArray = UsesArray.allocate(this, 0);
+
+  /// Mapping between constant values and corresponding [Constant]
+  /// instructions in the graph.
+  final Map<ConstantValue, Constant> _constants = {};
+
+  /// New [Constant] instructions are inserted after this instruction.
+  late Instruction _constantInsertionPoint;
+
+  /// Entry basic block in this control-flow graph.
+  late final EntryBlock entryBlock;
+
+  /// Basic block preorder.
+  final List<Block> preorder = [];
+
+  /// Basic block postorder.
+  final List<Block> postorder = [];
+
+  /// Basic block reverse postorder.
+  Iterable<Block> get reversePostorder => postorder.reversed;
+
+  /// Whether this graph was converted to SSA form.
+  bool inSSAForm = false;
+
+  /// Create a new control-flow graph for the given [function].
+  FlowGraph(this.function) {
+    entryBlock = EntryBlock(this, function.sourcePosition);
+    _constantInsertionPoint = entryBlock;
+  }
+
+  /// Return a [Constant] instruction with given [value], adding it to
+  /// the graph if necessary.
+  Constant getConstant(ConstantValue value) =>
+      _constants[value] ??= _createConstant(value);
+
+  Constant _createConstant(ConstantValue value) {
+    final instr = Constant(this, value);
+    final insertionPoint = _constantInsertionPoint;
+    if (insertionPoint.next != null) {
+      instr.insertAfter(insertionPoint);
+    } else {
+      insertionPoint.appendInstruction(instr);
+    }
+    _constantInsertionPoint = instr;
+    return instr;
+  }
+
+  /// Remove given [Constant] instruction from the graph.
+  /// The instruction should be unused.
+  void removeConstant(Constant instr) {
+    assert(!instr.hasUses);
+    _constants.remove(instr.value);
+    if (instr == _constantInsertionPoint) {
+      _constantInsertionPoint = instr.previous!;
+    }
+  }
+
+  /// Populate [preorder], [postorder], [Block.predecessors],
+  /// [Block.preorderNumber] and [Block.postorderNumber].
+  void discoverBlocks() {
+    preorder.clear();
+    postorder.clear();
+
+    final stack = <(Block, int)>[];
+    _discoverBlock(null, entryBlock);
+    stack.add((entryBlock, entryBlock.successors.length - 1));
+    while (stack.isNotEmpty) {
+      final (block, successorIndex) = stack.removeLast();
+      if (successorIndex >= 0) {
+        stack.add((block, successorIndex - 1));
+        final successor = block.successors[successorIndex];
+        if (_discoverBlock(block, successor)) {
+          stack.add((successor, successor.successors.length - 1));
+        }
+      } else {
+        // All successors have been processed.
+        block.postorderNumber = postorder.length;
+        postorder.add(block);
+      }
+    }
+    assert(postorder.length == preorder.length);
+  }
+
+  /// Detect that a block has been visited as part of the current
+  /// [discoverBlocks] ([discoverBlocks] can be called multiple times).
+  /// The block will be 'marked' by (1) having a preorder number in the range of the
+  /// [preorder] and (2) being in the preorder list at that index.
+  bool _isVisited(Block block) {
+    final preorderNumber = block.preorderNumber;
+    return preorderNumber >= 0 &&
+        preorderNumber < preorder.length &&
+        preorder[preorderNumber] == block;
+  }
+
+  /// Visit control-flow edge between [predecessor] and [block].
+  ///
+  /// Must be called on all graph blocks in preorder.
+  /// Sets [Block.preorderNumber], populates [Block.predecessors] and
+  /// [FlowGraph.preorder].
+  /// Returns true when called the first time on this particular block
+  /// within one graph traversal, and false on all successive calls.
+  bool _discoverBlock(Block? predecessor, Block block) {
+    // If this block has a predecessor (i.e., is not the graph entry) we can
+    // assume the preorder array is non-empty.
+    assert(predecessor == null || preorder.isNotEmpty);
+    // Blocks with a single predecessor cannot have been reached before.
+    assert(block is JoinBlock || !_isVisited(block));
+
+    // If the block has already been reached, add current block as a
+    // basic-block predecessor and we are done.
+    if (_isVisited(block)) {
+      block.predecessors.add(predecessor!);
+      return false;
+    }
+
+    // Otherwise, clear the predecessors which might have been computed on
+    // some earlier call to DiscoverBlocks and record this predecessor.
+    block.predecessors.clear();
+    if (predecessor != null) block.predecessors.add(predecessor);
+
+    // Assign the preorder number and add the block to the list.
+    block.preorderNumber = preorder.length;
+    preorder.add(block);
+
+    return true;
+  }
+}
diff --git a/pkg/cfg/lib/ir/instructions.dart b/pkg/cfg/lib/ir/instructions.dart
new file mode 100644
index 0000000..b397b41
--- /dev/null
+++ b/pkg/cfg/lib/ir/instructions.dart
@@ -0,0 +1,1253 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:kernel/ast.dart' as ast show DartType, Name;
+import 'package:cfg/ir/constant_value.dart';
+import 'package:cfg/ir/flow_graph.dart';
+import 'package:cfg/ir/functions.dart';
+import 'package:cfg/ir/local_variable.dart';
+import 'package:cfg/ir/source_position.dart';
+import 'package:cfg/ir/types.dart';
+import 'package:cfg/ir/use_lists.dart';
+import 'package:cfg/ir/visitor.dart';
+import 'package:cfg/utils/misc.dart';
+
+/// Base class for all instructions.
+abstract base class Instruction {
+  /// Enclosing control-flow graph.
+  final FlowGraph graph;
+
+  /// Index in the [FlowGraph.instructions].
+  final int id;
+
+  /// Source position associated with this instruction.
+  final SourcePosition sourcePosition;
+
+  /// Explicit inputs.
+  final UsesArray _inputs;
+
+  /// Enclosing basic block.
+  Block? block;
+
+  /// Next instruction in basic block.
+  Instruction? next;
+
+  /// Previous instruction in basic block.
+  Instruction? previous;
+
+  /// Values implicitly used by this instruction
+  /// e.g. for exception handling and deoptimization).
+  UsesArray? implicitInputs;
+
+  /// Create a new instruction.
+  ///
+  /// The newly created instruction belongs to [graph] but not linked into
+  /// a basic block and uses lists.
+  Instruction(this.graph, this.sourcePosition, int inputCount)
+    : id = graph.instructions.length,
+      _inputs = inputCount == 0
+          ? graph.emptyUsesArray
+          : UsesArray.allocate(graph, inputCount) {
+    for (int i = 0; i < inputCount; ++i) {
+      _inputs.at(graph, i).init(graph, this);
+    }
+    graph.instructions.add(this);
+  }
+
+  /// Returns true if this instruction is linked into a list of instructions
+  /// in a basic block (also imlplies inputs are linked in their use lists).
+  bool get isInGraph => block != null;
+
+  /// Number of explicit inputs.
+  int get inputCount => _inputs.getLength(graph);
+
+  /// [Use] corresponding to an [i]-th explicit input.
+  Use inputAt(int i) => _inputs.at(graph, i);
+
+  /// [Definition] of [i]-th explicit input.
+  Definition inputDefAt(int i) => inputAt(i).getDefinition(graph);
+
+  /// Set [i]-th input to the given [value]. Can be done only once.
+  void setInputAt(int i, Definition value) {
+    final input = inputAt(i);
+    assert(input.getNext(graph) == Use.Null);
+    assert(input.getPrevious(graph) == Use.Null);
+    input.setDefinition(graph, value);
+  }
+
+  /// Replace [i]-th input with [value]. This instruction should be
+  /// linked into a basic block.
+  void replaceInputAt(int i, Definition value) {
+    assert(isInGraph);
+    removeInputFromUseList(i);
+    setInputAt(i, value);
+    addInputToUseList(i);
+  }
+
+  /// Reduce number of inputs to [newInputCount].
+  void truncateInputs(int newInputCount) {
+    _inputs.truncateTo(graph, newInputCount);
+  }
+
+  /// Link this instruction to the [next] instruction in basic block.
+  void linkTo(Instruction next) {
+    assert(!identical(this, next));
+    this.next = next;
+    next.previous = this;
+    next.block = this.block;
+  }
+
+  /// Add [inputIndex]-th input to use list of its definition.
+  ///
+  /// This is a low-level operation which is rarely needed.
+  /// Prefer using [appendInstruction] or [insertAfter].
+  void addInputToUseList(int inputIndex) {
+    final input = inputAt(inputIndex);
+    assert(input.getNext(graph) == Use.Null);
+    assert(input.getPrevious(graph) == Use.Null);
+    final def = input.getDefinition(graph);
+    final nextUse = def._inputUses;
+    if (nextUse != Use.Null) {
+      assert(nextUse.getPrevious(graph) == Use.Null);
+      input.setNext(graph, nextUse);
+      nextUse.setPrevious(graph, input);
+    }
+    def._inputUses = input;
+  }
+
+  void _addInputsToUseLists() {
+    for (int i = 0, n = inputCount; i < n; ++i) {
+      addInputToUseList(i);
+    }
+    assert(implicitInputs == null);
+  }
+
+  /// Remove [inputIndex]-th input from use list of its definition.
+  ///
+  /// This is a low-level operation which is rarely needed.
+  /// Prefer using [replaceInputAt] or [removeFromGraph].
+  void removeInputFromUseList(int inputIndex) {
+    final input = inputAt(inputIndex);
+    assert(input.getInstruction(graph) == this);
+    final nextUse = input.getNext(graph);
+    final prevUse = input.getPrevious(graph);
+    if (prevUse == Use.Null) {
+      final def = input.getDefinition(graph);
+      assert(def._inputUses == input);
+      def._inputUses = nextUse;
+    } else {
+      prevUse.setNext(graph, nextUse);
+    }
+    if (nextUse != Use.Null) {
+      nextUse.setPrevious(graph, prevUse);
+    }
+    input.setNext(graph, Use.Null);
+    input.setPrevious(graph, Use.Null);
+  }
+
+  /// Remove all inputs from their use lists.
+  void removeInputsFromUseLists() {
+    for (int i = 0, n = inputCount; i < n; ++i) {
+      removeInputFromUseList(i);
+    }
+  }
+
+  /// Append an unlinked [instr] after this instruction,
+  /// which should be the last instruction appended into basic block.
+  ///
+  /// Also, add all inputs of [instr] to their use lists.
+  void appendInstruction(Instruction instr) {
+    assert(isInGraph);
+    assert(!instr.isInGraph);
+    assert(this.next == null);
+    assert(this.block!.lastInstruction == this);
+    assert(instr.next == null);
+    assert(instr.previous == null);
+    assert(instr.block == null);
+    linkTo(instr);
+    instr._addInputsToUseLists();
+    block!.lastInstruction = instr;
+  }
+
+  /// Insert this instruction between [previous] and [previous.next].
+  /// (which should be linked into a basic block).
+  ///
+  /// If [addInputsToUseLists], then also add all inputs to their use lists.
+  void insertAfter(Instruction previous, {bool addInputsToUseLists = true}) {
+    assert(previous.isInGraph);
+    assert(!isInGraph);
+    final next = previous.next!;
+    assert(previous.block != null);
+    assert(this.next == null);
+    assert(this.previous == null);
+    assert(this.block == null);
+
+    this.previous = previous;
+    this.next = next;
+    this.block = previous.block;
+    next.previous = this;
+    previous.next = this;
+
+    if (addInputsToUseLists) {
+      _addInputsToUseLists();
+    }
+  }
+
+  /// Insert this instruction between [next.previous] and [next].
+  /// (which should be linked into a basic block).
+  ///
+  /// If [addInputsToUseLists], then also add all inputs to their use lists.
+  void insertBefore(Instruction next, {bool addInputsToUseLists = true}) {
+    assert(next.isInGraph);
+    insertAfter(next.previous!, addInputsToUseLists: addInputsToUseLists);
+  }
+
+  /// Unlink this instruction from its basic block and use lists.
+  void removeFromGraph() {
+    assert(isInGraph);
+    assert(block != null);
+    assert(this != block);
+
+    removeInputsFromUseLists();
+    final next = this.next;
+    final previous = this.previous!;
+    previous.next = next;
+    if (next != null) {
+      next.previous = previous;
+    } else {
+      assert(this is ControlFlowInstruction);
+      assert(this == block!.lastInstruction);
+      block!.lastInstruction = previous;
+    }
+    this.next = null;
+    this.previous = null;
+    this.block = null;
+    assert(!isInGraph);
+  }
+
+  /// Whether this instruction can potentially throw a Dart exception.
+  bool get canThrow;
+
+  /// Whether this instruction can have any visible side-effects.
+  bool get hasSideEffects;
+
+  /// Returns true if this instruction is idempotent (i.e.
+  /// repeating this instruction does not have any effect),
+  /// and it is a subject to value numbering.
+  bool get isIdempotent => false;
+
+  /// Returns true if extra instruction attributes are equal.
+  /// Used only for idempotent instructions of the same type.
+  bool attributesEqual(Instruction other) =>
+      throw 'Not implemented for ${runtimeType}';
+
+  R accept<R>(InstructionVisitor<R> v);
+}
+
+/// Trait of instructions which can potentially throw Dart exceptions.
+base mixin CanThrow on Instruction {
+  bool get canThrow => true;
+}
+
+/// Trait of instructions which cannot throw Dart exceptions.
+base mixin NoThrow on Instruction {
+  bool get canThrow => false;
+}
+
+/// Trait of instructions which can have visible side-effects.
+base mixin HasSideEffects on Instruction {
+  bool get hasSideEffects => true;
+}
+
+/// Trait of instructions which do not have any side-effects.
+base mixin Pure on Instruction {
+  bool get hasSideEffects => false;
+}
+
+/// Trait of idempotent instructions.
+base mixin Idempotent on Instruction {
+  bool get isIdempotent => true;
+}
+
+/// Base class for instructions which yield a value.
+abstract base class Definition extends Instruction {
+  Use _inputUses = Use.Null;
+  Use _implicitUses = Use.Null;
+
+  Definition(super.graph, super.sourcePosition, super.inputCount);
+
+  UsesIterable get inputUses => UsesIterable(graph, _inputUses);
+  UsesIterable get implicitUses => UsesIterable(graph, _implicitUses);
+
+  bool get hasInputUses => _inputUses != Use.Null;
+  bool get hasImplicitUses => _implicitUses != Use.Null;
+  bool get hasUses => hasInputUses || hasImplicitUses;
+
+  /// Replace all uses of this instruction with [other].
+  void replaceUsesWith(Definition other) {
+    if (hasInputUses) {
+      Use last = Use.Null;
+      for (final use in inputUses) {
+        use.setDefinition(graph, other);
+        last = use;
+      }
+      final tail = other._inputUses;
+      last.setNext(graph, tail);
+      if (tail != Use.Null) {
+        tail.setPrevious(graph, last);
+      }
+      other._inputUses = this._inputUses;
+      this._inputUses = Use.Null;
+    }
+    if (hasImplicitUses) {
+      Use last = Use.Null;
+      for (final use in implicitUses) {
+        use.setDefinition(graph, other);
+        last = use;
+      }
+      final tail = other._implicitUses;
+      last.setNext(graph, tail);
+      if (tail != Use.Null) {
+        tail.setPrevious(graph, last);
+      }
+      other._implicitUses = this._implicitUses;
+      this._implicitUses = Use.Null;
+    }
+  }
+
+  /// Result type of this instruction.
+  CType get type;
+
+  /// Whether this instruction can yield a zero value.
+  bool get canBeZero => true;
+
+  /// Whether this instruction can yield a negative value.
+  bool get canBeNegative => true;
+
+  /// Returns the only instruction which uses result of this instruction as
+  /// an explicit input.
+  ///
+  /// Returns `null` if there are no or multiple uses.
+  Instruction? get singleUser {
+    if (hasInputUses && !hasImplicitUses) {
+      final use = _inputUses;
+      if (use.getNext(graph) == Use.Null) {
+        return use.getInstruction(graph);
+      }
+    }
+    return null;
+  }
+}
+
+/// Forward iterator over instructions in the block.
+final class _InstructionsIterator implements Iterator<Instruction> {
+  Instruction? _current;
+  Instruction? _next;
+
+  _InstructionsIterator(this._next);
+
+  @override
+  bool moveNext() {
+    _current = _next;
+    _next = _current?.next;
+    return (_current != null);
+  }
+
+  @override
+  Instruction get current => _current!;
+}
+
+/// Backwards iterator over instructions in the block.
+final class _ReverseInstructionsIterator implements Iterator<Instruction> {
+  Instruction? _current;
+  Instruction _previous;
+
+  _ReverseInstructionsIterator(this._previous);
+
+  @override
+  bool moveNext() {
+    final instr = _previous.previous;
+    if (instr == null) {
+      assert(_previous is Block);
+      _current = null;
+      return false;
+    } else {
+      _current = _previous;
+      _previous = instr;
+      return true;
+    }
+  }
+
+  @override
+  Instruction get current => _current!;
+}
+
+/// Iterable for backwards iteration of instructions in the block.
+final class _ReverseInstructionsIterable extends Iterable<Instruction> {
+  final Block _block;
+
+  _ReverseInstructionsIterable(this._block);
+
+  @override
+  Iterator<Instruction> get iterator =>
+      _ReverseInstructionsIterator(_block.lastInstruction);
+}
+
+/// Basic block.
+abstract base class Block extends Instruction
+    with NoThrow, Pure, Iterable<Instruction> {
+  final List<Block> predecessors = [];
+  final List<Block> successors = [];
+  late Instruction lastInstruction;
+  CatchBlock? exceptionHandler;
+
+  int preorderNumber = -1;
+  int postorderNumber = -1;
+
+  Block(FlowGraph graph, SourcePosition sourcePosition)
+    : super(graph, sourcePosition, 0) {
+    this.block = this;
+    this.lastInstruction = this;
+  }
+
+  /// Replace [oldPredecessor] with [newPredecessor].
+  void replacePredecessor(Block oldPredecessor, Block newPredecessor) {
+    final predecessors = this.predecessors;
+    for (int i = 0, n = predecessors.length; i < n; ++i) {
+      if (predecessors[i] == oldPredecessor) {
+        predecessors[i] = newPredecessor;
+      }
+    }
+  }
+
+  /// Iterate over instructions in the block from the first to the last.
+  /// Iteration is robust wrt removal of the current instruction.
+  @override
+  Iterator<Instruction> get iterator => _InstructionsIterator(next);
+
+  /// Iterate over instructions in the block from the last to the first.
+  /// Iteration is robust wrt removal of the current instruction.
+  Iterable<Instruction> get reversed => _ReverseInstructionsIterable(this);
+}
+
+/// The entry block in the [FlowGraph].
+///
+/// Contains all [Constant] instructions and [Parameter] instructions
+/// corresponding to the function parameters.
+final class EntryBlock extends Block {
+  EntryBlock(super.graph, super.sourcePosition);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitEntryBlock(this);
+}
+
+/// Iterator over [Phi] instructions in the [JoinBlock].
+final class _PhiIterator implements Iterator<Phi> {
+  Phi? _current;
+  Phi? _next;
+
+  _PhiIterator(JoinBlock block) {
+    final nextInstruction = block.next;
+    _next = (nextInstruction is Phi) ? nextInstruction : null;
+  }
+
+  @override
+  bool moveNext() {
+    _current = _next;
+    final nextInstruction = _current?.next;
+    _next = (nextInstruction is Phi) ? nextInstruction : null;
+    return (_current != null);
+  }
+
+  @override
+  Phi get current => _current!;
+}
+
+/// Iterable over [Phi] instructions in the [JoinBlock].
+final class _PhiIterable extends Iterable<Phi> {
+  final JoinBlock _block;
+
+  _PhiIterable(this._block);
+
+  @override
+  Iterator<Phi> get iterator => _PhiIterator(_block);
+}
+
+/// Basic block which can have multiple predecessors.
+///
+/// May contain [Phi] instructions in the beginning of the block.
+final class JoinBlock extends Block {
+  JoinBlock(super.graph, super.sourcePosition);
+
+  Iterable<Phi> get phis => _PhiIterable(this);
+
+  bool get hasPhis => next is Phi;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitJoinBlock(this);
+}
+
+/// Target block of a branch.
+final class TargetBlock extends Block {
+  TargetBlock(super.graph, super.sourcePosition);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTargetBlock(this);
+}
+
+/// Exception handler block.
+///
+/// May contain [Parameter] instructions in the beginning of the block
+/// to represent incoming values of local variables, exception object and
+/// stack trace.
+final class CatchBlock extends Block {
+  CatchBlock(super.graph, super.sourcePosition);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitCatchBlock(this);
+}
+
+/// Marker interface for instructions which can end basic block.
+abstract class ControlFlowInstruction {}
+
+/// Unconditional jump to the sole successor of this basic block.
+final class Goto extends Instruction
+    with NoThrow, Pure
+    implements ControlFlowInstruction {
+  Goto(FlowGraph graph, SourcePosition sourcePosition)
+    : super(graph, sourcePosition, 0);
+
+  Block get target => block!.successors.single;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitGoto(this);
+}
+
+/// Conditional branch to either true of false successors of this basic block.
+final class Branch extends Instruction
+    with NoThrow, Pure
+    implements ControlFlowInstruction {
+  Branch(FlowGraph graph, SourcePosition sourcePosition, Definition condition)
+    : super(graph, sourcePosition, 1) {
+    setInputAt(0, condition);
+  }
+
+  Definition get condition => inputDefAt(0);
+  TargetBlock get trueSuccessor => block!.successors[0] as TargetBlock;
+  TargetBlock get falseSuccessor => block!.successors[1] as TargetBlock;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitBranch(this);
+}
+
+/// Unconditional jump to the try block body (the first succesor of this
+/// basic block). The second successor of this basic block is a catch block.
+final class TryEntry extends Instruction
+    with NoThrow, Pure
+    implements ControlFlowInstruction {
+  TryEntry(FlowGraph graph, SourcePosition sourcePosition)
+    : super(graph, sourcePosition, 0);
+
+  TargetBlock get tryBody => block!.successors[0] as TargetBlock;
+  CatchBlock get catchBlock => block!.successors[1] as CatchBlock;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTryEntry(this);
+}
+
+/// The result of instruction `v = Phi(v[0],...,v[N-1])` is `v[i]` when
+/// control is transferred from the i-th predecessor block.
+///
+/// Number of inputs always matches number of predecessor blocks.
+final class Phi extends Definition with NoThrow, Pure {
+  /// Local variable for which this [Phi] was originally created.
+  final LocalVariable variable;
+
+  Phi(super.graph, super.sourcePosition, this.variable, super.inputCount);
+
+  @override
+  CType get type => variable.type;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitPhi(this);
+}
+
+/// Return value from the current function.
+final class Return extends Instruction
+    with NoThrow, Pure
+    implements ControlFlowInstruction {
+  Return(FlowGraph graph, SourcePosition sourcePosition, Definition value)
+    : super(graph, sourcePosition, 1) {
+    setInputAt(0, value);
+  }
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitReturn(this);
+}
+
+enum ComparisonOpcode {
+  // Simple object pointer equality.
+  equal('=='),
+  notEqual('!='),
+  // identical with special cases for num.
+  identical('==='),
+  notIdentical('!=='),
+  // int comparisons.
+  intEqual('int =='),
+  intNotEqual('int !='),
+  intLess('int <'),
+  intLessOrEqual('int <='),
+  intGreater('int >'),
+  intGreaterOrEqual('int >='),
+  // int bitwise tests
+  intTestIsZero('int & == 0'),
+  intTestIsNotZero('int & != 0'),
+  // double comparisons.
+  doubleEqual('double =='),
+  doubleNotEqual('double !='),
+  doubleLess('double <'),
+  doubleLessOrEqual('double <='),
+  doubleGreater('double >'),
+  doubleGreaterOrEqual('double >=');
+
+  final String token;
+  const ComparisonOpcode(this.token);
+
+  bool get isIntComparison => switch (this) {
+    intEqual ||
+    intNotEqual ||
+    intLess ||
+    intLessOrEqual ||
+    intGreater ||
+    intGreaterOrEqual ||
+    intTestIsZero ||
+    intTestIsNotZero => true,
+    _ => false,
+  };
+
+  bool get isDoubleComparison => switch (this) {
+    doubleEqual ||
+    doubleNotEqual ||
+    doubleLess ||
+    doubleLessOrEqual ||
+    doubleGreater ||
+    doubleGreaterOrEqual => true,
+    _ => false,
+  };
+
+  ComparisonOpcode flipOperands() => switch (this) {
+    equal ||
+    notEqual ||
+    identical ||
+    notIdentical ||
+    intEqual ||
+    intNotEqual ||
+    intTestIsZero ||
+    intTestIsNotZero ||
+    doubleEqual ||
+    doubleNotEqual => this,
+    intLess => intGreaterOrEqual,
+    intLessOrEqual => intGreater,
+    intGreater => intLessOrEqual,
+    intGreaterOrEqual => intLess,
+    doubleLess => doubleGreaterOrEqual,
+    doubleLessOrEqual => doubleGreater,
+    doubleGreater => doubleLessOrEqual,
+    doubleGreaterOrEqual => doubleLess,
+  };
+
+  bool get canBeNegated => switch (this) {
+    doubleLess ||
+    doubleLessOrEqual ||
+    doubleGreater ||
+    doubleGreaterOrEqual => false,
+    _ => true,
+  };
+
+  ComparisonOpcode negate() => switch (this) {
+    equal => notEqual,
+    notEqual => equal,
+    identical => notIdentical,
+    notIdentical => identical,
+    intEqual => intNotEqual,
+    intNotEqual => intEqual,
+    intLess => intGreaterOrEqual,
+    intLessOrEqual => intGreater,
+    intGreater => intLessOrEqual,
+    intGreaterOrEqual => intLess,
+    intTestIsZero => intTestIsNotZero,
+    intTestIsNotZero => intTestIsZero,
+    doubleEqual => doubleNotEqual,
+    doubleNotEqual => doubleEqual,
+    doubleLess ||
+    doubleLessOrEqual ||
+    doubleGreater ||
+    doubleGreaterOrEqual => throw '${token} cannot be negated',
+  };
+}
+
+/// Compare two operands.
+final class Comparison extends Definition with NoThrow, Pure, Idempotent {
+  ComparisonOpcode op;
+
+  Comparison(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition left,
+    Definition right,
+  ) : super(graph, sourcePosition, 2) {
+    setInputAt(0, left);
+    setInputAt(1, right);
+  }
+
+  Definition get left => inputDefAt(0);
+  Definition get right => inputDefAt(1);
+
+  @override
+  CType get type => const BoolType();
+
+  @override
+  bool attributesEqual(covariant Comparison other) => op == other.op;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitComparison(this);
+}
+
+/// Instruction representing a constant value.
+///
+/// Should not be created directly, use [FlowGraph.getConstant] instead.
+final class Constant extends Definition with NoThrow, Pure {
+  final ConstantValue value;
+
+  Constant(FlowGraph graph, this.value) : super(graph, noPosition, 0);
+
+  @override
+  bool get canBeZero => value.isZero;
+
+  @override
+  bool get canBeNegative => value.isNegative;
+
+  @override
+  CType get type => value.type;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitConstant(this);
+}
+
+/// Base class for various calls.
+abstract base class CallInstruction extends Definition
+    with CanThrow, HasSideEffects {
+  CallInstruction(super.graph, super.sourcePosition, super.inputCount);
+
+  bool get hasTypeArguments => inputCount > 0 && inputDefAt(0) is TypeArguments;
+  TypeArguments? get typeArguments =>
+      hasTypeArguments ? inputDefAt(0) as TypeArguments : null;
+}
+
+/// Direct call of the target function.
+final class DirectCall extends CallInstruction {
+  final CFunction target;
+
+  @override
+  final CType type;
+
+  DirectCall(
+    super.graph,
+    super.sourcePosition,
+    this.target,
+    super.inputCount,
+    this.type,
+  );
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitDirectCall(this);
+}
+
+/// Interface call via given interface target.
+final class InterfaceCall extends CallInstruction {
+  final CFunction interfaceTarget;
+
+  @override
+  final CType type;
+
+  InterfaceCall(
+    super.graph,
+    super.sourcePosition,
+    this.interfaceTarget,
+    super.inputCount,
+    this.type,
+  );
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitInterfaceCall(this);
+}
+
+enum DynamicCallKind { method, getter, setter }
+
+/// Dynamic call via given selector.
+final class DynamicCall extends CallInstruction {
+  final ast.Name selector;
+  final DynamicCallKind kind;
+
+  DynamicCall(
+    super.graph,
+    super.sourcePosition,
+    this.selector,
+    this.kind,
+    super.inputCount,
+  );
+
+  @override
+  CType get type => const TopType();
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitDynamicCall(this);
+}
+
+/// Parameter of a function or a catch block.
+final class Parameter extends Definition with NoThrow, Pure {
+  final LocalVariable variable;
+
+  Parameter(FlowGraph graph, SourcePosition sourcePosition, this.variable)
+    : super(graph, sourcePosition, 0);
+
+  bool get isFunctionParameter => block is EntryBlock;
+  bool get isCatchParameter => block is CatchBlock;
+
+  @override
+  CType get type => variable.type;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitParameter(this);
+}
+
+/// Loads value from the local variable.
+///
+/// [LoadLocal] instructions are only used before
+/// IR is converted to SSA form.
+final class LoadLocal extends Definition with NoThrow, Pure {
+  final LocalVariable variable;
+
+  LoadLocal(FlowGraph graph, SourcePosition sourcePosition, this.variable)
+    : super(graph, sourcePosition, 0);
+
+  @override
+  CType get type => variable.type;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitLoadLocal(this);
+}
+
+/// Store value to the local variable.
+///
+/// [StoreLocal] instructions are only used before
+/// IR is converted to SSA form.
+final class StoreLocal extends Instruction with NoThrow, HasSideEffects {
+  final LocalVariable variable;
+
+  StoreLocal(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.variable,
+    Definition value,
+  ) : super(graph, sourcePosition, 1) {
+    setInputAt(0, value);
+  }
+
+  Definition get value => inputDefAt(0);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitStoreLocal(this);
+}
+
+/// Throw given exception object. Also takes optional stack trace
+/// input to rethrow exception object without collecting a new stack trace.
+final class Throw extends Instruction
+    with CanThrow, HasSideEffects
+    implements ControlFlowInstruction {
+  Throw(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    Definition exception,
+    Definition? stackTrace,
+  ) : super(graph, sourcePosition, stackTrace != null ? 2 : 1) {
+    setInputAt(0, exception);
+    if (stackTrace != null) {
+      setInputAt(1, stackTrace);
+    }
+  }
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitThrow(this);
+}
+
+/// Represents collection of class and function type parameters.
+final class TypeParameters extends Definition with NoThrow, Pure {
+  TypeParameters(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    Definition? receiver,
+  ) : super(graph, sourcePosition, receiver != null ? 1 : 0) {
+    if (receiver != null) {
+      setInputAt(0, receiver);
+    }
+  }
+
+  @override
+  CType get type => const TypeParametersType();
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTypeParameters(this);
+}
+
+/// Casts input object to the given type.
+///
+/// Checked casts throw TypeError if
+/// object is not assignable to the given type.
+final class TypeCast extends Definition with CanThrow, Pure, Idempotent {
+  /// Target type for the type cast.
+  final CType testedType;
+
+  /// Whether this type cast involves check at runtime.
+  bool isChecked;
+
+  TypeCast(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    Definition object,
+    this.testedType,
+    Definition? typeParameters, {
+    this.isChecked = true,
+  }) : super(graph, sourcePosition, typeParameters != null ? 2 : 1) {
+    setInputAt(0, object);
+    if (typeParameters != null) {
+      setInputAt(1, typeParameters);
+    }
+  }
+
+  Definition get operand => inputDefAt(0);
+  Definition? get typeParameters => (inputCount > 1) ? inputDefAt(1) : null;
+
+  @override
+  CType get type => testedType;
+
+  @override
+  bool get canThrow => isChecked;
+
+  @override
+  bool attributesEqual(covariant TypeCast other) =>
+      // 'isChecked' is not taken into account as checked and unchecked casts
+      // against the same type are congruent wrt value numbering.
+      testedType == other.testedType;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTypeCast(this);
+}
+
+/// Test if input object is assignable to the given type.
+final class TypeTest extends Definition with NoThrow, Pure, Idempotent {
+  final CType testedType;
+
+  TypeTest(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    Definition object,
+    this.testedType,
+    Definition? typeParameters,
+  ) : super(graph, sourcePosition, typeParameters != null ? 2 : 1) {
+    setInputAt(0, object);
+    if (typeParameters != null) {
+      setInputAt(1, typeParameters);
+    }
+  }
+
+  Definition get operand => inputDefAt(0);
+  Definition? get typeParameters => (inputCount > 1) ? inputDefAt(1) : null;
+
+  @override
+  CType get type => const BoolType();
+
+  @override
+  bool attributesEqual(covariant TypeTest other) =>
+      testedType == other.testedType;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTypeTest(this);
+}
+
+/// Represents a list of type arguments passed to a call.
+///
+/// Only used as the first input of call instructions.
+final class TypeArguments extends Definition with NoThrow, Pure, Idempotent {
+  final List<ast.DartType> types;
+  TypeArguments(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.types,
+    Definition? typeParameters,
+  ) : super(graph, sourcePosition, typeParameters != null ? 1 : 0) {
+    if (typeParameters != null) {
+      setInputAt(0, typeParameters);
+    }
+  }
+
+  Definition? get typeParameters => (inputCount > 0) ? inputDefAt(0) : null;
+
+  @override
+  CType get type => const TypeArgumentsType();
+
+  @override
+  bool attributesEqual(covariant TypeArguments other) =>
+      listEquals(types, other.types);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitTypeArguments(this);
+}
+
+enum BinaryIntOpcode {
+  add('+'),
+  sub('-'),
+  mul('*'),
+  truncatingDiv('~/'),
+  mod('%'),
+  rem('remainder'),
+  bitOr('|'),
+  bitAnd('&'),
+  bitXor('^'),
+  shiftLeft('<<'),
+  shiftRight('>>'),
+  unsignedShiftRight('>>>');
+
+  final String token;
+  const BinaryIntOpcode(this.token);
+
+  bool get isCommutative => switch (this) {
+    add || mul || bitOr || bitAnd || bitXor => true,
+    _ => false,
+  };
+}
+
+/// Binary operation on two int operands.
+final class BinaryIntOp extends Definition with Pure, Idempotent {
+  BinaryIntOpcode op;
+
+  BinaryIntOp(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition left,
+    Definition right,
+  ) : super(graph, sourcePosition, 2) {
+    setInputAt(0, left);
+    setInputAt(1, right);
+  }
+
+  Definition get left => inputDefAt(0);
+  Definition get right => inputDefAt(1);
+
+  @override
+  bool get canThrow => switch (op) {
+    BinaryIntOpcode.truncatingDiv ||
+    BinaryIntOpcode.mod ||
+    BinaryIntOpcode.rem => right.canBeZero,
+    BinaryIntOpcode.shiftLeft ||
+    BinaryIntOpcode.shiftRight ||
+    BinaryIntOpcode.unsignedShiftRight => right.canBeNegative,
+    _ => false,
+  };
+
+  @override
+  CType get type => const IntType();
+
+  @override
+  bool attributesEqual(covariant BinaryIntOp other) => op == other.op;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitBinaryIntOp(this);
+}
+
+enum UnaryIntOpcode {
+  neg('-'),
+  bitNot('~'),
+  toDouble('toDouble'),
+  abs('abs'),
+  sign('sign');
+
+  final String token;
+  const UnaryIntOpcode(this.token);
+}
+
+/// Unary operation on the int operand.
+final class UnaryIntOp extends Definition with NoThrow, Pure, Idempotent {
+  UnaryIntOpcode op;
+
+  UnaryIntOp(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition operand,
+  ) : super(graph, sourcePosition, 1) {
+    setInputAt(0, operand);
+  }
+
+  Definition get operand => inputDefAt(0);
+
+  @override
+  CType get type => switch (op) {
+    UnaryIntOpcode.toDouble => const DoubleType(),
+    _ => const IntType(),
+  };
+
+  @override
+  bool attributesEqual(covariant UnaryIntOp other) => op == other.op;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitUnaryIntOp(this);
+}
+
+enum BinaryDoubleOpcode {
+  add('+'),
+  sub('-'),
+  mul('*'),
+  div('/'),
+  truncatingDiv('~/'),
+  mod('%'),
+  rem('remainder');
+
+  final String token;
+  const BinaryDoubleOpcode(this.token);
+
+  bool get isCommutative => switch (this) {
+    add || mul => true,
+    _ => false,
+  };
+}
+
+/// Binary operation on two double operands.
+final class BinaryDoubleOp extends Definition with NoThrow, Pure, Idempotent {
+  BinaryDoubleOpcode op;
+
+  BinaryDoubleOp(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition left,
+    Definition right,
+  ) : super(graph, sourcePosition, 2) {
+    setInputAt(0, left);
+    setInputAt(1, right);
+  }
+
+  Definition get left => inputDefAt(0);
+  Definition get right => inputDefAt(1);
+
+  @override
+  CType get type => switch (op) {
+    BinaryDoubleOpcode.truncatingDiv => const IntType(),
+    _ => const DoubleType(),
+  };
+
+  @override
+  bool attributesEqual(covariant BinaryDoubleOp other) => op == other.op;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitBinaryDoubleOp(this);
+}
+
+enum UnaryDoubleOpcode {
+  neg('-'),
+  abs('abs'),
+  sign('sign'),
+  square('square'),
+  round('round'),
+  floor('floor'),
+  ceil('ceil'),
+  truncate('truncate'),
+  roundToDouble('roundToDouble'),
+  floorToDouble('floorToDouble'),
+  ceilToDouble('ceilToDouble'),
+  truncateToDouble('truncateToDouble');
+
+  final String token;
+  const UnaryDoubleOpcode(this.token);
+}
+
+/// Unary operation on the double operand.
+final class UnaryDoubleOp extends Definition with NoThrow, Pure, Idempotent {
+  UnaryDoubleOpcode op;
+
+  UnaryDoubleOp(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition operand,
+  ) : super(graph, sourcePosition, 1) {
+    setInputAt(0, operand);
+  }
+
+  Definition get operand => inputDefAt(0);
+
+  @override
+  CType get type => switch (op) {
+    UnaryDoubleOpcode.round ||
+    UnaryDoubleOpcode.floor ||
+    UnaryDoubleOpcode.ceil ||
+    UnaryDoubleOpcode.truncate => const IntType(),
+    _ => const DoubleType(),
+  };
+
+  @override
+  bool attributesEqual(covariant UnaryDoubleOp other) => op == other.op;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitUnaryDoubleOp(this);
+}
+
+/// Marker for the back-end specific instructions.
+base mixin BackendInstruction on Instruction {}
+
+/// Combined comparison and branch.
+///
+/// Certain back-ends may create [CompareAndBranch] during lowering.
+final class CompareAndBranch extends Instruction
+    with NoThrow, Pure, BackendInstruction
+    implements ControlFlowInstruction {
+  ComparisonOpcode op;
+
+  CompareAndBranch(
+    FlowGraph graph,
+    SourcePosition sourcePosition,
+    this.op,
+    Definition left,
+    Definition right,
+  ) : super(graph, sourcePosition, 2) {
+    setInputAt(0, left);
+    setInputAt(1, right);
+  }
+
+  Definition get left => inputDefAt(0);
+  Definition get right => inputDefAt(1);
+  TargetBlock get trueSuccessor => block!.successors[0] as TargetBlock;
+  TargetBlock get falseSuccessor => block!.successors[1] as TargetBlock;
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitCompareAndBranch(this);
+}
+
+/// Base class for move operations, part of [ParallelMove].
+abstract base class MoveOp {}
+
+/// In native back-ends, register allocator inserts [ParallelMove]
+/// instructions to copy values atomically between registers
+/// and memory locations.
+final class ParallelMove extends Instruction
+    with NoThrow, HasSideEffects, BackendInstruction {
+  final List<MoveOp> moves = [];
+
+  ParallelMove(FlowGraph graph) : super(graph, noPosition, 0);
+
+  @override
+  R accept<R>(InstructionVisitor<R> v) => v.visitParallelMove(this);
+}
diff --git a/pkg/cfg/lib/ir/local_variable.dart b/pkg/cfg/lib/ir/local_variable.dart
new file mode 100644
index 0000000..52e5434
--- /dev/null
+++ b/pkg/cfg/lib/ir/local_variable.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:kernel/ast.dart' as ast show VariableDeclaration;
+import 'package:cfg/ir/types.dart';
+
+/// Local variable or a function parameter.
+///
+/// Local variables are used in the flow graph before
+/// it is converted to SSA form.
+class LocalVariable {
+  /// Name of the variable.
+  final String name;
+
+  /// Declaration of the variable in the AST, if any.
+  final ast.VariableDeclaration? declaration;
+
+  /// Index of the variable in the [FlowGraph.localVariables].
+  final int index;
+
+  /// Type of the variable.
+  final CType type;
+
+  LocalVariable(this.name, this.declaration, this.index, this.type);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkg/cfg/lib/ir/use_lists.dart b/pkg/cfg/lib/ir/use_lists.dart
new file mode 100644
index 0000000..4075280
--- /dev/null
+++ b/pkg/cfg/lib/ir/use_lists.dart
@@ -0,0 +1,125 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:cfg/ir/flow_graph.dart';
+import 'package:cfg/ir/instructions.dart';
+import 'package:cfg/utils/arena.dart';
+
+/// A single use of the result of the instruction in another instruction.
+extension type const Use(ArenaPointer _ptr) {
+  // Terminator in the use lists.
+  static const Use Null = Use(ArenaPointer.Null);
+
+  // Each use occupies 4 slots:
+  // instruction index, definition index, next use, previous use.
+  static const int instructionOffset = 0;
+  static const int definitionOffset = 1;
+  static const int nextOffset = 2;
+  static const int previousOffset = 3;
+
+  // Size of each use. Should be a power of 2 for efficiency.
+  static const int useSize = 4;
+
+  @pragma("vm:prefer-inline")
+  void init(FlowGraph graph, Instruction instr) {
+    setInstruction(graph, instr);
+    setNext(graph, Use.Null);
+    setPrevious(graph, Use.Null);
+  }
+
+  @pragma("vm:prefer-inline")
+  Instruction getInstruction(FlowGraph graph) =>
+      graph.instructions[graph[_ptr + instructionOffset]];
+
+  @pragma("vm:prefer-inline")
+  void setInstruction(FlowGraph graph, Instruction value) {
+    graph[_ptr + instructionOffset] = value.id;
+  }
+
+  @pragma("vm:prefer-inline")
+  Definition getDefinition(FlowGraph graph) =>
+      graph.instructions[graph[_ptr + definitionOffset]] as Definition;
+
+  @pragma("vm:prefer-inline")
+  void setDefinition(FlowGraph graph, Definition value) {
+    graph[_ptr + definitionOffset] = value.id;
+  }
+
+  @pragma("vm:prefer-inline")
+  Use getNext(FlowGraph graph) => Use(ArenaPointer(graph[_ptr + nextOffset]));
+
+  @pragma("vm:prefer-inline")
+  void setNext(FlowGraph graph, Use value) {
+    graph[_ptr + nextOffset] = value._ptr.toInt();
+  }
+
+  @pragma("vm:prefer-inline")
+  Use getPrevious(FlowGraph graph) =>
+      Use(ArenaPointer(graph[_ptr + previousOffset]));
+
+  @pragma("vm:prefer-inline")
+  void setPrevious(FlowGraph graph, Use value) {
+    graph[_ptr + previousOffset] = value._ptr.toInt();
+  }
+}
+
+/// Fixed-size array of uses.
+extension type const UsesArray(ArenaPointer _ptr) {
+  // Array has a length, followed by elements.
+  static const int lengthOffset = 0;
+  static const int elementsOffset = 1;
+
+  int getLength(FlowGraph graph) {
+    assert(_ptr != ArenaPointer.Null);
+    return graph[_ptr + lengthOffset];
+  }
+
+  Use at(FlowGraph graph, int index) {
+    assert(_ptr != ArenaPointer.Null);
+    assert(0 <= index && index < getLength(graph));
+    return Use(_ptr + elementsOffset + index * Use.useSize);
+  }
+
+  void truncateTo(FlowGraph graph, int newLength) {
+    assert(_ptr != ArenaPointer.Null);
+    assert((0 <= newLength) && (newLength <= getLength(graph)));
+    graph[_ptr + lengthOffset] = newLength;
+  }
+
+  static UsesArray allocate(FlowGraph graph, int length) {
+    assert(length >= 0);
+    final ptr = graph.allocate(elementsOffset + length * Use.useSize);
+    graph[ptr + lengthOffset] = length;
+    return UsesArray(ptr);
+  }
+}
+
+class _UsesIterator implements Iterator<Use> {
+  final FlowGraph graph;
+  Use _current = Use.Null;
+  Use _next;
+
+  _UsesIterator(this.graph, this._next);
+
+  @override
+  bool moveNext() {
+    _current = _next;
+    _next = (_current != Use.Null) ? _current.getNext(graph) : Use.Null;
+    return (_current != Use.Null);
+  }
+
+  @override
+  Use get current => _current;
+}
+
+class UsesIterable extends Iterable<Use> {
+  final FlowGraph graph;
+  Use _first;
+
+  UsesIterable(this.graph, this._first);
+  UsesIterable.empty(this.graph) : _first = Use.Null;
+
+  @override
+  Iterator<Use> get iterator => _UsesIterator(graph, _first);
+}
diff --git a/pkg/cfg/lib/ir/visitor.dart b/pkg/cfg/lib/ir/visitor.dart
new file mode 100644
index 0000000..f7a608f
--- /dev/null
+++ b/pkg/cfg/lib/ir/visitor.dart
@@ -0,0 +1,93 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:cfg/ir/instructions.dart';
+
+/// Visitor over [Instruction].
+abstract interface class InstructionVisitor<R> {
+  // Basic blocks.
+  R visitEntryBlock(EntryBlock instr);
+  R visitJoinBlock(JoinBlock instr);
+  R visitTargetBlock(TargetBlock instr);
+  R visitCatchBlock(CatchBlock instr);
+  // Regular instructions.
+  R visitGoto(Goto instr);
+  R visitBranch(Branch instr);
+  R visitTryEntry(TryEntry instr);
+  R visitPhi(Phi instr);
+  R visitReturn(Return instr);
+  R visitComparison(Comparison instr);
+  R visitConstant(Constant instr);
+  R visitDirectCall(DirectCall instr);
+  R visitInterfaceCall(InterfaceCall instr);
+  R visitDynamicCall(DynamicCall instr);
+  R visitParameter(Parameter instr);
+  R visitLoadLocal(LoadLocal instr);
+  R visitStoreLocal(StoreLocal instr);
+  R visitThrow(Throw instr);
+  R visitTypeParameters(TypeParameters instr);
+  R visitTypeCast(TypeCast instr);
+  R visitTypeTest(TypeTest instr);
+  R visitTypeArguments(TypeArguments instr);
+  R visitBinaryIntOp(BinaryIntOp instr);
+  R visitUnaryIntOp(UnaryIntOp instr);
+  R visitBinaryDoubleOp(BinaryDoubleOp instr);
+  R visitUnaryDoubleOp(UnaryDoubleOp instr);
+  // Back-end specific instructions.
+  R visitCompareAndBranch(CompareAndBranch instr);
+  R visitParallelMove(ParallelMove instr);
+}
+
+/// Visitor over [Instruction] which has an overridable default behavior for
+/// different categories of instructions.
+abstract mixin class DefaultInstructionVisitor<R>
+    implements InstructionVisitor<R> {
+  /// Default behavior for all instructions.
+  R defaultInstruction(Instruction instr);
+
+  /// Default behavior for basic blocks.
+  R defaultBlock(Block instr) => defaultInstruction(instr);
+
+  /// Default behavior for back-end specific instructions.
+  R defaultBackendInstruction(BackendInstruction instr) =>
+      defaultInstruction(instr);
+
+  // Basic blocks.
+  R visitEntryBlock(EntryBlock instr) => defaultBlock(instr);
+  R visitJoinBlock(JoinBlock instr) => defaultBlock(instr);
+  R visitTargetBlock(TargetBlock instr) => defaultBlock(instr);
+  R visitCatchBlock(CatchBlock instr) => defaultBlock(instr);
+  // Regular instructions.
+  R visitGoto(Goto instr) => defaultInstruction(instr);
+  R visitBranch(Branch instr) => defaultInstruction(instr);
+  R visitTryEntry(TryEntry instr) => defaultInstruction(instr);
+  R visitPhi(Phi instr) => defaultInstruction(instr);
+  R visitReturn(Return instr) => defaultInstruction(instr);
+  R visitComparison(Comparison instr) => defaultInstruction(instr);
+  R visitConstant(Constant instr) => defaultInstruction(instr);
+  R visitDirectCall(DirectCall instr) => defaultInstruction(instr);
+  R visitInterfaceCall(InterfaceCall instr) => defaultInstruction(instr);
+  R visitDynamicCall(DynamicCall instr) => defaultInstruction(instr);
+  R visitParameter(Parameter instr) => defaultInstruction(instr);
+  R visitLoadLocal(LoadLocal instr) => defaultInstruction(instr);
+  R visitStoreLocal(StoreLocal instr) => defaultInstruction(instr);
+  R visitThrow(Throw instr) => defaultInstruction(instr);
+  R visitTypeParameters(TypeParameters instr) => defaultInstruction(instr);
+  R visitTypeCast(TypeCast instr) => defaultInstruction(instr);
+  R visitTypeTest(TypeTest instr) => defaultInstruction(instr);
+  R visitTypeArguments(TypeArguments instr) => defaultInstruction(instr);
+  R visitBinaryIntOp(BinaryIntOp instr) => defaultInstruction(instr);
+  R visitUnaryIntOp(UnaryIntOp instr) => defaultInstruction(instr);
+  R visitBinaryDoubleOp(BinaryDoubleOp instr) => defaultInstruction(instr);
+  R visitUnaryDoubleOp(UnaryDoubleOp instr) => defaultInstruction(instr);
+  // Back-end specific instructions.
+  R visitCompareAndBranch(CompareAndBranch instr) =>
+      defaultBackendInstruction(instr);
+  R visitParallelMove(ParallelMove instr) => defaultBackendInstruction(instr);
+}
+
+/// Visitor over [Instruction] which does not yield a value.
+base class VoidInstructionVisitor extends DefaultInstructionVisitor<void> {
+  void defaultInstruction(Instruction instr) {}
+}
diff --git a/pkg/cfg/test/ir/constant_value_test.dart b/pkg/cfg/test/ir/constant_value_test.dart
new file mode 100644
index 0000000..41f238e
--- /dev/null
+++ b/pkg/cfg/test/ir/constant_value_test.dart
@@ -0,0 +1,512 @@
+// Copyright (c) 2025, the Dart 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.
+
+import 'package:cfg/ir/constant_value.dart';
+import 'package:cfg/ir/global_context.dart';
+import 'package:cfg/ir/instructions.dart';
+import 'package:cfg/ir/types.dart';
+import 'package:kernel/ast.dart' as ast;
+import 'package:kernel/class_hierarchy.dart' show ClassHierarchy;
+import 'package:kernel/core_types.dart' show CoreTypes;
+import 'package:kernel/type_environment.dart';
+import 'package:test/test.dart';
+import '../test_helpers.dart';
+
+void main() {
+  final component = readVmPlatformKernelFile();
+  final coreTypes = CoreTypes(component);
+  final classHierarchy = ClassHierarchy(component, coreTypes);
+  final typeEnvironment = TypeEnvironment(coreTypes, classHierarchy);
+  final globalContext = GlobalContext(typeEnvironment: typeEnvironment);
+
+  group('constant values', () {
+    setUp(() {
+      GlobalContext.setCurrentContext(globalContext);
+    });
+
+    tearDown(() {
+      GlobalContext.setCurrentContext(null);
+    });
+
+    test('int', () {
+      final values = <int>[
+        0x80000000_00000000,
+        0x7fffffff_ffffffff,
+        -1,
+        0,
+        1,
+        42,
+        -0xffffffff,
+      ];
+      for (final v in values) {
+        final cv = ConstantValue.fromInt(v);
+        expect(ConstantValue(ast.IntConstant(v)), equals(cv));
+        expect(cv.isInt, isTrue);
+        expect(cv.isDouble, isFalse);
+        expect(cv.isBool, isFalse);
+        expect(cv.isNull, isFalse);
+        expect(cv.isString, isFalse);
+        expect(cv.intValue, equals(v));
+        expect(cv.type is IntType, isTrue);
+        expect(cv.isZero, equals(v == 0));
+        expect(cv.isNegative, equals(v < 0));
+        expect(cv.valueToString(), equals(v.toString()));
+      }
+    });
+
+    test('double', () {
+      final values = <double>[
+        -1.0,
+        -0.0,
+        0.0,
+        1.0,
+        double.infinity,
+        double.negativeInfinity,
+        double.nan,
+        1e100,
+      ];
+      for (final v in values) {
+        final cv = ConstantValue.fromDouble(v);
+        expect(ConstantValue(ast.DoubleConstant(v)), equals(cv));
+        expect(cv.isInt, isFalse);
+        expect(cv.isDouble, isTrue);
+        expect(cv.isBool, isFalse);
+        expect(cv.isNull, isFalse);
+        expect(cv.isString, isFalse);
+        expect(cv.doubleValue, same(v));
+        expect(cv.type is DoubleType, isTrue);
+        expect(cv.isZero, equals(v == 0.0));
+        expect(cv.isNegative, equals(v.isNegative));
+        expect(cv.valueToString(), equals(v.toString()));
+      }
+    });
+
+    test('bool', () {
+      final values = <bool>[true, false];
+      for (final v in values) {
+        final cv = ConstantValue.fromBool(v);
+        expect(ConstantValue(ast.BoolConstant(v)), equals(cv));
+        expect(cv.isInt, isFalse);
+        expect(cv.isDouble, isFalse);
+        expect(cv.isBool, isTrue);
+        expect(cv.isNull, isFalse);
+        expect(cv.isString, isFalse);
+        expect(cv.boolValue, equals(v));
+        expect(cv.type is BoolType, isTrue);
+        expect(cv.isZero, isFalse);
+        expect(cv.isNegative, isFalse);
+        expect(cv.valueToString(), equals(v.toString()));
+      }
+    });
+
+    test('null', () {
+      final cv = ConstantValue.fromNull();
+      expect(ConstantValue(ast.NullConstant()), equals(cv));
+      expect(cv.isInt, isFalse);
+      expect(cv.isDouble, isFalse);
+      expect(cv.isBool, isFalse);
+      expect(cv.isNull, isTrue);
+      expect(cv.isString, isFalse);
+      expect(cv.type is NullType, isTrue);
+      expect(cv.isZero, isFalse);
+      expect(cv.isNegative, isFalse);
+      expect(cv.valueToString(), equals(null.toString()));
+    });
+
+    test('string', () {
+      final values = <String>['', 'a', 'abcdef'];
+      for (final v in values) {
+        final cv = ConstantValue.fromString(v);
+        expect(ConstantValue(ast.StringConstant(v)), equals(cv));
+        expect(cv.isInt, isFalse);
+        expect(cv.isDouble, isFalse);
+        expect(cv.isBool, isFalse);
+        expect(cv.isNull, isFalse);
+        expect(cv.isString, isTrue);
+        expect(cv.stringValue, equals(v));
+        expect(cv.type is StringType, isTrue);
+        expect(cv.isZero, isFalse);
+        expect(cv.isNegative, isFalse);
+        expect(cv.valueToString(), equals('"$v"'));
+      }
+    });
+
+    test('type arguments', () {
+      final values = <List<ast.DartType>>[
+        [],
+        [coreTypes.stringNullableRawType],
+        [coreTypes.intNonNullableRawType, coreTypes.stringNonNullableRawType],
+      ];
+      for (final v in values) {
+        final cv = ConstantValue(TypeArgumentsConstant(v));
+        expect(ConstantValue(TypeArgumentsConstant(List.of(v))), equals(cv));
+        expect(cv.isInt, isFalse);
+        expect(cv.isDouble, isFalse);
+        expect(cv.isBool, isFalse);
+        expect(cv.isNull, isFalse);
+        expect(cv.isString, isFalse);
+        expect(cv.type is TypeArgumentsType, isTrue);
+        expect(cv.isZero, isFalse);
+        expect(cv.isNegative, isFalse);
+      }
+    });
+  });
+
+  group('constant folding', () {
+    final constantFolding = const ConstantFolding();
+
+    setUp(() {
+      GlobalContext.setCurrentContext(globalContext);
+    });
+
+    tearDown(() {
+      GlobalContext.setCurrentContext(null);
+    });
+
+    test('comparison - equal', () {
+      void eq(ConstantValue left, ConstantValue right) {
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.equal, left, right)
+              .boolValue,
+          isTrue,
+        );
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.notEqual, left, right)
+              .boolValue,
+          isFalse,
+        );
+      }
+
+      void ne(ConstantValue left, ConstantValue right) {
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.equal, left, right)
+              .boolValue,
+          isFalse,
+        );
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.notEqual, left, right)
+              .boolValue,
+          isTrue,
+        );
+      }
+
+      eq(ConstantValue.fromString('a'), ConstantValue.fromString('a'));
+      ne(ConstantValue.fromString('a'), ConstantValue.fromString('b'));
+      eq(ConstantValue.fromInt(42), ConstantValue.fromInt(42));
+      ne(ConstantValue.fromInt(42), ConstantValue.fromInt(43));
+      ne(ConstantValue.fromInt(0), ConstantValue.fromDouble(0.0));
+      eq(
+        ConstantValue.fromDouble(double.nan),
+        ConstantValue.fromDouble(double.nan),
+      );
+      ne(ConstantValue.fromDouble(0.0), ConstantValue.fromDouble(-0.0));
+    });
+
+    test('comparison - identical', () {
+      void eq(ConstantValue left, ConstantValue right) {
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.identical, left, right)
+              .boolValue,
+          isTrue,
+        );
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.notIdentical, left, right)
+              .boolValue,
+          isFalse,
+        );
+      }
+
+      void ne(ConstantValue left, ConstantValue right) {
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.identical, left, right)
+              .boolValue,
+          isFalse,
+        );
+        expect(
+          constantFolding
+              .comparison(ComparisonOpcode.notIdentical, left, right)
+              .boolValue,
+          isTrue,
+        );
+      }
+
+      eq(ConstantValue.fromString('a'), ConstantValue.fromString('a'));
+      ne(ConstantValue.fromString('a'), ConstantValue.fromString('b'));
+      eq(ConstantValue.fromInt(42), ConstantValue.fromInt(42));
+      ne(ConstantValue.fromInt(42), ConstantValue.fromInt(43));
+      ne(ConstantValue.fromInt(0), ConstantValue.fromDouble(0.0));
+      eq(
+        ConstantValue.fromDouble(double.nan),
+        ConstantValue.fromDouble(double.nan),
+      );
+      ne(ConstantValue.fromDouble(0.0), ConstantValue.fromDouble(-0.0));
+    });
+
+    test('comparison - int', () {
+      final values = <(int, int)>[
+        (0, 1),
+        (1, 0),
+        (42, 6),
+        (10, -5),
+        (-3, 1),
+        (0x80000000_00000000, -1),
+        (-10, 0x7fffffff_ffffffff),
+      ];
+
+      void testOp(
+        ComparisonOpcode opcode,
+        bool Function(int, int) expectedBehavior,
+      ) {
+        for (final pair in values) {
+          expect(
+            constantFolding
+                .comparison(
+                  opcode,
+                  ConstantValue.fromInt(pair.$1),
+                  ConstantValue.fromInt(pair.$2),
+                )
+                .boolValue,
+            equals(expectedBehavior(pair.$1, pair.$2)),
+          );
+        }
+      }
+
+      testOp(ComparisonOpcode.intEqual, (int a, int b) => a == b);
+      testOp(ComparisonOpcode.intNotEqual, (int a, int b) => a != b);
+      testOp(ComparisonOpcode.intLess, (int a, int b) => a < b);
+      testOp(ComparisonOpcode.intLessOrEqual, (int a, int b) => a <= b);
+      testOp(ComparisonOpcode.intGreater, (int a, int b) => a > b);
+      testOp(ComparisonOpcode.intGreaterOrEqual, (int a, int b) => a >= b);
+    });
+
+    test('comparison - double', () {
+      final values = <(double, double)>[
+        (1.0, 1.0),
+        (1.0, -1.0),
+        (0.0, -0.0),
+        (42.0, double.infinity),
+        (double.negativeInfinity, -1e100),
+        (1.0, double.nan),
+        (double.nan, double.nan),
+      ];
+
+      void testOp(
+        ComparisonOpcode opcode,
+        bool Function(double, double) expectedBehavior,
+      ) {
+        for (final pair in values) {
+          expect(
+            constantFolding
+                .comparison(
+                  opcode,
+                  ConstantValue.fromDouble(pair.$1),
+                  ConstantValue.fromDouble(pair.$2),
+                )
+                .boolValue,
+            equals(expectedBehavior(pair.$1, pair.$2)),
+          );
+        }
+      }
+
+      testOp(ComparisonOpcode.doubleEqual, (double a, double b) => a == b);
+      testOp(ComparisonOpcode.doubleNotEqual, (double a, double b) => a != b);
+      testOp(ComparisonOpcode.doubleLess, (double a, double b) => a < b);
+      testOp(
+        ComparisonOpcode.doubleLessOrEqual,
+        (double a, double b) => a <= b,
+      );
+      testOp(ComparisonOpcode.doubleGreater, (double a, double b) => a > b);
+      testOp(
+        ComparisonOpcode.doubleGreaterOrEqual,
+        (double a, double b) => a >= b,
+      );
+    });
+
+    test('binary int op', () {
+      final values = <(int, int)>[
+        (0, 1),
+        (1, 0),
+        (42, 6),
+        (10, -5),
+        (-3, 1),
+        (0x80000000_00000000, -1),
+        (-10, 0x7fffffff_ffffffff),
+      ];
+
+      void testOp(
+        BinaryIntOpcode opcode,
+        int Function(int, int) expectedBehavior,
+      ) {
+        for (final pair in values) {
+          ConstantValue? expected;
+          try {
+            expected = ConstantValue.fromInt(
+              expectedBehavior(pair.$1, pair.$2),
+            );
+          } on Error {
+            // Ignore runtime errors.
+          }
+          expect(
+            constantFolding.binaryIntOp(
+              opcode,
+              ConstantValue.fromInt(pair.$1),
+              ConstantValue.fromInt(pair.$2),
+            ),
+            equals(expected),
+          );
+        }
+      }
+
+      testOp(BinaryIntOpcode.add, (int a, int b) => a + b);
+      testOp(BinaryIntOpcode.sub, (int a, int b) => a - b);
+      testOp(BinaryIntOpcode.mul, (int a, int b) => a * b);
+      testOp(BinaryIntOpcode.truncatingDiv, (int a, int b) => a ~/ b);
+      testOp(BinaryIntOpcode.mod, (int a, int b) => a % b);
+      testOp(BinaryIntOpcode.rem, (int a, int b) => a.remainder(b));
+      testOp(BinaryIntOpcode.bitOr, (int a, int b) => a | b);
+      testOp(BinaryIntOpcode.bitAnd, (int a, int b) => a & b);
+      testOp(BinaryIntOpcode.bitXor, (int a, int b) => a ^ b);
+      testOp(BinaryIntOpcode.shiftLeft, (int a, int b) => a << b);
+      testOp(BinaryIntOpcode.shiftRight, (int a, int b) => a >> b);
+      testOp(BinaryIntOpcode.unsignedShiftRight, (int a, int b) => a >>> b);
+    });
+
+    test('unary int op', () {
+      final values = <int>[
+        0,
+        1,
+        -1,
+        42,
+        -123,
+        0x80000000_00000000,
+        0x7fffffff_ffffffff,
+      ];
+
+      void testOp(UnaryIntOpcode opcode, num Function(int) expectedBehavior) {
+        for (final v in values) {
+          final expected = expectedBehavior(v);
+          final expectedConstValue = switch (expected) {
+            double() => ConstantValue.fromDouble(expected),
+            int() => ConstantValue.fromInt(expected),
+          };
+          expect(
+            constantFolding.unaryIntOp(opcode, ConstantValue.fromInt(v)),
+            equals(expectedConstValue),
+          );
+        }
+      }
+
+      testOp(UnaryIntOpcode.neg, (int v) => -v);
+      testOp(UnaryIntOpcode.bitNot, (int v) => ~v);
+      testOp(UnaryIntOpcode.toDouble, (int v) => v.toDouble());
+      testOp(UnaryIntOpcode.abs, (int v) => v.abs());
+      testOp(UnaryIntOpcode.sign, (int v) => v.sign);
+    });
+
+    test('binary double op', () {
+      final values = <(double, double)>[
+        (1.0, 1.0),
+        (1.0, -1.0),
+        (0.0, -0.0),
+        (42.0, double.infinity),
+        (double.negativeInfinity, -1e100),
+        (1.0, double.nan),
+        (double.nan, double.nan),
+      ];
+
+      void testOp(
+        BinaryDoubleOpcode opcode,
+        num Function(double, double) expectedBehavior,
+      ) {
+        for (final pair in values) {
+          ConstantValue? expectedConstValue;
+          try {
+            final expected = expectedBehavior(pair.$1, pair.$2);
+            expectedConstValue = switch (expected) {
+              double() => ConstantValue.fromDouble(expected),
+              int() => ConstantValue.fromInt(expected),
+            };
+          } on Error {
+            // Ignore runtime errors.
+          }
+          expect(
+            constantFolding.binaryDoubleOp(
+              opcode,
+              ConstantValue.fromDouble(pair.$1),
+              ConstantValue.fromDouble(pair.$2),
+            ),
+            equals(expectedConstValue),
+          );
+        }
+      }
+
+      testOp(BinaryDoubleOpcode.add, (double a, double b) => a + b);
+      testOp(BinaryDoubleOpcode.sub, (double a, double b) => a - b);
+      testOp(BinaryDoubleOpcode.mul, (double a, double b) => a * b);
+      testOp(BinaryDoubleOpcode.mod, (double a, double b) => a % b);
+      testOp(BinaryDoubleOpcode.rem, (double a, double b) => a.remainder(b));
+      testOp(BinaryDoubleOpcode.div, (double a, double b) => a / b);
+      testOp(BinaryDoubleOpcode.truncatingDiv, (double a, double b) => a ~/ b);
+    });
+
+    test('unary double op', () {
+      final values = <double>[
+        1.0,
+        -1.0,
+        0.0,
+        -0.0,
+        42.0,
+        -1e100,
+        double.infinity,
+        double.negativeInfinity,
+        double.nan,
+      ];
+
+      void testOp(
+        UnaryDoubleOpcode opcode,
+        num Function(double) expectedBehavior,
+      ) {
+        for (final v in values) {
+          ConstantValue? expectedConstValue;
+          try {
+            final expected = expectedBehavior(v);
+            expectedConstValue = switch (expected) {
+              double() => ConstantValue.fromDouble(expected),
+              int() => ConstantValue.fromInt(expected),
+            };
+          } on Error {
+            // Ignore runtime errors.
+          }
+          expect(
+            constantFolding.unaryDoubleOp(opcode, ConstantValue.fromDouble(v)),
+            equals(expectedConstValue),
+          );
+        }
+      }
+
+      testOp(UnaryDoubleOpcode.neg, (double v) => -v);
+      testOp(UnaryDoubleOpcode.abs, (double v) => v.abs());
+      testOp(UnaryDoubleOpcode.sign, (double v) => v.sign);
+      testOp(UnaryDoubleOpcode.square, (double v) => v * v);
+      testOp(UnaryDoubleOpcode.round, (double v) => v.round());
+      testOp(UnaryDoubleOpcode.floor, (double v) => v.floor());
+      testOp(UnaryDoubleOpcode.ceil, (double v) => v.ceil());
+      testOp(UnaryDoubleOpcode.truncate, (double v) => v.truncate());
+      testOp(UnaryDoubleOpcode.roundToDouble, (double v) => v.roundToDouble());
+      testOp(UnaryDoubleOpcode.floorToDouble, (double v) => v.floorToDouble());
+      testOp(UnaryDoubleOpcode.ceilToDouble, (double v) => v.ceilToDouble());
+      testOp(
+        UnaryDoubleOpcode.truncateToDouble,
+        (double v) => v.truncateToDouble(),
+      );
+    });
+  });
+}