Import Tint changes from Dawn

Changes:
  - 0958c626d4067957c6b50d78962d812a1b758b33 [ir][spirv-writer] Emit struct types by James Price <jrprice@google.com>
  - ec2bfb3abb587075216a82709de9d99d2e073d73 [tint] Fix implicit stride in array helper by James Price <jrprice@google.com>
  - e2abb623b8dfc2e22f3962038c43c95731e26b0a [ir] Add instruction helper methods. by dan sinclair <dsinclair@chromium.org>
  - ccc05b7c33769cba0f97edc8fe9d291f4f1de9cf [ir] Rename BuilderImpl tests. by dan sinclair <dsinclair@chromium.org>
  - 792001a5fedc5572082a4cc31bd2111f0ed9355f [ir] Convert block instructions to a list. by dan sinclair <dsinclair@chromium.org>
  - 829195972f5732b8738b8229a616315930d593be [tint][constant] Optimize Value::Equal() for splats by Ben Clayton <bclayton@google.com>
  - 811054153f8668e54b68df03a8cdfcac00def1ae [tint][reader][wgsl]: Fix quadratic parsing of ___... by Ben Clayton <bclayton@google.com>
  - 4a21f2436f2c3a4dc041926312c572eec17a4665 Fix tests which disable ice_reporter. by dan sinclair <dsinclair@chromium.org>
  - 2f4983ce51b2bff3420760b7fc47e83a41376553 [tint][ir][spirv-writer] Implement loop phis by Ben Clayton <bclayton@google.com>
  - 5d40159a743ab69dad377ff09c9e230b20009ea0 [tint][ir][spriv-writer] Rework ID allocation by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 0958c626d4067957c6b50d78962d812a1b758b33
Change-Id: I2ef4811afed7dd42886de6a0c9df4531e704d9be
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/135720
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index ce779bc..11f6a78 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1699,6 +1699,7 @@
       "constant/manager_test.cc",
       "constant/scalar_test.cc",
       "constant/splat_test.cc",
+      "constant/value_test.cc",
     ]
   }
 
@@ -2280,6 +2281,7 @@
       sources = [
         "ir/binary_test.cc",
         "ir/bitcast_test.cc",
+        "ir/block_test.cc",
         "ir/constant_test.cc",
         "ir/discard_test.cc",
         "ir/from_program_binary_test.cc",
@@ -2292,6 +2294,7 @@
         "ir/from_program_test.cc",
         "ir/from_program_unary_test.cc",
         "ir/from_program_var_test.cc",
+        "ir/instruction_test.cc",
         "ir/load_test.cc",
         "ir/module_test.cc",
         "ir/store_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 2ecb97a..40dd3ee 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -940,6 +940,7 @@
     constant/manager_test.cc
     constant/scalar_test.cc
     constant/splat_test.cc
+    constant/value_test.cc
     debug_test.cc
     diagnostic/diagnostic_test.cc
     diagnostic/formatter_test.cc
@@ -1483,6 +1484,7 @@
     list(APPEND TINT_TEST_SRCS
       ir/binary_test.cc
       ir/bitcast_test.cc
+      ir/block_test.cc
       ir/constant_test.cc
       ir/discard_test.cc
       ir/from_program_binary_test.cc
@@ -1495,6 +1497,7 @@
       ir/from_program_test.cc
       ir/from_program_unary_test.cc
       ir/from_program_var_test.cc
+      ir/instruction_test.cc
       ir/load_test.cc
       ir/module_test.cc
       ir/store_test.cc
diff --git a/src/tint/constant/splat_test.cc b/src/tint/constant/splat_test.cc
index 0e2f21e..e310d63 100644
--- a/src/tint/constant/splat_test.cc
+++ b/src/tint/constant/splat_test.cc
@@ -31,9 +31,9 @@
     auto* fNeg0 = constants.Get(-0_f);
     auto* fPos1 = constants.Get(1_f);
 
-    auto* SpfPos0 = constants.Splat(vec3f, fPos0, 2);
-    auto* SpfNeg0 = constants.Splat(vec3f, fNeg0, 2);
-    auto* SpfPos1 = constants.Splat(vec3f, fPos1, 2);
+    auto* SpfPos0 = constants.Splat(vec3f, fPos0, 3);
+    auto* SpfNeg0 = constants.Splat(vec3f, fNeg0, 3);
+    auto* SpfPos1 = constants.Splat(vec3f, fPos1, 3);
 
     EXPECT_TRUE(SpfPos0->AllZero());
     EXPECT_FALSE(SpfNeg0->AllZero());
@@ -47,9 +47,9 @@
     auto* fNeg0 = constants.Get(-0_f);
     auto* fPos1 = constants.Get(1_f);
 
-    auto* SpfPos0 = constants.Splat(vec3f, fPos0, 2);
-    auto* SpfNeg0 = constants.Splat(vec3f, fNeg0, 2);
-    auto* SpfPos1 = constants.Splat(vec3f, fPos1, 2);
+    auto* SpfPos0 = constants.Splat(vec3f, fPos0, 3);
+    auto* SpfNeg0 = constants.Splat(vec3f, fNeg0, 3);
+    auto* SpfPos1 = constants.Splat(vec3f, fPos1, 3);
 
     EXPECT_TRUE(SpfPos0->AnyZero());
     EXPECT_FALSE(SpfNeg0->AnyZero());
diff --git a/src/tint/constant/value.cc b/src/tint/constant/value.cc
index 7545731..989f2ba 100644
--- a/src/tint/constant/value.cc
+++ b/src/tint/constant/value.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/constant/value.h"
 
+#include "src/tint/constant/splat.h"
 #include "src/tint/switch.h"
 #include "src/tint/type/array.h"
 #include "src/tint/type/matrix.h"
@@ -30,51 +31,68 @@
 
 /// Equal returns true if the constants `a` and `b` are of the same type and value.
 bool Value::Equal(const constant::Value* b) const {
+    if (this == b) {
+        return true;
+    }
     if (Hash() != b->Hash()) {
         return false;
     }
     if (Type() != b->Type()) {
         return false;
     }
+
+    auto elements_equal = [&](size_t count) {
+        if (count == 0) {
+            return true;
+        }
+
+        // Avoid per-element comparisons if the constants are splats
+        bool a_is_splat = Is<Splat>();
+        bool b_is_splat = b->Is<Splat>();
+        if (a_is_splat && b_is_splat) {
+            return Index(0)->Equal(b->Index(0));
+        }
+
+        if (a_is_splat) {
+            auto* el_a = Index(0);
+            for (size_t i = 0; i < count; i++) {
+                if (!el_a->Equal(b->Index(i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        if (b_is_splat) {
+            auto* el_b = b->Index(0);
+            for (size_t i = 0; i < count; i++) {
+                if (!Index(i)->Equal(el_b)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        // Per-element comparison
+        for (size_t i = 0; i < count; i++) {
+            if (!Index(i)->Equal(b->Index(i))) {
+                return false;
+            }
+        }
+        return true;
+    };
+
     return Switch(
         Type(),  //
-        [&](const type::Vector* vec) {
-            for (size_t i = 0; i < vec->Width(); i++) {
-                if (!Index(i)->Equal(b->Index(i))) {
-                    return false;
-                }
-            }
-            return true;
-        },
-        [&](const type::Matrix* mat) {
-            for (size_t i = 0; i < mat->columns(); i++) {
-                if (!Index(i)->Equal(b->Index(i))) {
-                    return false;
-                }
-            }
-            return true;
-        },
+        [&](const type::Vector* vec) { return elements_equal(vec->Width()); },
+        [&](const type::Matrix* mat) { return elements_equal(mat->columns()); },
+        [&](const type::Struct* str) { return elements_equal(str->Members().Length()); },
         [&](const type::Array* arr) {
-            if (auto count = arr->ConstantCount()) {
-                for (size_t i = 0; i < count; i++) {
-                    if (!Index(i)->Equal(b->Index(i))) {
-                        return false;
-                    }
-                }
-                return true;
+            if (auto n = arr->ConstantCount()) {
+                return elements_equal(*n);
             }
-
             return false;
         },
-        [&](const type::Struct* str) {
-            auto count = str->Members().Length();
-            for (size_t i = 0; i < count; i++) {
-                if (!Index(i)->Equal(b->Index(i))) {
-                    return false;
-                }
-            }
-            return true;
-        },
         [&](Default) {
             auto va = InternalValue();
             auto vb = b->InternalValue();
diff --git a/src/tint/constant/value_test.cc b/src/tint/constant/value_test.cc
new file mode 100644
index 0000000..e715c21
--- /dev/null
+++ b/src/tint/constant/value_test.cc
@@ -0,0 +1,78 @@
+// Copyright 2023 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/constant/splat.h"
+
+#include "src/tint/constant/scalar.h"
+#include "src/tint/constant/test_helper.h"
+
+namespace tint::constant {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using ConstantTest_Value = TestHelper;
+
+TEST_F(ConstantTest_Value, Equal_Scalar_Scalar) {
+    EXPECT_TRUE(constants.Get(10_i)->Equal(constants.Get(10_i)));
+    EXPECT_FALSE(constants.Get(10_i)->Equal(constants.Get(20_i)));
+    EXPECT_FALSE(constants.Get(20_i)->Equal(constants.Get(10_i)));
+
+    EXPECT_TRUE(constants.Get(10_u)->Equal(constants.Get(10_u)));
+    EXPECT_FALSE(constants.Get(10_u)->Equal(constants.Get(20_u)));
+    EXPECT_FALSE(constants.Get(20_u)->Equal(constants.Get(10_u)));
+
+    EXPECT_TRUE(constants.Get(10_f)->Equal(constants.Get(10_f)));
+    EXPECT_FALSE(constants.Get(10_f)->Equal(constants.Get(20_f)));
+    EXPECT_FALSE(constants.Get(20_f)->Equal(constants.Get(10_f)));
+}
+
+TEST_F(ConstantTest_Value, Equal_Splat_Splat) {
+    auto* vec3f = create<type::Vector>(create<type::F32>(), 3u);
+
+    auto* vec3f_1_1_1 = constants.Splat(vec3f, constants.Get(1_f), 3);
+    auto* vec3f_2_2_2 = constants.Splat(vec3f, constants.Get(2_f), 3);
+
+    EXPECT_TRUE(vec3f_1_1_1->Equal(vec3f_1_1_1));
+    EXPECT_FALSE(vec3f_2_2_2->Equal(vec3f_1_1_1));
+    EXPECT_FALSE(vec3f_1_1_1->Equal(vec3f_2_2_2));
+}
+
+TEST_F(ConstantTest_Value, Equal_Composite_Composite) {
+    auto* vec3f = create<type::Vector>(create<type::F32>(), 3u);
+
+    auto* vec3f_1_1_2 = constants.Composite(
+        vec3f, utils::Vector{constants.Get(1_f), constants.Get(1_f), constants.Get(2_f)});
+    auto* vec3f_1_2_1 = constants.Composite(
+        vec3f, utils::Vector{constants.Get(1_f), constants.Get(2_f), constants.Get(1_f)});
+
+    EXPECT_TRUE(vec3f_1_1_2->Equal(vec3f_1_1_2));
+    EXPECT_FALSE(vec3f_1_2_1->Equal(vec3f_1_1_2));
+    EXPECT_FALSE(vec3f_1_1_2->Equal(vec3f_1_2_1));
+}
+
+TEST_F(ConstantTest_Value, Equal_Splat_Composite) {
+    auto* vec3f = create<type::Vector>(create<type::F32>(), 3u);
+
+    auto* vec3f_1_1_1 = constants.Splat(vec3f, constants.Get(1_f), 3);
+    auto* vec3f_1_2_1 = constants.Composite(
+        vec3f, utils::Vector{constants.Get(1_f), constants.Get(2_f), constants.Get(1_f)});
+
+    EXPECT_TRUE(vec3f_1_1_1->Equal(vec3f_1_1_1));
+    EXPECT_FALSE(vec3f_1_2_1->Equal(vec3f_1_1_1));
+    EXPECT_FALSE(vec3f_1_1_1->Equal(vec3f_1_2_1));
+}
+
+}  // namespace
+}  // namespace tint::constant
diff --git a/src/tint/debug.h b/src/tint/debug.h
index 5caeb5d..8b8c4b3 100644
--- a/src/tint/debug.h
+++ b/src/tint/debug.h
@@ -119,4 +119,21 @@
         }                                                                                \
     } while (false)
 
+/// TINT_ASSERT_OR_RETURN() is a macro for checking the expression is true, triggering a
+/// TINT_ICE if it is not and returning from the calling function.
+/// The ICE message contains the callsite's file and line.
+/// @warning: Unlike TINT_ICE() and TINT_UNREACHABLE(), TINT_ASSERT_OR_RETURN() does not
+/// append a message to an existing tint::diag::List. As such, TINT_ASSERT_OR_RETURN()
+/// may silently fail in builds where SetInternalCompilerErrorReporter() is not
+/// called. Only use in places where there's no sensible place to put proper
+/// error handling.
+#define TINT_ASSERT_OR_RETURN(system, condition)                                         \
+    do {                                                                                 \
+        if (TINT_UNLIKELY(!(condition))) {                                               \
+            tint::diag::List diagnostics;                                                \
+            TINT_ICE(system, diagnostics) << "TINT_ASSERT(" #system ", " #condition ")"; \
+            return;                                                                      \
+        }                                                                                \
+    } while (false)
+
 #endif  // SRC_TINT_DEBUG_H_
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 281d04b..4c4fab8 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -21,9 +21,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_InstructionTest = TestHelper;
+using IR_BinaryTest = TestHelper;
 
-TEST_F(IR_InstructionTest, CreateAnd) {
+TEST_F(IR_BinaryTest, CreateAnd) {
     Module mod;
     Builder b{mod};
 
@@ -44,7 +44,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateOr) {
+TEST_F(IR_BinaryTest, CreateOr) {
     Module mod;
     Builder b{mod};
 
@@ -64,7 +64,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateXor) {
+TEST_F(IR_BinaryTest, CreateXor) {
     Module mod;
     Builder b{mod};
 
@@ -84,7 +84,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateEqual) {
+TEST_F(IR_BinaryTest, CreateEqual) {
     Module mod;
     Builder b{mod};
 
@@ -104,7 +104,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateNotEqual) {
+TEST_F(IR_BinaryTest, CreateNotEqual) {
     Module mod;
     Builder b{mod};
 
@@ -124,7 +124,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateLessThan) {
+TEST_F(IR_BinaryTest, CreateLessThan) {
     Module mod;
     Builder b{mod};
 
@@ -144,7 +144,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateGreaterThan) {
+TEST_F(IR_BinaryTest, CreateGreaterThan) {
     Module mod;
     Builder b{mod};
 
@@ -164,7 +164,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateLessThanEqual) {
+TEST_F(IR_BinaryTest, CreateLessThanEqual) {
     Module mod;
     Builder b{mod};
 
@@ -184,7 +184,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
+TEST_F(IR_BinaryTest, CreateGreaterThanEqual) {
     Module mod;
     Builder b{mod};
 
@@ -204,7 +204,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateNot) {
+TEST_F(IR_BinaryTest, CreateNot) {
     Module mod;
     Builder b{mod};
     const auto* inst = b.Not(mod.Types().bool_(), b.Constant(true));
@@ -223,7 +223,7 @@
     EXPECT_FALSE(rhs->As<constant::Scalar<bool>>()->ValueAs<bool>());
 }
 
-TEST_F(IR_InstructionTest, CreateShiftLeft) {
+TEST_F(IR_BinaryTest, CreateShiftLeft) {
     Module mod;
     Builder b{mod};
 
@@ -243,7 +243,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateShiftRight) {
+TEST_F(IR_BinaryTest, CreateShiftRight) {
     Module mod;
     Builder b{mod};
 
@@ -263,7 +263,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateAdd) {
+TEST_F(IR_BinaryTest, CreateAdd) {
     Module mod;
     Builder b{mod};
 
@@ -283,7 +283,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateSubtract) {
+TEST_F(IR_BinaryTest, CreateSubtract) {
     Module mod;
     Builder b{mod};
 
@@ -303,7 +303,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateMultiply) {
+TEST_F(IR_BinaryTest, CreateMultiply) {
     Module mod;
     Builder b{mod};
 
@@ -323,7 +323,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateDivide) {
+TEST_F(IR_BinaryTest, CreateDivide) {
     Module mod;
     Builder b{mod};
 
@@ -343,7 +343,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateModulo) {
+TEST_F(IR_BinaryTest, CreateModulo) {
     Module mod;
     Builder b{mod};
 
@@ -363,7 +363,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, Binary_Usage) {
+TEST_F(IR_BinaryTest, Binary_Usage) {
     Module mod;
     Builder b{mod};
     const auto* inst = b.And(mod.Types().i32(), b.Constant(4_i), b.Constant(2_i));
@@ -379,7 +379,7 @@
     EXPECT_EQ(inst->RHS()->Usage()[0], inst);
 }
 
-TEST_F(IR_InstructionTest, Binary_Usage_DuplicateValue) {
+TEST_F(IR_BinaryTest, Binary_Usage_DuplicateValue) {
     Module mod;
     Builder b{mod};
     auto val = b.Constant(4_i);
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index bf66e39..d63853f 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -22,9 +22,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_InstructionTest = TestHelper;
+using IR_BitcastTest = TestHelper;
 
-TEST_F(IR_InstructionTest, Bitcast) {
+TEST_F(IR_BitcastTest, Bitcast) {
     Module mod;
     Builder b{mod};
     const auto* inst = b.Bitcast(mod.Types().i32(), b.Constant(4_i));
@@ -40,7 +40,7 @@
     EXPECT_EQ(4_i, val->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, Bitcast_Usage) {
+TEST_F(IR_BitcastTest, Bitcast_Usage) {
     Module mod;
     Builder b{mod};
     const auto* inst = b.Bitcast(mod.Types().i32(), b.Constant(4_i));
diff --git a/src/tint/ir/block.cc b/src/tint/ir/block.cc
index 4da1287..8942ea9 100644
--- a/src/tint/ir/block.cc
+++ b/src/tint/ir/block.cc
@@ -22,16 +22,141 @@
 
 Block::~Block() = default;
 
-void Block::AddInstruction(Instruction* inst) {
-    instructions_.Push(inst);
+void Block::Prepend(Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == nullptr);
+
     inst->SetBlock(this);
+    instructions_.count += 1;
+
+    if (instructions_.first == nullptr) {
+        instructions_.first = inst;
+        instructions_.last = inst;
+    } else {
+        inst->next = instructions_.first;
+        instructions_.first->prev = inst;
+        instructions_.first = inst;
+    }
+}
+
+void Block::Append(Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == nullptr);
+
+    inst->SetBlock(this);
+    instructions_.count += 1;
+
+    if (instructions_.first == nullptr) {
+        instructions_.first = inst;
+        instructions_.last = inst;
+    } else {
+        inst->prev = instructions_.last;
+        instructions_.last->next = inst;
+        instructions_.last = inst;
+    }
+}
+
+void Block::InsertBefore(Instruction* before, Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, before);
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, before->Block() == this);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == nullptr);
+
+    inst->SetBlock(this);
+    instructions_.count += 1;
+
+    inst->next = before;
+    inst->prev = before->prev;
+    before->prev = inst;
+
+    if (inst->prev) {
+        inst->prev->next = inst;
+    }
+
+    if (before == instructions_.first) {
+        instructions_.first = inst;
+    }
+}
+
+void Block::InsertAfter(Instruction* after, Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, after);
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, after->Block() == this);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == nullptr);
+
+    inst->SetBlock(this);
+    instructions_.count += 1;
+
+    inst->prev = after;
+    inst->next = after->next;
+    after->next = inst;
+
+    if (inst->next) {
+        inst->next->prev = inst;
+    }
+    if (after == instructions_.last) {
+        instructions_.last = inst;
+    }
+}
+
+void Block::Replace(Instruction* target, Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, target);
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, target->Block() == this);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == nullptr);
+
+    inst->SetBlock(this);
+    target->SetBlock(nullptr);
+
+    inst->next = target->next;
+    inst->prev = target->prev;
+
+    target->next = nullptr;
+    target->prev = nullptr;
+
+    if (inst->next) {
+        inst->next->prev = inst;
+    }
+    if (inst->prev) {
+        inst->prev->next = inst;
+    }
+
+    if (target == instructions_.first) {
+        instructions_.first = inst;
+    }
+    if (target == instructions_.last) {
+        instructions_.last = inst;
+    }
+}
+
+void Block::Remove(Instruction* inst) {
+    TINT_ASSERT_OR_RETURN(IR, inst);
+    TINT_ASSERT_OR_RETURN(IR, inst->Block() == this);
+
+    inst->SetBlock(nullptr);
+    instructions_.count -= 1;
+
+    if (inst->prev) {
+        inst->prev->next = inst->next;
+    }
+    if (inst->next) {
+        inst->next->prev = inst->prev;
+    }
+    if (inst == instructions_.first) {
+        instructions_.first = inst->next;
+    }
+    if (inst == instructions_.last) {
+        instructions_.last = inst->prev;
+    }
+
+    inst->prev = nullptr;
+    inst->next = nullptr;
 }
 
 void Block::SetInstructions(utils::VectorRef<Instruction*> instructions) {
     for (auto* i : instructions) {
-        i->SetBlock(this);
+        Append(i);
     }
-    instructions_ = std::move(instructions);
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 20a2da7..6b1697a 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -35,7 +35,7 @@
 
     /// @returns true if this is block has a branch target set
     bool HasBranchTarget() const {
-        return !instructions_.IsEmpty() && instructions_.Back()->Is<ir::Branch>();
+        return instructions_.last != nullptr && instructions_.last->Is<ir::Branch>();
     }
 
     /// @return the node this block branches to or nullptr if the block doesn't branch
@@ -43,7 +43,7 @@
         if (!HasBranchTarget()) {
             return nullptr;
         }
-        return instructions_.Back()->As<ir::Branch>();
+        return instructions_.last->As<ir::Branch>();
     }
 
     /// Sets the instructions in the block
@@ -51,11 +51,74 @@
     void SetInstructions(utils::VectorRef<Instruction*> instructions);
 
     /// @returns the instructions in the block
-    utils::VectorRef<const Instruction*> Instructions() const { return instructions_; }
+    Instruction* Instructions() const { return instructions_.first; }
 
+    /// Iterator for the instructions inside a block
+    class Iterator {
+      public:
+        /// Constructor
+        /// @param inst the instruction to start iterating from
+        explicit Iterator(Instruction* inst) : inst_(inst) {}
+        ~Iterator() = default;
+
+        /// Dereference operator
+        /// @returns the instruction for this iterator
+        Instruction* operator*() const { return inst_; }
+
+        /// Comparison operator
+        /// @param itr to compare against
+        /// @returns true if this iterator and @p itr point to the same instruction
+        bool operator==(const Iterator& itr) const { return itr.inst_ == inst_; }
+
+        /// Not equal operator
+        /// @param itr to compare against
+        /// @returns true if this iterator and @p itr point to different instructions
+        bool operator!=(const Iterator& itr) const { return !(*this == itr); }
+
+        /// Increment operator
+        /// @returns this iterator advanced to the next element
+        Iterator& operator++() {
+            inst_ = inst_->next;
+            return *this;
+        }
+
+      private:
+        Instruction* inst_ = nullptr;
+    };
+
+    /// @returns the iterator pointing to the start of the instruction list
+    Iterator begin() const { return Iterator{instructions_.first}; }
+
+    /// @returns the ending iterator
+    Iterator end() const { return Iterator{nullptr}; }
+
+    /// Adds the instruction to the beginning of the block
+    /// @param inst the instruction to add
+    void Prepend(Instruction* inst);
     /// Adds the instruction to the end of the block
     /// @param inst the instruction to add
-    void AddInstruction(Instruction* inst);
+    void Append(Instruction* inst);
+    /// Adds the new instruction before the given instruction
+    /// @param before the instruction to insert before
+    /// @param inst the instruction to insert
+    void InsertBefore(Instruction* before, Instruction* inst);
+    /// Adds the new instruction after the given instruction
+    /// @param after the instruction to insert after
+    /// @param inst the instruction to insert
+    void InsertAfter(Instruction* after, Instruction* inst);
+    /// Replaces the target instruction with the new instruction
+    /// @param target the instruction to replace
+    /// @param inst the instruction to insert
+    void Replace(Instruction* target, Instruction* inst);
+    /// Removes the target instruction
+    /// @param inst the instruction to remove
+    void Remove(Instruction* inst);
+
+    /// @returns true if the block contains no instructions
+    bool IsEmpty() const { return Length() == 0; }
+
+    /// @returns the number of instructions in the block
+    size_t Length() const { return instructions_.count; }
 
     /// Sets the params to the block
     /// @param params the params for the block
@@ -73,7 +136,12 @@
     void AddInboundBranch(ir::Branch* node) { inbound_branches_.Push(node); }
 
   private:
-    utils::Vector<const Instruction*, 16> instructions_;
+    struct {
+        Instruction* first = nullptr;
+        Instruction* last = nullptr;
+        size_t count = 0;
+    } instructions_;
+
     utils::Vector<const BlockParam*, 0> params_;
 
     /// The list of branches into this node. This list maybe empty for several
diff --git a/src/tint/ir/block_test.cc b/src/tint/ir/block_test.cc
new file mode 100644
index 0000000..adce60f
--- /dev/null
+++ b/src/tint/ir/block_test.cc
@@ -0,0 +1,668 @@
+// Copyright 2023 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/block.h"
+#include "gtest/gtest-spi.h"
+#include "gtest/gtest.h"
+#include "src/tint/ir/builder.h"
+#include "src/tint/ir/module.h"
+
+namespace tint::ir {
+namespace {
+
+class IR_BlockTest : public ::testing::Test {
+  public:
+    Module mod;
+    Builder b{mod};
+};
+
+TEST_F(IR_BlockTest, SetInstructions) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst1, inst2, inst3});
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Append) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Append(inst1);
+    blk->Append(inst2);
+    blk->Append(inst3);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Prepend) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Prepend(inst3);
+    blk->Prepend(inst2);
+    blk->Prepend(inst1);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, InsertBefore_AtStart) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Append(inst2);
+    blk->InsertBefore(inst2, inst1);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(2u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, InsertBefore_Middle) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Append(inst1);
+    blk->Append(inst3);
+    blk->InsertBefore(inst3, inst2);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, InsertAfter_AtEnd) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Append(inst1);
+    blk->InsertAfter(inst1, inst2);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(2u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, InsertAfter_Middle) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->Append(inst1);
+    blk->Append(inst3);
+    blk->InsertAfter(inst1, inst2);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Replace_Middle) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst3 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst1, inst4, inst3});
+    blk->Replace(inst4, inst2);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    ASSERT_EQ(inst3->Block(), blk);
+    EXPECT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(3u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst3);
+    ASSERT_EQ(inst->prev, inst2);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Replace_Start) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst4, inst2});
+    blk->Replace(inst4, inst1);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    EXPECT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(2u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Replace_End) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst1, inst4});
+    blk->Replace(inst4, inst2);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    ASSERT_EQ(inst2->Block(), blk);
+    EXPECT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(2u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Replace_OnlyNode) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst4});
+    blk->Replace(inst4, inst1);
+
+    ASSERT_EQ(inst1->Block(), blk);
+    EXPECT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(1u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Remove_Middle) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst1, inst4, inst2});
+    blk->Remove(inst4);
+
+    ASSERT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(2u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    inst = inst->next;
+
+    ASSERT_EQ(inst, inst2);
+    ASSERT_EQ(inst->prev, inst1);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Remove_Start) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst4, inst1});
+    blk->Remove(inst4);
+
+    ASSERT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(1u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Remove_End) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst1, inst4});
+    blk->Remove(inst4);
+
+    ASSERT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_FALSE(blk->IsEmpty());
+    EXPECT_EQ(1u, blk->Length());
+
+    auto* inst = blk->Instructions();
+    ASSERT_EQ(inst, inst1);
+    ASSERT_EQ(inst->prev, nullptr);
+    ASSERT_EQ(inst->next, nullptr);
+}
+
+TEST_F(IR_BlockTest, Remove_OnlyNode) {
+    auto* inst4 = b.CreateLoop();
+
+    auto* blk = b.CreateBlock();
+    blk->SetInstructions(utils::Vector{inst4});
+    blk->Remove(inst4);
+
+    ASSERT_EQ(inst4->Block(), nullptr);
+
+    EXPECT_TRUE(blk->IsEmpty());
+    EXPECT_EQ(0u, blk->Length());
+}
+
+TEST_F(IR_BlockTest, Fail_PrependNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* blk = b.CreateBlock();
+            blk->Prepend(nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_PrependAlreadyInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Prepend(inst1);
+
+            blk->Prepend(inst1);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_AppendNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* blk = b.CreateBlock();
+            blk->Append(nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_AppendAlreadyInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Append(inst1);
+            blk->Append(inst1);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertBeforeNullptrInst) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->InsertBefore(nullptr, inst1);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertBeforeInstNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Append(inst1);
+            blk->InsertBefore(inst1, nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertBeforeDifferentBlock) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            auto* blk2 = b.CreateBlock();
+            blk2->Append(inst1);
+            blk1->InsertBefore(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertBeforeAlreadyInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            blk1->Append(inst1);
+            blk1->Append(inst2);
+            blk1->InsertBefore(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertAfterNullptrInst) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->InsertAfter(nullptr, inst1);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertAfterInstNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Append(inst1);
+            blk->InsertAfter(inst1, nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertAfterDifferentBlock) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            auto* blk2 = b.CreateBlock();
+            blk2->Append(inst1);
+            blk1->InsertAfter(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_InsertAfterAlreadyInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            blk1->Append(inst1);
+            blk1->Append(inst2);
+            blk1->InsertAfter(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_ReplaceNullptrInst) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Replace(nullptr, inst1);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_ReplaceInstNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Append(inst1);
+            blk->Replace(inst1, nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_ReplaceDifferentBlock) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            auto* blk2 = b.CreateBlock();
+            blk2->Append(inst1);
+            blk1->Replace(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_ReplaceAlreadyInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            blk1->Append(inst1);
+            blk1->Append(inst2);
+            blk1->Replace(inst1, inst2);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_RemoveNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* blk = b.CreateBlock();
+            blk->Remove(nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(IR_BlockTest, Fail_RemoveDifferentBlock) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk1 = b.CreateBlock();
+            auto* blk2 = b.CreateBlock();
+            blk2->Append(inst1);
+            blk1->Remove(inst1);
+        },
+        "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index c7155d5..6ca9356 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -70,7 +70,7 @@
 }
 
 void Disassembler::EmitBlockInstructions(const Block* b) {
-    for (const auto* inst : b->Instructions()) {
+    for (const auto* inst : *b) {
         Indent();
         EmitInstruction(inst);
     }
diff --git a/src/tint/ir/discard_test.cc b/src/tint/ir/discard_test.cc
index 054727f..a09c2e9 100644
--- a/src/tint/ir/discard_test.cc
+++ b/src/tint/ir/discard_test.cc
@@ -19,9 +19,9 @@
 namespace tint::ir {
 namespace {
 
-using IR_InstructionTest = TestHelper;
+using IR_DiscardTest = TestHelper;
 
-TEST_F(IR_InstructionTest, Discard) {
+TEST_F(IR_DiscardTest, Discard) {
     Module mod;
     Builder b{mod};
 
diff --git a/src/tint/ir/from_program.cc b/src/tint/ir/from_program.cc
index 4a442ee..a3c36e3 100644
--- a/src/tint/ir/from_program.cc
+++ b/src/tint/ir/from_program.cc
@@ -173,7 +173,7 @@
         TINT_ASSERT(IR, current_block_);
         TINT_ASSERT(IR, !current_block_->HasBranchTarget());
 
-        current_block_->AddInstruction(br);
+        current_block_->Append(br);
         current_block_ = nullptr;
     }
 
@@ -487,7 +487,7 @@
             return;
         }
         auto store = builder_.Store(lhs.Get(), rhs.Get());
-        current_block_->AddInstruction(store);
+        current_block_->Append(store);
     }
 
     void EmitIncrementDecrement(const ast::IncrementDecrementStatement* stmt) {
@@ -498,7 +498,7 @@
 
         // Load from the LHS.
         auto* lhs_value = builder_.Load(lhs.Get());
-        current_block_->AddInstruction(lhs_value);
+        current_block_->Append(lhs_value);
 
         auto* ty = lhs_value->Type();
 
@@ -511,10 +511,10 @@
         } else {
             inst = builder_.Subtract(ty, lhs_value, rhs);
         }
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
 
         auto store = builder_.Store(lhs.Get(), inst);
-        current_block_->AddInstruction(store);
+        current_block_->Append(store);
     }
 
     void EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt) {
@@ -530,7 +530,7 @@
 
         // Load from the LHS.
         auto* lhs_value = builder_.Load(lhs.Get());
-        current_block_->AddInstruction(lhs_value);
+        current_block_->Append(lhs_value);
 
         auto* ty = lhs_value->Type();
 
@@ -580,10 +580,10 @@
                 TINT_ICE(IR, diagnostics_) << "missing binary operand type";
                 return;
         }
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
 
         auto store = builder_.Store(lhs.Get(), inst);
-        current_block_->AddInstruction(store);
+        current_block_->Append(store);
     }
 
     void EmitBlock(const ast::BlockStatement* block) {
@@ -603,7 +603,7 @@
             return;
         }
         auto* if_inst = builder_.CreateIf(reg.Get());
-        current_block_->AddInstruction(if_inst);
+        current_block_->Append(if_inst);
 
         {
             ControlStackScope scope(this, if_inst);
@@ -638,7 +638,7 @@
 
     void EmitLoop(const ast::LoopStatement* stmt) {
         auto* loop_inst = builder_.CreateLoop();
-        current_block_->AddInstruction(loop_inst);
+        current_block_->Append(loop_inst);
 
         {
             ControlStackScope scope(this, loop_inst);
@@ -682,7 +682,7 @@
 
     void EmitWhile(const ast::WhileStatement* stmt) {
         auto* loop_inst = builder_.CreateLoop();
-        current_block_->AddInstruction(loop_inst);
+        current_block_->Append(loop_inst);
 
         // Continue is always empty, just go back to the start
         current_block_ = loop_inst->Continuing();
@@ -701,7 +701,7 @@
 
             // Create an `if (cond) {} else {break;}` control flow
             auto* if_inst = builder_.CreateIf(reg.Get());
-            current_block_->AddInstruction(if_inst);
+            current_block_->Append(if_inst);
 
             current_block_ = if_inst->True();
             SetBranch(builder_.ExitIf(if_inst));
@@ -723,7 +723,7 @@
 
     void EmitForLoop(const ast::ForLoopStatement* stmt) {
         auto* loop_inst = builder_.CreateLoop();
-        current_block_->AddInstruction(loop_inst);
+        current_block_->Append(loop_inst);
 
         // Make sure the initializer ends up in a contained scope
         scopes_.Push();
@@ -748,7 +748,7 @@
 
                 // Create an `if (cond) {} else {break;}` control flow
                 auto* if_inst = builder_.CreateIf(reg.Get());
-                current_block_->AddInstruction(if_inst);
+                current_block_->Append(if_inst);
 
                 current_block_ = if_inst->True();
                 SetBranch(builder_.ExitIf(if_inst));
@@ -783,7 +783,7 @@
             return;
         }
         auto* switch_inst = builder_.CreateSwitch(reg.Get());
-        current_block_->AddInstruction(switch_inst);
+        current_block_->Append(switch_inst);
 
         {
             ControlStackScope scope(this, switch_inst);
@@ -856,7 +856,7 @@
     // figuring out the multi-level exit that is triggered.
     void EmitDiscard(const ast::DiscardStatement*) {
         auto* inst = builder_.Discard();
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
     }
 
     void EmitBreakIf(const ast::BreakIfStatement* stmt) {
@@ -914,7 +914,7 @@
         // If this expression maps to sem::Load, insert a load instruction to get the result.
         if (result && sem->Is<sem::Load>()) {
             auto* load = builder_.Load(result.Get());
-            current_block_->AddInstruction(load);
+            current_block_->Append(load);
             return load;
         }
 
@@ -940,7 +940,7 @@
                     }
                     val->SetInitializer(init.Get());
                 }
-                current_block_->AddInstruction(val);
+                current_block_->Append(val);
 
                 if (auto* gv = sem->As<sem::GlobalVariable>(); gv && var->HasBindingPoint()) {
                     val->SetBindingPoint(gv->BindingPoint().value().group,
@@ -1012,7 +1012,7 @@
                 break;
         }
 
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
         return inst;
     }
 
@@ -1036,7 +1036,7 @@
         }
 
         auto* if_inst = builder_.CreateIf(lhs.Get());
-        current_block_->AddInstruction(if_inst);
+        current_block_->Append(if_inst);
 
         auto* result = builder_.BlockParam(builder_.ir.Types().bool_());
         if_inst->Merge()->SetParams(utils::Vector{result});
@@ -1157,7 +1157,7 @@
                 return utils::Failure;
         }
 
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
         return inst;
     }
 
@@ -1171,7 +1171,7 @@
         auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
         auto* inst = builder_.Bitcast(ty, val.Get());
 
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
         return inst;
     }
 
@@ -1235,7 +1235,7 @@
         if (inst == nullptr) {
             return utils::Failure;
         }
-        current_block_->AddInstruction(inst);
+        current_block_->Append(inst);
         return inst;
     }
 
diff --git a/src/tint/ir/from_program_binary_test.cc b/src/tint/ir/from_program_binary_test.cc
index 5f7bcb2..2be72e1 100644
--- a/src/tint/ir/from_program_binary_test.cc
+++ b/src/tint/ir/from_program_binary_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramBinaryTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Add) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Add(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -49,7 +49,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Increment) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Increment) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = Increment("v1");
     WrapInFunction(expr);
@@ -73,7 +73,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAdd) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundAdd) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kAdd);
     WrapInFunction(expr);
@@ -97,7 +97,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Subtract) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Subtract) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Sub(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -120,7 +120,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Decrement) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Decrement) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
     auto* expr = Decrement("v1");
     WrapInFunction(expr);
@@ -144,7 +144,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundSubtract) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundSubtract) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kSubtract);
     WrapInFunction(expr);
@@ -168,7 +168,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Multiply) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Multiply) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Mul(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -191,7 +191,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundMultiply) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundMultiply) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kMultiply);
     WrapInFunction(expr);
@@ -215,7 +215,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Div) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Div) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Div(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -238,7 +238,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundDiv) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundDiv) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kDivide);
     WrapInFunction(expr);
@@ -262,7 +262,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Modulo) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Modulo) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Mod(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -285,7 +285,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundModulo) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundModulo) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kModulo);
     WrapInFunction(expr);
@@ -309,7 +309,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_And) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_And) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = And(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -332,7 +332,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAnd) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundAnd) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
     auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kAnd);
     WrapInFunction(expr);
@@ -356,7 +356,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Or) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Or) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Or(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -379,7 +379,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundOr) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundOr) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
     auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kOr);
     WrapInFunction(expr);
@@ -403,7 +403,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Xor) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Xor) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Xor(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -426,7 +426,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundXor) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundXor) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kXor);
     WrapInFunction(expr);
@@ -450,7 +450,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalAnd) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LogicalAnd) {
     Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
     auto* expr = If(LogicalAnd(Call("my_func"), false), Block());
     WrapInFunction(expr);
@@ -502,7 +502,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalOr) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LogicalOr) {
     Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
     auto* expr = If(LogicalOr(Call("my_func"), true), Block());
     WrapInFunction(expr);
@@ -554,7 +554,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Equal) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Equal) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Equal(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -577,7 +577,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_NotEqual) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_NotEqual) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = NotEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -600,7 +600,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThan) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LessThan) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = LessThan(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -623,7 +623,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThan) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_GreaterThan) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = GreaterThan(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -646,7 +646,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThanEqual) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LessThanEqual) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = LessThanEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -669,7 +669,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThanEqual) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_GreaterThanEqual) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = GreaterThanEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -692,7 +692,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftLeft) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_ShiftLeft) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Shl(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -715,7 +715,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftLeft) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundShiftLeft) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftLeft);
     WrapInFunction(expr);
@@ -739,7 +739,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftRight) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_ShiftRight) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Shr(Call("my_func"), 4_u);
     WrapInFunction(expr);
@@ -762,7 +762,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftRight) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_CompoundShiftRight) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
     auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftRight);
     WrapInFunction(expr);
@@ -786,7 +786,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Compound) {
     Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
 
     auto* expr = LogicalAnd(LessThan(Call("my_func"), 2_f),
@@ -831,7 +831,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound_WithConstEval) {
+TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_Compound_WithConstEval) {
     Func("my_func", utils::Vector{Param("p", ty.bool_())}, ty.bool_(), utils::Vector{Return(true)});
     auto* expr = Call("my_func", LogicalAnd(LessThan(2.4_f, 2_f),
                                             GreaterThan(2.5_f, Div(10_f, Mul(2.3_f, 9.4_f)))));
diff --git a/src/tint/ir/from_program_builtin_test.cc b/src/tint/ir/from_program_builtin_test.cc
index 4d21b48..7eafa17 100644
--- a/src/tint/ir/from_program_builtin_test.cc
+++ b/src/tint/ir/from_program_builtin_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramBuiltinTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Builtin) {
+TEST_F(IR_FromProgramBuiltinTest, EmitExpression_Builtin) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
     auto* expr = Call("asin", i);
     WrapInFunction(expr);
diff --git a/src/tint/ir/from_program_call_test.cc b/src/tint/ir/from_program_call_test.cc
index d305855..1aa224c 100644
--- a/src/tint/ir/from_program_call_test.cc
+++ b/src/tint/ir/from_program_call_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramCallTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Bitcast) {
+TEST_F(IR_FromProgramCallTest, EmitExpression_Bitcast) {
     Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
 
     auto* expr = Bitcast<f32>(Call("my_func"));
@@ -50,7 +50,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitStatement_Discard) {
+TEST_F(IR_FromProgramCallTest, EmitStatement_Discard) {
     auto* expr = Discard();
     Func("test_function", {}, ty.void_(), expr,
          utils::Vector{
@@ -69,7 +69,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
+TEST_F(IR_FromProgramCallTest, EmitStatement_UserFunction) {
     Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
 
     auto* stmt = CallStmt(Call("my_func", Mul(2_a, 3_a)));
@@ -91,7 +91,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Convert) {
+TEST_F(IR_FromProgramCallTest, EmitExpression_Convert) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
     auto* expr = Call(ty.f32(), i);
     WrapInFunction(expr);
@@ -114,7 +114,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_ConstructEmpty) {
+TEST_F(IR_FromProgramCallTest, EmitExpression_ConstructEmpty) {
     auto* expr = vec3(ty.f32());
     GlobalVar("i", builtin::AddressSpace::kPrivate, expr);
 
@@ -129,7 +129,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Construct) {
+TEST_F(IR_FromProgramCallTest, EmitExpression_Construct) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
     auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
     WrapInFunction(expr);
diff --git a/src/tint/ir/from_program_function_test.cc b/src/tint/ir/from_program_function_test.cc
index 57e78a3..a130281 100644
--- a/src/tint/ir/from_program_function_test.cc
+++ b/src/tint/ir/from_program_function_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramFunctionTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitFunction_Vertex) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_Vertex) {
     Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(0_f, 0_f, 0_f, 0_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
@@ -42,7 +42,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_Fragment) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_Fragment) {
     Func("test", utils::Empty, ty.void_(), utils::Empty,
          utils::Vector{Stage(ast::PipelineStage::kFragment)});
 
@@ -57,7 +57,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_Compute) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_Compute) {
     Func("test", utils::Empty, ty.void_(), utils::Empty,
          utils::Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(8_i, 4_i, 2_i)});
 
@@ -73,7 +73,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_Return) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_Return) {
     Func("test", utils::Empty, ty.vec3<f32>(), utils::Vector{Return(vec3<f32>(0_f, 0_f, 0_f))},
          utils::Empty);
 
@@ -88,7 +88,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnPosition) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnPosition) {
     Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
@@ -104,7 +104,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnPositionInvariant) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnPositionInvariant) {
     Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition), Invariant()});
@@ -121,7 +121,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnLocation) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnLocation) {
     Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kFragment)}, utils::Vector{Location(1_i)});
 
@@ -137,7 +137,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnLocation_Interpolate) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnLocation_Interpolate) {
     Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kFragment)},
          utils::Vector{Location(1_i), Interpolate(builtin::InterpolationType::kLinear,
@@ -156,7 +156,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnFragDepth) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnFragDepth) {
     Func("test", utils::Empty, ty.f32(), utils::Vector{Return(1_f)},
          utils::Vector{Stage(ast::PipelineStage::kFragment)},
          utils::Vector{Builtin(builtin::BuiltinValue::kFragDepth)});
@@ -172,7 +172,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitFunction_ReturnSampleMask) {
+TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnSampleMask) {
     Func("test", utils::Empty, ty.u32(), utils::Vector{Return(1_u)},
          utils::Vector{Stage(ast::PipelineStage::kFragment)},
          utils::Vector{Builtin(builtin::BuiltinValue::kSampleMask)});
diff --git a/src/tint/ir/from_program_literal_test.cc b/src/tint/ir/from_program_literal_test.cc
index f746bc0..1ea9706 100644
--- a/src/tint/ir/from_program_literal_test.cc
+++ b/src/tint/ir/from_program_literal_test.cc
@@ -26,13 +26,13 @@
 namespace {
 
 const Value* GlobalVarInitializer(const Module& m) {
-    const auto instr = m.root_block->Instructions();
-
-    if (instr.Length() == 0u) {
+    if (m.root_block->Length() == 0u) {
         ADD_FAILURE() << "m.root_block has no instruction";
         return nullptr;
     }
-    auto* var = instr[0]->As<ir::Var>();
+
+    const auto instr = m.root_block->Instructions();
+    auto* var = instr->As<ir::Var>();
     if (!var) {
         ADD_FAILURE() << "m.root_block.instructions[0] was not a var";
         return nullptr;
@@ -42,9 +42,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramLiteralTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_Bool_True) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_Bool_True) {
     auto* expr = Expr(true);
     GlobalVar("a", ty.bool_(), builtin::AddressSpace::kPrivate, expr);
 
@@ -58,7 +58,7 @@
     EXPECT_TRUE(val->As<constant::Scalar<bool>>()->ValueAs<bool>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_Bool_False) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_Bool_False) {
     auto* expr = Expr(false);
     GlobalVar("a", ty.bool_(), builtin::AddressSpace::kPrivate, expr);
 
@@ -72,7 +72,7 @@
     EXPECT_FALSE(val->As<constant::Scalar<bool>>()->ValueAs<bool>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_Bool_Deduped) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_Bool_Deduped) {
     GlobalVar("a", ty.bool_(), builtin::AddressSpace::kPrivate, Expr(true));
     GlobalVar("b", ty.bool_(), builtin::AddressSpace::kPrivate, Expr(false));
     GlobalVar("c", ty.bool_(), builtin::AddressSpace::kPrivate, Expr(true));
@@ -81,14 +81,20 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto instr = m.Get().root_block->Instructions();
-    auto* var_a = instr[0]->As<ir::Var>();
+    auto itr = m.Get().root_block->begin();
+    auto* var_a = (*itr)->As<ir::Var>();
+    ++itr;
+
     ASSERT_NE(var_a, nullptr);
-    auto* var_b = instr[1]->As<ir::Var>();
+    auto* var_b = (*itr)->As<ir::Var>();
+    ++itr;
+
     ASSERT_NE(var_b, nullptr);
-    auto* var_c = instr[2]->As<ir::Var>();
+    auto* var_c = (*itr)->As<ir::Var>();
+    ++itr;
+
     ASSERT_NE(var_c, nullptr);
-    auto* var_d = instr[3]->As<ir::Var>();
+    auto* var_d = (*itr)->As<ir::Var>();
     ASSERT_NE(var_d, nullptr);
 
     ASSERT_EQ(var_a->Initializer(), var_c->Initializer());
@@ -96,7 +102,7 @@
     ASSERT_NE(var_a->Initializer(), var_b->Initializer());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_F32) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_F32) {
     auto* expr = Expr(1.2_f);
     GlobalVar("a", ty.f32(), builtin::AddressSpace::kPrivate, expr);
 
@@ -110,7 +116,7 @@
     EXPECT_EQ(1.2_f, val->As<constant::Scalar<f32>>()->ValueAs<f32>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_F32_Deduped) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_F32_Deduped) {
     GlobalVar("a", ty.f32(), builtin::AddressSpace::kPrivate, Expr(1.2_f));
     GlobalVar("b", ty.f32(), builtin::AddressSpace::kPrivate, Expr(1.25_f));
     GlobalVar("c", ty.f32(), builtin::AddressSpace::kPrivate, Expr(1.2_f));
@@ -118,19 +124,23 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto instr = m.Get().root_block->Instructions();
-    auto* var_a = instr[0]->As<ir::Var>();
+    auto itr = m.Get().root_block->begin();
+    auto* var_a = (*itr)->As<ir::Var>();
     ASSERT_NE(var_a, nullptr);
-    auto* var_b = instr[1]->As<ir::Var>();
+    ++itr;
+
+    auto* var_b = (*itr)->As<ir::Var>();
     ASSERT_NE(var_b, nullptr);
-    auto* var_c = instr[2]->As<ir::Var>();
+    ++itr;
+
+    auto* var_c = (*itr)->As<ir::Var>();
     ASSERT_NE(var_c, nullptr);
 
     ASSERT_EQ(var_a->Initializer(), var_c->Initializer());
     ASSERT_NE(var_a->Initializer(), var_b->Initializer());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_F16) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_F16) {
     Enable(builtin::Extension::kF16);
     auto* expr = Expr(1.2_h);
     GlobalVar("a", ty.f16(), builtin::AddressSpace::kPrivate, expr);
@@ -145,7 +155,7 @@
     EXPECT_EQ(1.2_h, val->As<constant::Scalar<f16>>()->ValueAs<f32>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_F16_Deduped) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_F16_Deduped) {
     Enable(builtin::Extension::kF16);
     GlobalVar("a", ty.f16(), builtin::AddressSpace::kPrivate, Expr(1.2_h));
     GlobalVar("b", ty.f16(), builtin::AddressSpace::kPrivate, Expr(1.25_h));
@@ -154,19 +164,23 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto instr = m.Get().root_block->Instructions();
-    auto* var_a = instr[0]->As<ir::Var>();
+    auto itr = m.Get().root_block->begin();
+    auto* var_a = (*itr)->As<ir::Var>();
     ASSERT_NE(var_a, nullptr);
-    auto* var_b = instr[1]->As<ir::Var>();
+    ++itr;
+
+    auto* var_b = (*itr)->As<ir::Var>();
     ASSERT_NE(var_b, nullptr);
-    auto* var_c = instr[2]->As<ir::Var>();
+    ++itr;
+
+    auto* var_c = (*itr)->As<ir::Var>();
     ASSERT_NE(var_c, nullptr);
 
     ASSERT_EQ(var_a->Initializer(), var_c->Initializer());
     ASSERT_NE(var_a->Initializer(), var_b->Initializer());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_I32) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_I32) {
     auto* expr = Expr(-2_i);
     GlobalVar("a", ty.i32(), builtin::AddressSpace::kPrivate, expr);
 
@@ -180,7 +194,7 @@
     EXPECT_EQ(-2_i, val->As<constant::Scalar<i32>>()->ValueAs<f32>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_I32_Deduped) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_I32_Deduped) {
     GlobalVar("a", ty.i32(), builtin::AddressSpace::kPrivate, Expr(-2_i));
     GlobalVar("b", ty.i32(), builtin::AddressSpace::kPrivate, Expr(2_i));
     GlobalVar("c", ty.i32(), builtin::AddressSpace::kPrivate, Expr(-2_i));
@@ -188,19 +202,23 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto instr = m.Get().root_block->Instructions();
-    auto* var_a = instr[0]->As<ir::Var>();
+    auto itr = m.Get().root_block->begin();
+    auto* var_a = (*itr)->As<ir::Var>();
     ASSERT_NE(var_a, nullptr);
-    auto* var_b = instr[1]->As<ir::Var>();
+    ++itr;
+
+    auto* var_b = (*itr)->As<ir::Var>();
     ASSERT_NE(var_b, nullptr);
-    auto* var_c = instr[2]->As<ir::Var>();
+    ++itr;
+
+    auto* var_c = (*itr)->As<ir::Var>();
     ASSERT_NE(var_c, nullptr);
 
     ASSERT_EQ(var_a->Initializer(), var_c->Initializer());
     ASSERT_NE(var_a->Initializer(), var_b->Initializer());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_U32) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_U32) {
     auto* expr = Expr(2_u);
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
 
@@ -214,7 +232,7 @@
     EXPECT_EQ(2_u, val->As<constant::Scalar<u32>>()->ValueAs<f32>());
 }
 
-TEST_F(IR_BuilderImplTest, EmitLiteral_U32_Deduped) {
+TEST_F(IR_FromProgramLiteralTest, EmitLiteral_U32_Deduped) {
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, Expr(2_u));
     GlobalVar("b", ty.u32(), builtin::AddressSpace::kPrivate, Expr(3_u));
     GlobalVar("c", ty.u32(), builtin::AddressSpace::kPrivate, Expr(2_u));
@@ -222,12 +240,16 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto instr = m.Get().root_block->Instructions();
-    auto* var_a = instr[0]->As<ir::Var>();
+    auto itr = m.Get().root_block->begin();
+    auto* var_a = (*itr)->As<ir::Var>();
     ASSERT_NE(var_a, nullptr);
-    auto* var_b = instr[1]->As<ir::Var>();
+    ++itr;
+
+    auto* var_b = (*itr)->As<ir::Var>();
     ASSERT_NE(var_b, nullptr);
-    auto* var_c = instr[2]->As<ir::Var>();
+    ++itr;
+
+    auto* var_c = (*itr)->As<ir::Var>();
     ASSERT_NE(var_c, nullptr);
 
     ASSERT_EQ(var_a->Initializer(), var_c->Initializer());
diff --git a/src/tint/ir/from_program_materialize_test.cc b/src/tint/ir/from_program_materialize_test.cc
index 1262579..07b49c1 100644
--- a/src/tint/ir/from_program_materialize_test.cc
+++ b/src/tint/ir/from_program_materialize_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramMaterializeTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitExpression_MaterializedCall) {
+TEST_F(IR_FromProgramMaterializeTest, EmitExpression_MaterializedCall) {
     auto* expr = Return(Call("trunc", 2.5_f));
 
     Func("test_function", {}, ty.f32(), expr, utils::Empty);
diff --git a/src/tint/ir/from_program_store_test.cc b/src/tint/ir/from_program_store_test.cc
index 20ea290..daeed37 100644
--- a/src/tint/ir/from_program_store_test.cc
+++ b/src/tint/ir/from_program_store_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramStoreTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitStatement_Assign) {
+TEST_F(IR_FromProgramStoreTest, EmitStatement_Assign) {
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate);
 
     auto* expr = Assign("a", 4_u);
diff --git a/src/tint/ir/from_program_test.cc b/src/tint/ir/from_program_test.cc
index 404b6c3..d7825c5 100644
--- a/src/tint/ir/from_program_test.cc
+++ b/src/tint/ir/from_program_test.cc
@@ -51,9 +51,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, Func) {
+TEST_F(IR_FromProgramTest, Func) {
     Func("f", utils::Empty, ty.void_(), utils::Empty);
 
     auto m = Build();
@@ -74,7 +74,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithParam) {
+TEST_F(IR_FromProgramTest, Func_WithParam) {
     Func("f", utils::Vector{Param("a", ty.u32())}, ty.u32(), utils::Vector{Return("a")});
 
     auto m = Build();
@@ -95,7 +95,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithMultipleParam) {
+TEST_F(IR_FromProgramTest, Func_WithMultipleParam) {
     Func("f", utils::Vector{Param("a", ty.u32()), Param("b", ty.i32()), Param("c", ty.bool_())},
          ty.void_(), utils::Empty);
 
@@ -117,7 +117,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EntryPoint) {
+TEST_F(IR_FromProgramTest, EntryPoint) {
     Func("f", utils::Empty, ty.void_(), utils::Empty,
          utils::Vector{Stage(ast::PipelineStage::kFragment)});
 
@@ -127,7 +127,7 @@
     EXPECT_EQ(m->functions[0]->Stage(), Function::PipelineStage::kFragment);
 }
 
-TEST_F(IR_BuilderImplTest, IfStatement) {
+TEST_F(IR_FromProgramTest, IfStatement) {
     auto* ast_if = If(true, Block(), Else(Block()));
     WrapInFunction(ast_if);
 
@@ -167,7 +167,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, IfStatement_TrueReturns) {
+TEST_F(IR_FromProgramTest, IfStatement_TrueReturns) {
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_if);
 
@@ -207,7 +207,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, IfStatement_FalseReturns) {
+TEST_F(IR_FromProgramTest, IfStatement_FalseReturns) {
     auto* ast_if = If(true, Block(), Else(Block(Return())));
     WrapInFunction(ast_if);
 
@@ -247,7 +247,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, IfStatement_BothReturn) {
+TEST_F(IR_FromProgramTest, IfStatement_BothReturn) {
     auto* ast_if = If(true, Block(Return()), Else(Block(Return())));
     WrapInFunction(ast_if);
 
@@ -282,7 +282,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, IfStatement_JumpChainToMerge) {
+TEST_F(IR_FromProgramTest, IfStatement_JumpChainToMerge) {
     auto* ast_loop = Loop(Block(Break()));
     auto* ast_if = If(true, Block(ast_loop));
     WrapInFunction(ast_if);
@@ -330,7 +330,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithBreak) {
+TEST_F(IR_FromProgramTest, Loop_WithBreak) {
     auto* ast_loop = Loop(Block(Break()));
     WrapInFunction(ast_loop);
 
@@ -342,7 +342,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
 
@@ -364,7 +364,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithContinue) {
+TEST_F(IR_FromProgramTest, Loop_WithContinue) {
     auto* ast_if = If(true, Block(Break()));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
@@ -379,7 +379,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, loop_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
@@ -424,7 +424,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithContinuing_BreakIf) {
+TEST_F(IR_FromProgramTest, Loop_WithContinuing_BreakIf) {
     auto* ast_break_if = BreakIf(true);
     auto* ast_loop = Loop(Block(), Block(ast_break_if));
     WrapInFunction(ast_loop);
@@ -437,7 +437,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, loop_flow->Merge()->InboundBranches().Length());
 
@@ -464,7 +464,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_Continuing_Body_Scope) {
+TEST_F(IR_FromProgramTest, Loop_Continuing_Body_Scope) {
     auto* a = Decl(Let("a", Expr(true)));
     auto* ast_break_if = BreakIf("a");
     auto* ast_loop = Loop(Block(a), Block(ast_break_if));
@@ -497,7 +497,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithReturn) {
+TEST_F(IR_FromProgramTest, Loop_WithReturn) {
     auto* ast_if = If(true, Block(Return()));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
@@ -511,7 +511,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
@@ -551,7 +551,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithOnlyReturn) {
+TEST_F(IR_FromProgramTest, Loop_WithOnlyReturn) {
     auto* ast_loop = Loop(Block(Return(), Continue()));
     WrapInFunction(ast_loop, If(true, Block(Return())));
 
@@ -563,7 +563,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Merge()->InboundBranches().Length());
 
@@ -580,7 +580,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithOnlyReturn_ContinuingBreakIf) {
+TEST_F(IR_FromProgramTest, Loop_WithOnlyReturn_ContinuingBreakIf) {
     // Note, even though there is code in the loop merge (specifically, the
     // `ast_if` below), it doesn't get emitted as there is no way to reach the
     // loop merge due to the loop itself doing a `return`. This is why the
@@ -601,7 +601,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Merge()->InboundBranches().Length());
 
@@ -618,7 +618,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_WithIf_BothBranchesBreak) {
+TEST_F(IR_FromProgramTest, Loop_WithIf_BothBranchesBreak) {
     auto* ast_if = If(true, Block(Break()), Else(Block(Break())));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
@@ -632,7 +632,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop_flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(2u, loop_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
@@ -667,7 +667,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Loop_Nested) {
+TEST_F(IR_FromProgramTest, Loop_Nested) {
     auto* ast_if_a = If(true, Block(Break()));
     auto* ast_if_b = If(true, Block(Continue()));
     auto* ast_if_c = BreakIf(true);
@@ -789,7 +789,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, While) {
+TEST_F(IR_FromProgramTest, While) {
     auto* ast_while = While(false, Block());
     WrapInFunction(ast_while);
 
@@ -805,7 +805,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Body()->InboundBranches().Length());
     EXPECT_EQ(1u, flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
@@ -850,7 +850,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, While_Return) {
+TEST_F(IR_FromProgramTest, While_Return) {
     auto* ast_while = While(true, Block(Return()));
     WrapInFunction(ast_while);
 
@@ -866,7 +866,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
@@ -912,7 +912,7 @@
 }
 
 // TODO(dsinclair): Enable when variable declarations and increment are supported
-TEST_F(IR_BuilderImplTest, DISABLED_For) {
+TEST_F(IR_FromProgramTest, DISABLED_For) {
     // for(var i: 0; i < 10; i++) {
     // }
     //
@@ -950,7 +950,7 @@
     EXPECT_EQ(Disassemble(m), R"()");
 }
 
-TEST_F(IR_BuilderImplTest, For_NoInitCondOrContinuing) {
+TEST_F(IR_FromProgramTest, For_NoInitCondOrContinuing) {
     auto* ast_for = For(nullptr, nullptr, nullptr, Block(Break()));
     WrapInFunction(ast_for);
 
@@ -962,7 +962,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, flow->Body()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Body()->InboundBranches().Length());
     EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
     EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
 
@@ -984,7 +984,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Switch) {
+TEST_F(IR_FromProgramTest, Switch) {
     auto* ast_switch = Switch(
         1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)}, Block()),
                            Case(utils::Vector{CaseSelector(1_i)}, Block()), DefaultCase(Block())});
@@ -1049,7 +1049,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Switch_MultiSelector) {
+TEST_F(IR_FromProgramTest, Switch_MultiSelector) {
     auto* ast_switch = Switch(
         1_i,
         utils::Vector{Case(
@@ -1100,7 +1100,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Switch_OnlyDefault) {
+TEST_F(IR_FromProgramTest, Switch_OnlyDefault) {
     auto* ast_switch = Switch(1_i, utils::Vector{DefaultCase(Block())});
     WrapInFunction(ast_switch);
 
@@ -1139,7 +1139,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Switch_WithBreak) {
+TEST_F(IR_FromProgramTest, Switch_WithBreak) {
     auto* ast_switch = Switch(1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)},
                                                       Block(Break(), If(true, Block(Return())))),
                                                  DefaultCase(Block())});
@@ -1192,7 +1192,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Switch_AllReturn) {
+TEST_F(IR_FromProgramTest, Switch_AllReturn) {
     auto* ast_switch =
         Switch(1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)}, Block(Return())),
                                   DefaultCase(Block(Return()))});
@@ -1242,7 +1242,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_Phony) {
+TEST_F(IR_FromProgramTest, Emit_Phony) {
     Func("b", utils::Empty, ty.i32(), Return(1_i));
     WrapInFunction(Ignore(Call("b")));
 
@@ -1264,7 +1264,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithParam_WithAttribute_Invariant) {
+TEST_F(IR_FromProgramTest, Func_WithParam_WithAttribute_Invariant) {
     Func(
         "f",
         utils::Vector{Param("a", ty.vec4<f32>(),
@@ -1284,7 +1284,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithParam_WithAttribute_Location) {
+TEST_F(IR_FromProgramTest, Func_WithParam_WithAttribute_Location) {
     Func("f", utils::Vector{Param("a", ty.f32(), utils::Vector{Location(2_i)})}, ty.f32(),
          utils::Vector{Return("a")}, utils::Vector{Stage(ast::PipelineStage::kFragment)},
          utils::Vector{Location(1_i)});
@@ -1301,7 +1301,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithParam_WithAttribute_Location_WithInterpolation_LinearCentroid) {
+TEST_F(IR_FromProgramTest, Func_WithParam_WithAttribute_Location_WithInterpolation_LinearCentroid) {
     Func("f",
          utils::Vector{Param(
              "a", ty.f32(),
@@ -1323,7 +1323,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Func_WithParam_WithAttribute_Location_WithInterpolation_Flat) {
+TEST_F(IR_FromProgramTest, Func_WithParam_WithAttribute_Location_WithInterpolation_Flat) {
     Func("f",
          utils::Vector{
              Param("a", ty.f32(),
diff --git a/src/tint/ir/from_program_unary_test.cc b/src/tint/ir/from_program_unary_test.cc
index b83e5d5..a7af3dd 100644
--- a/src/tint/ir/from_program_unary_test.cc
+++ b/src/tint/ir/from_program_unary_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramUnaryTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Not) {
+TEST_F(IR_FromProgramUnaryTest, EmitExpression_Unary_Not) {
     Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(false)});
     auto* expr = Not(Call("my_func"));
     WrapInFunction(expr);
@@ -49,7 +49,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Complement) {
+TEST_F(IR_FromProgramUnaryTest, EmitExpression_Unary_Complement) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(1_u)});
     auto* expr = Complement(Call("my_func"));
     WrapInFunction(expr);
@@ -72,7 +72,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Negation) {
+TEST_F(IR_FromProgramUnaryTest, EmitExpression_Unary_Negation) {
     Func("my_func", utils::Empty, ty.i32(), utils::Vector{Return(1_i)});
     auto* expr = Negation(Call("my_func"));
     WrapInFunction(expr);
@@ -95,7 +95,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_AddressOf) {
+TEST_F(IR_FromProgramUnaryTest, EmitExpression_Unary_AddressOf) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
 
     auto* expr = Decl(Let("v2", AddressOf("v1")));
@@ -117,7 +117,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Indirection) {
+TEST_F(IR_FromProgramUnaryTest, EmitExpression_Unary_Indirection) {
     GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
     utils::Vector stmts = {
         Decl(Let("v3", AddressOf("v1"))),
diff --git a/src/tint/ir/from_program_var_test.cc b/src/tint/ir/from_program_var_test.cc
index 91b8b8f..5f6e78c 100644
--- a/src/tint/ir/from_program_var_test.cc
+++ b/src/tint/ir/from_program_var_test.cc
@@ -24,9 +24,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_BuilderImplTest = TestHelper;
+using IR_FromProgramVarTest = TestHelper;
 
-TEST_F(IR_BuilderImplTest, Emit_GlobalVar_NoInit) {
+TEST_F(IR_FromProgramVarTest, Emit_GlobalVar_NoInit) {
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate);
 
     auto m = Build();
@@ -40,7 +40,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_GlobalVar_Init) {
+TEST_F(IR_FromProgramVarTest, Emit_GlobalVar_Init) {
     auto* expr = Expr(2_u);
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
 
@@ -55,7 +55,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_GlobalVar_GroupBinding) {
+TEST_F(IR_FromProgramVarTest, Emit_GlobalVar_GroupBinding) {
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kStorage,
               utils::Vector{Group(2_u), Binding(3_u)});
 
@@ -70,7 +70,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_Var_NoInit) {
+TEST_F(IR_FromProgramVarTest, Emit_Var_NoInit) {
     auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction);
     WrapInFunction(a);
 
@@ -87,7 +87,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_Var_Init_Constant) {
+TEST_F(IR_FromProgramVarTest, Emit_Var_Init_Constant) {
     auto* expr = Expr(2_u);
     auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction, expr);
     WrapInFunction(a);
@@ -105,7 +105,7 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, Emit_Var_Init_NonConstant) {
+TEST_F(IR_FromProgramVarTest, Emit_Var_Init_NonConstant) {
     auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction);
     auto* b = Var("b", ty.u32(), builtin::AddressSpace::kFunction, Add("a", 2_u));
     WrapInFunction(a, b);
diff --git a/src/tint/ir/instruction.cc b/src/tint/ir/instruction.cc
index e54b13f..22e3904 100644
--- a/src/tint/ir/instruction.cc
+++ b/src/tint/ir/instruction.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/ir/instruction.h"
 
+#include "src/tint/debug.h"
+#include "src/tint/ir/block.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Instruction);
 
 namespace tint::ir {
@@ -22,4 +25,27 @@
 
 Instruction::~Instruction() = default;
 
+void Instruction::InsertBefore(Instruction* before) {
+    TINT_ASSERT_OR_RETURN(IR, before);
+    TINT_ASSERT_OR_RETURN(IR, before->Block() != nullptr);
+    before->Block()->InsertBefore(before, this);
+}
+
+void Instruction::InsertAfter(Instruction* after) {
+    TINT_ASSERT_OR_RETURN(IR, after);
+    TINT_ASSERT_OR_RETURN(IR, after->Block() != nullptr);
+    after->Block()->InsertAfter(after, this);
+}
+
+void Instruction::Replace(Instruction* replacement) {
+    TINT_ASSERT_OR_RETURN(IR, replacement);
+    TINT_ASSERT_OR_RETURN(IR, Block() != nullptr);
+    Block()->Replace(this, replacement);
+}
+
+void Instruction::Remove() {
+    TINT_ASSERT_OR_RETURN(IR, Block() != nullptr);
+    Block()->Remove(this);
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 13bcc00..a79b3da 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -37,10 +37,26 @@
 
     /// @returns the block that owns this instruction
     ir::Block* Block() { return block_; }
-
     /// @returns the block that owns this instruction
     const ir::Block* Block() const { return block_; }
 
+    /// Adds the new instruction before the given instruction in the owning block
+    /// @param before the instruction to insert before
+    void InsertBefore(Instruction* before);
+    /// Adds the new instruction after the given instruction in the owning block
+    /// @param after the instruction to insert after
+    void InsertAfter(Instruction* after);
+    /// Replaces this instruction with @p replacement in the owning block owning this instruction
+    /// @param replacement the instruction to replace with
+    void Replace(Instruction* replacement);
+    /// Removes this instruction from the owning block
+    void Remove();
+
+    /// Pointer to the next instruction in the list
+    Instruction* next = nullptr;
+    /// Pointer to the previous instruction in the list
+    Instruction* prev = nullptr;
+
   protected:
     /// Constructor
     Instruction();
diff --git a/src/tint/ir/instruction_test.cc b/src/tint/ir/instruction_test.cc
new file mode 100644
index 0000000..3fb8ab0
--- /dev/null
+++ b/src/tint/ir/instruction_test.cc
@@ -0,0 +1,162 @@
+// Copyright 2023 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 "gtest/gtest-spi.h"
+#include "gtest/gtest.h"
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/builder.h"
+#include "src/tint/ir/module.h"
+
+namespace tint::ir {
+namespace {
+
+class IR_InstructionTest : public ::testing::Test {
+  public:
+    Module mod;
+    Builder b{mod};
+};
+
+TEST_F(IR_InstructionTest, InsertBefore) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* blk = b.CreateBlock();
+    blk->Append(inst2);
+    inst1->InsertBefore(inst2);
+    EXPECT_EQ(2u, blk->Length());
+    EXPECT_EQ(inst1->Block(), blk);
+}
+
+TEST_F(IR_InstructionTest, Fail_InsertBeforeNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            inst1->InsertBefore(nullptr);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, Fail_InsertBeforeNotInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            inst1->InsertBefore(inst2);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, InsertAfter) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* blk = b.CreateBlock();
+    blk->Append(inst2);
+    inst1->InsertAfter(inst2);
+    EXPECT_EQ(2u, blk->Length());
+    EXPECT_EQ(inst1->Block(), blk);
+}
+
+TEST_F(IR_InstructionTest, Fail_InsertAfterNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            inst1->InsertAfter(nullptr);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, Fail_InsertAfterNotInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            inst1->InsertAfter(inst2);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, Replace) {
+    auto* inst1 = b.CreateLoop();
+    auto* inst2 = b.CreateLoop();
+    auto* blk = b.CreateBlock();
+    blk->Append(inst2);
+    inst2->Replace(inst1);
+    EXPECT_EQ(1u, blk->Length());
+    EXPECT_EQ(inst1->Block(), blk);
+    EXPECT_EQ(inst2->Block(), nullptr);
+}
+
+TEST_F(IR_InstructionTest, Fail_ReplaceNullptr) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* blk = b.CreateBlock();
+            blk->Append(inst1);
+            inst1->Replace(nullptr);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, Fail_ReplaceNotInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            auto* inst2 = b.CreateLoop();
+            inst1->Replace(inst2);
+        },
+        "");
+}
+
+TEST_F(IR_InstructionTest, Remove) {
+    auto* inst1 = b.CreateLoop();
+    auto* blk = b.CreateBlock();
+    blk->Append(inst1);
+    EXPECT_EQ(1u, blk->Length());
+
+    inst1->Remove();
+    EXPECT_EQ(0u, blk->Length());
+    EXPECT_EQ(inst1->Block(), nullptr);
+}
+
+TEST_F(IR_InstructionTest, Fail_RemoveNotInserted) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+
+            auto* inst1 = b.CreateLoop();
+            inst1->Remove();
+        },
+        "");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/load_test.cc b/src/tint/ir/load_test.cc
index 72359e7..02bc65b 100644
--- a/src/tint/ir/load_test.cc
+++ b/src/tint/ir/load_test.cc
@@ -21,9 +21,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_InstructionTest = TestHelper;
+using IR_LoadTest = TestHelper;
 
-TEST_F(IR_InstructionTest, CreateLoad) {
+TEST_F(IR_LoadTest, CreateLoad) {
     Module mod;
     Builder b{mod};
 
@@ -41,7 +41,7 @@
     EXPECT_EQ(inst->From(), var);
 }
 
-TEST_F(IR_InstructionTest, Load_Usage) {
+TEST_F(IR_LoadTest, Load_Usage) {
     Module mod;
     Builder b{mod};
 
diff --git a/src/tint/ir/loop.cc b/src/tint/ir/loop.cc
index 5b3e603..06764c8 100644
--- a/src/tint/ir/loop.cc
+++ b/src/tint/ir/loop.cc
@@ -28,6 +28,8 @@
     TINT_ASSERT(IR, body_);
     TINT_ASSERT(IR, continuing_);
     TINT_ASSERT(IR, merge_);
+
+    body_->AddInboundBranch(this);
 }
 
 Loop::~Loop() = default;
diff --git a/src/tint/ir/store_test.cc b/src/tint/ir/store_test.cc
index 1906cc6..bc68a98 100644
--- a/src/tint/ir/store_test.cc
+++ b/src/tint/ir/store_test.cc
@@ -21,9 +21,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_InstructionTest = TestHelper;
+using IR_StoreTest = TestHelper;
 
-TEST_F(IR_InstructionTest, CreateStore) {
+TEST_F(IR_StoreTest, CreateStore) {
     Module mod;
     Builder b{mod};
 
@@ -41,7 +41,7 @@
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, Store_Usage) {
+TEST_F(IR_StoreTest, Store_Usage) {
     Module mod;
     Builder b{mod};
 
diff --git a/src/tint/ir/to_program.cc b/src/tint/ir/to_program.cc
index 08436f3..33c9674 100644
--- a/src/tint/ir/to_program.cc
+++ b/src/tint/ir/to_program.cc
@@ -122,7 +122,7 @@
         while (block) {
             TINT_ASSERT(IR, block->HasBranchTarget());
 
-            for (auto* inst : block->Instructions()) {
+            for (auto* inst : *block) {
                 auto stmt = Stmt(inst);
                 if (TINT_UNLIKELY(!stmt)) {
                     return nullptr;
@@ -157,14 +157,14 @@
         }
 
         auto* false_blk = i->False();
-        if (false_blk->Instructions().Length() > 1 ||
-            (false_blk->Instructions().Length() == 1 && false_blk->HasBranchTarget() &&
-             !false_blk->Branch()->Is<ir::ExitIf>())) {
+        if (false_blk->Length() > 1 || (false_blk->Length() == 1 && false_blk->HasBranchTarget() &&
+                                        !false_blk->Branch()->Is<ir::ExitIf>())) {
             // If the else target is an `if` which has a merge target that just bounces to the outer
             // if merge target then emit an 'else if' instead of a block statement for the else.
-            if (auto* inst = i->False()->Instructions().Front()->As<ir::If>()) {
-                if (auto* br = inst->Merge()->Branch()->As<ir::ExitIf>(); br && br->If() == i) {
-                    auto* f = If(inst);
+            if (auto* inst = i->False()->Instructions(); inst && inst->As<ir::If>()) {
+                auto* if_ = inst->As<ir::If>();
+                if (auto* br = if_->Merge()->Branch()->As<ir::ExitIf>(); br && br->If() == i) {
+                    auto* f = If(if_);
                     if (!f) {
                         return nullptr;
                     }
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
index 92ad24c..d6e5d50 100644
--- a/src/tint/ir/unary_test.cc
+++ b/src/tint/ir/unary_test.cc
@@ -21,9 +21,9 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_InstructionTest = TestHelper;
+using IR_UnaryTest = TestHelper;
 
-TEST_F(IR_InstructionTest, CreateComplement) {
+TEST_F(IR_UnaryTest, CreateComplement) {
     Module mod;
     Builder b{mod};
     auto* inst = b.Complement(mod.Types().i32(), b.Constant(4_i));
@@ -37,7 +37,7 @@
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, CreateNegation) {
+TEST_F(IR_UnaryTest, CreateNegation) {
     Module mod;
     Builder b{mod};
     auto* inst = b.Negation(mod.Types().i32(), b.Constant(4_i));
@@ -51,7 +51,7 @@
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_InstructionTest, Unary_Usage) {
+TEST_F(IR_UnaryTest, Unary_Usage) {
     Module mod;
     Builder b{mod};
     auto* inst = b.Negation(mod.Types().i32(), b.Constant(4_i));
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 4ed4786..304a545 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -994,12 +994,11 @@
 
         // Consume continuing codepoint
         advance(n);
-    }
 
-    if (at(start) == '_') {
-        // Check for an underscore on its own (special token), or a
-        // double-underscore (not allowed).
-        if ((pos() == start + 1) || (at(start + 1) == '_')) {
+        if (pos() - start == 2 && substr(start, 2) == "__") {
+            // Identifiers prefixed with two or more underscores are not allowed.
+            // We check for these in the loop to bail early and prevent quadratic parse time for
+            // long sequences of ____.
             set_pos(start);
             return {};
         }
@@ -1008,8 +1007,8 @@
     auto str = substr(start, pos() - start);
     end_source(source);
 
-    if (auto t = check_keyword(source, str); t.has_value() && !t->IsUninitialized()) {
-        return t;
+    if (auto t = parse_keyword(str); t.has_value()) {
+        return Token{t.value(), source, str};
     }
 
     return Token{Token::Type::kIdentifier, source, str};
@@ -1194,92 +1193,95 @@
     return Token{type, source};
 }
 
-std::optional<Token> Lexer::check_keyword(const Source& source, std::string_view str) {
+std::optional<Token::Type> Lexer::parse_keyword(std::string_view str) {
     if (str == "alias") {
-        return Token{Token::Type::kAlias, source, "alias"};
+        return Token::Type::kAlias;
     }
     if (str == "bitcast") {
-        return Token{Token::Type::kBitcast, source, "bitcast"};
+        return Token::Type::kBitcast;
     }
     if (str == "break") {
-        return Token{Token::Type::kBreak, source, "break"};
+        return Token::Type::kBreak;
     }
     if (str == "case") {
-        return Token{Token::Type::kCase, source, "case"};
+        return Token::Type::kCase;
     }
     if (str == "const") {
-        return Token{Token::Type::kConst, source, "const"};
+        return Token::Type::kConst;
     }
     if (str == "const_assert") {
-        return Token{Token::Type::kConstAssert, source, "const_assert"};
+        return Token::Type::kConstAssert;
     }
     if (str == "continue") {
-        return Token{Token::Type::kContinue, source, "continue"};
+        return Token::Type::kContinue;
     }
     if (str == "continuing") {
-        return Token{Token::Type::kContinuing, source, "continuing"};
+        return Token::Type::kContinuing;
     }
     if (str == "diagnostic") {
-        return Token{Token::Type::kDiagnostic, source, "diagnostic"};
+        return Token::Type::kDiagnostic;
     }
     if (str == "discard") {
-        return Token{Token::Type::kDiscard, source, "discard"};
+        return Token::Type::kDiscard;
     }
     if (str == "default") {
-        return Token{Token::Type::kDefault, source, "default"};
+        return Token::Type::kDefault;
     }
     if (str == "else") {
-        return Token{Token::Type::kElse, source, "else"};
+        return Token::Type::kElse;
     }
     if (str == "enable") {
-        return Token{Token::Type::kEnable, source, "enable"};
+        return Token::Type::kEnable;
     }
     if (str == "fallthrough") {
-        return Token{Token::Type::kFallthrough, source, "fallthrough"};
+        return Token::Type::kFallthrough;
     }
     if (str == "false") {
-        return Token{Token::Type::kFalse, source, "false"};
+        return Token::Type::kFalse;
     }
     if (str == "fn") {
-        return Token{Token::Type::kFn, source, "fn"};
+        return Token::Type::kFn;
     }
     if (str == "for") {
-        return Token{Token::Type::kFor, source, "for"};
+        return Token::Type::kFor;
     }
     if (str == "if") {
-        return Token{Token::Type::kIf, source, "if"};
+        return Token::Type::kIf;
     }
     if (str == "let") {
-        return Token{Token::Type::kLet, source, "let"};
+        return Token::Type::kLet;
     }
     if (str == "loop") {
-        return Token{Token::Type::kLoop, source, "loop"};
+        return Token::Type::kLoop;
     }
     if (str == "override") {
-        return Token{Token::Type::kOverride, source, "override"};
+        return Token::Type::kOverride;
     }
     if (str == "return") {
-        return Token{Token::Type::kReturn, source, "return"};
+        return Token::Type::kReturn;
     }
     if (str == "requires") {
-        return Token{Token::Type::kRequires, source, "requires"};
+        return Token::Type::kRequires;
     }
     if (str == "struct") {
-        return Token{Token::Type::kStruct, source, "struct"};
+        return Token::Type::kStruct;
     }
     if (str == "switch") {
-        return Token{Token::Type::kSwitch, source, "switch"};
+        return Token::Type::kSwitch;
     }
     if (str == "true") {
-        return Token{Token::Type::kTrue, source, "true"};
+        return Token::Type::kTrue;
     }
     if (str == "var") {
-        return Token{Token::Type::kVar, source, "var"};
+        return Token::Type::kVar;
     }
     if (str == "while") {
-        return Token{Token::Type::kWhile, source, "while"};
+        return Token::Type::kWhile;
     }
-    return {};
+    if (str == "_") {
+        return Token::Type::kUnderscore;
+    }
+    return std::nullopt;
 }
 
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/lexer.h b/src/tint/reader/wgsl/lexer.h
index e56a79a..e0231d8 100644
--- a/src/tint/reader/wgsl/lexer.h
+++ b/src/tint/reader/wgsl/lexer.h
@@ -53,7 +53,7 @@
                                            size_t prefix_count,
                                            int32_t base);
 
-    std::optional<Token> check_keyword(const Source&, std::string_view);
+    std::optional<Token::Type> parse_keyword(std::string_view);
 
     /// The try_* methods have the following in common:
     /// - They assume there is at least one character to be consumed,
diff --git a/src/tint/type/manager.cc b/src/tint/type/manager.cc
index 4a98045..136f910 100644
--- a/src/tint/type/manager.cc
+++ b/src/tint/type/manager.cc
@@ -128,15 +128,18 @@
 const type::Array* Manager::array(const type::Type* elem_ty,
                                   uint32_t count,
                                   uint32_t stride /* = 0*/) {
+    uint32_t implicit_stride = utils::RoundUp(elem_ty->Align(), elem_ty->Size());
     if (stride == 0) {
-        stride = elem_ty->Align();
+        stride = implicit_stride;
     }
+    TINT_ASSERT(Type, stride >= implicit_stride);
+
     return Get<type::Array>(/* element type */ elem_ty,
                             /* element count */ Get<ConstantArrayCount>(count),
                             /* array alignment */ elem_ty->Align(),
                             /* array size */ count * stride,
                             /* element stride */ stride,
-                            /* implicit stride */ elem_ty->Align());
+                            /* implicit stride */ implicit_stride);
 }
 
 const type::Array* Manager::runtime_array(const type::Type* elem_ty, uint32_t stride /* = 0 */) {
diff --git a/src/tint/writer/spirv/builder_assign_test.cc b/src/tint/writer/spirv/builder_assign_test.cc
index cab0ed4..e37563a 100644
--- a/src/tint/writer/spirv/builder_assign_test.cc
+++ b/src/tint/writer/spirv/builder_assign_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
@@ -52,23 +53,23 @@
 }
 
 TEST_F(BuilderTest, Assign_Var_OutsideFunction_IsError) {
-    auto* v = GlobalVar("var", ty.f32(), builtin::AddressSpace::kPrivate);
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder pb;
 
-    auto* assign = Assign("var", Expr(1_f));
+            auto* v = pb.GlobalVar("var", pb.ty.f32(), builtin::AddressSpace::kPrivate);
 
-    WrapInFunction(assign);
+            auto* assign = pb.Assign("var", pb.Expr(1_f));
 
-    spirv::Builder& b = Build();
+            pb.WrapInFunction(assign);
 
-    EXPECT_TRUE(b.GenerateGlobalVariable(v)) << b.Diagnostics();
-    ASSERT_FALSE(b.has_error()) << b.Diagnostics();
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    tint::SetInternalCompilerErrorReporter(nullptr);
-
-    EXPECT_FALSE(b.GenerateAssignStatement(assign)) << b.Diagnostics();
-    EXPECT_TRUE(b.has_error());
-    EXPECT_THAT(b.Diagnostics().str(),
-                ::testing::HasSubstr("trying to add SPIR-V instruction 62 outside a function"));
+            b->GenerateGlobalVariable(v);
+            b->GenerateAssignStatement(assign);
+        },
+        "trying to add SPIR-V instruction 62 outside a function");
 }
 
 TEST_F(BuilderTest, Assign_Var_ZeroInitializer) {
diff --git a/src/tint/writer/spirv/builder_builtin_texture_test.cc b/src/tint/writer/spirv/builder_builtin_texture_test.cc
index 9d45519..6709f35 100644
--- a/src/tint/writer/spirv/builder_builtin_texture_test.cc
+++ b/src/tint/writer/spirv/builder_builtin_texture_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
 #include "src/tint/ast/builtin_texture_helper_test.h"
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/stage_attribute.h"
@@ -3761,34 +3762,38 @@
     Validate(b);
 }
 
-TEST_P(BuiltinTextureTest, OutsideFunction_IsError) {
-    auto param = GetParam();
+// TODO(dsinclair): This generates two fatal errors, but expect_fatal_failure can only handle 1
+TEST_P(BuiltinTextureTest, DISABLED_OutsideFunction_IsError) {
+    EXPECT_FATAL_FAILURE(
+        {
+            auto param = GetParam();
 
-    // The point of this test is to try to generate the texture
-    // builtin call outside a function.
+            // The point of this test is to try to generate the texture
+            // builtin call outside a function.
 
-    auto* texture = param.BuildTextureVariable(this);
-    auto* sampler = param.BuildSamplerVariable(this);
+            ProgramBuilder pb;
 
-    auto* call = Call(param.function, param.args(this));
-    auto* stmt = param.returns_value ? static_cast<const ast::Statement*>(Assign(Phony(), call))
-                                     : static_cast<const ast::Statement*>(CallStmt(call));
+            auto* texture = param.BuildTextureVariable(&pb);
+            auto* sampler = param.BuildSamplerVariable(&pb);
 
-    Func("func", utils::Empty, ty.void_(), utils::Vector{stmt},
-         utils::Vector{
-             Stage(ast::PipelineStage::kFragment),
-         });
+            auto* call = pb.Call(param.function, param.args(&pb));
+            auto* stmt = param.returns_value
+                             ? static_cast<const ast::Statement*>(pb.Assign(pb.Phony(), call))
+                             : static_cast<const ast::Statement*>(pb.CallStmt(call));
 
-    spirv::Builder& b = Build();
+            pb.Func("func", utils::Empty, pb.ty.void_(), utils::Vector{stmt},
+                    utils::Vector{
+                        pb.Stage(ast::PipelineStage::kFragment),
+                    });
 
-    tint::SetInternalCompilerErrorReporter(nullptr);
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    ASSERT_TRUE(b.GenerateGlobalVariable(texture)) << b.Diagnostics();
-    ASSERT_TRUE(b.GenerateGlobalVariable(sampler)) << b.Diagnostics();
-    EXPECT_EQ(b.GenerateExpression(call), 0u);
-    EXPECT_THAT(b.Diagnostics().str(),
-                ::testing::HasSubstr("Internal error: trying to add SPIR-V instruction "));
-    EXPECT_THAT(b.Diagnostics().str(), ::testing::HasSubstr(" outside a function"));
+            b->GenerateGlobalVariable(texture);
+            b->GenerateGlobalVariable(sampler);
+            b->GenerateExpression(call);
+        },
+        "Internal error: trying to add SPIR-V instruction ");
 }
 
 }  // namespace
diff --git a/src/tint/writer/spirv/builder_function_attribute_test.cc b/src/tint/writer/spirv/builder_function_attribute_test.cc
index c215f21..c47429b 100644
--- a/src/tint/writer/spirv/builder_function_attribute_test.cc
+++ b/src/tint/writer/spirv/builder_function_attribute_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/ast/workgroup_attribute.h"
 #include "src/tint/writer/spirv/spv_dump.h"
@@ -151,44 +152,44 @@
 }
 
 TEST_F(BuilderTest, Decoration_ExecutionMode_WorkgroupSize_OverridableConst) {
-    Override("width", ty.i32(), Call<i32>(2_i), Id(7_u));
-    Override("height", ty.i32(), Call<i32>(3_i), Id(8_u));
-    Override("depth", ty.i32(), Call<i32>(4_i), Id(9_u));
-    auto* func = Func("main", utils::Empty, ty.void_(), utils::Empty,
-                      utils::Vector{
-                          WorkgroupSize("width", "height", "depth"),
-                          Stage(ast::PipelineStage::kCompute),
-                      });
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder pb;
+            pb.Override("width", pb.ty.i32(), pb.Call<i32>(2_i), pb.Id(7_u));
+            pb.Override("height", pb.ty.i32(), pb.Call<i32>(3_i), pb.Id(8_u));
+            pb.Override("depth", pb.ty.i32(), pb.Call<i32>(4_i), pb.Id(9_u));
+            auto* func = pb.Func("main", utils::Empty, pb.ty.void_(), utils::Empty,
+                                 utils::Vector{
+                                     pb.WorkgroupSize("width", "height", "depth"),
+                                     pb.Stage(ast::PipelineStage::kCompute),
+                                 });
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    spirv::Builder& b = Build();
-
-    tint::SetInternalCompilerErrorReporter(nullptr);
-
-    EXPECT_FALSE(b.GenerateExecutionModes(func, 3)) << b.Diagnostics();
-    EXPECT_THAT(
-        b.Diagnostics().str(),
-        ::testing::HasSubstr(
-            "override-expressions should have been removed with the SubstituteOverride transform"));
+            b->GenerateExecutionModes(func, 3);
+        },
+        "override-expressions should have been removed with the SubstituteOverride transform");
 }
 
 TEST_F(BuilderTest, Decoration_ExecutionMode_WorkgroupSize_LiteralAndConst) {
-    Override("height", ty.i32(), Call<i32>(2_i), Id(7_u));
-    GlobalConst("depth", ty.i32(), Call<i32>(3_i));
-    auto* func = Func("main", utils::Empty, ty.void_(), utils::Empty,
-                      utils::Vector{
-                          WorkgroupSize(4_i, "height", "depth"),
-                          Stage(ast::PipelineStage::kCompute),
-                      });
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder pb;
 
-    spirv::Builder& b = Build();
+            pb.Override("height", pb.ty.i32(), pb.Call<i32>(2_i), pb.Id(7_u));
+            pb.GlobalConst("depth", pb.ty.i32(), pb.Call<i32>(3_i));
+            auto* func = pb.Func("main", utils::Empty, pb.ty.void_(), utils::Empty,
+                                 utils::Vector{
+                                     pb.WorkgroupSize(4_i, "height", "depth"),
+                                     pb.Stage(ast::PipelineStage::kCompute),
+                                 });
 
-    tint::SetInternalCompilerErrorReporter(nullptr);
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    EXPECT_FALSE(b.GenerateExecutionModes(func, 3)) << b.Diagnostics();
-    EXPECT_THAT(
-        b.Diagnostics().str(),
-        ::testing::HasSubstr(
-            "override-expressions should have been removed with the SubstituteOverride transform"));
+            b->GenerateExecutionModes(func, 3);
+        },
+        "override-expressions should have been removed with the SubstituteOverride transform");
 }
 
 TEST_F(BuilderTest, Decoration_ExecutionMode_MultipleFragment) {
diff --git a/src/tint/writer/spirv/builder_ident_expression_test.cc b/src/tint/writer/spirv/builder_ident_expression_test.cc
index 3917028..5ab7c19 100644
--- a/src/tint/writer/spirv/builder_ident_expression_test.cc
+++ b/src/tint/writer/spirv/builder_ident_expression_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "gtest/gtest-spi.h"
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
@@ -23,21 +24,24 @@
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, IdentifierExpression_GlobalConst) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder pb;
 
-    auto* v = GlobalConst("c", ty.vec3<f32>(), init);
+            auto* init = pb.vec3<f32>(1_f, 1_f, 3_f);
 
-    auto* expr = Expr("c");
-    WrapInFunction(expr);
+            auto* v = pb.GlobalConst("c", pb.ty.vec3<f32>(), init);
 
-    spirv::Builder& b = Build();
+            auto* expr = pb.Expr("c");
+            pb.WrapInFunction(expr);
 
-    EXPECT_TRUE(b.GenerateGlobalVariable(v)) << b.Diagnostics();
-    ASSERT_FALSE(b.has_error()) << b.Diagnostics();
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    EXPECT_EQ(DumpInstructions(b.Module().Types()), R"()");
-
-    EXPECT_EQ(b.GenerateIdentifierExpression(expr), 0u);
+            b->GenerateGlobalVariable(v);
+            b->GenerateIdentifierExpression(expr);
+        },
+        "internal compiler error: unable to find ID for variable: c");
 }
 
 TEST_F(BuilderTest, IdentifierExpression_GlobalVar) {
diff --git a/src/tint/writer/spirv/builder_if_test.cc b/src/tint/writer/spirv/builder_if_test.cc
index 3bd4bb2..fb34a68 100644
--- a/src/tint/writer/spirv/builder_if_test.cc
+++ b/src/tint/writer/spirv/builder_if_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
@@ -51,19 +52,20 @@
     // if (true) {
     // }
 
-    auto* block = Block();
-    auto* expr = If(true, block);
-    WrapInFunction(expr);
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder pb;
 
-    spirv::Builder& b = Build();
+            auto* block = pb.Block();
+            auto* expr = pb.If(true, block);
+            pb.WrapInFunction(expr);
 
-    tint::SetInternalCompilerErrorReporter(nullptr);
+            auto program = std::make_unique<Program>(std::move(pb));
+            auto b = std::make_unique<spirv::Builder>(program.get());
 
-    EXPECT_FALSE(b.GenerateIfStatement(expr)) << b.Diagnostics();
-    EXPECT_TRUE(b.has_error());
-    EXPECT_THAT(b.Diagnostics().str(),
-                ::testing::HasSubstr(
-                    "Internal error: trying to add SPIR-V instruction 247 outside a function"));
+            b->GenerateIfStatement(expr);
+        },
+        "Internal error: trying to add SPIR-V instruction 247 outside a function");
 }
 
 TEST_F(BuilderTest, If_WithStatements) {
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index 8ae01c8..89e6a1f 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -47,6 +47,7 @@
 #include "src/tint/type/i32.h"
 #include "src/tint/type/matrix.h"
 #include "src/tint/type/pointer.h"
+#include "src/tint/type/struct.h"
 #include "src/tint/type/type.h"
 #include "src/tint/type/u32.h"
 #include "src/tint/type/vector.h"
@@ -225,6 +226,7 @@
                     spv::Op::OpTypePointer,
                     {id, U32Operand(StorageClass(ptr->AddressSpace())), Type(ptr->StoreType())});
             },
+            [&](const type::Struct* str) { EmitStructType(id, str); },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_) << "unhandled type: " << ty->FriendlyName();
             });
@@ -237,12 +239,7 @@
         value,  //
         [&](const ir::Constant* constant) { return Constant(constant); },
         [&](const ir::Value*) {
-            auto id = values_.Find(value);
-            if (TINT_UNLIKELY(!id)) {
-                TINT_ICE(Writer, diagnostics_) << "missing result ID for value";
-                return 0u;
-            }
-            return *id;
+            return values_.GetOrCreate(value, [&] { return module_.NextId(); });
         });
 }
 
@@ -250,10 +247,49 @@
     return block_labels_.GetOrCreate(block, [&]() { return module_.NextId(); });
 }
 
+void GeneratorImplIr::EmitStructType(uint32_t id, const type::Struct* str) {
+    // Helper to return `type` or a potentially nested array element type within `type` as a matrix
+    // type, or nullptr if no such matrix type is present.
+    auto get_nested_matrix_type = [&](const type::Type* type) {
+        while (auto* arr = type->As<type::Array>()) {
+            type = arr->ElemType();
+        }
+        return type->As<type::Matrix>();
+    };
+
+    OperandList operands = {id};
+    for (auto* member : str->Members()) {
+        operands.push_back(Type(member->Type()));
+
+        // Generate struct member offset decoration.
+        module_.PushAnnot(
+            spv::Op::OpMemberDecorate,
+            {operands[0], member->Index(), U32Operand(SpvDecorationOffset), member->Offset()});
+
+        // Emit matrix layout decorations if necessary.
+        if (auto* matrix_type = get_nested_matrix_type(member->Type())) {
+            const uint32_t effective_row_count = (matrix_type->rows() == 2) ? 2 : 4;
+            module_.PushAnnot(spv::Op::OpMemberDecorate,
+                              {id, member->Index(), U32Operand(SpvDecorationColMajor)});
+            module_.PushAnnot(spv::Op::OpMemberDecorate,
+                              {id, member->Index(), U32Operand(SpvDecorationMatrixStride),
+                               Operand(effective_row_count * matrix_type->type()->Size())});
+        }
+
+        if (member->Name().IsValid()) {
+            module_.PushDebug(spv::Op::OpMemberName,
+                              {operands[0], member->Index(), Operand(member->Name().Name())});
+        }
+    }
+    module_.PushType(spv::Op::OpTypeStruct, std::move(operands));
+
+    if (str->Name().IsValid()) {
+        module_.PushDebug(spv::Op::OpName, {operands[0], Operand(str->Name().Name())});
+    }
+}
+
 void GeneratorImplIr::EmitFunction(const ir::Function* func) {
-    // Make an ID for the function.
-    auto id = module_.NextId();
-    values_.Add(func, id);
+    auto id = Value(func);
 
     // Emit the function name.
     module_.PushDebug(spv::Op::OpName, {id, Operand(ir_->NameOf(func).Name())});
@@ -272,9 +308,8 @@
     // Generate function parameter declarations and add their type IDs to the function signature.
     for (auto* param : func->Params()) {
         auto param_type_id = Type(param->Type());
-        auto param_id = module_.NextId();
+        auto param_id = Value(param);
         params.push_back(Instruction(spv::Op::OpFunctionParameter, {param_type_id, param_id}));
-        values_.Add(param, param_id);
         function_type.param_type_ids.Push(param_type_id);
         if (auto name = ir_->NameOf(param)) {
             module_.PushDebug(spv::Op::OpName, {param_id, Operand(name.Name())});
@@ -341,16 +376,14 @@
 }
 
 void GeneratorImplIr::EmitRootBlock(const ir::Block* root_block) {
-    for (auto* inst : root_block->Instructions()) {
-        auto result = Switch(
+    for (auto* inst : *root_block) {
+        Switch(
             inst,  //
             [&](const ir::Var* v) { return EmitVar(v); },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_)
                     << "unimplemented root block instruction: " << inst->TypeInfo().name;
-                return 0u;
             });
-        values_.Add(inst, result);
     }
 }
 
@@ -363,7 +396,7 @@
 
     // If there are no instructions in the block, it's a dead end, so we shouldn't be able to get
     // here to begin with.
-    if (block->Instructions().IsEmpty()) {
+    if (block->IsEmpty()) {
         current_function_.push_inst(spv::Op::OpUnreachable, {});
         return;
     }
@@ -371,9 +404,7 @@
     // Emit Phi nodes for all the incoming block parameters
     for (size_t param_idx = 0; param_idx < block->Params().Length(); param_idx++) {
         auto* param = block->Params()[param_idx];
-        auto id = module_.NextId();
-        values_.Add(param, id);
-        OperandList ops{Type(param->Type()), id};
+        OperandList ops{Type(param->Type()), Value(param)};
 
         for (auto* incoming : block->InboundBranches()) {
             auto* arg = incoming->Args()[param_idx];
@@ -385,40 +416,23 @@
     }
 
     // Emit the instructions.
-    for (auto* inst : block->Instructions()) {
-        auto result = Switch(
-            inst,  //
-            [&](const ir::Binary* b) { return EmitBinary(b); },
-            [&](const ir::Builtin* b) { return EmitBuiltin(b); },
-            [&](const ir::Load* l) { return EmitLoad(l); },
-            [&](const ir::Loop* l) {
-                EmitLoop(l);
-                return 0u;
-            },
-            [&](const ir::Switch* sw) {
-                EmitSwitch(sw);
-                return 0u;
-            },
-            [&](const ir::Store* s) {
-                EmitStore(s);
-                return 0u;
-            },
-            [&](const ir::UserCall* c) { return EmitUserCall(c); },
-            [&](const ir::Var* v) { return EmitVar(v); },
-            [&](const ir::If* i) {
-                EmitIf(i);
-                return 0u;
-            },
-            [&](const ir::Branch* b) {
-                EmitBranch(b);
-                return 0u;
-            },
+    for (auto* inst : *block) {
+        Switch(
+            inst,                                             //
+            [&](const ir::Binary* b) { EmitBinary(b); },      //
+            [&](const ir::Builtin* b) { EmitBuiltin(b); },    //
+            [&](const ir::Load* l) { EmitLoad(l); },          //
+            [&](const ir::Loop* l) { EmitLoop(l); },          //
+            [&](const ir::Switch* sw) { EmitSwitch(sw); },    //
+            [&](const ir::Store* s) { EmitStore(s); },        //
+            [&](const ir::UserCall* c) { EmitUserCall(c); },  //
+            [&](const ir::Var* v) { EmitVar(v); },            //
+            [&](const ir::If* i) { EmitIf(i); },              //
+            [&](const ir::Branch* b) { EmitBranch(b); },      //
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_)
                     << "unimplemented instruction: " << inst->TypeInfo().name;
-                return 0u;
             });
-        values_.Add(inst, result);
     }
 }
 
@@ -477,11 +491,11 @@
     uint32_t merge_label = Label(merge_block);
     uint32_t true_label = merge_label;
     uint32_t false_label = merge_label;
-    if (true_block->Instructions().Length() > 1 || !merge_block->Params().IsEmpty() ||
+    if (true_block->Length() > 1 || !merge_block->Params().IsEmpty() ||
         (true_block->HasBranchTarget() && !true_block->Branch()->Is<ir::ExitIf>())) {
         true_label = Label(true_block);
     }
-    if (false_block->Instructions().Length() > 1 || !merge_block->Params().IsEmpty() ||
+    if (false_block->Length() > 1 || !merge_block->Params().IsEmpty() ||
         (false_block->HasBranchTarget() && !false_block->Branch()->Is<ir::ExitIf>())) {
         false_label = Label(false_block);
     }
@@ -504,8 +518,8 @@
     EmitBlock(merge_block);
 }
 
-uint32_t GeneratorImplIr::EmitBinary(const ir::Binary* binary) {
-    auto id = module_.NextId();
+void GeneratorImplIr::EmitBinary(const ir::Binary* binary) {
+    auto id = Value(binary);
     auto* lhs_ty = binary->LHS()->Type();
 
     // Determine the opcode.
@@ -603,14 +617,20 @@
     // Emit the instruction.
     current_function_.push_inst(
         op, {Type(binary->Type()), id, Value(binary->LHS()), Value(binary->RHS())});
-
-    return id;
 }
 
-uint32_t GeneratorImplIr::EmitBuiltin(const ir::Builtin* builtin) {
-    auto id = module_.NextId();
+void GeneratorImplIr::EmitBuiltin(const ir::Builtin* builtin) {
     auto* result_ty = builtin->Type();
 
+    if (builtin->Func() == builtin::Function::kAbs &&
+        result_ty->is_unsigned_integer_scalar_or_vector()) {
+        // abs() is a no-op for unsigned integers.
+        values_.Add(builtin, Value(builtin->Args()[0]));
+        return;
+    }
+
+    auto id = Value(builtin);
+
     spv::Op op = spv::Op::Max;
     OperandList operands = {Type(result_ty), id};
 
@@ -634,9 +654,6 @@
                 glsl_ext_inst(GLSLstd450FAbs);
             } else if (result_ty->is_signed_integer_scalar_or_vector()) {
                 glsl_ext_inst(GLSLstd450SAbs);
-            } else if (result_ty->is_unsigned_integer_scalar_or_vector()) {
-                // abs() is a no-op for unsigned integers.
-                return Value(builtin->Args()[0]);
             }
             break;
         case builtin::Function::kMax:
@@ -659,7 +676,6 @@
             break;
         default:
             TINT_ICE(Writer, diagnostics_) << "unimplemented builtin function: " << builtin->Func();
-            return 0u;
     }
     TINT_ASSERT(Writer, op != spv::Op::Max);
 
@@ -670,14 +686,11 @@
 
     // Emit the instruction.
     current_function_.push_inst(op, operands);
-
-    return id;
 }
 
-uint32_t GeneratorImplIr::EmitLoad(const ir::Load* load) {
-    auto id = module_.NextId();
-    current_function_.push_inst(spv::Op::OpLoad, {Type(load->Type()), id, Value(load->From())});
-    return id;
+void GeneratorImplIr::EmitLoad(const ir::Load* load) {
+    current_function_.push_inst(spv::Op::OpLoad,
+                                {Type(load->Type()), Value(load), Value(load->From())});
 }
 
 void GeneratorImplIr::EmitLoop(const ir::Loop* loop) {
@@ -754,18 +767,17 @@
     current_function_.push_inst(spv::Op::OpStore, {Value(store->To()), Value(store->From())});
 }
 
-uint32_t GeneratorImplIr::EmitUserCall(const ir::UserCall* call) {
-    auto id = module_.NextId();
-    OperandList operands = {Type(call->Type()), id, values_.Get(call->Func()).value()};
+void GeneratorImplIr::EmitUserCall(const ir::UserCall* call) {
+    auto id = Value(call);
+    OperandList operands = {Type(call->Type()), id, Value(call->Func())};
     for (auto* arg : call->Args()) {
         operands.push_back(Value(arg));
     }
     current_function_.push_inst(spv::Op::OpFunctionCall, operands);
-    return id;
 }
 
-uint32_t GeneratorImplIr::EmitVar(const ir::Var* var) {
-    auto id = module_.NextId();
+void GeneratorImplIr::EmitVar(const ir::Var* var) {
+    auto id = Value(var);
     auto* ptr = var->Type()->As<type::Pointer>();
     TINT_ASSERT(Writer, ptr);
     auto ty = Type(ptr);
@@ -803,7 +815,6 @@
         default: {
             TINT_ICE(Writer, diagnostics_)
                 << "unimplemented variable address space " << ptr->AddressSpace();
-            return 0u;
         }
     }
 
@@ -811,8 +822,6 @@
     if (auto name = ir_->NameOf(var)) {
         module_.PushDebug(spv::Op::OpName, {id, Operand(name.Name())});
     }
-
-    return id;
 }
 
 }  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.h b/src/tint/writer/spirv/ir/generator_impl_ir.h
index 5793325..7c3105b 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.h
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.h
@@ -31,6 +31,7 @@
 namespace tint::ir {
 class Binary;
 class Block;
+class BlockParam;
 class Branch;
 class Builtin;
 class If;
@@ -45,6 +46,7 @@
 class Var;
 }  // namespace tint::ir
 namespace tint::type {
+class Struct;
 class Type;
 }  // namespace tint::type
 
@@ -96,6 +98,11 @@
     /// @returns the ID of the block's label
     uint32_t Label(const ir::Block* block);
 
+    /// Emit a struct type.
+    /// @param id the result ID to use
+    /// @param str the struct type to emit
+    void EmitStructType(uint32_t id, const type::Struct* str);
+
     /// Emit a function.
     /// @param func the function to emit
     void EmitFunction(const ir::Function* func);
@@ -119,18 +126,15 @@
 
     /// Emit a binary instruction.
     /// @param binary the binary instruction to emit
-    /// @returns the result ID of the instruction
-    uint32_t EmitBinary(const ir::Binary* binary);
+    void EmitBinary(const ir::Binary* binary);
 
     /// Emit a builtin function call instruction.
     /// @param call the builtin call instruction to emit
-    /// @returns the result ID of the instruction
-    uint32_t EmitBuiltin(const ir::Builtin* call);
+    void EmitBuiltin(const ir::Builtin* call);
 
     /// Emit a load instruction.
     /// @param load the load instruction to emit
-    /// @returns the result ID of the instruction
-    uint32_t EmitLoad(const ir::Load* load);
+    void EmitLoad(const ir::Load* load);
 
     /// Emit a loop instruction.
     /// @param loop the loop instruction to emit
@@ -146,13 +150,11 @@
 
     /// Emit a user call instruction.
     /// @param call the user call instruction to emit
-    /// @returns the result ID of the instruction
-    uint32_t EmitUserCall(const ir::UserCall* call);
+    void EmitUserCall(const ir::UserCall* call);
 
     /// Emit a var instruction.
     /// @param var the var instruction to emit
-    /// @returns the result ID of the instruction
-    uint32_t EmitVar(const ir::Var* var);
+    void EmitVar(const ir::Var* var);
 
     /// Emit a branch instruction.
     /// @param b the branch instruction to emit
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
index f62848e..3bc06c1 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
@@ -76,10 +76,10 @@
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
 %2 = OpTypeInt 32 0
 %3 = OpTypeFunction %2
-%6 = OpConstant %2 1
+%5 = OpConstant %2 1
 %1 = OpFunction %2 None %3
 %4 = OpLabel
-OpReturnValue %6
+OpReturnValue %5
 OpFunctionEnd
 )");
 }
@@ -95,12 +95,12 @@
 %3 = OpTypeInt 32 0
 %2 = OpTypeVector %3 2
 %4 = OpTypeFunction %2
-%8 = OpConstant %3 42
-%9 = OpConstant %3 10
-%7 = OpConstantComposite %2 %8 %9
+%7 = OpConstant %3 42
+%8 = OpConstant %3 10
+%6 = OpConstantComposite %2 %7 %8
 %1 = OpFunction %2 None %4
 %5 = OpLabel
-OpReturnValue %7
+OpReturnValue %6
 OpFunctionEnd
 )");
 }
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
index 1c9b9ac..8d88852 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
@@ -161,9 +161,9 @@
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%11 = OpTypeInt 32 1
-%12 = OpConstant %11 10
-%13 = OpConstant %11 20
+%10 = OpTypeInt 32 1
+%12 = OpConstant %10 10
+%13 = OpConstant %10 20
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %5 None
@@ -173,8 +173,8 @@
 %7 = OpLabel
 OpBranch %5
 %5 = OpLabel
-%10 = OpPhi %11 %12 %6 %13 %7
-OpReturnValue %10
+%11 = OpPhi %10 %12 %6 %13 %7
+OpReturnValue %11
 OpFunctionEnd
 )");
 }
@@ -277,9 +277,9 @@
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%11 = OpTypeInt 32 1
-%12 = OpConstant %11 10
-%13 = OpConstant %11 20
+%10 = OpTypeInt 32 1
+%12 = OpConstant %10 10
+%13 = OpConstant %10 20
 %15 = OpConstantFalse %9
 %1 = OpFunction %2 None %3
 %4 = OpLabel
@@ -290,9 +290,9 @@
 %7 = OpLabel
 OpBranch %5
 %5 = OpLabel
-%10 = OpPhi %11 %12 %6 %13 %7
+%11 = OpPhi %10 %12 %6 %13 %7
 %14 = OpPhi %9 %8 %6 %15 %7
-OpReturnValue %10
+OpReturnValue %11
 OpFunctionEnd
 )");
 }
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
index a32a751..577d9d2 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
@@ -24,11 +24,11 @@
 
     auto* loop = b.CreateLoop();
 
-    loop->Body()->AddInstruction(b.Continue(loop));
-    loop->Continuing()->AddInstruction(b.BreakIf(b.Constant(true), loop));
-    loop->Merge()->AddInstruction(b.Return(func));
+    loop->Body()->Append(b.Continue(loop));
+    loop->Continuing()->Append(b.BreakIf(b.Constant(true), loop));
+    loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -58,10 +58,10 @@
 
     auto* loop = b.CreateLoop();
 
-    loop->Body()->AddInstruction(b.ExitLoop(loop));
-    loop->Merge()->AddInstruction(b.Return(func));
+    loop->Body()->Append(b.ExitLoop(loop));
+    loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -89,15 +89,15 @@
     auto* loop = b.CreateLoop();
 
     auto* cond_break = b.CreateIf(b.Constant(true));
-    cond_break->True()->AddInstruction(b.ExitLoop(loop));
-    cond_break->False()->AddInstruction(b.ExitIf(cond_break));
-    cond_break->Merge()->AddInstruction(b.Continue(loop));
+    cond_break->True()->Append(b.ExitLoop(loop));
+    cond_break->False()->Append(b.ExitIf(cond_break));
+    cond_break->Merge()->Append(b.Continue(loop));
 
-    loop->Body()->AddInstruction(cond_break);
-    loop->Continuing()->AddInstruction(b.NextIteration(loop));
-    loop->Merge()->AddInstruction(b.Return(func));
+    loop->Body()->Append(cond_break);
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -132,15 +132,15 @@
     auto* loop = b.CreateLoop();
 
     auto* cond_break = b.CreateIf(b.Constant(true));
-    cond_break->True()->AddInstruction(b.Continue(loop));
-    cond_break->False()->AddInstruction(b.ExitIf(cond_break));
-    cond_break->Merge()->AddInstruction(b.ExitLoop(loop));
+    cond_break->True()->Append(b.Continue(loop));
+    cond_break->False()->Append(b.ExitIf(cond_break));
+    cond_break->Merge()->Append(b.ExitLoop(loop));
 
-    loop->Body()->AddInstruction(cond_break);
-    loop->Continuing()->AddInstruction(b.NextIteration(loop));
-    loop->Merge()->AddInstruction(b.Return(func));
+    loop->Body()->Append(cond_break);
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -176,9 +176,9 @@
 
     auto* loop = b.CreateLoop();
 
-    loop->Body()->AddInstruction(b.Return(func));
+    loop->Body()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -207,11 +207,11 @@
 
     auto* result = b.Equal(mod.Types().i32(), b.Constant(1_i), b.Constant(2_i));
 
-    loop->Body()->AddInstruction(result);
-    loop->Continuing()->AddInstruction(b.BreakIf(result, loop));
-    loop->Merge()->AddInstruction(b.Return(func));
+    loop->Body()->Append(result);
+    loop->Continuing()->Append(b.BreakIf(result, loop));
+    loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(loop);
+    func->StartTarget()->Append(loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -242,15 +242,15 @@
     auto* outer_loop = b.CreateLoop();
     auto* inner_loop = b.CreateLoop();
 
-    inner_loop->Body()->AddInstruction(b.ExitLoop(inner_loop));
-    inner_loop->Continuing()->AddInstruction(b.NextIteration(inner_loop));
-    inner_loop->Merge()->AddInstruction(b.Continue(outer_loop));
+    inner_loop->Body()->Append(b.ExitLoop(inner_loop));
+    inner_loop->Continuing()->Append(b.NextIteration(inner_loop));
+    inner_loop->Merge()->Append(b.Continue(outer_loop));
 
-    outer_loop->Body()->AddInstruction(inner_loop);
-    outer_loop->Continuing()->AddInstruction(b.BreakIf(b.Constant(true), outer_loop));
-    outer_loop->Merge()->AddInstruction(b.Return(func));
+    outer_loop->Body()->Append(inner_loop);
+    outer_loop->Continuing()->Append(b.BreakIf(b.Constant(true), outer_loop));
+    outer_loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(outer_loop);
+    func->StartTarget()->Append(outer_loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -289,15 +289,15 @@
     auto* outer_loop = b.CreateLoop();
     auto* inner_loop = b.CreateLoop();
 
-    inner_loop->Body()->AddInstruction(b.Continue(inner_loop));
-    inner_loop->Continuing()->AddInstruction(b.BreakIf(b.Constant(true), inner_loop));
-    inner_loop->Merge()->AddInstruction(b.BreakIf(b.Constant(true), outer_loop));
+    inner_loop->Body()->Append(b.Continue(inner_loop));
+    inner_loop->Continuing()->Append(b.BreakIf(b.Constant(true), inner_loop));
+    inner_loop->Merge()->Append(b.BreakIf(b.Constant(true), outer_loop));
 
-    outer_loop->Body()->AddInstruction(b.Continue(outer_loop));
-    outer_loop->Continuing()->AddInstruction(inner_loop);
-    outer_loop->Merge()->AddInstruction(b.Return(func));
+    outer_loop->Body()->Append(b.Continue(outer_loop));
+    outer_loop->Continuing()->Append(inner_loop);
+    outer_loop->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(outer_loop);
+    func->StartTarget()->Append(outer_loop);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -330,5 +330,105 @@
 )");
 }
 
+TEST_F(SpvGeneratorImplTest, Loop_Phi_SingleValue) {
+    auto* func = b.CreateFunction("foo", mod.Types().void_());
+
+    auto* l = b.CreateLoop(utils::Vector{b.Constant(1_i)});
+    func->StartTarget()->Append(l);
+
+    auto* loop_param = b.BlockParam(b.ir.Types().i32());
+    l->Body()->SetParams(utils::Vector{loop_param});
+    auto* inc = b.Add(b.ir.Types().i32(), loop_param, b.Constant(1_i));
+    l->Body()->Append(inc);
+    l->Body()->Append(b.Continue(l, utils::Vector{inc}));
+
+    auto* cont_param = b.BlockParam(b.ir.Types().i32());
+    l->Continuing()->SetParams(utils::Vector{cont_param});
+    auto* cmp = b.GreaterThan(b.ir.Types().bool_(), cont_param, b.Constant(5_i));
+    l->Continuing()->Append(cmp);
+    l->Continuing()->Append(b.BreakIf(cmp, l, utils::Vector{cont_param}));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%9 = OpTypeInt 32 1
+%11 = OpConstant %9 1
+%16 = OpTypeBool
+%17 = OpConstant %9 5
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+OpBranch %5
+%5 = OpLabel
+OpLoopMerge %8 %7 None
+OpBranch %6
+%6 = OpLabel
+%10 = OpPhi %9 %11 %12 %13 %7
+%14 = OpIAdd %9 %10 %11
+OpBranch %7
+%7 = OpLabel
+%13 = OpPhi %9 %14 %5
+%15 = OpSGreaterThan %16 %13 %17
+OpBranchConditional %15 %8 %5
+%8 = OpLabel
+OpUnreachable
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Loop_Phi_MultipleValue) {
+    auto* func = b.CreateFunction("foo", mod.Types().void_());
+
+    auto* l = b.CreateLoop(utils::Vector{b.Constant(1_i), b.Constant(false)});
+    func->StartTarget()->Append(l);
+
+    auto* loop_param_a = b.BlockParam(b.ir.Types().i32());
+    auto* loop_param_b = b.BlockParam(b.ir.Types().bool_());
+    l->Body()->SetParams(utils::Vector{loop_param_a, loop_param_b});
+    auto* inc = b.Add(b.ir.Types().i32(), loop_param_a, b.Constant(1_i));
+    l->Body()->Append(inc);
+    l->Body()->Append(b.Continue(l, utils::Vector{inc, loop_param_b}));
+
+    auto* cont_param_a = b.BlockParam(b.ir.Types().i32());
+    auto* cont_param_b = b.BlockParam(b.ir.Types().bool_());
+    l->Continuing()->SetParams(utils::Vector{cont_param_a, cont_param_b});
+    auto* cmp = b.GreaterThan(b.ir.Types().bool_(), cont_param_a, b.Constant(5_i));
+    l->Continuing()->Append(cmp);
+    auto* not_b = b.Not(b.ir.Types().bool_(), cont_param_b);
+    l->Continuing()->Append(not_b);
+    l->Continuing()->Append(b.BreakIf(cmp, l, utils::Vector{cont_param_a, not_b}));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%9 = OpTypeInt 32 1
+%11 = OpConstant %9 1
+%14 = OpTypeBool
+%16 = OpConstantFalse %14
+%21 = OpConstant %9 5
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+OpBranch %5
+%5 = OpLabel
+OpLoopMerge %8 %7 None
+OpBranch %6
+%6 = OpLabel
+%10 = OpPhi %9 %11 %12 %13 %7
+%15 = OpPhi %14 %16 %12 %17 %7
+%18 = OpIAdd %9 %10 %11
+OpBranch %7
+%7 = OpLabel
+%13 = OpPhi %9 %18 %5
+%19 = OpPhi %14 %15 %5
+%20 = OpSGreaterThan %14 %13 %21
+%17 = OpLogicalEqual %14 %19 %16
+OpBranchConditional %20 %8 %5
+%8 = OpLabel
+OpUnreachable
+OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
index 1c4d5d0..2773ae0 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
@@ -25,11 +25,11 @@
     auto* swtch = b.CreateSwitch(b.Constant(42_i));
 
     auto* def_case = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector()});
-    def_case->AddInstruction(b.ExitSwitch(swtch));
+    def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->AddInstruction(b.Return(func));
+    swtch->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(swtch);
+    func->StartTarget()->Append(swtch);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -55,17 +55,17 @@
     auto* swtch = b.CreateSwitch(b.Constant(42_i));
 
     auto* case_a = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)}});
-    case_a->AddInstruction(b.ExitSwitch(swtch));
+    case_a->Append(b.ExitSwitch(swtch));
 
     auto* case_b = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
-    case_b->AddInstruction(b.ExitSwitch(swtch));
+    case_b->Append(b.ExitSwitch(swtch));
 
     auto* def_case = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector()});
-    def_case->AddInstruction(b.ExitSwitch(swtch));
+    def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->AddInstruction(b.Return(func));
+    swtch->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(swtch);
+    func->StartTarget()->Append(swtch);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -96,19 +96,19 @@
 
     auto* case_a = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                                      ir::Switch::CaseSelector{b.Constant(3_i)}});
-    case_a->AddInstruction(b.ExitSwitch(swtch));
+    case_a->Append(b.ExitSwitch(swtch));
 
     auto* case_b = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)},
                                                      ir::Switch::CaseSelector{b.Constant(4_i)}});
-    case_b->AddInstruction(b.ExitSwitch(swtch));
+    case_b->Append(b.ExitSwitch(swtch));
 
     auto* def_case = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(5_i)},
                                                        ir::Switch::CaseSelector()});
-    def_case->AddInstruction(b.ExitSwitch(swtch));
+    def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->AddInstruction(b.Return(func));
+    swtch->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(swtch);
+    func->StartTarget()->Append(swtch);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -138,15 +138,15 @@
     auto* swtch = b.CreateSwitch(b.Constant(42_i));
 
     auto* case_a = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)}});
-    case_a->AddInstruction(b.Return(func));
+    case_a->Append(b.Return(func));
 
     auto* case_b = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
-    case_b->AddInstruction(b.Return(func));
+    case_b->Append(b.Return(func));
 
     auto* def_case = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector()});
-    def_case->AddInstruction(b.Return(func));
+    def_case->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(swtch);
+    func->StartTarget()->Append(swtch);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -176,19 +176,19 @@
     auto* swtch = b.CreateSwitch(b.Constant(42_i));
 
     auto* cond_break = b.CreateIf(b.Constant(true));
-    cond_break->True()->AddInstruction(b.ExitSwitch(swtch));
-    cond_break->False()->AddInstruction(b.ExitIf(cond_break));
-    cond_break->Merge()->AddInstruction(b.Return(func));
+    cond_break->True()->Append(b.ExitSwitch(swtch));
+    cond_break->False()->Append(b.ExitIf(cond_break));
+    cond_break->Merge()->Append(b.Return(func));
 
     auto* case_a = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)}});
-    case_a->AddInstruction(cond_break);
+    case_a->Append(cond_break);
 
     auto* def_case = b.CreateCase(swtch, utils::Vector{ir::Switch::CaseSelector()});
-    def_case->AddInstruction(b.ExitSwitch(swtch));
+    def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->AddInstruction(b.Return(func));
+    swtch->Merge()->Append(b.Return(func));
 
-    func->StartTarget()->AddInstruction(swtch);
+    func->StartTarget()->Append(swtch);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -225,13 +225,13 @@
     auto* s = b.CreateSwitch(b.Constant(42_i));
     auto* case_a = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                                  ir::Switch::CaseSelector{nullptr}});
-    case_a->AddInstruction(b.ExitSwitch(s, utils::Vector{b.Constant(10_i)}));
+    case_a->Append(b.ExitSwitch(s, utils::Vector{b.Constant(10_i)}));
 
     auto* case_b = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
-    case_b->AddInstruction(b.ExitSwitch(s, utils::Vector{b.Constant(20_i)}));
+    case_b->Append(b.ExitSwitch(s, utils::Vector{b.Constant(20_i)}));
 
     s->Merge()->SetParams(utils::Vector{merge_param});
-    s->Merge()->AddInstruction(b.Return(func));
+    s->Merge()->Append(b.Return(func));
 
     func->StartTarget()->SetInstructions(utils::Vector{s});
 
@@ -264,13 +264,13 @@
     auto* s = b.CreateSwitch(b.Constant(42_i));
     auto* case_a = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                                  ir::Switch::CaseSelector{nullptr}});
-    case_a->AddInstruction(b.Return(func, utils::Vector{b.Constant(10_i)}));
+    case_a->Append(b.Return(func, utils::Vector{b.Constant(10_i)}));
 
     auto* case_b = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
-    case_b->AddInstruction(b.ExitSwitch(s, utils::Vector{b.Constant(20_i)}));
+    case_b->Append(b.ExitSwitch(s, utils::Vector{b.Constant(20_i)}));
 
     s->Merge()->SetParams(utils::Vector{b.BlockParam(b.ir.Types().i32())});
-    s->Merge()->AddInstruction(b.Return(func));
+    s->Merge()->Append(b.Return(func));
 
     func->StartTarget()->SetInstructions(utils::Vector{s});
 
@@ -306,13 +306,13 @@
     auto* s = b.CreateSwitch(b.Constant(42_i));
     auto* case_a = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                                  ir::Switch::CaseSelector{nullptr}});
-    case_a->AddInstruction(b.ExitSwitch(s, utils::Vector{b.Constant(10_i), b.Constant(true)}));
+    case_a->Append(b.ExitSwitch(s, utils::Vector{b.Constant(10_i), b.Constant(true)}));
 
     auto* case_b = b.CreateCase(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
-    case_b->AddInstruction(b.ExitSwitch(s, utils::Vector{b.Constant(20_i), b.Constant(false)}));
+    case_b->Append(b.ExitSwitch(s, utils::Vector{b.Constant(20_i), b.Constant(false)}));
 
     s->Merge()->SetParams(utils::Vector{merge_param_0, merge_param_1});
-    s->Merge()->AddInstruction(b.Return(func, utils::Vector{merge_param_0}));
+    s->Merge()->Append(b.Return(func, utils::Vector{merge_param_0}));
 
     func->StartTarget()->SetInstructions(utils::Vector{s});
 
@@ -324,9 +324,9 @@
 %6 = OpConstant %7 42
 %11 = OpConstant %7 10
 %12 = OpConstant %7 20
-%14 = OpTypeBool
-%15 = OpConstantTrue %14
-%16 = OpConstantFalse %14
+%13 = OpTypeBool
+%15 = OpConstantTrue %13
+%16 = OpConstantFalse %13
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %9 None
@@ -337,7 +337,7 @@
 OpBranch %9
 %9 = OpLabel
 %10 = OpPhi %7 %11 %5 %12 %8
-%13 = OpPhi %14 %15 %5 %16 %8
+%14 = OpPhi %13 %15 %5 %16 %8
 OpReturnValue %10
 OpFunctionEnd
 )");
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
index 7b2142c..d6c654f 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
@@ -149,6 +149,22 @@
     EXPECT_EQ(DumpInstructions(generator_.Module().Annots()), "OpDecorate %1 ArrayStride 16\n");
 }
 
+TEST_F(SpvGeneratorImplTest, Type_Array_NestedArray) {
+    auto* arr = mod.Types().array(mod.Types().array(mod.Types().f32(), 64u), 4u);
+    auto id = generator_.Type(arr);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%3 = OpTypeFloat 32\n"
+              "%5 = OpTypeInt 32 0\n"
+              "%4 = OpConstant %5 64\n"
+              "%2 = OpTypeArray %3 %4\n"
+              "%6 = OpConstant %5 4\n"
+              "%1 = OpTypeArray %2 %6\n");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Annots()),
+              "OpDecorate %2 ArrayStride 4\n"
+              "OpDecorate %1 ArrayStride 256\n");
+}
+
 TEST_F(SpvGeneratorImplTest, Type_RuntimeArray_DefaultStride) {
     auto* arr = mod.Types().runtime_array(mod.Types().f32());
     auto id = generator_.Type(arr);
@@ -169,6 +185,76 @@
     EXPECT_EQ(DumpInstructions(generator_.Module().Annots()), "OpDecorate %1 ArrayStride 16\n");
 }
 
+TEST_F(SpvGeneratorImplTest, Type_Struct) {
+    auto* str = mod.Types().Get<type::Struct>(
+        mod.symbols.Register("MyStruct"),
+        utils::Vector{
+            mod.Types().Get<type::StructMember>(mod.symbols.Register("a"), mod.Types().f32(), 0u,
+                                                0u, 4u, 4u, type::StructMemberAttributes{}),
+            mod.Types().Get<type::StructMember>(mod.symbols.Register("b"),
+                                                mod.Types().vec4(mod.Types().i32()), 1u, 16u, 16u,
+                                                16u, type::StructMemberAttributes{}),
+        },
+        16u, 32u, 32u);
+    auto id = generator_.Type(str);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeFloat 32
+%4 = OpTypeInt 32 1
+%3 = OpTypeVector %4 4
+%1 = OpTypeStruct %2 %3
+)");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Annots()), R"(OpMemberDecorate %1 0 Offset 0
+OpMemberDecorate %1 1 Offset 16
+)");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Debug()), R"(OpMemberName %1 0 "a"
+OpMemberName %1 1 "b"
+OpName %1 "MyStruct"
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Type_Struct_MatrixLayout) {
+    auto* str = mod.Types().Get<type::Struct>(
+        mod.symbols.Register("MyStruct"),
+        utils::Vector{
+            mod.Types().Get<type::StructMember>(mod.symbols.Register("m"),
+                                                mod.Types().mat3x3(mod.Types().f32()), 0u, 0u, 16u,
+                                                48u, type::StructMemberAttributes{}),
+            // Matrices nested inside arrays need layout decorations on the struct member too.
+            mod.Types().Get<type::StructMember>(
+                mod.symbols.Register("arr"),
+                mod.Types().array(mod.Types().array(mod.Types().mat2x4(mod.Types().f16()), 4), 4),
+                1u, 64u, 8u, 64u, type::StructMemberAttributes{}),
+        },
+        16u, 128u, 128u);
+    auto id = generator_.Type(str);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(), R"(%4 = OpTypeFloat 32
+%3 = OpTypeVector %4 3
+%2 = OpTypeMatrix %3 3
+%9 = OpTypeFloat 16
+%8 = OpTypeVector %9 4
+%7 = OpTypeMatrix %8 2
+%11 = OpTypeInt 32 0
+%10 = OpConstant %11 4
+%6 = OpTypeArray %7 %10
+%5 = OpTypeArray %6 %10
+%1 = OpTypeStruct %2 %5
+)");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Annots()), R"(OpMemberDecorate %1 0 Offset 0
+OpMemberDecorate %1 0 ColMajor
+OpMemberDecorate %1 0 MatrixStride 16
+OpDecorate %6 ArrayStride 16
+OpDecorate %5 ArrayStride 64
+OpMemberDecorate %1 1 Offset 64
+OpMemberDecorate %1 1 ColMajor
+OpMemberDecorate %1 1 MatrixStride 8
+)");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Debug()), R"(OpMemberName %1 0 "m"
+OpMemberName %1 1 "arr"
+OpName %1 "MyStruct"
+)");
+}
+
 // Test that we can emit multiple types.
 // Includes types with the same opcode but different parameters.
 TEST_F(SpvGeneratorImplTest, Type_Multiple) {