[ir] Add EmitBinary

This CL adds the machinery to emit binary operations to the IR. The
debug helper is split into Debug and Disassembler. The Disassembler is
used to help test the IR output.

Bug: tint:1718
Change-Id: Iffdd3be92e69a87828655ac41be91b34d5618174
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/110841
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 16ba8cf..dfe9433 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -658,6 +658,8 @@
     ir/builder_impl.h
     ir/debug.cc
     ir/debug.h
+    ir/disassembler.cc
+    ir/disassembler.h
     ir/flow_node.cc
     ir/flow_node.h
     ir/function.cc
@@ -668,6 +670,8 @@
     ir/loop.h
     ir/module.cc
     ir/module.h
+    ir/op.cc
+    ir/op.h
     ir/register.cc
     ir/register.h
     ir/switch.cc
@@ -1335,6 +1339,7 @@
   if (${TINT_BUILD_IR})
     list(APPEND TINT_TEST_SRCS
       ir/builder_impl_test.cc
+      ir/op_test.cc
       ir/register_test.cc
       ir/test_helper.h
     )
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index 4eababd..ae2e2e2 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -42,6 +42,7 @@
 
 #if TINT_BUILD_IR
 #include "src/tint/ir/debug.h"
+#include "src/tint/ir/disassembler.h"
 #include "src/tint/ir/module.h"
 #endif  // TINT_BUILD_IR
 
@@ -1343,7 +1344,8 @@
         } else {
             auto mod = result.Move();
             if (options.dump_ir) {
-                std::cout << tint::ir::Debug::AsString(&mod) << std::endl;
+                tint::ir::Disassembler d;
+                std::cout << d.Disassemble(mod) << std::endl;
             }
             if (options.dump_ir_graph) {
                 auto graph = tint::ir::Debug::AsDotGraph(&mod);
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 022aff2..850c13c 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -16,6 +16,8 @@
 #define SRC_TINT_IR_BLOCK_H_
 
 #include "src/tint/ir/flow_node.h"
+#include "src/tint/ir/op.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::ir {
 
@@ -30,6 +32,9 @@
 
     /// The node this block branches too.
     const FlowNode* branch_target = nullptr;
+
+    /// The operations in the block
+    utils::Vector<Op, 16> ops;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 851d2d6..57a40a7 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -93,4 +93,84 @@
     to->inbound_branches.Push(from);
 }
 
+Register::Id Builder::AllocateRegister() {
+    return next_register_id++;
+}
+
+Op Builder::CreateOp(Op::Kind kind, Register lhs, Register rhs) {
+    return Op(kind, Register(AllocateRegister()), lhs, rhs);
+}
+
+Op Builder::And(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kAnd, lhs, rhs);
+}
+
+Op Builder::Or(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kOr, lhs, rhs);
+}
+
+Op Builder::Xor(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kXor, lhs, rhs);
+}
+
+Op Builder::LogicalAnd(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kLogicalAnd, lhs, rhs);
+}
+
+Op Builder::LogicalOr(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kLogicalOr, lhs, rhs);
+}
+
+Op Builder::Equal(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kEqual, lhs, rhs);
+}
+
+Op Builder::NotEqual(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kNotEqual, lhs, rhs);
+}
+
+Op Builder::LessThan(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kLessThan, lhs, rhs);
+}
+
+Op Builder::GreaterThan(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kGreaterThan, lhs, rhs);
+}
+
+Op Builder::LessThanEqual(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kLessThanEqual, lhs, rhs);
+}
+
+Op Builder::GreaterThanEqual(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kGreaterThanEqual, lhs, rhs);
+}
+
+Op Builder::ShiftLeft(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kShiftLeft, lhs, rhs);
+}
+
+Op Builder::ShiftRight(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kShiftRight, lhs, rhs);
+}
+
+Op Builder::Add(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kAdd, lhs, rhs);
+}
+
+Op Builder::Subtract(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kSubtract, lhs, rhs);
+}
+
+Op Builder::Multiply(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kMultiply, lhs, rhs);
+}
+
+Op Builder::Divide(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kDivide, lhs, rhs);
+}
+
+Op Builder::Modulo(Register lhs, Register rhs) {
+    return CreateOp(Op::Kind::kModulo, lhs, rhs);
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index cf0de70..091aac8 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -19,6 +19,8 @@
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/ir/op.h"
+#include "src/tint/ir/register.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
 
@@ -81,8 +83,129 @@
     /// @param to the node to branch too
     void Branch(Block* from, FlowNode* to);
 
+    /// Creates an op for `lhs kind rhs`
+    /// @param kind the kind of operation
+    /// @param lhs the left-hand-side of the operation
+    /// @param rhs the right-hand-side of the operation
+    /// @returns the operation
+    Op CreateOp(Op::Kind kind, Register lhs, Register rhs);
+
+    /// Creates an And operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op And(Register lhs, Register rhs);
+
+    /// Creates an Or operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Or(Register lhs, Register rhs);
+
+    /// Creates an Xor operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Xor(Register lhs, Register rhs);
+
+    /// Creates an LogicalAnd operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op LogicalAnd(Register lhs, Register rhs);
+
+    /// Creates an LogicalOr operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op LogicalOr(Register lhs, Register rhs);
+
+    /// Creates an Equal operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Equal(Register lhs, Register rhs);
+
+    /// Creates an NotEqual operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op NotEqual(Register lhs, Register rhs);
+
+    /// Creates an LessThan operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op LessThan(Register lhs, Register rhs);
+
+    /// Creates an GreaterThan operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op GreaterThan(Register lhs, Register rhs);
+
+    /// Creates an LessThanEqual operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op LessThanEqual(Register lhs, Register rhs);
+
+    /// Creates an GreaterThanEqual operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op GreaterThanEqual(Register lhs, Register rhs);
+
+    /// Creates an ShiftLeft operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op ShiftLeft(Register lhs, Register rhs);
+
+    /// Creates an ShiftRight operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op ShiftRight(Register lhs, Register rhs);
+
+    /// Creates an Add operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Add(Register lhs, Register rhs);
+
+    /// Creates an Subtract operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Subtract(Register lhs, Register rhs);
+
+    /// Creates an Multiply operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Multiply(Register lhs, Register rhs);
+
+    /// Creates an Divide operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Divide(Register lhs, Register rhs);
+
+    /// Creates an Modulo operation
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    Op Modulo(Register lhs, Register rhs);
+
+    /// @returns a unique register id
+    Register::Id AllocateRegister();
+
     /// The IR module.
     Module ir;
+
+    /// The next register number to allocate
+    Register::Id next_register_id = 1;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index b2dab25..06ed837 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -15,6 +15,7 @@
 #include "src/tint/ir/builder_impl.h"
 
 #include "src/tint/ast/alias.h"
+#include "src/tint/ast/binary_expression.h"
 #include "src/tint/ast/block_statement.h"
 #include "src/tint/ast/bool_literal_expression.h"
 #include "src/tint/ast/break_if_statement.h"
@@ -519,7 +520,7 @@
     return tint::Switch(
         expr,
         // [&](const ast::IndexAccessorExpression* a) { return EmitIndexAccessor(a); },
-        // [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
+        [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
         // [&](const ast::BitcastExpression* b) { return EmitBitcast(b); },
         // [&](const ast::CallExpression* c) { return EmitCall(c); },
         // [&](const ast::IdentifierExpression* i) { return EmitIdentifier(i); },
@@ -550,6 +551,83 @@
         });
 }
 
+utils::Result<Register> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
+    auto lhs = EmitExpression(expr->lhs);
+    if (!lhs) {
+        return utils::Failure;
+    }
+
+    auto rhs = EmitExpression(expr->rhs);
+    if (!rhs) {
+        return utils::Failure;
+    }
+
+    Op op;
+    switch (expr->op) {
+        case ast::BinaryOp::kAnd:
+            op = builder.And(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kOr:
+            op = builder.Or(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kXor:
+            op = builder.Xor(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLogicalAnd:
+            op = builder.LogicalAnd(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLogicalOr:
+            op = builder.LogicalOr(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kEqual:
+            op = builder.Equal(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kNotEqual:
+            op = builder.NotEqual(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLessThan:
+            op = builder.LessThan(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kGreaterThan:
+            op = builder.GreaterThan(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLessThanEqual:
+            op = builder.LessThanEqual(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kGreaterThanEqual:
+            op = builder.GreaterThanEqual(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kShiftLeft:
+            op = builder.ShiftLeft(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kShiftRight:
+            op = builder.ShiftRight(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kAdd:
+            op = builder.Add(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kSubtract:
+            op = builder.Subtract(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kMultiply:
+            op = builder.Multiply(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kDivide:
+            op = builder.Divide(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kModulo:
+            op = builder.Modulo(lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kNone:
+            TINT_ICE(IR, diagnostics_) << "missing binary operand type";
+            return utils::Failure;
+    }
+
+    auto result = op.Result();
+    current_flow_block->ops.Push(op);
+    return result;
+}
+
 utils::Result<Register> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
     return tint::Switch(  //
         lit,
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
index 18c4d3e..ef5238e 100644
--- a/src/tint/ir/builder_impl.h
+++ b/src/tint/ir/builder_impl.h
@@ -31,6 +31,7 @@
 class Program;
 }  // namespace tint
 namespace tint::ast {
+class BinaryExpression;
 class BlockStatement;
 class BreakIfStatement;
 class BreakStatement;
@@ -146,6 +147,11 @@
     /// @returns true if successful, false otherwise
     bool EmitVariable(const ast::Variable* var);
 
+    /// Emits a binary expression
+    /// @param expr the binary expression
+    /// @returns the register storing the result if successful, utils::Failure otherwise
+    utils::Result<Register> EmitBinary(const ast::BinaryExpression* expr);
+
     /// Emits a literal expression
     /// @param lit the literal to emit
     /// @returns true if successful, false otherwise
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index 04c8d00..4a1efd5 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -1396,5 +1396,221 @@
     EXPECT_EQ(2_u, reg.AsU32());
 }
 
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Add(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 + 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Subtract) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Sub(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 - 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Multiply) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Mul(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 * 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Div) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Div(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 / 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Modulo) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Mod(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 % 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_And) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(And(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 & 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Or) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Or(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 | 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Xor) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Xor(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 ^ 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalAnd) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(LogicalAnd(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 && 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalOr) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(LogicalOr(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 || 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Eqaul) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Equal(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 == 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_NotEqual) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(NotEqual(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 != 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThan) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(LessThan(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 < 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThan) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(GreaterThan(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 > 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThanEqual) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(LessThanEqual(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 <= 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThanEqual) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(GreaterThanEqual(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 >= 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftLeft) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Shl(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 << 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftRight) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(Shr(3_u, 4_u));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 >> 4
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound) {
+    auto& b = CreateEmptyBuilder();
+    auto r = b.EmitExpression(LogicalOr(  //
+        LessThan(1_u, Add(Shr(3_u, 4_u), 9_u)), GreaterThan(2.5_f, Div(6.7_f, Mul(2.3_f, 5.5_f)))));
+    ASSERT_TRUE(r);
+
+    Disassembler d;
+    d.EmitBlockOps(b.current_flow_block);
+    EXPECT_EQ(d.AsString(), R"(%1 = 3 >> 4
+%2 = %1 + 9
+%3 = 1 < %2
+%4 = 2.300000 * 5.500000
+%5 = 6.700000 / %4
+%6 = 2.500000 > %5
+%7 = %3 || %6
+)");
+}
+
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
index bcac46d..be83111 100644
--- a/src/tint/ir/debug.cc
+++ b/src/tint/ir/debug.cc
@@ -26,33 +26,6 @@
 #include "src/tint/program.h"
 
 namespace tint::ir {
-namespace {
-
-class ScopedStopNode {
-  public:
-    ScopedStopNode(std::unordered_set<const FlowNode*>* stop_nodes, const FlowNode* node)
-        : stop_nodes_(stop_nodes), node_(node) {
-        stop_nodes_->insert(node_);
-    }
-
-    ~ScopedStopNode() { stop_nodes_->erase(node_); }
-
-  private:
-    std::unordered_set<const FlowNode*>* stop_nodes_;
-    const FlowNode* node_;
-};
-
-class ScopedIndent {
-  public:
-    explicit ScopedIndent(uint32_t* indent) : indent_(indent) { (*indent_) += 2; }
-
-    ~ScopedIndent() { (*indent_) -= 2; }
-
-  private:
-    uint32_t* indent_;
-};
-
-}  // namespace
 
 // static
 std::string Debug::AsDotGraph(const Module* mod) {
@@ -183,102 +156,4 @@
     return out.str();
 }
 
-// static
-std::string Debug::AsString(const Module* mod) {
-    std::stringstream out;
-
-    std::unordered_set<const FlowNode*> visited;
-    std::unordered_set<const FlowNode*> stop_nodes;
-    uint32_t indent_size = 0;
-
-    std::function<std::ostream&(void)> indent = [&]() -> std::ostream& {
-        for (uint32_t i = 0; i < indent_size; i++) {
-            out << " ";
-        }
-        return out;
-    };
-
-    std::function<void(const FlowNode*)> Walk = [&](const FlowNode* node) {
-        if ((visited.count(node) > 0) || (stop_nodes.count(node) > 0)) {
-            return;
-        }
-        visited.insert(node);
-
-        tint::Switch(
-            node,
-            [&](const ir::Function* f) {
-                out << "Function" << std::endl;
-
-                {
-                    ScopedIndent func_indent(&indent_size);
-                    ScopedStopNode scope(&stop_nodes, f->end_target);
-                    Walk(f->start_target);
-                }
-                Walk(f->end_target);
-            },
-            [&](const ir::Block* b) {
-                indent() << "Block" << std::endl;
-                Walk(b->branch_target);
-            },
-            [&](const ir::Switch* s) {
-                indent() << "Switch (" << s->condition.AsString() << ")" << std::endl;
-
-                {
-                    ScopedIndent switch_indent(&indent_size);
-                    ScopedStopNode scope(&stop_nodes, s->merge_target);
-                    for (const auto& c : s->cases) {
-                        indent() << "Case" << std::endl;
-                        ScopedIndent case_indent(&indent_size);
-                        Walk(c.start_target);
-                    }
-                }
-
-                indent() << "Switch Merge" << std::endl;
-                Walk(s->merge_target);
-            },
-            [&](const ir::If* i) {
-                indent() << "if (" << i->condition.AsString() << ")" << std::endl;
-                {
-                    ScopedIndent if_indent(&indent_size);
-                    ScopedStopNode scope(&stop_nodes, i->merge_target);
-
-                    indent() << "true branch" << std::endl;
-                    Walk(i->true_target);
-
-                    indent() << "false branch" << std::endl;
-                    Walk(i->false_target);
-                }
-
-                indent() << "if merge" << std::endl;
-                Walk(i->merge_target);
-            },
-            [&](const ir::Loop* l) {
-                indent() << "loop" << std::endl;
-                {
-                    ScopedStopNode loop_scope(&stop_nodes, l->merge_target);
-                    ScopedIndent loop_indent(&indent_size);
-                    {
-                        ScopedStopNode inner_scope(&stop_nodes, l->continuing_target);
-                        indent() << "loop start" << std::endl;
-                        Walk(l->start_target);
-                    }
-
-                    indent() << "loop continuing" << std::endl;
-                    ScopedIndent continuing_indent(&indent_size);
-                    Walk(l->continuing_target);
-                }
-
-                indent() << "loop merge" << std::endl;
-                Walk(l->merge_target);
-            },
-            [&](const ir::Terminator*) { indent() << "Function end" << std::endl; });
-    };
-
-    for (const auto* func : mod->functions) {
-        Walk(func);
-    }
-
-    return out.str();
-}
-
 }  // namespace tint::ir
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
new file mode 100644
index 0000000..88e2e69
--- /dev/null
+++ b/src/tint/ir/disassembler.cc
@@ -0,0 +1,154 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/disassembler.h"
+
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/loop.h"
+#include "src/tint/ir/switch.h"
+#include "src/tint/ir/terminator.h"
+#include "src/tint/program.h"
+
+namespace tint::ir {
+namespace {
+
+class ScopedStopNode {
+  public:
+    ScopedStopNode(std::unordered_set<const FlowNode*>* stop_nodes_, const FlowNode* node)
+        : stop_nodes__(stop_nodes_), node_(node) {
+        stop_nodes__->insert(node_);
+    }
+
+    ~ScopedStopNode() { stop_nodes__->erase(node_); }
+
+  private:
+    std::unordered_set<const FlowNode*>* stop_nodes__;
+    const FlowNode* node_;
+};
+
+class ScopedIndent {
+  public:
+    explicit ScopedIndent(uint32_t* indent) : indent_(indent) { (*indent_) += 2; }
+
+    ~ScopedIndent() { (*indent_) -= 2; }
+
+  private:
+    uint32_t* indent_;
+};
+
+}  // namespace
+
+Disassembler::Disassembler() = default;
+
+Disassembler::~Disassembler() = default;
+
+std::ostream& Disassembler::Indent() {
+    for (uint32_t i = 0; i < indent_size_; i++) {
+        out_ << " ";
+    }
+    return out_;
+}
+
+void Disassembler::EmitBlockOps(const Block* b) {
+    for (const auto& op : b->ops) {
+        out_ << op << std::endl;
+    }
+}
+
+void Disassembler::Walk(const FlowNode* node) {
+    if ((visited_.count(node) > 0) || (stop_nodes_.count(node) > 0)) {
+        return;
+    }
+    visited_.insert(node);
+
+    tint::Switch(
+        node,
+        [&](const ir::Function* f) {
+            Indent() << "Function" << std::endl;
+
+            {
+                ScopedIndent func_indent(&indent_size_);
+                ScopedStopNode scope(&stop_nodes_, f->end_target);
+                Walk(f->start_target);
+            }
+            Walk(f->end_target);
+        },
+        [&](const ir::Block* b) {
+            Indent() << "Block" << std::endl;
+            EmitBlockOps(b);
+            Walk(b->branch_target);
+        },
+        [&](const ir::Switch* s) {
+            Indent() << "Switch (" << s->condition << ")" << std::endl;
+
+            {
+                ScopedIndent switch_indent(&indent_size_);
+                ScopedStopNode scope(&stop_nodes_, s->merge_target);
+                for (const auto& c : s->cases) {
+                    Indent() << "Case" << std::endl;
+                    ScopedIndent case_indent(&indent_size_);
+                    Walk(c.start_target);
+                }
+            }
+
+            Indent() << "Switch Merge" << std::endl;
+            Walk(s->merge_target);
+        },
+        [&](const ir::If* i) {
+            Indent() << "if (" << i->condition << ")" << std::endl;
+            {
+                ScopedIndent if_indent(&indent_size_);
+                ScopedStopNode scope(&stop_nodes_, i->merge_target);
+
+                Indent() << "true branch" << std::endl;
+                Walk(i->true_target);
+
+                Indent() << "false branch" << std::endl;
+                Walk(i->false_target);
+            }
+
+            Indent() << "if merge" << std::endl;
+            Walk(i->merge_target);
+        },
+        [&](const ir::Loop* l) {
+            Indent() << "loop" << std::endl;
+            {
+                ScopedStopNode loop_scope(&stop_nodes_, l->merge_target);
+                ScopedIndent loop_indent(&indent_size_);
+                {
+                    ScopedStopNode inner_scope(&stop_nodes_, l->continuing_target);
+                    Indent() << "loop start" << std::endl;
+                    Walk(l->start_target);
+                }
+
+                Indent() << "loop continuing" << std::endl;
+                ScopedIndent continuing_indent(&indent_size_);
+                Walk(l->continuing_target);
+            }
+
+            Indent() << "loop merge" << std::endl;
+            Walk(l->merge_target);
+        },
+        [&](const ir::Terminator*) { Indent() << "Function end" << std::endl; });
+}
+
+std::string Disassembler::Disassemble(const Module& mod) {
+    for (const auto* func : mod.functions) {
+        Walk(func);
+    }
+    return out_.str();
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
new file mode 100644
index 0000000..9cb8993
--- /dev/null
+++ b/src/tint/ir/disassembler.h
@@ -0,0 +1,58 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_IR_DISASSEMBLER_H_
+#define SRC_TINT_IR_DISASSEMBLER_H_
+
+#include <sstream>
+#include <string>
+#include <unordered_set>
+
+#include "src/tint/ir/flow_node.h"
+#include "src/tint/ir/module.h"
+
+namespace tint::ir {
+
+/// Helper class to disassemble the IR
+class Disassembler {
+  public:
+    /// Constructor
+    Disassembler();
+    ~Disassembler();
+
+    /// Returns the module as a string
+    /// @param mod the module to emit
+    /// @returns the string representation of the module
+    std::string Disassemble(const Module& mod);
+
+    /// Writes the block ops to the stream
+    /// @param b the block containing the ops
+    void EmitBlockOps(const Block* b);
+
+    /// @returns the string representation
+    std::string AsString() const { return out_.str(); }
+
+  private:
+    std::ostream& Indent();
+    void Walk(const FlowNode* node);
+
+    std::stringstream out_;
+    std::unordered_set<const FlowNode*> visited_;
+    std::unordered_set<const FlowNode*> stop_nodes_;
+    uint32_t indent_size_ = 0;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_DISASSEMBLER_H_
diff --git a/src/tint/ir/op.cc b/src/tint/ir/op.cc
new file mode 100644
index 0000000..3e6d9936
--- /dev/null
+++ b/src/tint/ir/op.cc
@@ -0,0 +1,105 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/op.h"
+
+namespace tint::ir {
+
+Op::Op() {}
+
+Op::Op(Kind kind, Register result, Register lhs, Register rhs)
+    : kind_(kind), result_(result), args_({lhs, rhs}) {}
+
+Op::Op(const Op&) = default;
+
+Op::Op(Op&& o) = default;
+
+Op::~Op() = default;
+
+Op& Op::operator=(const Op& o) = default;
+
+Op& Op::operator=(Op&& o) = default;
+
+std::ostream& operator<<(std::ostream& out, const Op& op) {
+    out << op.Result() << " = ";
+    if (op.HasLHS()) {
+        out << op.LHS();
+    }
+    out << " ";
+
+    switch (op.GetKind()) {
+        case Op::Kind::kAdd:
+            out << "+";
+            break;
+        case Op::Kind::kSubtract:
+            out << "-";
+            break;
+        case Op::Kind::kMultiply:
+            out << "*";
+            break;
+        case Op::Kind::kDivide:
+            out << "/";
+            break;
+        case Op::Kind::kModulo:
+            out << "%";
+            break;
+        case Op::Kind::kAnd:
+            out << "&";
+            break;
+        case Op::Kind::kOr:
+            out << "|";
+            break;
+        case Op::Kind::kXor:
+            out << "^";
+            break;
+        case Op::Kind::kLogicalAnd:
+            out << "&&";
+            break;
+        case Op::Kind::kLogicalOr:
+            out << "||";
+            break;
+        case Op::Kind::kEqual:
+            out << "==";
+            break;
+        case Op::Kind::kNotEqual:
+            out << "!=";
+            break;
+        case Op::Kind::kLessThan:
+            out << "<";
+            break;
+        case Op::Kind::kGreaterThan:
+            out << ">";
+            break;
+        case Op::Kind::kLessThanEqual:
+            out << "<=";
+            break;
+        case Op::Kind::kGreaterThanEqual:
+            out << ">=";
+            break;
+        case Op::Kind::kShiftLeft:
+            out << "<<";
+            break;
+        case Op::Kind::kShiftRight:
+            out << ">>";
+            break;
+    }
+
+    if (op.HasRHS()) {
+        out << " " << op.RHS();
+    }
+
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/op.h b/src/tint/ir/op.h
new file mode 100644
index 0000000..a9fc78f
--- /dev/null
+++ b/src/tint/ir/op.h
@@ -0,0 +1,113 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_IR_OP_H_
+#define SRC_TINT_IR_OP_H_
+
+#include <ostream>
+
+#include "src/tint/ir/register.h"
+#include "src/tint/utils/vector.h"
+
+namespace tint::ir {
+
+/// An operation in the IR.
+class Op {
+  public:
+    /// The kind of operation.
+    enum class Kind {
+        kAdd,
+        kSubtract,
+        kMultiply,
+        kDivide,
+        kModulo,
+
+        kAnd,
+        kOr,
+        kXor,
+
+        kLogicalAnd,
+        kLogicalOr,
+
+        kEqual,
+        kNotEqual,
+        kLessThan,
+        kGreaterThan,
+        kLessThanEqual,
+        kGreaterThanEqual,
+
+        kShiftLeft,
+        kShiftRight
+    };
+
+    /// Constructor
+    Op();
+    /// Constructor
+    /// @param kind the kind of operation
+    /// @param result the result register
+    /// @param lhs the lhs of the operation
+    /// @param rhs the rhs of the operation
+    Op(Kind kind, Register result, Register lhs, Register rhs);
+    /// Copy constructor
+    /// @param o the op to copy from
+    Op(const Op& o);
+    /// Move constructor
+    /// @param o the op to move from
+    Op(Op&& o);
+    /// Destructor
+    ~Op();
+
+    /// Copy assign
+    /// @param o the op to copy from
+    /// @returns a reference to this
+    Op& operator=(const Op& o);
+    /// Move assign
+    /// @param o the op to move from
+    /// @returns a reference to this
+    Op& operator=(Op&& o);
+
+    /// @returns the kind of operation
+    Kind GetKind() const { return kind_; }
+
+    /// @returns the result register for the operation
+    const Register& Result() const { return result_; }
+
+    /// @returns true if the op has a LHS
+    bool HasLHS() const { return args_.Length() >= 1; }
+    /// @returns the left-hand-side register for the operation
+    const Register& LHS() const {
+        TINT_ASSERT(IR, HasLHS());
+        return args_[0];
+    }
+
+    /// @returns true if the op has a RHS
+    bool HasRHS() const { return args_.Length() >= 2; }
+    /// @returns the right-hand-side register for the operation
+    const Register& RHS() const {
+        TINT_ASSERT(IR, HasRHS());
+        return args_[1];
+    }
+
+  private:
+    Kind kind_;
+
+    Register result_;
+    utils::Vector<Register, 2> args_;
+};
+
+std::ostream& operator<<(std::ostream& out, const Op&);
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_OP_H_
diff --git a/src/tint/ir/op_test.cc b/src/tint/ir/op_test.cc
new file mode 100644
index 0000000..01604d1
--- /dev/null
+++ b/src/tint/ir/op_test.cc
@@ -0,0 +1,494 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <sstream>
+
+#include "src/tint/ir/op.h"
+#include "src/tint/ir/test_helper.h"
+
+namespace tint::ir {
+namespace {
+
+using IR_OpTest = TestHelper;
+
+TEST_F(IR_OpTest, CreateAnd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.And(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kAnd);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 & 2");
+}
+
+TEST_F(IR_OpTest, CreateOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Or(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kOr);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 | 2");
+}
+
+TEST_F(IR_OpTest, CreateXor) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Xor(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kXor);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 ^ 2");
+}
+
+TEST_F(IR_OpTest, CreateLogicalAnd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.LogicalAnd(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kLogicalAnd);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 && 2");
+}
+
+TEST_F(IR_OpTest, CreateLogicalOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.LogicalOr(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kLogicalOr);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 || 2");
+}
+
+TEST_F(IR_OpTest, CreateEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Equal(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kEqual);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 == 2");
+}
+
+TEST_F(IR_OpTest, CreateNotEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.NotEqual(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kNotEqual);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 != 2");
+}
+
+TEST_F(IR_OpTest, CreateLessThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.LessThan(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kLessThan);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 < 2");
+}
+
+TEST_F(IR_OpTest, CreateGreaterThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.GreaterThan(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kGreaterThan);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 > 2");
+}
+
+TEST_F(IR_OpTest, CreateLessThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.LessThanEqual(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kLessThanEqual);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 <= 2");
+}
+
+TEST_F(IR_OpTest, CreateGreaterThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.GreaterThanEqual(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kGreaterThanEqual);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 >= 2");
+}
+
+TEST_F(IR_OpTest, CreateShiftLeft) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.ShiftLeft(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kShiftLeft);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 << 2");
+}
+
+TEST_F(IR_OpTest, CreateShiftRight) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.ShiftRight(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kShiftRight);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 >> 2");
+}
+
+TEST_F(IR_OpTest, CreateAdd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Add(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kAdd);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 + 2");
+}
+
+TEST_F(IR_OpTest, CreateSubtract) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Subtract(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kSubtract);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 - 2");
+}
+
+TEST_F(IR_OpTest, CreateMultiply) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Multiply(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kMultiply);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 * 2");
+}
+
+TEST_F(IR_OpTest, CreateDivide) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Divide(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kDivide);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 / 2");
+}
+
+TEST_F(IR_OpTest, CreateModulo) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_register_id = Register::Id(42);
+    auto o = b.builder.Modulo(Register(i32(4)), Register(i32(2)));
+
+    EXPECT_EQ(o.GetKind(), Op::Kind::kModulo);
+
+    ASSERT_TRUE(o.Result().IsTemp());
+    EXPECT_EQ(Register::Id(42), o.Result().AsId());
+
+    ASSERT_TRUE(o.HasLHS());
+    auto& lhs = o.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(o.HasRHS());
+    auto& rhs = o.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << o;
+    EXPECT_EQ(str.str(), "%42 = 4 % 2");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/register.cc b/src/tint/ir/register.cc
index ba156e2..f3caa76 100644
--- a/src/tint/ir/register.cc
+++ b/src/tint/ir/register.cc
@@ -42,27 +42,35 @@
 
 Register& Register::operator=(Register&& o) = default;
 
-std::string Register::AsString() const {
-    switch (kind_) {
-        case Kind::kTemp:
-            return "%" + std::to_string(AsId());
-        case Kind::kF32:
-            return std::to_string(AsF32().value);
-        case Kind::kF16:
-            return std::to_string(AsF16().value);
-        case Kind::kI32:
-            return std::to_string(AsI32().value);
-        case Kind::kU32:
-            return std::to_string(AsU32().value);
-        // TODO(dsinclair): Emit the symbol instead of v
-        case Kind::kVar:
-            return "%v" + std::to_string(AsVarData().id);
-        case Kind::kBool:
-            return AsBool() ? "true" : "false";
-        case Kind::kUninitialized:
+std::ostream& operator<<(std::ostream& out, const Register& r) {
+    switch (r.GetKind()) {
+        case Register::Kind::kTemp:
+            out << "%" << std::to_string(r.AsId());
+            break;
+        case Register::Kind::kF32:
+            out << std::to_string(r.AsF32().value);
+            break;
+        case Register::Kind::kF16:
+            out << std::to_string(r.AsF16().value);
+            break;
+        case Register::Kind::kI32:
+            out << std::to_string(r.AsI32().value);
+            break;
+        case Register::Kind::kU32:
+            out << std::to_string(r.AsU32().value);
+            break;
+            // TODO(dsinclair): Emit the symbol instead of v
+        case Register::Kind::kVar:
+            out << "%v" << std::to_string(r.AsVarData().id);
+            break;
+        case Register::Kind::kBool:
+            out << (r.AsBool() ? "true" : "false");
+            break;
+        case Register::Kind::kUninitialized:
+            out << "unknown register";
             break;
     }
-    return "unknown register";
+    return out;
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/register.h b/src/tint/ir/register.h
index 54d863d..d1c41eb 100644
--- a/src/tint/ir/register.h
+++ b/src/tint/ir/register.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_IR_REGISTER_H_
 #define SRC_TINT_IR_REGISTER_H_
 
-#include <string>
+#include <ostream>
 #include <variant>
 
 #include "src/tint/number.h"
@@ -31,6 +31,26 @@
     /// A register id.
     using Id = uint32_t;
 
+    /// The type of the register
+    enum class Kind {
+        /// A uninitialized register
+        kUninitialized,
+        /// A temporary allocated register
+        kTemp,
+        /// A f32 register
+        kF32,
+        /// A f16 register
+        kF16,
+        /// An i32 register
+        kI32,
+        /// A u32 register
+        kU32,
+        /// A variable register
+        kVar,
+        /// A boolean register
+        kBool,
+    };
+
     /// Stores data for a given variable. There will be multiple `VarData` entries for a given `id`.
     /// The `id` acts like a generation number (although they aren't sequential, they are
     /// increasing). As the variable is stored too a new register will be created and the the `id`
@@ -110,6 +130,9 @@
     /// @returns true if this is a bool register
     bool IsBool() const { return kind_ == Kind::kBool; }
 
+    /// @returns the kind of register
+    Kind GetKind() const { return kind_; }
+
     /// @returns the register data as a `f32`.
     /// @note, must only be called if `IsF32()` is true
     f32 AsF32() const { return std::get<f32>(data_); }
@@ -132,36 +155,15 @@
     /// @note, must only be called if `IsBool()` is true
     bool AsBool() const { return std::get<bool>(data_); }
 
-    /// @returns the string representation of the register
-    std::string AsString() const;
-
   private:
-    /// The type of the register
-    enum class Kind {
-        /// A uninitialized register
-        kUninitialized,
-        /// A temporary allocated register
-        kTemp,
-        /// A f32 register
-        kF32,
-        /// A f16 register
-        kF16,
-        /// An i32 register
-        kI32,
-        /// A u32 register
-        kU32,
-        /// A variable register
-        kVar,
-        /// A boolean register
-        kBool,
-    };
-
     /// The type of data stored in this register
     Kind kind_;
     /// The data stored in the register
     std::variant<Id, f32, f16, u32, i32, VarData, bool> data_;
 };
 
+std::ostream& operator<<(std::ostream& out, const Register& r);
+
 }  // namespace tint::ir
 
 #endif  // SRC_TINT_IR_REGISTER_H_
diff --git a/src/tint/ir/register_test.cc b/src/tint/ir/register_test.cc
index af4b659..fb0194f 100644
--- a/src/tint/ir/register_test.cc
+++ b/src/tint/ir/register_test.cc
@@ -12,9 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ir/test_helper.h"
+#include <sstream>
 
 #include "src/tint/ir/register.h"
+#include "src/tint/ir/test_helper.h"
 
 namespace tint::ir {
 namespace {
@@ -24,9 +25,13 @@
 using IR_RegisterTest = TestHelper;
 
 TEST_F(IR_RegisterTest, f32) {
+    std::stringstream str;
+
     Register r(1.2_f);
     EXPECT_EQ(1.2_f, r.AsF32());
-    EXPECT_EQ("1.200000", r.AsString());
+
+    str << r;
+    EXPECT_EQ("1.200000", str.str());
 
     EXPECT_TRUE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
@@ -38,9 +43,13 @@
 }
 
 TEST_F(IR_RegisterTest, f16) {
+    std::stringstream str;
+
     Register r(1.1_h);
     EXPECT_EQ(1.1_h, r.AsF16());
-    EXPECT_EQ("1.099609", r.AsString());
+
+    str << r;
+    EXPECT_EQ("1.099609", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_TRUE(r.IsF16());
@@ -52,9 +61,13 @@
 }
 
 TEST_F(IR_RegisterTest, i32) {
+    std::stringstream str;
+
     Register r(1_i);
     EXPECT_EQ(1_i, r.AsI32());
-    EXPECT_EQ("1", r.AsString());
+
+    str << r;
+    EXPECT_EQ("1", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
@@ -66,9 +79,13 @@
 }
 
 TEST_F(IR_RegisterTest, u32) {
+    std::stringstream str;
+
     Register r(2_u);
     EXPECT_EQ(2_u, r.AsU32());
-    EXPECT_EQ("2", r.AsString());
+
+    str << r;
+    EXPECT_EQ("2", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
@@ -80,9 +97,13 @@
 }
 
 TEST_F(IR_RegisterTest, id) {
+    std::stringstream str;
+
     Register r(Register::Id(4));
     EXPECT_EQ(4u, r.AsId());
-    EXPECT_EQ("%4", r.AsString());
+
+    str << r;
+    EXPECT_EQ("%4", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
@@ -94,13 +115,20 @@
 }
 
 TEST_F(IR_RegisterTest, bool) {
+    std::stringstream str;
+
     Register r(false);
     EXPECT_FALSE(r.AsBool());
-    EXPECT_EQ("false", r.AsString());
 
+    str << r;
+    EXPECT_EQ("false", str.str());
+
+    str.str("");
     r = Register(true);
     EXPECT_TRUE(r.AsBool());
-    EXPECT_EQ("true", r.AsString());
+
+    str << r;
+    EXPECT_EQ("true", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
@@ -112,16 +140,23 @@
 }
 
 TEST_F(IR_RegisterTest, var) {
+    std::stringstream str;
+
     Symbol s;
     Register r(s, 2);
     EXPECT_EQ(2u, r.AsVarData().id);
     EXPECT_EQ(s, r.AsVarData().sym);
-    EXPECT_EQ("%v2", r.AsString());
+
+    str << r;
+    EXPECT_EQ("%v2", str.str());
+    str.str("");
 
     r = Register(s, 4);
     EXPECT_EQ(4u, r.AsVarData().id);
     EXPECT_EQ(s, r.AsVarData().sym);
-    EXPECT_EQ("%v4", r.AsString());
+
+    str << r;
+    EXPECT_EQ("%v4", str.str());
 
     EXPECT_FALSE(r.IsF32());
     EXPECT_FALSE(r.IsF16());
diff --git a/src/tint/ir/test_helper.h b/src/tint/ir/test_helper.h
index f2bdeb5..10694ee 100644
--- a/src/tint/ir/test_helper.h
+++ b/src/tint/ir/test_helper.h
@@ -20,6 +20,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/ir/builder_impl.h"
+#include "src/tint/ir/disassembler.h"
 #include "src/tint/program_builder.h"
 
 namespace tint::ir {