[ir] Split Binary from Instruction

This CL pulls a Binary instruction out of the Instruction class and
changes Instruction to just be the base class.

Bug: tint:1718
Change-Id: Iab234bd8c3eeebedb56dffff7ec7244cda51d4d5
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/112320
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 15532b2..adcb118 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -650,6 +650,8 @@
 
 if(${TINT_BUILD_IR})
   list(APPEND TINT_LIB_SRCS
+    ir/binary.cc
+    ir/binary.h
     ir/block.cc
     ir/block.h
     ir/builder.cc
@@ -1344,9 +1346,9 @@
 
   if (${TINT_BUILD_IR})
     list(APPEND TINT_TEST_SRCS
+      ir/binary_test.cc
       ir/builder_impl_test.cc
       ir/constant_test.cc
-      ir/instruction_test.cc
       ir/temp_test.cc
       ir/test_helper.h
     )
diff --git a/src/tint/ir/binary.cc b/src/tint/ir/binary.cc
new file mode 100644
index 0000000..1a35b61
--- /dev/null
+++ b/src/tint/ir/binary.cc
@@ -0,0 +1,97 @@
+// 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/binary.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Binary);
+
+namespace tint::ir {
+
+Binary::Binary(Kind kind, const Value* result, const Value* lhs, const Value* rhs)
+    : kind_(kind), result_(result), lhs_(lhs), rhs_(rhs) {
+    TINT_ASSERT(IR, result_);
+    TINT_ASSERT(IR, lhs_);
+    TINT_ASSERT(IR, rhs_);
+}
+
+Binary::~Binary() = default;
+
+std::ostream& Binary::ToString(std::ostream& out) const {
+    Result()->ToString(out) << " = ";
+    lhs_->ToString(out) << " ";
+
+    switch (GetKind()) {
+        case Binary::Kind::kAdd:
+            out << "+";
+            break;
+        case Binary::Kind::kSubtract:
+            out << "-";
+            break;
+        case Binary::Kind::kMultiply:
+            out << "*";
+            break;
+        case Binary::Kind::kDivide:
+            out << "/";
+            break;
+        case Binary::Kind::kModulo:
+            out << "%";
+            break;
+        case Binary::Kind::kAnd:
+            out << "&";
+            break;
+        case Binary::Kind::kOr:
+            out << "|";
+            break;
+        case Binary::Kind::kXor:
+            out << "^";
+            break;
+        case Binary::Kind::kLogicalAnd:
+            out << "&&";
+            break;
+        case Binary::Kind::kLogicalOr:
+            out << "||";
+            break;
+        case Binary::Kind::kEqual:
+            out << "==";
+            break;
+        case Binary::Kind::kNotEqual:
+            out << "!=";
+            break;
+        case Binary::Kind::kLessThan:
+            out << "<";
+            break;
+        case Binary::Kind::kGreaterThan:
+            out << ">";
+            break;
+        case Binary::Kind::kLessThanEqual:
+            out << "<=";
+            break;
+        case Binary::Kind::kGreaterThanEqual:
+            out << ">=";
+            break;
+        case Binary::Kind::kShiftLeft:
+            out << "<<";
+            break;
+        case Binary::Kind::kShiftRight:
+            out << ">>";
+            break;
+    }
+    out << " ";
+    rhs_->ToString(out);
+
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
new file mode 100644
index 0000000..755dd9c
--- /dev/null
+++ b/src/tint/ir/binary.h
@@ -0,0 +1,96 @@
+// 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_BINARY_H_
+#define SRC_TINT_IR_BINARY_H_
+
+#include <ostream>
+
+#include "src/tint/castable.h"
+#include "src/tint/ir/instruction.h"
+#include "src/tint/ir/value.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class Binary : public Castable<Binary, Instruction> {
+  public:
+    /// The kind of instruction.
+    enum class Kind {
+        kAdd,
+        kSubtract,
+        kMultiply,
+        kDivide,
+        kModulo,
+
+        kAnd,
+        kOr,
+        kXor,
+
+        kLogicalAnd,
+        kLogicalOr,
+
+        kEqual,
+        kNotEqual,
+        kLessThan,
+        kGreaterThan,
+        kLessThanEqual,
+        kGreaterThanEqual,
+
+        kShiftLeft,
+        kShiftRight
+    };
+
+    /// Constructor
+    /// @param kind the kind of binary instruction
+    /// @param result the result value
+    /// @param lhs the lhs of the instruction
+    /// @param rhs the rhs of the instruction
+    Binary(Kind kind, const Value* result, const Value* lhs, const Value* rhs);
+    Binary(const Binary& instr) = delete;
+    Binary(Binary&& instr) = delete;
+    ~Binary() override;
+
+    Binary& operator=(const Binary& instr) = delete;
+    Binary& operator=(Binary&& instr) = delete;
+
+    /// @returns the kind of instruction
+    Kind GetKind() const { return kind_; }
+
+    /// @returns the result value for the instruction
+    const Value* Result() const { return result_; }
+
+    /// @returns the left-hand-side value for the instruction
+    const Value* LHS() const { return lhs_; }
+
+    /// @returns the right-hand-side value for the instruction
+    const Value* RHS() const { return rhs_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    std::ostream& ToString(std::ostream& out) const override;
+
+  private:
+    Kind kind_;
+    const Value* result_ = nullptr;
+    const Value* lhs_ = nullptr;
+    const Value* rhs_ = nullptr;
+};
+
+std::ostream& operator<<(std::ostream& out, const Binary&);
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_BINARY_H_
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
new file mode 100644
index 0000000..81d0ada
--- /dev/null
+++ b/src/tint/ir/binary_test.cc
@@ -0,0 +1,499 @@
+// 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/instruction.h"
+#include "src/tint/ir/test_helper.h"
+
+namespace tint::ir {
+namespace {
+
+using IR_InstructionTest = TestHelper;
+
+TEST_F(IR_InstructionTest, CreateAnd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.And(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 & 2");
+}
+
+TEST_F(IR_InstructionTest, CreateOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Or(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kOr);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 | 2");
+}
+
+TEST_F(IR_InstructionTest, CreateXor) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Xor(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kXor);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 ^ 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLogicalAnd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr =
+        b.builder.LogicalAnd(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalAnd);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 && 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLogicalOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.LogicalOr(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalOr);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 || 2");
+}
+
+TEST_F(IR_InstructionTest, CreateEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Equal(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kEqual);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 == 2");
+}
+
+TEST_F(IR_InstructionTest, CreateNotEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.NotEqual(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kNotEqual);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 != 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLessThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.LessThan(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThan);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 < 2");
+}
+
+TEST_F(IR_InstructionTest, CreateGreaterThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr =
+        b.builder.GreaterThan(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThan);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 > 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLessThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr =
+        b.builder.LessThanEqual(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThanEqual);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 <= 2");
+}
+
+TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr =
+        b.builder.GreaterThanEqual(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThanEqual);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 >= 2");
+}
+
+TEST_F(IR_InstructionTest, CreateShiftLeft) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.ShiftLeft(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftLeft);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 << 2");
+}
+
+TEST_F(IR_InstructionTest, CreateShiftRight) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr =
+        b.builder.ShiftRight(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftRight);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 >> 2");
+}
+
+TEST_F(IR_InstructionTest, CreateAdd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Add(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAdd);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 + 2");
+}
+
+TEST_F(IR_InstructionTest, CreateSubtract) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Subtract(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kSubtract);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 - 2");
+}
+
+TEST_F(IR_InstructionTest, CreateMultiply) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Multiply(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kMultiply);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 * 2");
+}
+
+TEST_F(IR_InstructionTest, CreateDivide) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Divide(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kDivide);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 / 2");
+}
+
+TEST_F(IR_InstructionTest, CreateModulo) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_temp_id = Temp::Id(42);
+    const auto* instr = b.builder.Modulo(b.builder.Constant(i32(4)), b.builder.Constant(i32(2)));
+
+    EXPECT_EQ(instr->GetKind(), Binary::Kind::kModulo);
+
+    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+
+    ASSERT_TRUE(instr->LHS()->Is<Constant>());
+    auto lhs = instr->LHS()->As<Constant>();
+    ASSERT_TRUE(lhs->IsI32());
+    EXPECT_EQ(i32(4), lhs->AsI32());
+
+    ASSERT_TRUE(instr->RHS()->Is<Constant>());
+    auto rhs = instr->RHS()->As<Constant>();
+    ASSERT_TRUE(rhs->IsI32());
+    EXPECT_EQ(i32(2), rhs->AsI32());
+
+    std::stringstream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 = 4 % 2");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 5c55b91..0f0200d 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -97,82 +97,80 @@
     return next_temp_id++;
 }
 
-const Instruction* Builder::CreateInstruction(Instruction::Kind kind,
-                                              const Value* lhs,
-                                              const Value* rhs) {
-    return ir.instructions.Create<ir::Instruction>(kind, Temp(), lhs, rhs);
+const Binary* Builder::CreateBinary(Binary::Kind kind, const Value* lhs, const Value* rhs) {
+    return ir.instructions.Create<ir::Binary>(kind, Temp(), lhs, rhs);
 }
 
-const Instruction* Builder::And(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kAnd, lhs, rhs);
+const Binary* Builder::And(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kAnd, lhs, rhs);
 }
 
-const Instruction* Builder::Or(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kOr, lhs, rhs);
+const Binary* Builder::Or(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kOr, lhs, rhs);
 }
 
-const Instruction* Builder::Xor(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kXor, lhs, rhs);
+const Binary* Builder::Xor(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kXor, lhs, rhs);
 }
 
-const Instruction* Builder::LogicalAnd(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kLogicalAnd, lhs, rhs);
+const Binary* Builder::LogicalAnd(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kLogicalAnd, lhs, rhs);
 }
 
-const Instruction* Builder::LogicalOr(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kLogicalOr, lhs, rhs);
+const Binary* Builder::LogicalOr(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kLogicalOr, lhs, rhs);
 }
 
-const Instruction* Builder::Equal(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kEqual, lhs, rhs);
+const Binary* Builder::Equal(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kEqual, lhs, rhs);
 }
 
-const Instruction* Builder::NotEqual(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kNotEqual, lhs, rhs);
+const Binary* Builder::NotEqual(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kNotEqual, lhs, rhs);
 }
 
-const Instruction* Builder::LessThan(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kLessThan, lhs, rhs);
+const Binary* Builder::LessThan(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kLessThan, lhs, rhs);
 }
 
-const Instruction* Builder::GreaterThan(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kGreaterThan, lhs, rhs);
+const Binary* Builder::GreaterThan(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kGreaterThan, lhs, rhs);
 }
 
-const Instruction* Builder::LessThanEqual(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kLessThanEqual, lhs, rhs);
+const Binary* Builder::LessThanEqual(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kLessThanEqual, lhs, rhs);
 }
 
-const Instruction* Builder::GreaterThanEqual(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kGreaterThanEqual, lhs, rhs);
+const Binary* Builder::GreaterThanEqual(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kGreaterThanEqual, lhs, rhs);
 }
 
-const Instruction* Builder::ShiftLeft(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kShiftLeft, lhs, rhs);
+const Binary* Builder::ShiftLeft(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kShiftLeft, lhs, rhs);
 }
 
-const Instruction* Builder::ShiftRight(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kShiftRight, lhs, rhs);
+const Binary* Builder::ShiftRight(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kShiftRight, lhs, rhs);
 }
 
-const Instruction* Builder::Add(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kAdd, lhs, rhs);
+const Binary* Builder::Add(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kAdd, lhs, rhs);
 }
 
-const Instruction* Builder::Subtract(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kSubtract, lhs, rhs);
+const Binary* Builder::Subtract(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kSubtract, lhs, rhs);
 }
 
-const Instruction* Builder::Multiply(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kMultiply, lhs, rhs);
+const Binary* Builder::Multiply(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kMultiply, lhs, rhs);
 }
 
-const Instruction* Builder::Divide(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kDivide, lhs, rhs);
+const Binary* Builder::Divide(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kDivide, lhs, rhs);
 }
 
-const Instruction* Builder::Modulo(const Value* lhs, const Value* rhs) {
-    return CreateInstruction(Instruction::Kind::kModulo, lhs, rhs);
+const Binary* Builder::Modulo(const Value* lhs, const Value* rhs) {
+    return CreateBinary(Binary::Kind::kModulo, lhs, rhs);
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index d5065b8..3f2e011 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -15,10 +15,10 @@
 #ifndef SRC_TINT_IR_BUILDER_H_
 #define SRC_TINT_IR_BUILDER_H_
 
+#include "src/tint/ir/binary.h"
 #include "src/tint/ir/constant.h"
 #include "src/tint/ir/function.h"
 #include "src/tint/ir/if.h"
-#include "src/tint/ir/instruction.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
 #include "src/tint/ir/switch.h"
@@ -102,117 +102,115 @@
     /// @param lhs the left-hand-side of the operation
     /// @param rhs the right-hand-side of the operation
     /// @returns the operation
-    const Instruction* CreateInstruction(Instruction::Kind kind,
-                                         const Value* lhs,
-                                         const Value* rhs);
+    const Binary* CreateBinary(Binary::Kind kind, const Value* lhs, const Value* rhs);
 
     /// Creates an And operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* And(const Value* lhs, const Value* rhs);
+    const Binary* And(const Value* lhs, const Value* rhs);
 
     /// Creates an Or operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Or(const Value* lhs, const Value* rhs);
+    const Binary* Or(const Value* lhs, const Value* rhs);
 
     /// Creates an Xor operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Xor(const Value* lhs, const Value* rhs);
+    const Binary* Xor(const Value* lhs, const Value* rhs);
 
     /// Creates an LogicalAnd operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* LogicalAnd(const Value* lhs, const Value* rhs);
+    const Binary* LogicalAnd(const Value* lhs, const Value* rhs);
 
     /// Creates an LogicalOr operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* LogicalOr(const Value* lhs, const Value* rhs);
+    const Binary* LogicalOr(const Value* lhs, const Value* rhs);
 
     /// Creates an Equal operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Equal(const Value* lhs, const Value* rhs);
+    const Binary* Equal(const Value* lhs, const Value* rhs);
 
     /// Creates an NotEqual operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* NotEqual(const Value* lhs, const Value* rhs);
+    const Binary* NotEqual(const Value* lhs, const Value* rhs);
 
     /// Creates an LessThan operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* LessThan(const Value* lhs, const Value* rhs);
+    const Binary* LessThan(const Value* lhs, const Value* rhs);
 
     /// Creates an GreaterThan operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* GreaterThan(const Value* lhs, const Value* rhs);
+    const Binary* GreaterThan(const Value* lhs, const Value* rhs);
 
     /// Creates an LessThanEqual operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* LessThanEqual(const Value* lhs, const Value* rhs);
+    const Binary* LessThanEqual(const Value* lhs, const Value* rhs);
 
     /// Creates an GreaterThanEqual operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* GreaterThanEqual(const Value* lhs, const Value* rhs);
+    const Binary* GreaterThanEqual(const Value* lhs, const Value* rhs);
 
     /// Creates an ShiftLeft operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* ShiftLeft(const Value* lhs, const Value* rhs);
+    const Binary* ShiftLeft(const Value* lhs, const Value* rhs);
 
     /// Creates an ShiftRight operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* ShiftRight(const Value* lhs, const Value* rhs);
+    const Binary* ShiftRight(const Value* lhs, const Value* rhs);
 
     /// Creates an Add operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Add(const Value* lhs, const Value* rhs);
+    const Binary* Add(const Value* lhs, const Value* rhs);
 
     /// Creates an Subtract operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Subtract(const Value* lhs, const Value* rhs);
+    const Binary* Subtract(const Value* lhs, const Value* rhs);
 
     /// Creates an Multiply operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Multiply(const Value* lhs, const Value* rhs);
+    const Binary* Multiply(const Value* lhs, const Value* rhs);
 
     /// Creates an Divide operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Divide(const Value* lhs, const Value* rhs);
+    const Binary* Divide(const Value* lhs, const Value* rhs);
 
     /// Creates an Modulo operation
     /// @param lhs the lhs of the add
     /// @param rhs the rhs of the add
     /// @returns the operation
-    const Instruction* Modulo(const Value* lhs, const Value* rhs);
+    const Binary* Modulo(const Value* lhs, const Value* rhs);
 
     /// @returns a unique temp id
     Temp::Id AllocateTempId();
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 2286bf4..042a7d5 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -562,7 +562,7 @@
         return utils::Failure;
     }
 
-    const Instruction* instr = nullptr;
+    const Binary* instr = nullptr;
     switch (expr->op) {
         case ast::BinaryOp::kAnd:
             instr = builder.And(lhs.Get(), rhs.Get());
diff --git a/src/tint/ir/instruction.cc b/src/tint/ir/instruction.cc
index d8ce2dd..e54b13f 100644
--- a/src/tint/ir/instruction.cc
+++ b/src/tint/ir/instruction.cc
@@ -18,91 +18,8 @@
 
 namespace tint::ir {
 
-Instruction::Instruction() {}
-
-Instruction::Instruction(Kind kind, const Value* result, const Value* lhs, const Value* rhs)
-    : kind_(kind), result_(result), args_({lhs, rhs}) {}
-
-Instruction::Instruction(const Instruction&) = default;
-
-Instruction::Instruction(Instruction&& instr) = default;
+Instruction::Instruction() = default;
 
 Instruction::~Instruction() = default;
 
-Instruction& Instruction::operator=(const Instruction& instr) = default;
-
-Instruction& Instruction::operator=(Instruction&& instr) = default;
-
-std::ostream& Instruction::ToString(std::ostream& out) const {
-    Result()->ToString(out) << " = ";
-    if (HasLHS()) {
-        LHS()->ToString(out);
-    }
-    out << " ";
-
-    switch (GetKind()) {
-        case Instruction::Kind::kAdd:
-            out << "+";
-            break;
-        case Instruction::Kind::kSubtract:
-            out << "-";
-            break;
-        case Instruction::Kind::kMultiply:
-            out << "*";
-            break;
-        case Instruction::Kind::kDivide:
-            out << "/";
-            break;
-        case Instruction::Kind::kModulo:
-            out << "%";
-            break;
-        case Instruction::Kind::kAnd:
-            out << "&";
-            break;
-        case Instruction::Kind::kOr:
-            out << "|";
-            break;
-        case Instruction::Kind::kXor:
-            out << "^";
-            break;
-        case Instruction::Kind::kLogicalAnd:
-            out << "&&";
-            break;
-        case Instruction::Kind::kLogicalOr:
-            out << "||";
-            break;
-        case Instruction::Kind::kEqual:
-            out << "==";
-            break;
-        case Instruction::Kind::kNotEqual:
-            out << "!=";
-            break;
-        case Instruction::Kind::kLessThan:
-            out << "<";
-            break;
-        case Instruction::Kind::kGreaterThan:
-            out << ">";
-            break;
-        case Instruction::Kind::kLessThanEqual:
-            out << "<=";
-            break;
-        case Instruction::Kind::kGreaterThanEqual:
-            out << ">=";
-            break;
-        case Instruction::Kind::kShiftLeft:
-            out << "<<";
-            break;
-        case Instruction::Kind::kShiftRight:
-            out << ">>";
-            break;
-    }
-
-    if (HasRHS()) {
-        out << " ";
-        RHS()->ToString(out);
-    }
-
-    return out;
-}
-
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 1ae8446..8b68465 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -18,99 +18,28 @@
 #include <ostream>
 
 #include "src/tint/castable.h"
-#include "src/tint/debug.h"
-#include "src/tint/ir/value.h"
-#include "src/tint/utils/vector.h"
 
 namespace tint::ir {
 
 /// An instruction in the IR.
 class Instruction : public Castable<Instruction> {
   public:
-    /// The kind of instruction.
-    enum class Kind {
-        kAdd,
-        kSubtract,
-        kMultiply,
-        kDivide,
-        kModulo,
-
-        kAnd,
-        kOr,
-        kXor,
-
-        kLogicalAnd,
-        kLogicalOr,
-
-        kEqual,
-        kNotEqual,
-        kLessThan,
-        kGreaterThan,
-        kLessThanEqual,
-        kGreaterThanEqual,
-
-        kShiftLeft,
-        kShiftRight
-    };
-
-    /// Constructor
-    Instruction();
-    /// Constructor
-    /// @param kind the kind of instruction
-    /// @param result the result value
-    /// @param lhs the lhs of the instruction
-    /// @param rhs the rhs of the instruction
-    Instruction(Kind kind, const Value* result, const Value* lhs, const Value* rhs);
-    /// Copy constructor
-    /// @param instr the instruction to copy from
-    Instruction(const Instruction& instr);
-    /// Move constructor
-    /// @param instr the instruction to move from
-    Instruction(Instruction&& instr);
+    Instruction(const Instruction& instr) = delete;
+    Instruction(Instruction&& instr) = delete;
     /// Destructor
     ~Instruction() override;
 
-    /// Copy assign
-    /// @param instr the instruction to copy from
-    /// @returns a reference to this
-    Instruction& operator=(const Instruction& instr);
-    /// Move assign
-    /// @param instr the instruction to move from
-    /// @returns a reference to this
-    Instruction& operator=(Instruction&& instr);
+    Instruction& operator=(const Instruction& instr) = delete;
+    Instruction& operator=(Instruction&& instr) = delete;
 
-    /// @returns the kind of instruction
-    Kind GetKind() const { return kind_; }
-
-    /// @returns the result value for the instruction
-    const Value* Result() const { return result_; }
-
-    /// @returns true if the instruction has a LHS
-    bool HasLHS() const { return args_.Length() >= 1; }
-    /// @returns the left-hand-side value for the instruction
-    const Value* LHS() const {
-        TINT_ASSERT(IR, HasLHS());
-        return args_[0];
-    }
-
-    /// @returns true if the instruction has a RHS
-    bool HasRHS() const { return args_.Length() >= 2; }
-    /// @returns the right-hand-side value for the instruction
-    const Value* RHS() const {
-        TINT_ASSERT(IR, HasRHS());
-        return args_[1];
-    }
-
-    /// Write the instructino to the given stream
+    /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    std::ostream& ToString(std::ostream& out) const;
+    virtual std::ostream& ToString(std::ostream& out) const = 0;
 
-  private:
-    Kind kind_;
-
-    const Value* result_;
-    utils::Vector<const Value*, 2> args_;
+  protected:
+    /// Constructor
+    Instruction();
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction_test.cc b/src/tint/ir/instruction_test.cc
index 20bc1f0..08302ed 100644
--- a/src/tint/ir/instruction_test.cc
+++ b/src/tint/ir/instruction_test.cc
@@ -20,9 +20,9 @@
 namespace tint::ir {
 namespace {
 
-using IR_InstructionTest = TestHelper;
+using IR_BinaryTest = TestHelper;
 
-TEST_F(IR_InstructionTest, CreateAnd) {
+TEST_F(IR_BinaryTest, CreateAnd) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -33,13 +33,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -50,7 +48,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 & 2");
 }
 
-TEST_F(IR_InstructionTest, CreateOr) {
+TEST_F(IR_BinaryTest, CreateOr) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -61,13 +59,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -78,7 +74,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 | 2");
 }
 
-TEST_F(IR_InstructionTest, CreateXor) {
+TEST_F(IR_BinaryTest, CreateXor) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -89,13 +85,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -106,7 +100,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 ^ 2");
 }
 
-TEST_F(IR_InstructionTest, CreateLogicalAnd) {
+TEST_F(IR_BinaryTest, CreateLogicalAnd) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -118,13 +112,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -135,7 +127,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 && 2");
 }
 
-TEST_F(IR_InstructionTest, CreateLogicalOr) {
+TEST_F(IR_BinaryTest, CreateLogicalOr) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -146,13 +138,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -163,7 +153,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 || 2");
 }
 
-TEST_F(IR_InstructionTest, CreateEqual) {
+TEST_F(IR_BinaryTest, CreateEqual) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -174,13 +164,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -191,7 +179,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 == 2");
 }
 
-TEST_F(IR_InstructionTest, CreateNotEqual) {
+TEST_F(IR_BinaryTest, CreateNotEqual) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -202,13 +190,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -219,7 +205,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 != 2");
 }
 
-TEST_F(IR_InstructionTest, CreateLessThan) {
+TEST_F(IR_BinaryTest, CreateLessThan) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -230,13 +216,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -247,7 +231,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 < 2");
 }
 
-TEST_F(IR_InstructionTest, CreateGreaterThan) {
+TEST_F(IR_BinaryTest, CreateGreaterThan) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -259,13 +243,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -276,7 +258,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 > 2");
 }
 
-TEST_F(IR_InstructionTest, CreateLessThanEqual) {
+TEST_F(IR_BinaryTest, CreateLessThanEqual) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -288,13 +270,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -305,7 +285,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 <= 2");
 }
 
-TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
+TEST_F(IR_BinaryTest, CreateGreaterThanEqual) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -317,13 +297,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -334,7 +312,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 >= 2");
 }
 
-TEST_F(IR_InstructionTest, CreateShiftLeft) {
+TEST_F(IR_BinaryTest, CreateShiftLeft) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -345,13 +323,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -362,7 +338,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 << 2");
 }
 
-TEST_F(IR_InstructionTest, CreateShiftRight) {
+TEST_F(IR_BinaryTest, CreateShiftRight) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -374,13 +350,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -391,7 +365,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 >> 2");
 }
 
-TEST_F(IR_InstructionTest, CreateAdd) {
+TEST_F(IR_BinaryTest, CreateAdd) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -402,13 +376,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -419,7 +391,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 + 2");
 }
 
-TEST_F(IR_InstructionTest, CreateSubtract) {
+TEST_F(IR_BinaryTest, CreateSubtract) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -430,13 +402,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -447,7 +417,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 - 2");
 }
 
-TEST_F(IR_InstructionTest, CreateMultiply) {
+TEST_F(IR_BinaryTest, CreateMultiply) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -458,13 +428,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -475,7 +443,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 * 2");
 }
 
-TEST_F(IR_InstructionTest, CreateDivide) {
+TEST_F(IR_BinaryTest, CreateDivide) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -486,13 +454,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());
@@ -503,7 +469,7 @@
     EXPECT_EQ(str.str(), "%42 = 4 / 2");
 }
 
-TEST_F(IR_InstructionTest, CreateModulo) {
+TEST_F(IR_BinaryTest, CreateModulo) {
     auto& b = CreateEmptyBuilder();
 
     b.builder.next_temp_id = Temp::Id(42);
@@ -514,13 +480,11 @@
     ASSERT_TRUE(instr->Result()->Is<Temp>());
     EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
 
-    ASSERT_TRUE(instr->HasLHS());
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>();
     ASSERT_TRUE(lhs->IsI32());
     EXPECT_EQ(i32(4), lhs->AsI32());
 
-    ASSERT_TRUE(instr->HasRHS());
     ASSERT_TRUE(instr->RHS()->Is<Constant>());
     auto rhs = instr->RHS()->As<Constant>();
     ASSERT_TRUE(rhs->IsI32());