Import Tint changes from Dawn

Changes:
  - 62a07738c809d1586fb9862f0f399b00cd399ec8 [ir] Renaming Register and Op by dan sinclair <dsinclair@chromium.org>
  - 3728a505d661508aaf1c9a2757281241f0ad6582 tint: const eval of refract builtin by Antonio Maiorano <amaiorano@google.com>
  - ee7d6db0472c8f0b863408e0a0163bc466bcb428 tint: const eval of reflect builtin by Antonio Maiorano <amaiorano@google.com>
GitOrigin-RevId: 62a07738c809d1586fb9862f0f399b00cd399ec8
Change-Id: I4571fa94514fd00a26feeae0acf20c3d6365eb50
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/112220
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index a8644b7..06aa483 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -666,18 +666,18 @@
     ir/function.h
     ir/if.cc
     ir/if.h
+    ir/instruction.cc
+    ir/instruction.h
     ir/loop.cc
     ir/loop.h
     ir/module.cc
     ir/module.h
-    ir/op.cc
-    ir/op.h
-    ir/register.cc
-    ir/register.h
     ir/switch.cc
     ir/switch.h
     ir/terminator.cc
     ir/terminator.h
+    ir/value.cc
+    ir/value.h
   )
 endif()
 
@@ -1341,9 +1341,9 @@
   if (${TINT_BUILD_IR})
     list(APPEND TINT_TEST_SRCS
       ir/builder_impl_test.cc
-      ir/op_test.cc
-      ir/register_test.cc
+      ir/instruction_test.cc
       ir/test_helper.h
+      ir/value_test.cc
     )
   endif()
 
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 687b8e1..fb75c97 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -518,8 +518,8 @@
 @const fn quantizeToF16<N: num>(vec<N, f32>) -> vec<N, f32>
 @const fn radians<T: fa_f32_f16>(T) -> T
 @const fn radians<N: num, T: fa_f32_f16>(vec<N, T>) -> vec<N, T>
-fn reflect<N: num, T: f32_f16>(vec<N, T>, vec<N, T>) -> vec<N, T>
-fn refract<N: num, T: f32_f16>(vec<N, T>, vec<N, T>, T) -> vec<N, T>
+@const fn reflect<N: num, T: fa_f32_f16>(vec<N, T>, vec<N, T>) -> vec<N, T>
+@const fn refract<N: num, T: fa_f32_f16>(vec<N, T>, vec<N, T>, T) -> vec<N, T>
 @const fn reverseBits<T: iu32>(T) -> T
 @const fn reverseBits<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
 @const fn round<T: fa_f32_f16>(@test_value(3.4) T) -> T
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 850c13c..d27fdf4 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -16,7 +16,7 @@
 #define SRC_TINT_IR_BLOCK_H_
 
 #include "src/tint/ir/flow_node.h"
-#include "src/tint/ir/op.h"
+#include "src/tint/ir/instruction.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::ir {
@@ -33,8 +33,8 @@
     /// The node this block branches too.
     const FlowNode* branch_target = nullptr;
 
-    /// The operations in the block
-    utils::Vector<Op, 16> ops;
+    /// The instructions in the block
+    utils::Vector<Instruction, 16> instructions;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 57a40a7..0403807 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -93,84 +93,84 @@
     to->inbound_branches.Push(from);
 }
 
-Register::Id Builder::AllocateRegister() {
-    return next_register_id++;
+Value::Id Builder::AllocateValue() {
+    return next_value_id++;
 }
 
-Op Builder::CreateOp(Op::Kind kind, Register lhs, Register rhs) {
-    return Op(kind, Register(AllocateRegister()), lhs, rhs);
+Instruction Builder::CreateInstruction(Instruction::Kind kind, Value lhs, Value rhs) {
+    return Instruction(kind, Value(AllocateValue()), lhs, rhs);
 }
 
-Op Builder::And(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kAnd, lhs, rhs);
+Instruction Builder::And(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kAnd, lhs, rhs);
 }
 
-Op Builder::Or(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kOr, lhs, rhs);
+Instruction Builder::Or(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kOr, lhs, rhs);
 }
 
-Op Builder::Xor(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kXor, lhs, rhs);
+Instruction Builder::Xor(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kXor, lhs, rhs);
 }
 
-Op Builder::LogicalAnd(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kLogicalAnd, lhs, rhs);
+Instruction Builder::LogicalAnd(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kLogicalAnd, lhs, rhs);
 }
 
-Op Builder::LogicalOr(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kLogicalOr, lhs, rhs);
+Instruction Builder::LogicalOr(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kLogicalOr, lhs, rhs);
 }
 
-Op Builder::Equal(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kEqual, lhs, rhs);
+Instruction Builder::Equal(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kEqual, lhs, rhs);
 }
 
-Op Builder::NotEqual(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kNotEqual, lhs, rhs);
+Instruction Builder::NotEqual(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kNotEqual, lhs, rhs);
 }
 
-Op Builder::LessThan(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kLessThan, lhs, rhs);
+Instruction Builder::LessThan(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kLessThan, lhs, rhs);
 }
 
-Op Builder::GreaterThan(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kGreaterThan, lhs, rhs);
+Instruction Builder::GreaterThan(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kGreaterThan, lhs, rhs);
 }
 
-Op Builder::LessThanEqual(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kLessThanEqual, lhs, rhs);
+Instruction Builder::LessThanEqual(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kLessThanEqual, lhs, rhs);
 }
 
-Op Builder::GreaterThanEqual(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kGreaterThanEqual, lhs, rhs);
+Instruction Builder::GreaterThanEqual(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kGreaterThanEqual, lhs, rhs);
 }
 
-Op Builder::ShiftLeft(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kShiftLeft, lhs, rhs);
+Instruction Builder::ShiftLeft(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kShiftLeft, lhs, rhs);
 }
 
-Op Builder::ShiftRight(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kShiftRight, lhs, rhs);
+Instruction Builder::ShiftRight(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kShiftRight, lhs, rhs);
 }
 
-Op Builder::Add(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kAdd, lhs, rhs);
+Instruction Builder::Add(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kAdd, lhs, rhs);
 }
 
-Op Builder::Subtract(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kSubtract, lhs, rhs);
+Instruction Builder::Subtract(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kSubtract, lhs, rhs);
 }
 
-Op Builder::Multiply(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kMultiply, lhs, rhs);
+Instruction Builder::Multiply(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kMultiply, lhs, rhs);
 }
 
-Op Builder::Divide(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kDivide, lhs, rhs);
+Instruction Builder::Divide(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kDivide, lhs, rhs);
 }
 
-Op Builder::Modulo(Register lhs, Register rhs) {
-    return CreateOp(Op::Kind::kModulo, lhs, rhs);
+Instruction Builder::Modulo(Value lhs, Value rhs) {
+    return CreateInstruction(Instruction::Kind::kModulo, lhs, rhs);
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 091aac8..6776d35 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -17,12 +17,12 @@
 
 #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/op.h"
-#include "src/tint/ir/register.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
+#include "src/tint/ir/value.h"
 
 // Forward Declarations
 namespace tint {
@@ -88,124 +88,124 @@
     /// @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);
+    Instruction CreateInstruction(Instruction::Kind kind, Value lhs, Value 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);
+    Instruction And(Value lhs, Value 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);
+    Instruction Or(Value lhs, Value 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);
+    Instruction Xor(Value lhs, Value 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);
+    Instruction LogicalAnd(Value lhs, Value 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);
+    Instruction LogicalOr(Value lhs, Value 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);
+    Instruction Equal(Value lhs, Value 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);
+    Instruction NotEqual(Value lhs, Value 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);
+    Instruction LessThan(Value lhs, Value 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);
+    Instruction GreaterThan(Value lhs, Value 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);
+    Instruction LessThanEqual(Value lhs, Value 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);
+    Instruction GreaterThanEqual(Value lhs, Value 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);
+    Instruction ShiftLeft(Value lhs, Value 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);
+    Instruction ShiftRight(Value lhs, Value 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);
+    Instruction Add(Value lhs, Value 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);
+    Instruction Subtract(Value lhs, Value 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);
+    Instruction Multiply(Value lhs, Value 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);
+    Instruction Divide(Value lhs, Value 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);
+    Instruction Modulo(Value lhs, Value rhs);
 
-    /// @returns a unique register id
-    Register::Id AllocateRegister();
+    /// @returns a unique Value id
+    Value::Id AllocateValue();
 
     /// The IR module.
     Module ir;
 
-    /// The next register number to allocate
-    Register::Id next_register_id = 1;
+    /// The next Value number to allocate
+    Value::Id next_value_id = 1;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 06ed837..3af54f9 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -516,7 +516,7 @@
     return true;
 }
 
-utils::Result<Register> BuilderImpl::EmitExpression(const ast::Expression* expr) {
+utils::Result<Value> BuilderImpl::EmitExpression(const ast::Expression* expr) {
     return tint::Switch(
         expr,
         // [&](const ast::IndexAccessorExpression* a) { return EmitIndexAccessor(a); },
@@ -551,7 +551,7 @@
         });
 }
 
-utils::Result<Register> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
+utils::Result<Value> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
     auto lhs = EmitExpression(expr->lhs);
     if (!lhs) {
         return utils::Failure;
@@ -562,89 +562,87 @@
         return utils::Failure;
     }
 
-    Op op;
+    Instruction instr;
     switch (expr->op) {
         case ast::BinaryOp::kAnd:
-            op = builder.And(lhs.Get(), rhs.Get());
+            instr = builder.And(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kOr:
-            op = builder.Or(lhs.Get(), rhs.Get());
+            instr = builder.Or(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kXor:
-            op = builder.Xor(lhs.Get(), rhs.Get());
+            instr = builder.Xor(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalAnd:
-            op = builder.LogicalAnd(lhs.Get(), rhs.Get());
+            instr = builder.LogicalAnd(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalOr:
-            op = builder.LogicalOr(lhs.Get(), rhs.Get());
+            instr = builder.LogicalOr(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kEqual:
-            op = builder.Equal(lhs.Get(), rhs.Get());
+            instr = builder.Equal(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kNotEqual:
-            op = builder.NotEqual(lhs.Get(), rhs.Get());
+            instr = builder.NotEqual(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThan:
-            op = builder.LessThan(lhs.Get(), rhs.Get());
+            instr = builder.LessThan(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThan:
-            op = builder.GreaterThan(lhs.Get(), rhs.Get());
+            instr = builder.GreaterThan(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThanEqual:
-            op = builder.LessThanEqual(lhs.Get(), rhs.Get());
+            instr = builder.LessThanEqual(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThanEqual:
-            op = builder.GreaterThanEqual(lhs.Get(), rhs.Get());
+            instr = builder.GreaterThanEqual(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftLeft:
-            op = builder.ShiftLeft(lhs.Get(), rhs.Get());
+            instr = builder.ShiftLeft(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftRight:
-            op = builder.ShiftRight(lhs.Get(), rhs.Get());
+            instr = builder.ShiftRight(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kAdd:
-            op = builder.Add(lhs.Get(), rhs.Get());
+            instr = builder.Add(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kSubtract:
-            op = builder.Subtract(lhs.Get(), rhs.Get());
+            instr = builder.Subtract(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kMultiply:
-            op = builder.Multiply(lhs.Get(), rhs.Get());
+            instr = builder.Multiply(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kDivide:
-            op = builder.Divide(lhs.Get(), rhs.Get());
+            instr = builder.Divide(lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kModulo:
-            op = builder.Modulo(lhs.Get(), rhs.Get());
+            instr = 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);
+    auto result = instr.Result();
+    current_flow_block->instructions.Push(instr);
     return result;
 }
 
-utils::Result<Register> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
+utils::Result<Value> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
     return tint::Switch(  //
         lit,
-        [&](const ast::BoolLiteralExpression* l) {
-            return utils::Result<Register>{Register(l->value)};
-        },
+        [&](const ast::BoolLiteralExpression* l) { return utils::Result<Value>{Value(l->value)}; },
         [&](const ast::FloatLiteralExpression* l) {
             if (l->suffix == ast::FloatLiteralExpression::Suffix::kF) {
-                return utils::Result<Register>{Register(f32(static_cast<float>(l->value)))};
+                return utils::Result<Value>{Value(f32(static_cast<float>(l->value)))};
             }
-            return utils::Result<Register>{Register(f16(static_cast<float>(l->value)))};
+            return utils::Result<Value>{Value(f16(static_cast<float>(l->value)))};
         },
         [&](const ast::IntLiteralExpression* l) {
             if (l->suffix == ast::IntLiteralExpression::Suffix::kI) {
-                return utils::Result<Register>{Register(i32(l->value))};
+                return utils::Result<Value>{Value(i32(l->value))};
             }
-            return utils::Result<Register>{Register(u32(l->value))};
+            return utils::Result<Value>{Value(u32(l->value))};
         },
         [&](Default) {
             diagnostics_.add_warning(tint::diag::System::IR,
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
index ef5238e..60eda73 100644
--- a/src/tint/ir/builder_impl.h
+++ b/src/tint/ir/builder_impl.h
@@ -23,7 +23,7 @@
 #include "src/tint/ir/builder.h"
 #include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/module.h"
-#include "src/tint/ir/register.h"
+#include "src/tint/ir/value.h"
 #include "src/tint/utils/result.h"
 
 // Forward Declarations
@@ -140,7 +140,7 @@
     /// Emits an expression
     /// @param expr the expression to emit
     /// @returns true if successful, false otherwise
-    utils::Result<Register> EmitExpression(const ast::Expression* expr);
+    utils::Result<Value> EmitExpression(const ast::Expression* expr);
 
     /// Emits a variable
     /// @param var the variable to emit
@@ -149,13 +149,13 @@
 
     /// 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);
+    /// @returns the value storing the result if successful, utils::Failure otherwise
+    utils::Result<Value> EmitBinary(const ast::BinaryExpression* expr);
 
     /// Emits a literal expression
     /// @param lit the literal to emit
     /// @returns true if successful, false otherwise
-    utils::Result<Register> EmitLiteral(const ast::LiteralExpression* lit);
+    utils::Result<Value> EmitLiteral(const ast::LiteralExpression* lit);
 
     /// Emits a type
     /// @param ty the type to emit
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index 4a1efd5..6c8e496 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -101,9 +101,9 @@
     EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
 
     // Check condition
-    auto op = flow->condition;
-    ASSERT_TRUE(op.IsBool());
-    EXPECT_TRUE(op.AsBool());
+    auto instr = flow->condition;
+    ASSERT_TRUE(instr.IsBool());
+    EXPECT_TRUE(instr.AsBool());
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement_TrueReturns) {
@@ -502,9 +502,9 @@
     EXPECT_EQ(loop_flow->merge_target->branch_target, nullptr);
 
     // Check condition
-    auto op = if_flow->condition;
-    ASSERT_TRUE(op.IsBool());
-    EXPECT_TRUE(op.AsBool());
+    auto instr = if_flow->condition;
+    ASSERT_TRUE(instr.IsBool());
+    EXPECT_TRUE(instr.AsBool());
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithOnlyReturn) {
@@ -947,9 +947,9 @@
     EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
 
     // Check condition
-    auto op = if_flow->condition;
-    ASSERT_TRUE(op.IsBool());
-    EXPECT_FALSE(op.AsBool());
+    auto instr = if_flow->condition;
+    ASSERT_TRUE(instr.IsBool());
+    EXPECT_FALSE(instr.AsBool());
 }
 
 TEST_F(IR_BuilderImplTest, While_Return) {
@@ -1071,9 +1071,9 @@
     EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
 
     // Check condition
-    auto op = if_flow->condition;
-    ASSERT_TRUE(op.IsBool());
-    EXPECT_FALSE(op.AsBool());
+    auto instr = if_flow->condition;
+    ASSERT_TRUE(instr.IsBool());
+    EXPECT_FALSE(instr.AsBool());
 }
 
 TEST_F(IR_BuilderImplTest, For_NoInitCondOrContinuing) {
@@ -1171,9 +1171,9 @@
     EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
 
     // Check condition
-    auto op = flow->condition;
-    ASSERT_TRUE(op.IsI32());
-    EXPECT_EQ(1_i, op.AsI32());
+    auto instr = flow->condition;
+    ASSERT_TRUE(instr.IsI32());
+    EXPECT_EQ(1_i, instr.AsI32());
 }
 
 TEST_F(IR_BuilderImplTest, Switch_OnlyDefault) {
@@ -1402,7 +1402,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 + 4
 )");
 }
@@ -1413,7 +1413,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 - 4
 )");
 }
@@ -1424,7 +1424,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 * 4
 )");
 }
@@ -1435,7 +1435,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 / 4
 )");
 }
@@ -1446,7 +1446,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 % 4
 )");
 }
@@ -1457,7 +1457,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 & 4
 )");
 }
@@ -1468,7 +1468,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 | 4
 )");
 }
@@ -1479,7 +1479,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 ^ 4
 )");
 }
@@ -1490,7 +1490,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 && 4
 )");
 }
@@ -1501,7 +1501,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 || 4
 )");
 }
@@ -1512,7 +1512,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 == 4
 )");
 }
@@ -1523,7 +1523,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 != 4
 )");
 }
@@ -1534,7 +1534,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 < 4
 )");
 }
@@ -1545,7 +1545,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 > 4
 )");
 }
@@ -1556,7 +1556,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 <= 4
 )");
 }
@@ -1567,7 +1567,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 >= 4
 )");
 }
@@ -1578,7 +1578,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 << 4
 )");
 }
@@ -1589,7 +1589,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 >> 4
 )");
 }
@@ -1601,7 +1601,7 @@
     ASSERT_TRUE(r);
 
     Disassembler d;
-    d.EmitBlockOps(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block);
     EXPECT_EQ(d.AsString(), R"(%1 = 3 >> 4
 %2 = %1 + 9
 %3 = 1 < %2
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index a648d68..f98950f 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -61,9 +61,9 @@
     return out_;
 }
 
-void Disassembler::EmitBlockOps(const Block* b) {
-    for (const auto& op : b->ops) {
-        out_ << op << std::endl;
+void Disassembler::EmitBlockInstructions(const Block* b) {
+    for (const auto& instr : b->instructions) {
+        out_ << instr << std::endl;
     }
 }
 
@@ -87,7 +87,7 @@
         },
         [&](const ir::Block* b) {
             Indent() << "Block" << std::endl;
-            EmitBlockOps(b);
+            EmitBlockInstructions(b);
             Walk(b->branch_target);
         },
         [&](const ir::Switch* s) {
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index 9cb8993..e24524a 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -36,9 +36,9 @@
     /// @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);
+    /// Writes the block instructions to the stream
+    /// @param b the block containing the instructions
+    void EmitBlockInstructions(const Block* b);
 
     /// @returns the string representation
     std::string AsString() const { return out_.str(); }
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index 109b990..4b2969d 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -17,7 +17,7 @@
 
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ir/flow_node.h"
-#include "src/tint/ir/register.h"
+#include "src/tint/ir/value.h"
 
 // Forward declarations
 namespace tint::ir {
@@ -44,8 +44,8 @@
     /// An block to reconvert the true/false barnches. The block always exists, but there maybe no
     /// branches into it. (e.g. if both branches `return`)
     Block* merge_target = nullptr;
-    /// Register holding the condition result
-    Register condition;
+    /// Value holding the condition result
+    Value condition;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction.cc b/src/tint/ir/instruction.cc
new file mode 100644
index 0000000..9460f39
--- /dev/null
+++ b/src/tint/ir/instruction.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/instruction.h"
+
+namespace tint::ir {
+
+Instruction::Instruction() {}
+
+Instruction::Instruction(Kind kind, Value result, Value lhs, Value rhs)
+    : kind_(kind), result_(result), args_({lhs, rhs}) {}
+
+Instruction::Instruction(const Instruction&) = default;
+
+Instruction::Instruction(Instruction&& instr) = default;
+
+Instruction::~Instruction() = default;
+
+Instruction& Instruction::operator=(const Instruction& instr) = default;
+
+Instruction& Instruction::operator=(Instruction&& instr) = default;
+
+std::ostream& operator<<(std::ostream& out, const Instruction& instr) {
+    out << instr.Result() << " = ";
+    if (instr.HasLHS()) {
+        out << instr.LHS();
+    }
+    out << " ";
+
+    switch (instr.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 (instr.HasRHS()) {
+        out << " " << instr.RHS();
+    }
+
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
new file mode 100644
index 0000000..b62300c
--- /dev/null
+++ b/src/tint/ir/instruction.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_INSTRUCTION_H_
+#define SRC_TINT_IR_INSTRUCTION_H_
+
+#include <ostream>
+
+#include "src/tint/ir/value.h"
+#include "src/tint/utils/vector.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class 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, Value result, Value lhs, 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);
+    /// Destructor
+    ~Instruction();
+
+    /// 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);
+
+    /// @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];
+    }
+
+  private:
+    Kind kind_;
+
+    Value result_;
+    utils::Vector<Value, 2> args_;
+};
+
+std::ostream& operator<<(std::ostream& out, const Instruction&);
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_INSTRUCTION_H_
diff --git a/src/tint/ir/instruction_test.cc b/src/tint/ir/instruction_test.cc
new file mode 100644
index 0000000..8fdd541
--- /dev/null
+++ b/src/tint/ir/instruction_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/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_value_id = Value::Id(42);
+    auto instr = b.builder.And(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kAnd);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 & 2");
+}
+
+TEST_F(IR_InstructionTest, CreateOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Or(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kOr);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 | 2");
+}
+
+TEST_F(IR_InstructionTest, CreateXor) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Xor(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kXor);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 ^ 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLogicalAnd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.LogicalAnd(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kLogicalAnd);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 && 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLogicalOr) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.LogicalOr(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kLogicalOr);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 || 2");
+}
+
+TEST_F(IR_InstructionTest, CreateEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Equal(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kEqual);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 == 2");
+}
+
+TEST_F(IR_InstructionTest, CreateNotEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.NotEqual(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kNotEqual);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 != 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLessThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.LessThan(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kLessThan);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 < 2");
+}
+
+TEST_F(IR_InstructionTest, CreateGreaterThan) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.GreaterThan(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kGreaterThan);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 > 2");
+}
+
+TEST_F(IR_InstructionTest, CreateLessThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.LessThanEqual(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kLessThanEqual);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 <= 2");
+}
+
+TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.GreaterThanEqual(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kGreaterThanEqual);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 >= 2");
+}
+
+TEST_F(IR_InstructionTest, CreateShiftLeft) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.ShiftLeft(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kShiftLeft);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 << 2");
+}
+
+TEST_F(IR_InstructionTest, CreateShiftRight) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.ShiftRight(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kShiftRight);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 >> 2");
+}
+
+TEST_F(IR_InstructionTest, CreateAdd) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Add(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kAdd);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 + 2");
+}
+
+TEST_F(IR_InstructionTest, CreateSubtract) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Subtract(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kSubtract);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 - 2");
+}
+
+TEST_F(IR_InstructionTest, CreateMultiply) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Multiply(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kMultiply);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 * 2");
+}
+
+TEST_F(IR_InstructionTest, CreateDivide) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Divide(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kDivide);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 / 2");
+}
+
+TEST_F(IR_InstructionTest, CreateModulo) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_value_id = Value::Id(42);
+    auto instr = b.builder.Modulo(Value(i32(4)), Value(i32(2)));
+
+    EXPECT_EQ(instr.GetKind(), Instruction::Kind::kModulo);
+
+    ASSERT_TRUE(instr.Result().IsTemp());
+    EXPECT_EQ(Value::Id(42), instr.Result().AsId());
+
+    ASSERT_TRUE(instr.HasLHS());
+    auto& lhs = instr.LHS();
+    ASSERT_TRUE(lhs.IsI32());
+    EXPECT_EQ(i32(4), lhs.AsI32());
+
+    ASSERT_TRUE(instr.HasRHS());
+    auto& rhs = instr.RHS();
+    ASSERT_TRUE(rhs.IsI32());
+    EXPECT_EQ(i32(2), rhs.AsI32());
+
+    std::stringstream str;
+    str << instr;
+    EXPECT_EQ(str.str(), "%42 = 4 % 2");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/op.cc b/src/tint/ir/op.cc
deleted file mode 100644
index 3e6d993..0000000
--- a/src/tint/ir/op.cc
+++ /dev/null
@@ -1,105 +0,0 @@
-// 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
deleted file mode 100644
index a9fc78f..0000000
--- a/src/tint/ir/op.h
+++ /dev/null
@@ -1,113 +0,0 @@
-// 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
deleted file mode 100644
index 01604d1..0000000
--- a/src/tint/ir/op_test.cc
+++ /dev/null
@@ -1,494 +0,0 @@
-// 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
deleted file mode 100644
index f3caa76..0000000
--- a/src/tint/ir/register.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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/register.h"
-
-namespace tint::ir {
-
-Register::Register() : kind_(Kind::kUninitialized), data_(Id(0)) {}
-
-Register::Register(Id id) : kind_(Kind::kTemp), data_(id) {}
-
-Register::Register(f32 f) : kind_(Kind::kF32), data_(f) {}
-
-Register::Register(f16 f) : kind_(Kind::kF16), data_(f) {}
-
-Register::Register(u32 u) : kind_(Kind::kU32), data_(u) {}
-
-Register::Register(i32 i) : kind_(Kind::kI32), data_(i) {}
-
-Register::Register(bool b) : kind_(Kind::kBool), data_(b) {}
-
-Register::Register(Symbol s, Id id) : kind_(Kind::kVar), data_(VarData{s, id}) {}
-
-Register::~Register() = default;
-
-Register::Register(const Register& o) = default;
-
-Register::Register(Register&& o) = default;
-
-Register& Register::operator=(const Register& o) = default;
-
-Register& Register::operator=(Register&& o) = default;
-
-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 out;
-}
-
-}  // namespace tint::ir
diff --git a/src/tint/ir/register.h b/src/tint/ir/register.h
deleted file mode 100644
index d1c41eb..0000000
--- a/src/tint/ir/register.h
+++ /dev/null
@@ -1,169 +0,0 @@
-// 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_REGISTER_H_
-#define SRC_TINT_IR_REGISTER_H_
-
-#include <ostream>
-#include <variant>
-
-#include "src/tint/number.h"
-#include "src/tint/symbol.h"
-
-namespace tint::ir {
-
-/// Register in the IR. The register can be one of several types these include, but aren't limited
-/// to, `f32`, `u32`, `temp`, `var`. The type of the register determines the type of data stored
-/// in the register.
-class Register {
-  public:
-    /// 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`
-    /// will be incremented.
-    struct VarData {
-        /// The symbol for the variable
-        Symbol sym;
-        /// The id for the variable.
-        Id id;
-        // TODO(dsinclair): Should var type data be stored here along side the variable info?
-    };
-
-    /// Constructor
-    /// Creates a uninitialized register
-    Register();
-
-    /// Constructor
-    /// @param id the id for the register
-    explicit Register(Id id);
-
-    /// Constructor
-    /// @param s the symbol for the register
-    /// @param id the id for the register
-    Register(Symbol s, Id id);
-
-    /// Constructor
-    /// @param b the `bool` value to store in the register
-    explicit Register(bool b);
-
-    /// Constructor
-    /// @param f the `f32` value to store in the register
-    explicit Register(f32 f);
-
-    /// Constructor
-    /// @param f the `f16` value to store in the register
-    explicit Register(f16 f);
-
-    /// Constructor
-    /// @param u the `u32` value to store in the register
-    explicit Register(u32 u);
-
-    /// Constructor
-    /// @param i the `i32` value to store in the register
-    explicit Register(i32 i);
-
-    /// Destructor
-    ~Register();
-
-    /// Copy constructor
-    /// @param o the register to copy from
-    Register(const Register& o);
-    /// Move constructor
-    /// @param o the register to move from
-    Register(Register&& o);
-
-    /// Copy assign
-    /// @param o the register to copy from
-    /// @returns this
-    Register& operator=(const Register& o);
-    /// Move assign
-    /// @param o the register to move from
-    /// @returns this
-    Register& operator=(Register&& o);
-
-    /// @returns true if this is a temporary register
-    bool IsTemp() const { return kind_ == Kind::kTemp; }
-    /// @returns true if this is a f32 register
-    bool IsF32() const { return kind_ == Kind::kF32; }
-    /// @returns true if this is a f16 register
-    bool IsF16() const { return kind_ == Kind::kF16; }
-    /// @returns true if this is an i32 register
-    bool IsI32() const { return kind_ == Kind::kI32; }
-    /// @returns true if this is a u32 register
-    bool IsU32() const { return kind_ == Kind::kU32; }
-    /// @returns true if this is a var register
-    bool IsVar() const { return kind_ == Kind::kVar; }
-    /// @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_); }
-    /// @returns the register data as a `f16`.
-    /// @note, must only be called if `IsF16()` is true
-    f16 AsF16() const { return std::get<f16>(data_); }
-    /// @returns the register data as an `i32`.
-    /// @note, must only be called if `IsI32()` is true
-    i32 AsI32() const { return std::get<i32>(data_); }
-    /// @returns the register data as a `u32`.
-    /// @note, must only be called if `IsU32()` is true
-    u32 AsU32() const { return std::get<u32>(data_); }
-    /// @returns the register data as an `Id`.
-    /// @note, must only be called if `IsTemp()` is true
-    Id AsId() const { return std::get<Id>(data_); }
-    /// @returns the register data as a `VarData` structure.
-    /// @note, must only be called if `IsVar()` is true
-    VarData AsVarData() const { return std::get<VarData>(data_); }
-    /// @returns the register data as a `bool`.
-    /// @note, must only be called if `IsBool()` is true
-    bool AsBool() const { return std::get<bool>(data_); }
-
-  private:
-    /// 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
deleted file mode 100644
index fb0194f..0000000
--- a/src/tint/ir/register_test.cc
+++ /dev/null
@@ -1,183 +0,0 @@
-// 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/register.h"
-#include "src/tint/ir/test_helper.h"
-
-namespace tint::ir {
-namespace {
-
-using namespace tint::number_suffixes;  // NOLINT
-
-using IR_RegisterTest = TestHelper;
-
-TEST_F(IR_RegisterTest, f32) {
-    std::stringstream str;
-
-    Register r(1.2_f);
-    EXPECT_EQ(1.2_f, r.AsF32());
-
-    str << r;
-    EXPECT_EQ("1.200000", str.str());
-
-    EXPECT_TRUE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, f16) {
-    std::stringstream str;
-
-    Register r(1.1_h);
-    EXPECT_EQ(1.1_h, r.AsF16());
-
-    str << r;
-    EXPECT_EQ("1.099609", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_TRUE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, i32) {
-    std::stringstream str;
-
-    Register r(1_i);
-    EXPECT_EQ(1_i, r.AsI32());
-
-    str << r;
-    EXPECT_EQ("1", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_TRUE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, u32) {
-    std::stringstream str;
-
-    Register r(2_u);
-    EXPECT_EQ(2_u, r.AsU32());
-
-    str << r;
-    EXPECT_EQ("2", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_TRUE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, id) {
-    std::stringstream str;
-
-    Register r(Register::Id(4));
-    EXPECT_EQ(4u, r.AsId());
-
-    str << r;
-    EXPECT_EQ("%4", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_TRUE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, bool) {
-    std::stringstream str;
-
-    Register r(false);
-    EXPECT_FALSE(r.AsBool());
-
-    str << r;
-    EXPECT_EQ("false", str.str());
-
-    str.str("");
-    r = Register(true);
-    EXPECT_TRUE(r.AsBool());
-
-    str << r;
-    EXPECT_EQ("true", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_TRUE(r.IsBool());
-}
-
-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);
-
-    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);
-
-    str << r;
-    EXPECT_EQ("%v4", str.str());
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_TRUE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-TEST_F(IR_RegisterTest, uninitialized) {
-    Register r;
-
-    EXPECT_FALSE(r.IsF32());
-    EXPECT_FALSE(r.IsF16());
-    EXPECT_FALSE(r.IsI32());
-    EXPECT_FALSE(r.IsU32());
-    EXPECT_FALSE(r.IsTemp());
-    EXPECT_FALSE(r.IsVar());
-    EXPECT_FALSE(r.IsBool());
-}
-
-}  // namespace
-}  // namespace tint::ir
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 73284cf..e526fe8 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -17,7 +17,7 @@
 
 #include "src/tint/ir/block.h"
 #include "src/tint/ir/flow_node.h"
-#include "src/tint/ir/register.h"
+#include "src/tint/ir/value.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -52,8 +52,8 @@
     /// The switch case statements
     utils::Vector<Case, 4> cases;
 
-    /// Register holding the condition result
-    Register condition;
+    /// Value holding the condition result
+    Value condition;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/value.cc b/src/tint/ir/value.cc
new file mode 100644
index 0000000..bd45a98
--- /dev/null
+++ b/src/tint/ir/value.cc
@@ -0,0 +1,76 @@
+// 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/value.h"
+
+namespace tint::ir {
+
+Value::Value() : kind_(Kind::kUninitialized), data_(Id(0)) {}
+
+Value::Value(Id id) : kind_(Kind::kTemp), data_(id) {}
+
+Value::Value(f32 f) : kind_(Kind::kF32), data_(f) {}
+
+Value::Value(f16 f) : kind_(Kind::kF16), data_(f) {}
+
+Value::Value(u32 u) : kind_(Kind::kU32), data_(u) {}
+
+Value::Value(i32 i) : kind_(Kind::kI32), data_(i) {}
+
+Value::Value(bool b) : kind_(Kind::kBool), data_(b) {}
+
+Value::Value(Symbol s, Id id) : kind_(Kind::kVar), data_(VarData{s, id}) {}
+
+Value::~Value() = default;
+
+Value::Value(const Value& o) = default;
+
+Value::Value(Value&& o) = default;
+
+Value& Value::operator=(const Value& o) = default;
+
+Value& Value::operator=(Value&& o) = default;
+
+std::ostream& operator<<(std::ostream& out, const Value& r) {
+    switch (r.GetKind()) {
+        case Value::Kind::kTemp:
+            out << "%" << std::to_string(r.AsId());
+            break;
+        case Value::Kind::kF32:
+            out << std::to_string(r.AsF32().value);
+            break;
+        case Value::Kind::kF16:
+            out << std::to_string(r.AsF16().value);
+            break;
+        case Value::Kind::kI32:
+            out << std::to_string(r.AsI32().value);
+            break;
+        case Value::Kind::kU32:
+            out << std::to_string(r.AsU32().value);
+            break;
+            // TODO(dsinclair): Emit the symbol instead of v
+        case Value::Kind::kVar:
+            out << "%v" << std::to_string(r.AsVarData().id);
+            break;
+        case Value::Kind::kBool:
+            out << (r.AsBool() ? "true" : "false");
+            break;
+        case Value::Kind::kUninitialized:
+            out << "unknown value";
+            break;
+    }
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
new file mode 100644
index 0000000..7c9f8b5
--- /dev/null
+++ b/src/tint/ir/value.h
@@ -0,0 +1,169 @@
+// 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_VALUE_H_
+#define SRC_TINT_IR_VALUE_H_
+
+#include <ostream>
+#include <variant>
+
+#include "src/tint/number.h"
+#include "src/tint/symbol.h"
+
+namespace tint::ir {
+
+/// Value in the IR. The value can be one of several types these include, but aren't limited
+/// to, `f32`, `u32`, `temp`, `var`. The type of the value determines the type of data stored
+/// in the value.
+class Value {
+  public:
+    /// A value id.
+    using Id = uint32_t;
+
+    /// The type of the value
+    enum class Kind {
+        /// A uninitialized value
+        kUninitialized,
+        /// A temporary allocated value
+        kTemp,
+        /// A f32 value
+        kF32,
+        /// A f16 value
+        kF16,
+        /// An i32 value
+        kI32,
+        /// A u32 value
+        kU32,
+        /// A variable value
+        kVar,
+        /// A boolean value
+        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 value will be created and the the `id`
+    /// will be incremented.
+    struct VarData {
+        /// The symbol for the variable
+        Symbol sym;
+        /// The id for the variable.
+        Id id;
+        // TODO(dsinclair): Should var type data be stored here along side the variable info?
+    };
+
+    /// Constructor
+    /// Creates a uninitialized value
+    Value();
+
+    /// Constructor
+    /// @param id the id for the value
+    explicit Value(Id id);
+
+    /// Constructor
+    /// @param s the symbol for the value
+    /// @param id the id for the value
+    Value(Symbol s, Id id);
+
+    /// Constructor
+    /// @param b the `bool` value to store in the value
+    explicit Value(bool b);
+
+    /// Constructor
+    /// @param f the `f32` value to store in the value
+    explicit Value(f32 f);
+
+    /// Constructor
+    /// @param f the `f16` value to store in the value
+    explicit Value(f16 f);
+
+    /// Constructor
+    /// @param u the `u32` value to store in the value
+    explicit Value(u32 u);
+
+    /// Constructor
+    /// @param i the `i32` value to store in the value
+    explicit Value(i32 i);
+
+    /// Destructor
+    ~Value();
+
+    /// Copy constructor
+    /// @param o the value to copy from
+    Value(const Value& o);
+    /// Move constructor
+    /// @param o the value to move from
+    Value(Value&& o);
+
+    /// Copy assign
+    /// @param o the value to copy from
+    /// @returns this
+    Value& operator=(const Value& o);
+    /// Move assign
+    /// @param o the value to move from
+    /// @returns this
+    Value& operator=(Value&& o);
+
+    /// @returns true if this is a temporary value
+    bool IsTemp() const { return kind_ == Kind::kTemp; }
+    /// @returns true if this is a f32 value
+    bool IsF32() const { return kind_ == Kind::kF32; }
+    /// @returns true if this is a f16 value
+    bool IsF16() const { return kind_ == Kind::kF16; }
+    /// @returns true if this is an i32 value
+    bool IsI32() const { return kind_ == Kind::kI32; }
+    /// @returns true if this is a u32 value
+    bool IsU32() const { return kind_ == Kind::kU32; }
+    /// @returns true if this is a var value
+    bool IsVar() const { return kind_ == Kind::kVar; }
+    /// @returns true if this is a bool value
+    bool IsBool() const { return kind_ == Kind::kBool; }
+
+    /// @returns the kind of value
+    Kind GetKind() const { return kind_; }
+
+    /// @returns the value data as a `f32`.
+    /// @note, must only be called if `IsF32()` is true
+    f32 AsF32() const { return std::get<f32>(data_); }
+    /// @returns the value data as a `f16`.
+    /// @note, must only be called if `IsF16()` is true
+    f16 AsF16() const { return std::get<f16>(data_); }
+    /// @returns the value data as an `i32`.
+    /// @note, must only be called if `IsI32()` is true
+    i32 AsI32() const { return std::get<i32>(data_); }
+    /// @returns the value data as a `u32`.
+    /// @note, must only be called if `IsU32()` is true
+    u32 AsU32() const { return std::get<u32>(data_); }
+    /// @returns the value data as an `Id`.
+    /// @note, must only be called if `IsTemp()` is true
+    Id AsId() const { return std::get<Id>(data_); }
+    /// @returns the value data as a `VarData` structure.
+    /// @note, must only be called if `IsVar()` is true
+    VarData AsVarData() const { return std::get<VarData>(data_); }
+    /// @returns the value data as a `bool`.
+    /// @note, must only be called if `IsBool()` is true
+    bool AsBool() const { return std::get<bool>(data_); }
+
+  private:
+    /// The type of data stored in this value
+    Kind kind_;
+    /// The data stored in the value
+    std::variant<Id, f32, f16, u32, i32, VarData, bool> data_;
+};
+
+std::ostream& operator<<(std::ostream& out, const Value& r);
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_VALUE_H_
diff --git a/src/tint/ir/value_test.cc b/src/tint/ir/value_test.cc
new file mode 100644
index 0000000..77ee3e4
--- /dev/null
+++ b/src/tint/ir/value_test.cc
@@ -0,0 +1,183 @@
+// 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/test_helper.h"
+#include "src/tint/ir/value.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_ValueTest = TestHelper;
+
+TEST_F(IR_ValueTest, f32) {
+    std::stringstream str;
+
+    Value val(1.2_f);
+    EXPECT_EQ(1.2_f, val.AsF32());
+
+    str << val;
+    EXPECT_EQ("1.200000", str.str());
+
+    EXPECT_TRUE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, f16) {
+    std::stringstream str;
+
+    Value val(1.1_h);
+    EXPECT_EQ(1.1_h, val.AsF16());
+
+    str << val;
+    EXPECT_EQ("1.099609", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_TRUE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, i32) {
+    std::stringstream str;
+
+    Value val(1_i);
+    EXPECT_EQ(1_i, val.AsI32());
+
+    str << val;
+    EXPECT_EQ("1", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_TRUE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, u32) {
+    std::stringstream str;
+
+    Value val(2_u);
+    EXPECT_EQ(2_u, val.AsU32());
+
+    str << val;
+    EXPECT_EQ("2", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_TRUE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, id) {
+    std::stringstream str;
+
+    Value val(Value::Id(4));
+    EXPECT_EQ(4u, val.AsId());
+
+    str << val;
+    EXPECT_EQ("%4", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_TRUE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, bool) {
+    std::stringstream str;
+
+    Value val(false);
+    EXPECT_FALSE(val.AsBool());
+
+    str << val;
+    EXPECT_EQ("false", str.str());
+
+    str.str("");
+    val = Value(true);
+    EXPECT_TRUE(val.AsBool());
+
+    str << val;
+    EXPECT_EQ("true", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_TRUE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, var) {
+    std::stringstream str;
+
+    Symbol s;
+    Value val(s, 2);
+    EXPECT_EQ(2u, val.AsVarData().id);
+    EXPECT_EQ(s, val.AsVarData().sym);
+
+    str << val;
+    EXPECT_EQ("%v2", str.str());
+    str.str("");
+
+    val = Value(s, 4);
+    EXPECT_EQ(4u, val.AsVarData().id);
+    EXPECT_EQ(s, val.AsVarData().sym);
+
+    str << val;
+    EXPECT_EQ("%v4", str.str());
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_TRUE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+TEST_F(IR_ValueTest, uninitialized) {
+    Value val;
+
+    EXPECT_FALSE(val.IsF32());
+    EXPECT_FALSE(val.IsF16());
+    EXPECT_FALSE(val.IsI32());
+    EXPECT_FALSE(val.IsU32());
+    EXPECT_FALSE(val.IsTemp());
+    EXPECT_FALSE(val.IsVar());
+    EXPECT_FALSE(val.IsBool());
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index f1197ce..b7674dc 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -1189,6 +1189,26 @@
     return Dispatch_fa_f32_f16(SqrtFunc(source, ty), d.Get());
 }
 
+ConstEval::Result ConstEval::Mul(const Source& source,
+                                 const sem::Type* ty,
+                                 const sem::Constant* v1,
+                                 const sem::Constant* v2) {
+    auto transform = [&](const sem::Constant* c0, const sem::Constant* c1) {
+        return Dispatch_fia_fiu32_f16(MulFunc(source, c0->Type()), c0, c1);
+    };
+    return TransformBinaryElements(builder, ty, transform, v1, v2);
+}
+
+ConstEval::Result ConstEval::Sub(const Source& source,
+                                 const sem::Type* ty,
+                                 const sem::Constant* v1,
+                                 const sem::Constant* v2) {
+    auto transform = [&](const sem::Constant* c0, const sem::Constant* c1) {
+        return Dispatch_fia_fiu32_f16(SubFunc(source, c0->Type()), c0, c1);
+    };
+    return TransformBinaryElements(builder, ty, transform, v1, v2);
+}
+
 auto ConstEval::Det2Func(const Source& source, const sem::Type* elem_ty) {
     return [=](auto a, auto b, auto c, auto d) -> ImplResult {
         if (auto r = Det2(source, a, b, c, d)) {
@@ -1481,21 +1501,13 @@
 ConstEval::Result ConstEval::OpMinus(const sem::Type* ty,
                                      utils::VectorRef<const sem::Constant*> args,
                                      const Source& source) {
-    auto transform = [&](const sem::Constant* c0, const sem::Constant* c1) {
-        return Dispatch_fia_fiu32_f16(SubFunc(source, c0->Type()), c0, c1);
-    };
-
-    return TransformBinaryElements(builder, ty, transform, args[0], args[1]);
+    return Sub(source, ty, args[0], args[1]);
 }
 
 ConstEval::Result ConstEval::OpMultiply(const sem::Type* ty,
                                         utils::VectorRef<const sem::Constant*> args,
                                         const Source& source) {
-    auto transform = [&](const sem::Constant* c0, const sem::Constant* c1) {
-        return Dispatch_fia_fiu32_f16(MulFunc(source, c0->Type()), c0, c1);
-    };
-
-    return TransformBinaryElements(builder, ty, transform, args[0], args[1]);
+    return Mul(source, ty, args[0], args[1]);
 }
 
 ConstEval::Result ConstEval::OpMultiplyMatVec(const sem::Type* ty,
@@ -2213,7 +2225,7 @@
 ConstEval::Result ConstEval::determinant(const sem::Type* ty,
                                          utils::VectorRef<const sem::Constant*> args,
                                          const Source& source) {
-    auto calculate = [&]() -> ImplResult {
+    auto calculate = [&]() -> ConstEval::Result {
         auto* m = args[0];
         auto* mat_ty = m->Type()->As<sem::Matrix>();
         auto me = [&](size_t r, size_t c) { return m->Index(c)->Index(r); };
@@ -2899,6 +2911,147 @@
     return TransformElements(builder, ty, transform, args[0]);
 }
 
+ConstEval::Result ConstEval::reflect(const sem::Type* ty,
+                                     utils::VectorRef<const sem::Constant*> args,
+                                     const Source& source) {
+    auto calculate = [&]() -> ConstEval::Result {
+        // For the incident vector e1 and surface orientation e2, returns the reflection direction
+        // e1 - 2 * dot(e2, e1) * e2.
+        auto* e1 = args[0];
+        auto* e2 = args[1];
+        auto* vec_ty = ty->As<sem::Vector>();
+        auto* el_ty = vec_ty->type();
+
+        // dot(e2, e1)
+        auto dot_e2_e1 = Dot(source, e2, e1);
+        if (!dot_e2_e1) {
+            return utils::Failure;
+        }
+
+        // 2 * dot(e2, e1)
+        auto mul2 = [&](auto v) -> ImplResult {
+            using NumberT = decltype(v);
+            return CreateElement(builder, source, el_ty, NumberT{NumberT{2} * v});
+        };
+        auto dot_e2_e1_2 = Dispatch_fa_f32_f16(mul2, dot_e2_e1.Get());
+        if (!dot_e2_e1_2) {
+            return utils::Failure;
+        }
+
+        // 2 * dot(e2, e1) * e2
+        auto dot_e2_e1_2_e2 = Mul(source, ty, dot_e2_e1_2.Get(), e2);
+        if (!dot_e2_e1_2_e2) {
+            return utils::Failure;
+        }
+
+        // e1 - 2 * dot(e2, e1) * e2
+        return Sub(source, ty, e1, dot_e2_e1_2_e2.Get());
+    };
+    auto r = calculate();
+    if (!r) {
+        AddNote("when calculating reflect", source);
+    }
+    return r;
+}
+
+ConstEval::Result ConstEval::refract(const sem::Type* ty,
+                                     utils::VectorRef<const sem::Constant*> args,
+                                     const Source& source) {
+    auto* vec_ty = ty->As<sem::Vector>();
+    auto* el_ty = vec_ty->type();
+
+    auto compute_k = [&](auto e3, auto dot_e2_e1) -> ConstEval::Result {
+        using NumberT = decltype(e3);
+        // let k = 1.0 - e3 * e3 * (1.0 - dot(e2, e1) * dot(e2, e1))
+        auto e3_squared = Mul(source, e3, e3);
+        if (!e3_squared) {
+            return utils::Failure;
+        }
+        auto dot_e2_e1_squared = Mul(source, dot_e2_e1, dot_e2_e1);
+        if (!dot_e2_e1_squared) {
+            return utils::Failure;
+        }
+        auto r = Sub(source, NumberT(1), dot_e2_e1_squared.Get());
+        if (!r) {
+            return utils::Failure;
+        }
+        r = Mul(source, e3_squared.Get(), r.Get());
+        if (!r) {
+            return utils::Failure;
+        }
+        r = Sub(source, NumberT(1), r.Get());
+        if (!r) {
+            return utils::Failure;
+        }
+        return CreateElement(builder, source, el_ty, r.Get());
+    };
+
+    auto compute_e2_scale = [&](auto e3, auto dot_e2_e1, auto k) -> ConstEval::Result {
+        // e3 * dot(e2, e1) + sqrt(k)
+        auto sqrt_k = Sqrt(source, k);
+        if (!sqrt_k) {
+            return utils::Failure;
+        }
+        auto r = Mul(source, e3, dot_e2_e1);
+        if (!r) {
+            return utils::Failure;
+        }
+        r = Add(source, r.Get(), sqrt_k.Get());
+        if (!r) {
+            return utils::Failure;
+        }
+        return CreateElement(builder, source, el_ty, r.Get());
+    };
+
+    auto calculate = [&]() -> ConstEval::Result {
+        auto* e1 = args[0];
+        auto* e2 = args[1];
+        auto* e3 = args[2];
+
+        // For the incident vector e1 and surface normal e2, and the ratio of indices of refraction
+        // e3, let k = 1.0 - e3 * e3 * (1.0 - dot(e2, e1) * dot(e2, e1)). If k < 0.0, returns the
+        // refraction vector 0.0, otherwise return the refraction vector e3 * e1 - (e3 * dot(e2, e1)
+        // + sqrt(k)) * e2.
+
+        // dot(e2, e1)
+        auto dot_e2_e1 = Dot(source, e2, e1);
+        if (!dot_e2_e1) {
+            return utils::Failure;
+        }
+
+        // let k = 1.0 - e3 * e3 * (1.0 - dot(e2, e1) * dot(e2, e1))
+        auto k = Dispatch_fa_f32_f16(compute_k, e3, dot_e2_e1.Get());
+        if (!k) {
+            return utils::Failure;
+        }
+
+        // If k < 0.0, returns the refraction vector 0.0
+        if (k.Get()->As<AFloat>() < 0) {
+            return ZeroValue(builder, ty);
+        }
+
+        // Otherwise return the refraction vector e3 * e1 - (e3 * dot(e2, e1) + sqrt(k)) * e2
+        auto e1_scaled = Mul(source, ty, e3, e1);
+        if (!e1_scaled) {
+            return utils::Failure;
+        }
+        auto e2_scale = Dispatch_fa_f32_f16(compute_e2_scale, e3, dot_e2_e1.Get(), k.Get());
+        if (!e2_scale) {
+            return utils::Failure;
+        }
+        auto e2_scaled = Mul(source, ty, e2_scale.Get(), e2);
+        if (!e1_scaled) {
+            return utils::Failure;
+        }
+        return Sub(source, ty, e1_scaled.Get(), e2_scaled.Get());
+    };
+    auto r = calculate();
+    if (!r) {
+        AddNote("when calculating refract", source);
+    }
+    return r;
+}
+
 ConstEval::Result ConstEval::reverseBits(const sem::Type* ty,
                                          utils::VectorRef<const sem::Constant*> args,
                                          const Source& source) {
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index df1db17..d92dc3f 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -791,6 +791,24 @@
                    utils::VectorRef<const sem::Constant*> args,
                    const Source& source);
 
+    /// reflect builtin
+    /// @param ty the expression type
+    /// @param args the input arguments
+    /// @param source the source location of the conversion
+    /// @return the result value, or null if the value cannot be calculated
+    Result reflect(const sem::Type* ty,
+                   utils::VectorRef<const sem::Constant*> args,
+                   const Source& source);
+
+    /// refract builtin
+    /// @param ty the expression type
+    /// @param args the input arguments
+    /// @param source the source location of the conversion
+    /// @return the result value, or null if the value cannot be calculated
+    Result refract(const sem::Type* ty,
+                   utils::VectorRef<const sem::Constant*> args,
+                   const Source& source);
+
     /// reverseBits builtin
     /// @param ty the expression type
     /// @param args the input arguments
@@ -1261,13 +1279,35 @@
     /// @returns the dot product
     Result Dot(const Source& source, const sem::Constant* v1, const sem::Constant* v2);
 
-    /// Return sthe length of c0
+    /// Returns the length of c0
     /// @param source the source location
     /// @param ty the return type
     /// @param c0 the constant to calculate the length of
     /// @returns the length of c0
     Result Length(const Source& source, const sem::Type* ty, const sem::Constant* c0);
 
+    /// Returns the product of v1 and v2
+    /// @param source the source location
+    /// @param ty the return type
+    /// @param v1 lhs value
+    /// @param v2 rhs value
+    /// @returns the product of v1 and v2
+    Result Mul(const Source& source,
+               const sem::Type* ty,
+               const sem::Constant* v1,
+               const sem::Constant* v2);
+
+    /// Returns the difference between v2 and v1
+    /// @param source the source location
+    /// @param ty the return type
+    /// @param v1 lhs value
+    /// @param v2 rhs value
+    /// @returns the difference between v2 and v1
+    Result Sub(const Source& source,
+               const sem::Type* ty,
+               const sem::Constant* v1,
+               const sem::Constant* v2);
+
     ProgramBuilder& builder;
 };
 
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 7c5d708..b7f506c 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -1935,6 +1935,141 @@
                                               ReverseBitsCases<u32>()))));
 
 template <typename T>
+std::vector<Case> ReflectCases() {
+    auto pos_y = Vec(T(0), T(1), T(0));
+    auto neg_y = Vec(T(0), -T(1), T(0));
+    auto pos_large_y = Vec(T(0), T(10000), T(0));
+    auto neg_large_y = Vec(T(0), -T(10000), T(0));
+
+    auto cos_45 = T(0.70710678118654752440084436210485);
+    auto pos_xyz = Vec(cos_45, cos_45, cos_45);
+
+    auto r = std::vector<Case>{
+        C({Vec(T(1), -T(1), T(0)), pos_y}, Vec(T(1), T(1), T(0))),
+        C({Vec(T(24), -T(42), T(0)), pos_y}, Vec(T(24), T(42), T(0))),
+        // Flipping reflection vector doesn't change the result
+        C({Vec(T(1), -T(1), T(0)), neg_y}, Vec(T(1), T(1), T(0))),
+        C({Vec(T(24), -T(42), T(0)), neg_y}, Vec(T(24), T(42), T(0))),
+        // Parallel input and reflection vectors: result is negation of input
+        C({pos_y, pos_y}, neg_y),
+        C({neg_y, pos_y}, pos_y),
+        C({pos_large_y, pos_y}, neg_large_y),
+        C({neg_large_y, pos_y}, pos_large_y),
+        // Input axis vectors reflected by normalized(vec(1,1,1)) vector.
+        C({Vec(T(1), T(0), T(0)), pos_xyz}, Vec(T(0), -T(1), -T(1))).FloatComp(0.02),
+        C({Vec(T(0), T(1), T(0)), pos_xyz}, Vec(-T(1), T(0), -T(1))).FloatComp(0.02),
+        C({Vec(T(0), T(0), T(1)), pos_xyz}, Vec(-T(1), -T(1), T(0))).FloatComp(0.02),
+        C({Vec(-T(1), T(0), T(0)), pos_xyz}, Vec(T(0), T(1), T(1))).FloatComp(0.02),
+        C({Vec(T(0), -T(1), T(0)), pos_xyz}, Vec(T(1), T(0), T(1))).FloatComp(0.02),
+        C({Vec(T(0), T(0), -T(1)), pos_xyz}, Vec(T(1), T(1), T(0))).FloatComp(0.02),
+    };
+
+    auto error_msg = [](auto a, const char* op, auto b) {
+        return "12:34 error: " + OverflowErrorMessage(a, op, b) + R"(
+12:34 note: when calculating reflect)";
+    };
+    ConcatInto(  //
+        r, std::vector<Case>{
+               // Overflow the dot product operation
+               E({Vec(T::Highest(), T::Highest(), T(0)), Vec(T(1), T(1), T(0))},
+                 error_msg(T::Highest(), "+", T::Highest())),
+               E({Vec(T::Lowest(), T::Lowest(), T(0)), Vec(T(1), T(1), T(0))},
+                 error_msg(T::Lowest(), "+", T::Lowest())),
+           });
+
+    return r;
+}
+INSTANTIATE_TEST_SUITE_P(  //
+    Reflect,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kReflect),
+                     testing::ValuesIn(Concat(ReflectCases<AFloat>(),  //
+                                              ReflectCases<f32>(),     //
+                                              ReflectCases<f16>()))));
+
+template <typename T>
+std::vector<Case> RefractCases() {
+    // Returns "eta" (Greek letter) that denotes the ratio of indices of refraction for the input
+    // and output vector angles from the normal vector.
+    auto eta = [](auto angle1, auto angle2) {
+        // Snell's law: sin(angle1) / sin(angle2) == n2 / n1
+        // We want the ratio of n1 to n2, so sin(angle2) / sin(angle1)
+        auto angle1_rads = T(angle1) * kPi<T> / T(180);
+        auto angle2_rads = T(angle2) * kPi<T> / T(180);
+        return T(std::sin(angle2_rads) / std::sin(angle1_rads));
+    };
+
+    auto zero = Vec(T(0), T(0), T(0));
+    auto pos_y = Vec(T(0), T(1), T(0));
+    auto neg_y = Vec(T(0), -T(1), T(0));
+    auto pos_x = Vec(T(1), T(0), T(0));
+    auto neg_x = Vec(-T(1), T(0), T(0));
+    auto cos_45 = T(0.70710678118654752440084436210485);
+    auto cos_30 = T(0.86602540378443864676372317075294);
+    auto down_right = Vec(T(cos_45), -T(cos_45), T(0));
+    auto up_right = Vec(T(cos_45), T(cos_45), T(0));
+
+    auto eps = 0.001;
+    if constexpr (std::is_same_v<T, f16>) {
+        eps = 0.1;
+    }
+
+    auto r = std::vector<Case>{
+        // e3 (eta) == 1, no refraction, so input is same as output
+        C({down_right, pos_y, Val(T(1))}, down_right),
+        C({neg_y, pos_y, Val(T(1))}, neg_y),
+        // Varying etas
+        C({down_right, pos_y, Val(eta(45, 45))}, down_right).FloatComp(eps),  // e3 == 1
+        C({down_right, pos_y, Val(eta(45, 30))}, Vec(T(0.5), -T(cos_30), T(0))).FloatComp(eps),
+        C({down_right, pos_y, Val(eta(45, 60))}, Vec(T(cos_30), -T(0.5), T(0))).FloatComp(eps),
+        C({down_right, pos_y, Val(eta(45, 90))}, Vec(T(1), T(0), T(0))).FloatComp(eps),
+        // Flip input and normal, same result
+        C({up_right, neg_y, Val(eta(45, 45))}, up_right).FloatComp(eps),  // e3 == 1
+        C({up_right, neg_y, Val(eta(45, 30))}, Vec(T(0.5), T(cos_30), T(0))).FloatComp(eps),
+        C({up_right, neg_y, Val(eta(45, 60))}, Vec(T(cos_30), T(0.5), T(0))).FloatComp(eps),
+        C({up_right, neg_y, Val(eta(45, 90))}, Vec(T(1), T(0), T(0))).FloatComp(eps),
+        // Flip only normal, result is flipped
+        C({down_right, neg_y, Val(eta(45, 45))}, up_right).FloatComp(eps),  // e3 == 1
+        C({down_right, neg_y, Val(eta(45, 30))}, Vec(T(0.5), T(cos_30), T(0))).FloatComp(eps),
+        C({down_right, neg_y, Val(eta(45, 60))}, Vec(T(cos_30), T(0.5), T(0))).FloatComp(eps),
+        C({down_right, neg_y, Val(eta(45, 90))}, Vec(T(1), T(0), T(0))).FloatComp(eps),
+
+        // If k < 0.0, returns the refraction vector 0.0
+        C({down_right, pos_y, Val(T(2))}, zero).FloatComp(eps),
+
+        // A few more with a different normal (e2)
+        C({down_right, neg_x, Val(eta(45, 45))}, down_right).FloatComp(eps),  // e3 == 1
+        C({down_right, neg_x, Val(eta(45, 30))}, Vec(cos_30, -T(0.5), T(0))).FloatComp(eps),
+        C({down_right, neg_x, Val(eta(45, 60))}, Vec(T(0.5), -T(cos_30), T(0))).FloatComp(eps),
+    };
+
+    auto error_msg = [](auto a, const char* op, auto b) {
+        return "12:34 error: " + OverflowErrorMessage(a, op, b) + R"(
+12:34 note: when calculating refract)";
+    };
+    ConcatInto(  //
+        r,
+        std::vector<Case>{
+            // Overflow the dot product operation
+            E({Vec(T::Highest(), T::Highest(), T(0)), Vec(T(1), T(1), T(0)), Val(T(1))},
+              error_msg(T::Highest(), "+", T::Highest())),
+            E({Vec(T::Lowest(), T::Lowest(), T(0)), Vec(T(1), T(1), T(0)), Val(T(1))},
+              error_msg(T::Lowest(), "+", T::Lowest())),
+            // Overflow the k^2 operation
+            E({down_right, pos_y, Val(T::Highest())}, error_msg(T::Highest(), "*", T::Highest())),
+        });
+
+    return r;
+}
+INSTANTIATE_TEST_SUITE_P(  //
+    Refract,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kRefract),
+                     testing::ValuesIn(Concat(RefractCases<AFloat>(),  //
+                                              RefractCases<f32>(),     //
+                                              RefractCases<f16>()))));
+
+template <typename T>
 std::vector<Case> RadiansCases() {
     return std::vector<Case>{
         C({T(0)}, T(0)),                         //
diff --git a/src/tint/resolver/const_eval_test.h b/src/tint/resolver/const_eval_test.h
index 0d27805..42b6f92 100644
--- a/src/tint/resolver/const_eval_test.h
+++ b/src/tint/resolver/const_eval_test.h
@@ -105,34 +105,34 @@
                 auto got = std::get<T>(got_scalar);
 
                 if constexpr (std::is_same_v<bool, T>) {
-                    EXPECT_EQ(got, expected);
+                    EXPECT_EQ(got, expected) << "index: " << i;
                 } else if constexpr (IsFloatingPoint<T>) {
                     if (std::isnan(expected)) {
-                        EXPECT_TRUE(std::isnan(got));
+                        EXPECT_TRUE(std::isnan(got)) << "index: " << i;
                     } else {
                         if (flags.pos_or_neg) {
                             got = Abs(got);
                         }
                         if (flags.float_compare) {
                             if (flags.float_compare_epsilon) {
-                                EXPECT_NEAR(got, expected, *flags.float_compare_epsilon);
+                                EXPECT_NEAR(got, expected, *flags.float_compare_epsilon)
+                                    << "index: " << i;
                             } else {
-                                EXPECT_FLOAT_EQ(got, expected);
+                                EXPECT_FLOAT_EQ(got, expected) << "index: " << i;
                             }
                         } else {
-                            EXPECT_EQ(got, expected);
+                            EXPECT_EQ(got, expected) << "index: " << i;
                         }
                     }
                 } else {
                     if (flags.pos_or_neg) {
-                        auto got_abs = Abs(got);
-                        EXPECT_EQ(got_abs, expected);
-                    } else {
-                        EXPECT_EQ(got, expected);
+                        got = Abs(got);
                     }
+                    EXPECT_EQ(got, expected) << "index: " << i;
+
                     // Check that the constant's integer doesn't contain unexpected
                     // data in the MSBs that are outside of the bit-width of T.
-                    EXPECT_EQ(AInt(got), AInt(expected));
+                    EXPECT_EQ(AInt(got), AInt(expected)) << "index: " << i;
                 }
             },
             expected_scalar);
diff --git a/src/tint/resolver/intrinsic_table.inl b/src/tint/resolver/intrinsic_table.inl
index e521a9c..d0752da 100644
--- a/src/tint/resolver/intrinsic_table.inl
+++ b/src/tint/resolver/intrinsic_table.inl
@@ -13698,24 +13698,24 @@
     /* num parameters */ 2,
     /* num template types */ 1,
     /* num template numbers */ 1,
-    /* template types */ &kTemplateTypes[26],
+    /* template types */ &kTemplateTypes[23],
     /* template numbers */ &kTemplateNumbers[4],
     /* parameters */ &kParameters[626],
     /* return matcher indices */ &kMatcherIndices[30],
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
-    /* const eval */ nullptr,
+    /* const eval */ &ConstEval::reflect,
   },
   {
     /* [449] */
     /* num parameters */ 3,
     /* num template types */ 1,
     /* num template numbers */ 1,
-    /* template types */ &kTemplateTypes[26],
+    /* template types */ &kTemplateTypes[23],
     /* template numbers */ &kTemplateNumbers[4],
     /* parameters */ &kParameters[477],
     /* return matcher indices */ &kMatcherIndices[30],
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
-    /* const eval */ nullptr,
+    /* const eval */ &ConstEval::refract,
   },
   {
     /* [450] */
@@ -14415,13 +14415,13 @@
   },
   {
     /* [63] */
-    /* fn reflect<N : num, T : f32_f16>(vec<N, T>, vec<N, T>) -> vec<N, T> */
+    /* fn reflect<N : num, T : fa_f32_f16>(vec<N, T>, vec<N, T>) -> vec<N, T> */
     /* num overloads */ 1,
     /* overloads */ &kOverloads[448],
   },
   {
     /* [64] */
-    /* fn refract<N : num, T : f32_f16>(vec<N, T>, vec<N, T>, T) -> vec<N, T> */
+    /* fn refract<N : num, T : fa_f32_f16>(vec<N, T>, vec<N, T>, T) -> vec<N, T> */
     /* num overloads */ 1,
     /* overloads */ &kOverloads[449],
   },