[tint][ir][val] Split up unittests into multiple files

Breaks up the tests in validator_test.cc into different files of
conceptually connected tests, i.e. 'builtins', 'types', etc. This
helps get the size of the testing files to a couple of kLOC each
instead of previous ~11kLOC single file.

The actual implementation the common test fixture and a variety of
misc tests are left in validator_test.cc.

This CL is primarily just moving existing code around, and not
intended to be a systematic refactoring of the existing tests.

Fixes: 388064480

Change-Id: Ie33da1fc4813e23535e162a152159e94721f3a58
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/221496
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
index b981753..b8f23df 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -230,7 +230,15 @@
     "traverse_test.cc",
     "unreachable_test.cc",
     "user_call_test.cc",
+    "validator_access_test.cc",
+    "validator_builtin_test.cc",
+    "validator_call_test.cc",
+    "validator_flow_control_test.cc",
+    "validator_function_test.cc",
     "validator_test.cc",
+    "validator_test.h",
+    "validator_type_test.cc",
+    "validator_value_test.cc",
     "value_test.cc",
     "var_test.cc",
   ],
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index 4935148..1ee319f 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -236,7 +236,15 @@
   lang/core/ir/traverse_test.cc
   lang/core/ir/unreachable_test.cc
   lang/core/ir/user_call_test.cc
+  lang/core/ir/validator_access_test.cc
+  lang/core/ir/validator_builtin_test.cc
+  lang/core/ir/validator_call_test.cc
+  lang/core/ir/validator_flow_control_test.cc
+  lang/core/ir/validator_function_test.cc
   lang/core/ir/validator_test.cc
+  lang/core/ir/validator_test.h
+  lang/core/ir/validator_type_test.cc
+  lang/core/ir/validator_value_test.cc
   lang/core/ir/value_test.cc
   lang/core/ir/var_test.cc
 )
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 402f840..2970b7b 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -231,7 +231,15 @@
       "traverse_test.cc",
       "unreachable_test.cc",
       "user_call_test.cc",
+      "validator_access_test.cc",
+      "validator_builtin_test.cc",
+      "validator_call_test.cc",
+      "validator_flow_control_test.cc",
+      "validator_function_test.cc",
       "validator_test.cc",
+      "validator_test.h",
+      "validator_type_test.cc",
+      "validator_value_test.cc",
       "value_test.cc",
       "var_test.cc",
     ]
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index fee5529..b5ab776 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -30,7 +30,6 @@
 #include <algorithm>
 #include <cstdint>
 #include <functional>
-#include <memory>
 #include <string>
 #include <string_view>
 #include <utility>
diff --git a/src/tint/lang/core/ir/validator_access_test.cc b/src/tint/lang/core/ir/validator_access_test.cc
new file mode 100644
index 0000000..c28497e
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_access_test.cc
@@ -0,0 +1,1732 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+#include <tuple>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/texel_format.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/memory_view.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Access_NoOperands) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        auto* access = b.Access(ty.f32(), obj, 0_i);
+        access->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: access: expected at least 2 operands, got 0
+    %3:f32 = access
+             ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:vec3<f32>):void {
+  $B1: {
+    %3:f32 = access
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_NoIndices) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: access: expected at least 2 operands, got 1
+    %3:f32 = access %2
+             ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:vec3<f32>):void {
+  $B1: {
+    %3:f32 = access %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_NoResults) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        auto* access = b.Access(ty.f32(), obj, 0_i);
+        access->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:13 error: access: expected exactly 1 results, got 0
+    undef = access %2, 0i
+            ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:vec3<f32>):void {
+  $B1: {
+    undef = access %2, 0i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_NullObject) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), nullptr, 0_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:21 error: access: operand is undefined
+    %2:f32 = access undef, 0u
+                    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:f32 = access undef, 0u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_NullIndex) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:25 error: access: operand is undefined
+    %3:f32 = access %2, undef
+                        ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:vec3<f32>):void {
+  $B1: {
+    %3:f32 = access %2, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_NegativeIndex) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, -1_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:25 error: access: constant index must be positive, got -1
+    %3:f32 = access %2, -1i
+                        ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:vec3<f32>):void {
+  $B1: {
+    %3:f32 = access %2, -1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_OOB_Index_Value) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u, 3_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:29 error: access: index out of bounds for type 'vec2<f32>'
+    %3:f32 = access %2, 1u, 3u
+                            ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:3:29 note: acceptable range: [0..1]
+    %3:f32 = access %2, 1u, 3u
+                            ^^
+
+note: # Disassembly
+%my_func = func(%2:mat3x2<f32>):void {
+  $B1: {
+    %3:f32 = access %2, 1u, 3u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_OOB_Index_Ptr) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<private_, f32>(), obj, 1_u, 3_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:55 error: access: index out of bounds for type 'ptr<private, array<f32, 2>, read_write>'
+    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
+                                                      ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:3:55 note: acceptable range: [0..1]
+    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
+                                                      ^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
+  $B1: {
+    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_StaticallyUnindexableType_Value) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.f32());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:25 error: access: type 'f32' cannot be indexed
+    %3:f32 = access %2, 1u
+                        ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:f32):void {
+  $B1: {
+    %3:f32 = access %2, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_StaticallyUnindexableType_Ptr) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<private_, f32>(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:51 error: access: type 'ptr<private, f32, read_write>' cannot be indexed
+    %3:ptr<private, f32, read_write> = access %2, 1u
+                                                  ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, f32, read_write>):void {
+  $B1: {
+    %3:ptr<private, f32, read_write> = access %2, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_DynamicallyUnindexableType_Value) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.i32()},
+                                                          });
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(str_ty);
+    auto* idx = b.FunctionParam(ty.i32());
+    f->SetParams({obj, idx});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.i32(), obj, idx);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:25 error: access: type 'MyStruct' cannot be dynamically indexed
+    %4:i32 = access %2, %3
+                        ^^
+
+:7:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:i32 @offset(4)
+}
+
+%my_func = func(%2:MyStruct, %3:i32):void {
+  $B1: {
+    %4:i32 = access %2, %3
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_DynamicallyUnindexableType_Ptr) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.i32()},
+                                                          });
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, read_write>(str_ty));
+    auto* idx = b.FunctionParam(ty.i32());
+    f->SetParams({obj, idx});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.i32(), obj, idx);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:8:25 error: access: type 'ptr<private, MyStruct, read_write>' cannot be dynamically indexed
+    %4:i32 = access %2, %3
+                        ^^
+
+:7:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:i32 @offset(4)
+}
+
+%my_func = func(%2:ptr<private, MyStruct, read_write>, %3:i32):void {
+  $B1: {
+    %4:i32 = access %2, %3
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Value_Value) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.i32(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:14 error: access: result of access chain is type 'f32' but instruction type is 'i32'
+    %3:i32 = access %2, 1u, 1u
+             ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:mat3x2<f32>):void {
+  $B1: {
+    %3:i32 = access %2, 1u, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Ptr_Ptr) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<private_, i32>(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:40 error: access: result of access chain is type 'ptr<private, f32, read_write>' but instruction type is 'ptr<private, i32, read_write>'
+    %3:ptr<private, i32, read_write> = access %2, 1u, 1u
+                                       ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
+  $B1: {
+    %3:ptr<private, i32, read_write> = access %2, 1u, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Ptr_Value) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:14 error: access: result of access chain is type 'ptr<private, f32, read_write>' but instruction type is 'f32'
+    %3:f32 = access %2, 1u, 1u
+             ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
+  $B1: {
+    %3:f32 = access %2, 1u, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVectorPtr) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, vec3<f32>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:25 error: access: cannot obtain address of vector element
+    %3:f32 = access %2, 1u
+                        ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, vec3<f32>, read_write>):void {
+  $B1: {
+    %3:f32 = access %2, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_WithCapability) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, vec3<f32>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<private_, f32>(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowVectorElementPointer});
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_ViaMatrixPtr) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:29 error: access: cannot obtain address of vector element
+    %3:f32 = access %2, 1u, 1u
+                            ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<private, mat3x2<f32>, read_write>):void {
+  $B1: {
+    %3:f32 = access %2, 1u, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_ViaMatrixPtr_WithCapability) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<private_, f32>(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowVectorElementPointer});
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Access_Incorrect_Ptr_AddressSpace) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<storage, array<f32, 2>, read>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<uniform, f32, read>(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:34 error: access: result of access chain is type 'ptr<storage, f32, read>' but instruction type is 'ptr<uniform, f32, read>'
+    %3:ptr<uniform, f32, read> = access %2, 1u
+                                 ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<storage, array<f32, 2>, read>):void {
+  $B1: {
+    %3:ptr<uniform, f32, read> = access %2, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_Incorrect_Ptr_Access) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.ptr<storage, array<f32, 2>, read>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.ptr<storage, f32, read_write>(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:40 error: access: result of access chain is type 'ptr<storage, f32, read>' but instruction type is 'ptr<storage, f32, read_write>'
+    %3:ptr<storage, f32, read_write> = access %2, 1u
+                                       ^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func(%2:ptr<storage, array<f32, 2>, read>):void {
+  $B1: {
+    %3:ptr<storage, f32, read_write> = access %2, 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVector) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.vec3<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Access_IndexVector_ViaMatrix) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ty.f32(), obj, 1_u, 1_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Access_ExtractPointerFromStruct) {
+    auto* ptr = ty.ptr<private_, i32>();
+    Vector<core::type::Manager::StructMemberDesc, 1> members{
+        core::type::Manager::StructMemberDesc{mod.symbols.New("a"), ptr},
+    };
+    auto* str = ty.Struct(mod.symbols.New("MyStruct"), std::move(members));
+    auto* f = b.Function("my_func", ty.void_());
+    auto* obj = b.FunctionParam("obj", str);
+    f->SetParams({obj});
+
+    b.Append(f->Block(), [&] {
+        b.Access(ptr, obj, 0_u);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Load_NullFrom) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.i32()), nullptr));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:19 error: load: operand is undefined
+    %2:i32 = load undef
+                  ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32 = load undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Load_SourceNotMemoryView) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* let = b.Let("l", 1_i);
+        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.f32()), let->Result(0)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:19 error: load: load source operand is not a memory view
+    %3:f32 = load %l
+                  ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %l:i32 = let 1i
+    %3:f32 = load %l
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Load_TypeMismatch) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.f32()), var->Result(0)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:19 error: load: result type 'f32' does not match source store type 'i32'
+    %3:f32 = load %2
+                  ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    %3:f32 = load %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Load_MissingResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        auto* load = mod.CreateInstruction<ir::Load>(nullptr, var->Result(0));
+        load->ClearResults();
+        b.Append(load);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:13 error: load: expected exactly 1 results, got 0
+    undef = load %2
+            ^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    undef = load %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Load_NonReadableSource) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32, core::Access::kWrite>());
+        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.i32()), var->Result(0)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:19 error: load: load source operand has a non-readable access type, 'write'
+    %3:i32 = load %2
+                  ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, write> = var
+    %3:i32 = load %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NullTo) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(mod.CreateInstruction<ir::Store>(nullptr, b.Constant(42_i)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:11 error: store: operand is undefined
+    store undef, 42i
+          ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    store undef, 42i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NullFrom) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), nullptr));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:15 error: store: operand is undefined
+    store %2, undef
+              ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    store %2, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NullToAndFrom) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(mod.CreateInstruction<ir::Store>(nullptr, nullptr));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:11 error: store: operand is undefined
+    store undef, undef
+          ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:3:18 error: store: operand is undefined
+    store undef, undef
+                 ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    store undef, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NonEmptyResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        auto* store = mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_i));
+        store->SetResults(Vector{b.InstructionResult(ty.i32())});
+        b.Append(store);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: store: expected exactly 0 results, got 1
+    store %2, 42i
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    store %2, 42i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_TargetNotMemoryView) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* let = b.Let("l", 1_i);
+        b.Append(mod.CreateInstruction<ir::Store>(let->Result(0), b.Constant(42_u)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:11 error: store: store target operand is not a memory view
+    store %l, 42u
+          ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %l:i32 = let 1i
+    store %l, 42u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_TypeMismatch) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_u)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:15 error: store: value type 'u32' does not match store type 'i32'
+    store %2, 42u
+              ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    store %2, 42u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NoStoreType) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* result = b.InstructionResult(ty.u32());
+        result->SetType(nullptr);
+        b.Append(mod.CreateInstruction<ir::Store>(result, b.Constant(42_u)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:11 error: store: operand type is undefined
+    store %2, 42u
+          ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    store %2, 42u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NoValueType) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32>());
+        auto* val = b.Construct(ty.u32(), 42_u);
+        val->Result(0)->SetType(nullptr);
+
+        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), val->Result(0)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: construct: result type is undefined
+    %3:undef = construct 42u
+    ^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:5:15 error: store: operand type is undefined
+    store %2, %3
+              ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read_write> = var
+    %3:undef = construct 42u
+    store %2, %3
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Store_NonWriteableTarget) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, i32, core::Access::kRead>());
+        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_i)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:11 error: store: store target operand has a non-writeable access type, 'read'
+    store %2, 42i
+          ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, i32, read> = var
+    store %2, 42i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, LoadVectorElement_NullResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        b.Append(
+            mod.CreateInstruction<ir::LoadVectorElement>(nullptr, var->Result(0), b.Constant(1_i)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: load_vector_element: result is undefined
+    undef = load_vector_element %2, 1i
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    undef = load_vector_element %2, 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, LoadVectorElement_NullFrom) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(mod.CreateInstruction<ir::LoadVectorElement>(b.InstructionResult(ty.f32()),
+                                                              nullptr, b.Constant(1_i)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:34 error: load_vector_element: operand is undefined
+    %2:f32 = load_vector_element undef, 1i
+                                 ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:f32 = load_vector_element undef, 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, LoadVectorElement_NullIndex) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        b.Append(mod.CreateInstruction<ir::LoadVectorElement>(b.InstructionResult(ty.f32()),
+                                                              var->Result(0), nullptr));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:38 error: load_vector_element: operand is undefined
+    %3:f32 = load_vector_element %2, undef
+                                     ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    %3:f32 = load_vector_element %2, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, LoadVectorElement_MissingResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        auto* load = b.LoadVectorElement(var, b.Constant(1_i));
+        load->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:13 error: load_vector_element: expected exactly 1 results, got 0
+    undef = load_vector_element %2, 1i
+            ^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    undef = load_vector_element %2, 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, LoadVectorElement_MissingOperands) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        auto* load = b.LoadVectorElement(var, b.Constant(1_i));
+        load->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:14 error: load_vector_element: expected exactly 2 operands, got 0
+    %3:f32 = load_vector_element
+             ^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    %3:f32 = load_vector_element
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, StoreVectorElement_NullTo) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(nullptr, b.Constant(1_i),
+                                                               b.Constant(2_f)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:26 error: store_vector_element: operand is undefined
+    store_vector_element undef, 1i, 2.0f
+                         ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    store_vector_element undef, 1i, 2.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, StoreVectorElement_NullIndex) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(var->Result(0), nullptr,
+                                                               b.Constant(2_f)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:30 error: store_vector_element: operand is undefined
+    store_vector_element %2, undef, 2.0f
+                             ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    store_vector_element %2, undef, 2.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, StoreVectorElement_NullValue) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(var->Result(0), b.Constant(1_i),
+                                                               nullptr));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:34 error: store_vector_element: operand is undefined
+    store_vector_element %2, 1i, undef
+                                 ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    store_vector_element %2, 1i, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, StoreVectorElement_MissingOperands) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        auto* store = b.StoreVectorElement(var, b.Constant(1_i), b.Constant(2_f));
+        store->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: store_vector_element: expected exactly 3 operands, got 0
+    store_vector_element
+    ^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    store_vector_element
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, StoreVectorElement_UnexpectedResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
+        auto* store = b.StoreVectorElement(var, b.Constant(1_i), b.Constant(2_f));
+        store->SetResults(Vector{b.InstructionResult(ty.f32())});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: store_vector_element: expected exactly 0 results, got 1
+    store_vector_element %2, 1i, 2.0f
+    ^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec3<f32>, read_write> = var
+    store_vector_element %2, 1i, 2.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_MissingValue) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        swizzle->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: swizzle: expected exactly 1 operands, got 0
+    %3:vec4<f32> = swizzle undef, wzyx
+                   ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    %3:vec4<f32> = swizzle undef, wzyx
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_NullValue) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        swizzle->SetOperand(0, nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(error: swizzle: operand is undefined
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    %3:vec4<f32> = swizzle undef, wzyx
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_MissingResult) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        swizzle->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:13 error: swizzle: expected exactly 1 results, got 0
+    undef = swizzle %2, wzyx
+            ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    undef = swizzle %2, wzyx
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_NullResult) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        swizzle->SetResults(Vector<ir::InstructionResult*, 1>{nullptr});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: swizzle: result is undefined
+    undef = swizzle %2, wzyx
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    undef = swizzle %2, wzyx
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_NoIndices) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        auto indices = Vector<uint32_t, 0>();
+        swizzle->SetIndices(std::move(indices));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: swizzle: expected at least 1 indices
+    %3:vec4<f32> = swizzle %2,
+                   ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    %3:vec4<f32> = swizzle %2,
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_TooManyIndices) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        auto indices = Vector<uint32_t, 5>{1, 1, 1, 1, 1};
+        swizzle->SetIndices(std::move(indices));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: swizzle: expected at most 4 indices
+    %3:vec4<f32> = swizzle %2, yyyyy
+                   ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    %3:vec4<f32> = swizzle %2, yyyyy
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Swizzle_InvalidIndices) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
+        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
+        auto indices = Vector<uint32_t, 4>{4, 3, 2, 1};
+        swizzle->SetIndices(std::move(indices));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: swizzle: invalid index value
+    %3:vec4<f32> = swizzle %2, wzy
+                   ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, vec4<f32>, read_write> = var
+    %3:vec4<f32> = swizzle %2, wzy
+    ret
+  }
+}
+)");
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_builtin_test.cc b/src/tint/lang/core/ir/validator_builtin_test.cc
new file mode 100644
index 0000000..cb97890
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_builtin_test.cc
@@ -0,0 +1,1388 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "size", BuiltinValue::kPointSize, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: __point_size must be used in a vertex shader entry point
+%f = @fragment func():f32 [@__point_size] {
+^^
+
+note: # Disassembly
+%f = @fragment func():f32 [@__point_size] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongIODirection) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "size", BuiltinValue::kPointSize, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: __point_size must be an output of a shader entry point
+%f = @vertex func(%size:f32 [@__point_size]):vec4<f32> [@position] {
+                  ^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%size:f32 [@__point_size]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongType) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinReturn(f, "size", BuiltinValue::kPointSize, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: __point_size must be a f32
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  size:u32 @offset(16), @builtin(__point_size)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "distances", BuiltinValue::kClipDistances, ty.array<f32, 2>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: clip_distances must be used in a vertex shader entry point
+%f = @fragment func():array<f32, 2> [@clip_distances] {
+^^
+
+note: # Disassembly
+%f = @fragment func():array<f32, 2> [@clip_distances] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongIODirection) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "distances", BuiltinValue::kClipDistances, ty.array<f32, 2>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: clip_distances must be an output of a shader entry point
+%f = @vertex func(%distances:array<f32, 2> [@clip_distances]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%distances:array<f32, 2> [@clip_distances]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongType) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinReturn(f, "distances", BuiltinValue::kClipDistances, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: clip_distances must be an array<f32, N>, where N <= 8
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  distances:f32 @offset(16), @builtin(clip_distances)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinReturn(f, "depth", BuiltinValue::kFragDepth, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: frag_depth must be used in a fragment shader entry point
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  depth:f32 @offset(16), @builtin(frag_depth)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongIODirection) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "depth", BuiltinValue::kFragDepth, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: frag_depth must be an output of a shader entry point
+%f = @fragment func(%depth:f32 [@frag_depth]):void {
+                    ^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%depth:f32 [@frag_depth]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongType) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "depth", BuiltinValue::kFragDepth, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: frag_depth must be a f32
+%f = @fragment func():u32 [@frag_depth] {
+^^
+
+note: # Disassembly
+%f = @fragment func():u32 [@frag_depth] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "facing", BuiltinValue::kFrontFacing, ty.bool_());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: front_facing must be used in a fragment shader entry point
+%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^
+
+:1:19 error: entry point params can only be a bool for fragment shaders
+%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongIODirection) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "facing", BuiltinValue::kFrontFacing, ty.bool_());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: front_facing must be an input of a shader entry point
+%f = @fragment func():bool [@front_facing] {
+^^
+
+note: # Disassembly
+%f = @fragment func():bool [@front_facing] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongType) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "facing", BuiltinValue::kFrontFacing, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: front_facing must be a bool
+%f = @fragment func(%facing:u32 [@front_facing]):void {
+                    ^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%facing:u32 [@front_facing]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: global_invocation_id must be used in a compute shader entry point
+%f = @fragment func(%invocation:vec3<u32> [@global_invocation_id]):void {
+                    ^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%invocation:vec3<u32> [@global_invocation_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongIODirection) {
+    // This will also trigger the compute entry points should have void returns check
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: global_invocation_id must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
+^^
+
+:1:1 error: compute entry point must not have a return type
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: global_invocation_id must be an vec3<u32>
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%invocation:u32 [@global_invocation_id]):void {
+                                               ^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%invocation:u32 [@global_invocation_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "instance", BuiltinValue::kInstanceIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: instance_index must be used in a vertex shader entry point
+%f = @fragment func(%instance:u32 [@instance_index]):void {
+                    ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%instance:u32 [@instance_index]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongIODirection) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinReturn(f, "instance", BuiltinValue::kInstanceIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: instance_index must be an input of a shader entry point
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  instance:u32 @offset(16), @builtin(instance_index)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongType) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "instance", BuiltinValue::kInstanceIndex, ty.i32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: instance_index must be an u32
+%f = @vertex func(%instance:i32 [@instance_index]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%instance:i32 [@instance_index]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kLocalInvocationId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: local_invocation_id must be used in a compute shader entry point
+%f = @fragment func(%id:vec3<u32> [@local_invocation_id]):void {
+                    ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%id:vec3<u32> [@local_invocation_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongIODirection) {
+    // This will also trigger the compute entry points should have void returns check
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "id", BuiltinValue::kLocalInvocationId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: local_invocation_id must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
+^^
+
+:1:1 error: compute entry point must not have a return type
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kLocalInvocationId, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: local_invocation_id must be an vec3<u32>
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@local_invocation_id]):void {
+                                               ^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@local_invocation_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kLocalInvocationIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: local_invocation_index must be used in a compute shader entry point
+%f = @fragment func(%index:u32 [@local_invocation_index]):void {
+                    ^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%index:u32 [@local_invocation_index]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongIODirection) {
+    // This will also trigger the compute entry points should have void returns check
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "index", BuiltinValue::kLocalInvocationIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: local_invocation_index must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
+^^
+
+:1:1 error: compute entry point must not have a return type
+%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kLocalInvocationIndex, ty.i32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: local_invocation_index must be an u32
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%index:i32 [@local_invocation_index]):void {
+                                               ^^^^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%index:i32 [@local_invocation_index]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "num", BuiltinValue::kNumWorkgroups, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: num_workgroups must be used in a compute shader entry point
+%f = @fragment func(%num:vec3<u32> [@num_workgroups]):void {
+                    ^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%num:vec3<u32> [@num_workgroups]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongIODirection) {
+    // This will also trigger the compute entry points should have void returns check
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "num", BuiltinValue::kNumWorkgroups, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: num_workgroups must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
+^^
+
+:1:1 error: compute entry point must not have a return type
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "num", BuiltinValue::kNumWorkgroups, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: num_workgroups must be an vec3<u32>
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%num:u32 [@num_workgroups]):void {
+                                               ^^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%num:u32 [@num_workgroups]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kSampleIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: sample_index must be used in a fragment shader entry point
+%f = @vertex func(%index:u32 [@sample_index]):vec4<f32> [@position] {
+                  ^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%index:u32 [@sample_index]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongIODirection) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "index", BuiltinValue::kSampleIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: sample_index must be an input of a shader entry point
+%f = @fragment func():u32 [@sample_index] {
+^^
+
+note: # Disassembly
+%f = @fragment func():u32 [@sample_index] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongType) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kSampleIndex, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: sample_index must be an u32
+%f = @fragment func(%index:f32 [@sample_index]):void {
+                    ^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%index:f32 [@sample_index]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kVertexIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: vertex_index must be used in a vertex shader entry point
+%f = @fragment func(%index:u32 [@vertex_index]):void {
+                    ^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%index:u32 [@vertex_index]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongIODirection) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinReturn(f, "index", BuiltinValue::kVertexIndex, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: vertex_index must be an input of a shader entry point
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  index:u32 @offset(16), @builtin(vertex_index)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongType) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "index", BuiltinValue::kVertexIndex, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: vertex_index must be an u32
+%f = @vertex func(%index:f32 [@vertex_index]):vec4<f32> [@position] {
+                  ^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%index:f32 [@vertex_index]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongStage) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kWorkgroupId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: workgroup_id must be used in a compute shader entry point
+%f = @fragment func(%id:vec3<u32> [@workgroup_id]):void {
+                    ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%id:vec3<u32> [@workgroup_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongIODirection) {
+    // This will also trigger the compute entry points should have void returns check
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "id", BuiltinValue::kWorkgroupId, ty.vec3<u32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: workgroup_id must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
+^^
+
+:1:1 error: compute entry point must not have a return type
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kWorkgroupId, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: workgroup_id must be an vec3<u32>
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@workgroup_id]):void {
+                                               ^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@workgroup_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_Position_WrongStage) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: position must be used in a fragment or vertex shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%pos:vec4<f32> [@position]):void {
+                                               ^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%pos:vec4<f32> [@position]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_Position_WrongIODirectionForVertex) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: position must be an output for a vertex entry point
+%f = @vertex func(%pos:vec4<f32> [@position]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%pos:vec4<f32> [@position]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_Position_WrongIODirectionForFragment) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: position must be an input for a fragment entry point
+%f = @fragment func():vec4<f32> [@position] {
+^^
+
+note: # Disassembly
+%f = @fragment func():vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_Position_WrongType) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: position must be an vec4<f32>
+%f = @fragment func(%pos:f32 [@position]):void {
+                    ^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%pos:f32 [@position]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleMask_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: sample_mask must be used in a fragment entry point
+%f = @vertex func(%mask:u32 [@sample_mask]):vec4<f32> [@position] {
+                  ^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%mask:u32 [@sample_mask]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleMask_InputValid) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleMask_OutputValid) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "mask", BuiltinValue::kSampleMask, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SampleMask_WrongType) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.f32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:21 error: sample_mask must be an u32
+%f = @fragment func(%mask:f32 [@sample_mask]):void {
+                    ^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%mask:f32 [@sample_mask]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "size", BuiltinValue::kSubgroupSize, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: subgroup_size must be used in a compute or fragment shader entry point
+%f = @vertex func(%size:u32 [@subgroup_size]):vec4<f32> [@position] {
+                  ^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%size:u32 [@subgroup_size]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongIODirection) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "size", BuiltinValue::kSubgroupSize, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: subgroup_size must be an input of a shader entry point
+%f = @fragment func():u32 [@subgroup_size] {
+^^
+
+note: # Disassembly
+%f = @fragment func():u32 [@subgroup_size] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "size", BuiltinValue::kSubgroupSize, ty.i32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: subgroup_size must be an u32
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%size:i32 [@subgroup_size]):void {
+                                               ^^^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%size:i32 [@subgroup_size]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kSubgroupInvocationId, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:19 error: subgroup_invocation_id must be used in a compute or fragment shader entry point
+%f = @vertex func(%id:u32 [@subgroup_invocation_id]):vec4<f32> [@position] {
+                  ^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%id:u32 [@subgroup_invocation_id]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongIODirection) {
+    auto* f = FragmentEntryPoint();
+    AddBuiltinReturn(f, "id", BuiltinValue::kSubgroupInvocationId, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: subgroup_invocation_id must be an input of a shader entry point
+%f = @fragment func():u32 [@subgroup_invocation_id] {
+^^
+
+note: # Disassembly
+%f = @fragment func():u32 [@subgroup_invocation_id] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "id", BuiltinValue::kSubgroupInvocationId, ty.i32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:48 error: subgroup_invocation_id must be an u32
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:i32 [@subgroup_invocation_id]):void {
+                                               ^^^^^^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:i32 [@subgroup_invocation_id]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Bitcast_MissingArg) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* bitcast = b.Bitcast(ty.i32(), 1_u);
+        bitcast->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: bitcast: expected exactly 1 operands, got 0
+    %2:i32 = bitcast
+             ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:i32 = bitcast
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Bitcast_NullArg) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Bitcast(ty.i32(), nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:22 error: bitcast: operand is undefined
+    %2:i32 = bitcast undef
+                     ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:i32 = bitcast undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Bitcast_MissingResult) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* bitcast = b.Bitcast(ty.i32(), 1_u);
+        bitcast->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:13 error: bitcast: expected exactly 1 results, got 0
+    undef = bitcast 1u
+            ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    undef = bitcast 1u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Bitcast_NullResult) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Bitcast(ty.i32(), 1_u);
+        c->SetResults(Vector<InstructionResult*, 1>{nullptr});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: bitcast: result is undefined
+    undef = bitcast 1u
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    undef = bitcast 1u
+    ret
+  }
+}
+)");
+}
+
+namespace {
+template <typename T>
+static const core::type::Type* TypeBuilder(core::type::Manager& m) {
+    return m.Get<T>();
+}
+
+using TypeBuilderFn = decltype(&TypeBuilder<i32>);
+}  // namespace
+
+using BitcastTypeTest = IRTestParamHelper<std::tuple<
+    /* bitcast allowed */ bool,
+    /* src type_builder */ TypeBuilderFn,
+    /* dest type_builder */ TypeBuilderFn>>;
+
+TEST_P(BitcastTypeTest, Check) {
+    bool bitcast_allowed = std::get<0>(GetParam());
+    auto* src_ty = std::get<1>(GetParam())(ty);
+    auto* dest_ty = std::get<2>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Bitcast(dest_ty, b.Zero(src_ty));
+        b.Return(fn);
+    });
+
+    auto res = ir::Validate(mod);
+    if (bitcast_allowed) {
+        ASSERT_EQ(res, Success) << "Bitcast should be defined for '" << src_ty->FriendlyName()
+                                << "' -> '" << dest_ty->FriendlyName() << "': " << res.Failure();
+    } else {
+        ASSERT_NE(res, Success) << "Bitcast should NOT be defined for '" << src_ty->FriendlyName()
+                                << "' -> '" << dest_ty->FriendlyName() << "'";
+        EXPECT_THAT(res.Failure().reason.Str(), testing::HasSubstr("bitcast is not defined"));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IR_ValidatorTest,
+    BitcastTypeTest,
+    testing::Values(
+        // Scalar identity
+        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<u32>),
+        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<i32>),
+        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<f32>),
+        std::make_tuple(true, TypeBuilder<f16>, TypeBuilder<f16>),
+
+        // Scalar reinterpretation
+        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<i32>),
+        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<f32>),
+        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<vec2<f16>>),
+        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<u32>),
+        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<f32>),
+        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<vec2<f16>>),
+        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<u32>),
+        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<i32>),
+        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<vec2<f16>>),
+        std::make_tuple(false, TypeBuilder<u32>, TypeBuilder<f16>),
+        std::make_tuple(false, TypeBuilder<i32>, TypeBuilder<f16>),
+        std::make_tuple(false, TypeBuilder<f32>, TypeBuilder<f16>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<u32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<i32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<f32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<vec2<f16>>),
+
+        // Component-wise identity, sparsely (non-exhaustively) covering types and vector sizes
+        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec2<u32>>),
+        std::make_tuple(true, TypeBuilder<vec3<i32>>, TypeBuilder<vec3<i32>>),
+        std::make_tuple(true, TypeBuilder<vec4<f32>>, TypeBuilder<vec4<f32>>),
+        std::make_tuple(true, TypeBuilder<vec3<f16>>, TypeBuilder<vec3<f16>>),
+        std::make_tuple(false, TypeBuilder<vec2<u32>>, TypeBuilder<vec3<u32>>),
+        std::make_tuple(false, TypeBuilder<vec2<u32>>, TypeBuilder<vec4<u32>>),
+        std::make_tuple(false, TypeBuilder<vec3<i32>>, TypeBuilder<vec2<i32>>),
+        std::make_tuple(false, TypeBuilder<vec3<i32>>, TypeBuilder<vec4<i32>>),
+        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec2<f32>>),
+        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec3<f32>>),
+
+        // Component-wise reinterpretation, sparsely (non-exhaustively) covering types and
+        // vector sizes
+        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec2<i32>>),
+        std::make_tuple(true, TypeBuilder<vec3<u32>>, TypeBuilder<vec3<f32>>),
+        std::make_tuple(true, TypeBuilder<vec4<i32>>, TypeBuilder<vec4<u32>>),
+        std::make_tuple(true, TypeBuilder<vec3<i32>>, TypeBuilder<vec3<f32>>),
+        std::make_tuple(true, TypeBuilder<vec3<f32>>, TypeBuilder<vec3<u32>>),
+        std::make_tuple(true, TypeBuilder<vec2<f32>>, TypeBuilder<vec2<i32>>),
+        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec4<f16>>),
+        std::make_tuple(true, TypeBuilder<vec2<i32>>, TypeBuilder<vec4<f16>>),
+        std::make_tuple(true, TypeBuilder<vec2<f32>>, TypeBuilder<vec4<f16>>),
+        std::make_tuple(false, TypeBuilder<vec4<u32>>, TypeBuilder<vec4<f16>>),
+        std::make_tuple(false, TypeBuilder<vec2<i32>>, TypeBuilder<vec2<f16>>),
+        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec4<f16>>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<u32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<i32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<f32>),
+        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<vec2<f16>>)));
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_call_test.cc b/src/tint/lang/core/ir/validator_call_test.cc
new file mode 100644
index 0000000..1353318
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_call_test.cc
@@ -0,0 +1,558 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/function_param.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, CallToFunctionOutsideModule) {
+    auto* f = b.Function("f", ty.void_());
+    auto* g = b.Function("g", ty.void_());
+    mod.functions.Pop();  // Remove g
+
+    b.Append(f->Block(), [&] {
+        b.Call(g);
+        b.Return(f);
+    });
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:20 error: call: %g is not part of the module
+    %2:void = call %g
+                   ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:void = call %g
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToEntryPointFunction) {
+    auto* f = b.Function("f", ty.void_());
+    auto* g = ComputeEntryPoint("g");
+
+    b.Append(f->Block(), [&] {
+        b.Call(g);
+        b.Return(f);
+    });
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:20 error: call: call target must not have a pipeline stage
+    %2:void = call %g
+                   ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:void = call %g
+    ret
+  }
+}
+%g = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionTooFewArguments) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 42_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:20 error: call: function has 2 parameters, but call provides 1 arguments
+    %5:void = call %g, 42i
+                   ^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32):void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %5:void = call %g, 42i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionTooManyArguments) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 1_i, 2_i, 3_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:20 error: call: function has 2 parameters, but call provides 3 arguments
+    %5:void = call %g, 1i, 2i, 3i
+                   ^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32):void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %5:void = call %g, 1i, 2i, 3i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionWrongArgType) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 1_i, 2_f, 3_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:8:28 error: call: function parameter 1 is of type 'i32', but argument is of type 'f32'
+    %6:void = call %g, 1i, 2.0f, 3i
+                           ^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32, %4:i32):void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %6:void = call %g, 1i, 2.0f, 3i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionNullArg) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:24 error: call: operand is undefined
+    %4:void = call %g, undef
+                       ^^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func(%2:i32):void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %4:void = call %g, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToNullFunction) {
+    auto* g = b.Function("g", ty.void_());
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(g);
+        c->SetOperands(Vector{static_cast<ir::Value*>(nullptr)});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:20 error: call: operand is undefined
+    %3:void = call undef
+                   ^^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func():void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %3:void = call undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionNoResult) {
+    auto* g = b.Function("g", ty.void_());
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(g);
+        c->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:13 error: call: expected exactly 1 results, got 0
+    undef = call %g
+            ^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func():void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    undef = call %g
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionNoOperands) {
+    auto* g = b.Function("g", ty.void_());
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(g);
+        c->ClearOperands();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:15 error: call: expected at least 1 operands, got 0
+    %3:void = call undef
+              ^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%g = func():void {
+  $B1: {
+    ret
+  }
+}
+%f = func():void {
+  $B2: {
+    %3:void = call undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToNonFunctionTarget) {
+    auto* g = b.Function("g", ty.void_());
+    mod.functions.Pop();  // Remove g, since it isn't actually going to be used, it is just
+                          // needed to create the UserCall before mangling it
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(g);
+        c->SetOperands(Vector{b.Value(0_i)});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:20 error: call: target not defined or not a function
+    %2:void = call 0i
+                   ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:void = call 0i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToBuiltin_MissingResult) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(ty.f32(), BuiltinFn::kAbs, 1_f);
+        c->SetResults(Vector{static_cast<ir::InstructionResult*>(nullptr)});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:13 error: abs: call to builtin does not have a return type
+    undef = abs 1.0f
+            ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    undef = abs 1.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToBuiltin_MismatchResultType) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Call(ty.f32(), BuiltinFn::kAbs, 1_f);
+        c->Result(0)->SetType(ty.i32());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: abs: call result type does not match builtin return type
+    %2:i32 = abs 1.0f
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %2:i32 = abs 1.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToBuiltin_ArgNullType) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* i = b.Var<function, f32>("i");
+        i->SetInitializer(b.Constant(0_f));
+        auto* load = b.Load(i);
+        auto* load_ret = load->Result(0);
+        b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
+        load_ret->SetType(nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: load: result type is undefined
+    %3:undef = load %i
+    ^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:5:14 error: abs: argument to builtin has undefined type
+    %4:f32 = abs %3
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %i:ptr<function, f32, read_write> = var, 0.0f
+    %3:undef = load %i
+    %4:f32 = abs %3
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToBuiltin_NonSingularResult) {
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* i = b.Var<function, f32>("i");
+        i->SetInitializer(b.Constant(0_f));
+        auto* load = b.Load(i);
+        auto* load_ret = load->Result(0);
+        auto* too_many_call = b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
+        too_many_call->SetResults(Vector{load_ret, load_ret});
+        auto* too_few_call = b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
+        too_few_call->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:14 error: abs: call to builtin has 2 results, when 1 is expected
+    %3:f32 = abs %3
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:6:13 error: abs: call to builtin has 0 results, when 1 is expected
+    undef = abs %3
+            ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%f = func():void {
+  $B1: {
+    %i:ptr<function, f32, read_write> = var, 0.0f
+    %3:f32 = load %i
+    %3:f32 = abs %3
+    undef = abs %3
+    ret
+  }
+}
+)");
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_flow_control_test.cc b/src/tint/lang/core/ir/validator_flow_control_test.cc
new file mode 100644
index 0000000..4fd4171
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_flow_control_test.cc
@@ -0,0 +1,3291 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Discard_TooManyOperands) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* d = b.Discard();
+        d->SetOperands(Vector{b.Value(0_i)});
+        b.Return(func);
+    });
+
+    auto* ep = FragmentEntryPoint("ep");
+    b.Append(ep->Block(), [&] {
+        b.Call(func);
+        b.Return(ep);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: discard: expected exactly 0 operands, got 1
+    discard
+    ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%foo = func():void {
+  $B1: {
+    discard
+    ret
+  }
+}
+%ep = @fragment func():void {
+  $B2: {
+    %3:void = call %foo
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Discard_TooManyResults) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* d = b.Discard();
+        d->SetResults(Vector{b.InstructionResult(ty.i32())});
+        b.Return(func);
+    });
+
+    auto* ep = FragmentEntryPoint("ep");
+    b.Append(ep->Block(), [&] {
+        b.Call(func);
+        b.Return(ep);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: discard: expected exactly 0 results, got 1
+    discard
+    ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%foo = func():void {
+  $B1: {
+    discard
+    ret
+  }
+}
+%ep = @fragment func():void {
+  $B2: {
+    %3:void = call %foo
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Discard_RootBlock) {
+    mod.root_block->Append(b.Discard());
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:3 error: discard: root block: invalid instruction: tint::core::ir::Discard
+  discard
+  ^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  discard
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Discard_NotInFragment) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        b.Discard();
+        b.Return(func);
+    });
+
+    auto* ep = ComputeEntryPoint("ep");
+
+    b.Append(ep->Block(), [&] {
+        b.Call(func);
+        b.Return(ep);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: discard: cannot be called in non-fragment end point
+    discard
+    ^^^^^^^
+
+note: # Disassembly
+%foo = func():void {
+  $B1: {
+    discard
+    ret
+  }
+}
+%ep = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    %3:void = call %foo
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Terminator_RootBlock) {
+    auto f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    mod.root_block->Append(b.Return(f));
+    mod.root_block->Append(b.Unreachable());
+    mod.root_block->Append(b.TerminateInvocation());
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:3 error: return: root block: invalid instruction: tint::core::ir::Return
+  ret
+  ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+:3:3 error: unreachable: root block: invalid instruction: tint::core::ir::Unreachable
+  unreachable
+  ^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+:4:3 error: terminate_invocation: root block: invalid instruction: tint::core::ir::TerminateInvocation
+  terminate_invocation
+  ^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  ret
+  unreachable
+  terminate_invocation
+}
+
+%f = func():void {
+  $B2: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Terminator_HasResult) {
+    auto* ret_func = b.Function("ret_func", ty.void_());
+    b.Append(ret_func->Block(), [&] {
+        auto* r = b.Return(ret_func);
+        r->SetResults(Vector{b.InstructionResult(ty.i32())});
+    });
+
+    auto* unreachable_func = b.Function("unreachable_func", ty.void_());
+    b.Append(unreachable_func->Block(), [&] {
+        auto* r = b.Unreachable();
+        r->SetResults(Vector{b.InstructionResult(ty.i32())});
+    });
+
+    auto* terminate_func = b.Function("terminate_func", ty.void_());
+    b.Append(terminate_func->Block(), [&] {
+        auto* r = b.TerminateInvocation();
+        r->SetResults(Vector{b.InstructionResult(ty.i32())});
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: return: expected exactly 0 results, got 1
+    ret
+    ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:8:5 error: unreachable: expected exactly 0 results, got 1
+    unreachable
+    ^^^^^^^^^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+:13:5 error: terminate_invocation: expected exactly 0 results, got 1
+    terminate_invocation
+    ^^^^^^^^^^^^^^^^^^^^
+
+:12:3 note: in block
+  $B3: {
+  ^^^
+
+note: # Disassembly
+%ret_func = func():void {
+  $B1: {
+    ret
+  }
+}
+%unreachable_func = func():void {
+  $B2: {
+    unreachable
+  }
+}
+%terminate_func = func():void {
+  $B3: {
+    terminate_invocation
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Block_TerminatorInMiddle) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Return(f);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: return: must be the last instruction in the block
+    ret
+    ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    ret
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, If_RootBlock) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.Unreachable());
+    mod.root_block->Append(if_);
+
+    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:3 error: if: root block: invalid instruction: tint::core::ir::If
+  if true [t: $B2] {  # if_1
+  ^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  if true [t: $B2] {  # if_1
+    $B2: {  # true
+      unreachable
+    }
+  }
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, If_EmptyFalse) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, If_EmptyTrue) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->False()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:7 error: block does not end in a terminator instruction
+      $B2: {  # true
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+      }
+      $B3: {  # false
+        ret
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, If_ConditionIsBool) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(1_i);
+    if_->True()->Append(b.Return(f));
+    if_->False()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:8 error: if: condition type must be 'bool'
+    if 1i [t: $B2, f: $B3] {  # if_1
+       ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if 1i [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        ret
+      }
+      $B3: {  # false
+        ret
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, If_ConditionIsNullptr) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(nullptr);
+    if_->True()->Append(b.Return(f));
+    if_->False()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:8 error: if: operand is undefined
+    if undef [t: $B2, f: $B3] {  # if_1
+       ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if undef [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        ret
+      }
+      $B3: {  # false
+        ret
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, If_NullResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.Return(f));
+    if_->False()->Append(b.Return(f));
+
+    if_->SetResults(Vector<InstructionResult*, 1>{nullptr});
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: if: result is undefined
+    undef = if true [t: $B2, f: $B3] {  # if_1
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    undef = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        ret
+      }
+      $B3: {  # false
+        ret
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Loop_RootBlock) {
+    auto* l = b.Loop();
+    l->Body()->Append(b.ExitLoop(l));
+    mod.root_block->Append(l);
+
+    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:3 error: loop: root block: invalid instruction: tint::core::ir::Loop
+  loop [b: $B2] {  # loop_1
+  ^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  loop [b: $B2] {  # loop_1
+    $B2: {  # body
+      exit_loop  # loop_1
+    }
+  }
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Loop_OnlyBody) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* l = b.Loop();
+    l->Body()->Append(b.ExitLoop(l));
+
+    auto sb = b.Append(f->Block());
+    sb.Append(l);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, Loop_EmptyBody) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(b.Loop());
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:7 error: block does not end in a terminator instruction
+      $B2: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2: {  # body
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Switch_RootBlock) {
+    auto* switch_ = b.Switch(1_i);
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_));
+    mod.root_block->Append(switch_);
+
+    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:3 error: switch: root block: invalid instruction: tint::core::ir::Switch
+  switch 1i [c: (default, $B2)] {  # switch_1
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  switch 1i [c: (default, $B2)] {  # switch_1
+    $B2: {  # case
+      exit_switch  # switch_1
+    }
+  }
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.ExitIf(if_));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_NullIf) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(mod.CreateInstruction<ExitIf>(nullptr));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_if: has no parent control instruction
+        exit_if  # undef
+        ^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # true
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2] {  # if_1
+      $B2: {  # true
+        exit_if  # undef
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_LessOperandsThenIfParams) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.ExitIf(if_, 1_i));
+
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    if_->SetResults(Vector{r1, r2});
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_if: provides 1 value but 'if' expects 2 values
+        exit_if 1i  # if_1
+        ^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # true
+      ^^^
+
+:3:5 note: 'if' declared here
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+      $B2: {  # true
+        exit_if 1i  # if_1
+      }
+      # implicit false block: exit_if undef, undef
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_MoreOperandsThenIfParams) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.ExitIf(if_, 1_i, 2_f, 3_i));
+
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    if_->SetResults(Vector{r1, r2});
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_if: provides 3 values but 'if' expects 2 values
+        exit_if 1i, 2.0f, 3i  # if_1
+        ^^^^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # true
+      ^^^
+
+:3:5 note: 'if' declared here
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+      $B2: {  # true
+        exit_if 1i, 2.0f, 3i  # if_1
+      }
+      # implicit false block: exit_if undef, undef
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_WithResult) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.ExitIf(if_, 1_i, 2_f));
+
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    if_->SetResults(Vector{r1, r2});
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_IncorrectResultType) {
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.ExitIf(if_, 1_i, 2_i));
+
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    if_->SetResults(Vector{r1, r2});
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:21 error: exit_if: operand with type 'i32' does not match 'if' target type 'f32'
+        exit_if 1i, 2i  # if_1
+                    ^^
+
+:4:7 note: in block
+      $B2: {  # true
+      ^^^
+
+:3:13 note: %3 declared here
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+            ^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
+      $B2: {  # true
+        exit_if 1i, 2i  # if_1
+      }
+      # implicit false block: exit_if undef, undef
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_NotInParentIf) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.Return(f));
+
+    auto sb = b.Append(f->Block());
+    sb.Append(if_);
+    sb.ExitIf(if_);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:5 error: exit_if: found outside all control instructions
+    exit_if  # if_1
+    ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2] {  # if_1
+      $B2: {  # true
+        ret
+      }
+    }
+    exit_if  # if_1
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpsOverIf) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_inner = b.If(true);
+
+    auto* if_outer = b.If(true);
+    b.Append(if_outer->True(), [&] {
+        b.Append(if_inner);
+        b.ExitIf(if_outer);
+    });
+
+    b.Append(if_inner->True(), [&] { b.ExitIf(if_outer); });
+
+    b.Append(f->Block(), [&] {
+        b.Append(if_outer);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
+            exit_if  # if_1
+            ^^^^^^^
+
+:6:11 note: in block
+          $B3: {  # true
+          ^^^
+
+:5:9 note: first control instruction jumped
+        if true [t: $B3] {  # if_2
+        ^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2] {  # if_1
+      $B2: {  # true
+        if true [t: $B3] {  # if_2
+          $B3: {  # true
+            exit_if  # if_1
+          }
+        }
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpOverSwitch) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* switch_inner = b.Switch(1_i);
+
+    auto* if_outer = b.If(true);
+    b.Append(if_outer->True(), [&] {
+        b.Append(switch_inner);
+        b.ExitIf(if_outer);
+    });
+
+    auto* c = b.Case(switch_inner, {b.Constant(1_i), nullptr});
+    b.Append(c, [&] { b.ExitIf(if_outer); });
+
+    b.Append(f->Block(), [&] {
+        b.Append(if_outer);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
+            exit_if  # if_1
+            ^^^^^^^
+
+:6:11 note: in block
+          $B3: {  # case
+          ^^^
+
+:5:9 note: first control instruction jumped
+        switch 1i [c: (1i default, $B3)] {  # switch_1
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2] {  # if_1
+      $B2: {  # true
+        switch 1i [c: (1i default, $B3)] {  # switch_1
+          $B3: {  # case
+            exit_if  # if_1
+          }
+        }
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpOverLoop) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* loop = b.Loop();
+
+    auto* if_outer = b.If(true);
+    b.Append(if_outer->True(), [&] {
+        b.Append(loop);
+        b.ExitIf(if_outer);
+    });
+
+    b.Append(loop->Body(), [&] { b.ExitIf(if_outer); });
+
+    b.Append(f->Block(), [&] {
+        b.Append(if_outer);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
+            exit_if  # if_1
+            ^^^^^^^
+
+:6:11 note: in block
+          $B3: {  # body
+          ^^^
+
+:5:9 note: first control instruction jumped
+        loop [b: $B3] {  # loop_1
+        ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    if true [t: $B2] {  # if_1
+      $B2: {  # true
+        loop [b: $B3] {  # loop_1
+          $B3: {  # body
+            exit_if  # if_1
+          }
+        }
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_NullSwitch) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(mod.CreateInstruction<ExitSwitch>(nullptr));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_switch: has no parent control instruction
+        exit_switch  # undef
+        ^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # case
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch  # undef
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_LessOperandsThenSwitchParams) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    switch_->SetResults(Vector{r1, r2});
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_, 1_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_switch: provides 1 value but 'switch' expects 2 values
+        exit_switch 1i  # switch_1
+        ^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # case
+      ^^^
+
+:3:5 note: 'switch' declared here
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch 1i  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_MoreOperandsThenSwitchParams) {
+    auto* switch_ = b.Switch(1_i);
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    switch_->SetResults(Vector{r1, r2});
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_, 1_i, 2_f, 3_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_switch: provides 3 values but 'switch' expects 2 values
+        exit_switch 1i, 2.0f, 3i  # switch_1
+        ^^^^^^^^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # case
+      ^^^
+
+:3:5 note: 'switch' declared here
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch 1i, 2.0f, 3i  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_WithResult) {
+    auto* switch_ = b.Switch(1_i);
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    switch_->SetResults(Vector{r1, r2});
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_, 1_i, 2_f));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_IncorrectResultType) {
+    auto* switch_ = b.Switch(1_i);
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    switch_->SetResults(Vector{r1, r2});
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.ExitSwitch(switch_, 1_i, 2_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:25 error: exit_switch: operand with type 'i32' does not match 'switch' target type 'f32'
+        exit_switch 1i, 2i  # switch_1
+                        ^^
+
+:4:7 note: in block
+      $B2: {  # case
+      ^^^
+
+:3:13 note: %3 declared here
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+            ^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch 1i, 2i  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_NotInParentSwitch) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* def = b.DefaultCase(switch_);
+    def->Append(b.Return(f));
+
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+
+    auto* if_ = sb.Append(b.If(true));
+    b.Append(if_->True(), [&] { b.ExitSwitch(switch_); });
+    sb.Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:10:9 error: exit_switch: switch not found in parent control instructions
+        exit_switch  # switch_1
+        ^^^^^^^^^^^
+
+:9:7 note: in block
+      $B3: {  # true
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        ret
+      }
+    }
+    if true [t: $B3] {  # if_1
+      $B3: {  # true
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_JumpsOverIfs) {
+    // switch(true) {
+    //   default: {
+    //     if (true) {
+    //      if (false) {
+    //         break;
+    //       }
+    //     }
+    //     break;
+    //  }
+    auto* switch_ = b.Switch(1_i);
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* def = b.DefaultCase(switch_);
+    b.Append(def, [&] {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] {
+            auto* inner_if_ = b.If(false);
+            b.Append(inner_if_->True(), [&] { b.ExitSwitch(switch_); });
+            b.Return(f);
+        });
+        b.ExitSwitch(switch_);
+    });
+
+    auto sb = b.Append(f->Block());
+    sb.Append(switch_);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverSwitch) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* def = b.DefaultCase(switch_);
+    b.Append(def, [&] {
+        auto* inner = b.Switch(0_i);
+        b.ExitSwitch(switch_);
+
+        auto* inner_def = b.DefaultCase(inner);
+        b.Append(inner_def, [&] { b.ExitSwitch(switch_); });
+    });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(switch_);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
+            exit_switch  # switch_1
+            ^^^^^^^^^^^
+
+:6:11 note: in block
+          $B3: {  # case
+          ^^^
+
+:5:9 note: first control instruction jumped
+        switch 0i [c: (default, $B3)] {  # switch_2
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        switch 0i [c: (default, $B3)] {  # switch_2
+          $B3: {  # case
+            exit_switch  # switch_1
+          }
+        }
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverLoop) {
+    auto* switch_ = b.Switch(1_i);
+
+    auto* def = b.DefaultCase(switch_);
+    b.Append(def, [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitSwitch(switch_); });
+        b.ExitSwitch(switch_);
+    });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(switch_);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
+            exit_switch  # switch_1
+            ^^^^^^^^^^^
+
+:6:11 note: in block
+          $B3: {  # body
+          ^^^
+
+:5:9 note: first control instruction jumped
+        loop [b: $B3] {  # loop_1
+        ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        loop [b: $B3] {  # loop_1
+          $B3: {  # body
+            exit_switch  # switch_1
+          }
+        }
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_OutsideOfLoop) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Continue(loop);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:5 error: continue: called outside of associated loop
+    continue  # -> $B3
+    ^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2: {  # body
+        exit_loop  # loop_1
+      }
+    }
+    continue  # -> $B3
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_InLoopInit) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] { b.Continue(loop); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: continue: must only be called from loop body
+        continue  # -> $B4
+        ^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        continue  # -> $B4
+      }
+      $B3: {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_InLoopBody) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Continue_InLoopContinuing) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Append(loop->Continuing(), [&] { b.Continue(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: continue: must only be called from loop body
+        continue  # -> $B3
+        ^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        exit_loop  # loop_1
+      }
+      $B3: {  # continuing
+        continue  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_UnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: continue: provides 2 values but 'loop' block $B3 expects 0 values
+        continue 1i, 2.0f  # -> $B3
+        ^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3: {  # continuing
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue 1i, 2.0f  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: continue: provides 0 values but 'loop' block $B3 expects 2 values
+        continue  # -> $B3
+        ^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3 (%2:i32, %3:i32): {  # continuing
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3 (%2:i32, %3:i32): {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_i, 3_f, false); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:22 error: continue: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
+        continue 1i, 2i, 3.0f, false  # -> $B3
+                     ^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:20 note: %3 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+                   ^^
+
+:5:26 error: continue: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
+        continue 1i, 2i, 3.0f, false  # -> $B3
+                         ^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:28 note: %4 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue 1i, 2i, 3.0f, false  # -> $B3
+      }
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f, 3_u, false); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_OutsideOfLoop) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.NextIteration(loop);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:5 error: next_iteration: called outside of associated loop
+    next_iteration  # -> $B2
+    ^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2: {  # body
+        exit_loop  # loop_1
+      }
+    }
+    next_iteration  # -> $B2
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_InLoopInit) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_InLoopBody) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.NextIteration(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: next_iteration: must only be called from loop initializer or continuing
+        next_iteration  # -> $B2
+        ^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2: {  # body
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_InLoopContinuing) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Append(loop->Continuing(), [&] { b.NextIteration(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_UnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: next_iteration: provides 2 values but 'loop' block $B3 expects 0 values
+        next_iteration 1i, 2.0f  # -> $B3
+        ^^^^^^^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration 1i, 2.0f  # -> $B3
+      }
+      $B3: {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: next_iteration: provides 0 values but 'loop' block $B3 expects 2 values
+        next_iteration  # -> $B3
+        ^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3 (%2:i32, %3:i32): {  # body
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration  # -> $B3
+      }
+      $B3 (%2:i32, %3:i32): {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_i, 3_f, false); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:28 error: next_iteration: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+                           ^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:20 note: %3 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                   ^^
+
+:5:32 error: next_iteration: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+                               ^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:28 note: %4 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+      }
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f, 3_u, false); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, LoopBodyParamsWithoutInitializer) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: loop: loop with body block parameters must have an initializer
+    loop [b: $B2] {  # loop_1
+    ^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2 (%2:i32, %3:i32): {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ContinuingUseValueBeforeContinue) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* value = b.Let("value", 1_i);
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            b.Append(value);
+            b.Append(b.If(true)->True(), [&] { b.Continue(loop); });
+            b.ExitLoop(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            b.Let("use", value);
+            b.NextIteration(loop);
+        });
+        b.Return(f);
+    });
+
+    ASSERT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ContinuingUseValueAfterContinue) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* value = b.Let("value", 1_i);
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            b.Append(b.If(true)->True(), [&] { b.Continue(loop); });
+            b.Append(value);
+            b.ExitLoop(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            b.Let("use", value);
+            b.NextIteration(loop);
+        });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:14:24 error: let: %value cannot be used in continuing block as it is declared after the first 'continue' in the loop's body
+        %use:i32 = let %value
+                       ^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:10:9 note: %value declared here
+        %value:i32 = let 1i
+        ^^^^^^^^^^
+
+:7:13 note: loop body's first 'continue'
+            continue  # -> $B3
+            ^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        if true [t: $B4] {  # if_1
+          $B4: {  # true
+            continue  # -> $B3
+          }
+        }
+        %value:i32 = let 1i
+        exit_loop  # loop_1
+      }
+      $B3: {  # continuing
+        %use:i32 = let %value
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterUnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 2 values but 'loop' block $B2 expects 0 values
+        break_if true next_iteration: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:4:7 note: 'loop' block $B2 declared here
+      $B2: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true next_iteration: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, nullptr, nullptr); });
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:11:9 error: break_if: provides 0 values but 'loop' block $B3 expects 2 values
+        break_if true  # -> [t: exit_loop loop_1, f: $B3]
+        ^^^^^^^^^^^^^
+
+:10:7 note: in block
+      $B4: {  # continuing
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3 (%2:i32, %3:i32): {  # body
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration undef, undef  # -> $B3
+      }
+      $B3 (%2:i32, %3:i32): {  # body
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B3]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(),
+                 [&] { b.NextIteration(loop, nullptr, nullptr, nullptr, nullptr); });
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i, 3_f, false), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:11:45 error: break_if: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
+        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
+                                            ^^
+
+:10:7 note: in block
+      $B4: {  # continuing
+      ^^^
+
+:7:20 note: %3 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                   ^^
+
+:11:49 error: break_if: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
+        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
+                                                ^^^^
+
+:10:7 note: in block
+      $B4: {  # continuing
+      ^^^
+
+:7:28 note: %4 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration undef, undef, undef, undef  # -> $B3
+      }
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(),
+                 [&] { b.NextIteration(loop, nullptr, nullptr, nullptr, nullptr); });
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_f, 3_u, false), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitUnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 2 values but 'loop' expects 0 values
+        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:5 note: 'loop' declared here
+    loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<i32>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 0 values but 'loop' expects 2 values
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:5 note: 'loop' declared here
+    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
+                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i, 3_f, false)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:8:40 error: break_if: operand with type 'i32' does not match 'loop' target type 'f32'
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+                                       ^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:13 note: %3 declared here
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+            ^^^^^^
+
+:8:44 error: break_if: operand with type 'f32' does not match 'loop' target type 'u32'
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+                                           ^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:21 note: %4 declared here
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+                    ^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
+                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_f, 3_u, false)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop) {
+    auto* loop = b.Loop();
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.ExitLoop(loop));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_NullLoop) {
+    auto* loop = b.Loop();
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(mod.CreateInstruction<ExitLoop>(nullptr));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: has no parent control instruction
+        exit_loop  # undef
+        ^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        exit_loop  # undef
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_LessOperandsThenLoopParams) {
+    auto* loop = b.Loop();
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    loop->SetResults(Vector{r1, r2});
+
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.ExitLoop(loop, 1_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: provides 1 value but 'loop' expects 2 values
+        exit_loop 1i  # loop_1
+        ^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:3:5 note: 'loop' declared here
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        exit_loop 1i  # loop_1
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_MoreOperandsThenLoopParams) {
+    auto* loop = b.Loop();
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    loop->SetResults(Vector{r1, r2});
+
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_f, 3_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: provides 3 values but 'loop' expects 2 values
+        exit_loop 1i, 2.0f, 3i  # loop_1
+        ^^^^^^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:3:5 note: 'loop' declared here
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        exit_loop 1i, 2.0f, 3i  # loop_1
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_WithResult) {
+    auto* loop = b.Loop();
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    loop->SetResults(Vector{r1, r2});
+
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_f));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_IncorrectResultType) {
+    auto* loop = b.Loop();
+    auto* r1 = b.InstructionResult(ty.i32());
+    auto* r2 = b.InstructionResult(ty.f32());
+    loop->SetResults(Vector{r1, r2});
+
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:23 error: exit_loop: operand with type 'i32' does not match 'loop' target type 'f32'
+        exit_loop 1i, 2i  # loop_1
+                      ^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:3:13 note: %3 declared here
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+            ^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        exit_loop 1i, 2i  # loop_1
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_NotInParentLoop) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* loop = b.Loop();
+    loop->Continuing()->Append(b.NextIteration(loop));
+    loop->Body()->Append(b.Return(f));
+
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+
+    auto* if_ = sb.Append(b.If(true));
+    b.Append(if_->True(), [&] { b.ExitLoop(loop); });
+    sb.Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:13:9 error: exit_loop: loop not found in parent control instructions
+        exit_loop  # loop_1
+        ^^^^^^^^^
+
+:12:7 note: in block
+      $B4: {  # true
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        ret
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    if true [t: $B4] {  # if_1
+      $B4: {  # true
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_JumpsOverIfs) {
+    // loop {
+    //   if (true) {
+    //    if (false) {
+    //       break;
+    //     }
+    //   }
+    //   break;
+    // }
+    auto* loop = b.Loop();
+    loop->Continuing()->Append(b.NextIteration(loop));
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(loop->Body(), [&] {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] {
+            auto* inner_if_ = b.If(false);
+            b.Append(inner_if_->True(), [&] { b.ExitLoop(loop); });
+            b.Return(f);
+        });
+        b.ExitLoop(loop);
+    });
+
+    auto sb = b.Append(f->Block());
+    sb.Append(loop);
+    sb.Return(f);
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidJumpOverSwitch) {
+    auto* loop = b.Loop();
+    loop->Continuing()->Append(b.NextIteration(loop));
+
+    b.Append(loop->Body(), [&] {
+        auto* inner = b.Switch(1_i);
+        b.ExitLoop(loop);
+
+        auto* inner_def = b.DefaultCase(inner);
+        b.Append(inner_def, [&] { b.ExitLoop(loop); });
+    });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
+            exit_loop  # loop_1
+            ^^^^^^^^^
+
+:6:11 note: in block
+          $B4: {  # case
+          ^^^
+
+:5:9 note: first control instruction jumped
+        switch 1i [c: (default, $B4)] {  # switch_1
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        switch 1i [c: (default, $B4)] {  # switch_1
+          $B4: {  # case
+            exit_loop  # loop_1
+          }
+        }
+        exit_loop  # loop_1
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidJumpOverLoop) {
+    auto* outer_loop = b.Loop();
+
+    outer_loop->Continuing()->Append(b.NextIteration(outer_loop));
+
+    b.Append(outer_loop->Body(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.ExitLoop(outer_loop); });
+        b.ExitLoop(outer_loop);
+    });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(outer_loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
+            exit_loop  # loop_1
+            ^^^^^^^^^
+
+:6:11 note: in block
+          $B4: {  # body
+          ^^^
+
+:5:9 note: first control instruction jumped
+        loop [b: $B4] {  # loop_2
+        ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        loop [b: $B4] {  # loop_2
+          $B4: {  # body
+            exit_loop  # loop_1
+          }
+        }
+        exit_loop  # loop_1
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideContinuing) {
+    auto* loop = b.Loop();
+
+    loop->Continuing()->Append(b.ExitLoop(loop));
+    loop->Body()->Append(b.Continue(loop));
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: exit_loop: loop exit jumps out of continuing block
+        exit_loop  # loop_1
+        ^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideContinuingNested) {
+    auto* loop = b.Loop();
+
+    b.Append(loop->Continuing(), [&]() {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&]() { b.ExitLoop(loop); });
+        b.NextIteration(loop);
+    });
+
+    b.Append(loop->Body(), [&] { b.Continue(loop); });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:10:13 error: exit_loop: loop exit jumps out of continuing block
+            exit_loop  # loop_1
+            ^^^^^^^^^
+
+:9:11 note: in block
+          $B4: {  # true
+          ^^^
+
+:7:7 note: in continuing block
+      $B3: {  # continuing
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        if true [t: $B4] {  # if_1
+          $B4: {  # true
+            exit_loop  # loop_1
+          }
+        }
+        next_iteration  # -> $B2
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideInitializer) {
+    auto* loop = b.Loop();
+
+    loop->Initializer()->Append(b.ExitLoop(loop));
+    loop->Continuing()->Append(b.NextIteration(loop));
+
+    b.Append(loop->Body(), [&] { b.Continue(loop); });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: loop exit not permitted in loop initializer
+        exit_loop  # loop_1
+        ^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        exit_loop  # loop_1
+      }
+      $B3: {  # body
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideInitializerNested) {
+    auto* loop = b.Loop();
+
+    b.Append(loop->Initializer(), [&]() {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&]() { b.ExitLoop(loop); });
+        b.NextIteration(loop);
+    });
+    loop->Continuing()->Append(b.NextIteration(loop));
+
+    b.Append(loop->Body(), [&] { b.Continue(loop); });
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Append(loop);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:13 error: exit_loop: loop exit not permitted in loop initializer
+            exit_loop  # loop_1
+            ^^^^^^^^^
+
+:6:11 note: in block
+          $B5: {  # true
+          ^^^
+
+:4:7 note: in initializer block
+      $B2: {  # initializer
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        if true [t: $B5] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+        }
+        next_iteration  # -> $B3
+      }
+      $B3: {  # body
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {  //
+        b.Return(f);
+    });
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, Return_WithValue) {
+    auto* f = b.Function("my_func", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        b.Return(f, 42_i);
+    });
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, Return_UnexpectedResult) {
+    auto* f = b.Function("my_func", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        auto* r = b.Return(f, 42_i);
+        r->SetResults(Vector{b.InstructionResult(ty.i32())});
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: return: expected exactly 0 results, got 1
+    ret 42i
+    ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():i32 {
+  $B1: {
+    ret 42i
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return_NotFunction) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {  //
+        auto* var = b.Var(ty.ptr<function, f32>());
+        auto* r = b.Return(nullptr);
+        r->SetOperand(0, var->Result(0));
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: return: expected function for first operand
+    ret
+    ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, f32, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return_MissingFunction) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* r = b.Return(f);
+        r->ClearOperands();
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: return: expected between 1 and 2 operands, got 0
+    ret
+    ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return_UnexpectedValue) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {  //
+        b.Return(f, 42_i);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: return: unexpected return value
+    ret 42i
+    ^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    ret 42i
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return_MissingValue) {
+    auto* f = b.Function("my_func", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: return: expected return value
+    ret
+    ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():i32 {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Return_WrongValueType) {
+    auto* f = b.Function("my_func", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        b.Return(f, 42_f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:5 error: return: return value type 'f32' does not match function return type 'i32'
+    ret 42.0f
+    ^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():i32 {
+  $B1: {
+    ret 42.0f
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Unreachable_UnexpectedResult) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {  //
+        auto* u = b.Unreachable();
+        u->SetResults(Vector{b.InstructionResult(ty.i32())});
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: unreachable: expected exactly 0 results, got 1
+    unreachable
+    ^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Unreachable_UnexpectedOperand) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {  //
+        auto* u = b.Unreachable();
+        u->SetOperands(Vector{b.Value(0_i)});
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: unreachable: expected exactly 0 operands, got 1
+    unreachable
+    ^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Switch_ConditionPointer) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* s = b.Switch(b.Var("a", b.Zero<i32>()));
+        b.Append(b.DefaultCase(s), [&] { b.ExitSwitch(s); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(error: switch: condition type must be an integer scalar
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %a:ptr<function, i32, read_write> = var, 0i
+    switch %a [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Switch_NoCases) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Switch(1_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: switch: missing default case for switch
+    switch 1i [] {  # switch_1
+    ^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [] {  # switch_1
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Switch_NoDefaultCase) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* s = b.Switch(1_i);
+        b.Append(b.Case(s, {b.Constant(0_i)}), [&] { b.ExitSwitch(s); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: switch: missing default case for switch
+    switch 1i [c: (0i, $B2)] {  # switch_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch 1i [c: (0i, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Switch_NoCondition) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* s = b.ir.CreateInstruction<ir::Switch>();
+    f->Block()->Append(s);
+    b.Append(b.DefaultCase(s), [&] { b.ExitSwitch(s); });
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(error: switch: operand is undefined
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    switch undef [c: (default, $B2)] {  # switch_1
+      $B2: {  # case
+        exit_switch  # switch_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_function_test.cc b/src/tint/lang/core/ir/validator_function_test.cc
new file mode 100644
index 0000000..2c2d7f5
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_function_test.cc
@@ -0,0 +1,1468 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/memory_view.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Function) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    f->SetParams({b.FunctionParam(ty.i32()), b.FunctionParam(ty.f32())});
+    f->Block()->Append(b.Return(f));
+
+    EXPECT_EQ(ir::Validate(mod), Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_NoType) {
+    auto* valid = b.Function("valid", ty.void_());
+    valid->Block()->Append(b.Return(valid));
+
+    auto* invalid = mod.CreateValue<ir::Function>(nullptr, ty.void_());
+    invalid->SetBlock(mod.blocks.Create<ir::Block>());
+    mod.SetName(invalid, "invalid");
+    mod.functions.Push(invalid);
+    invalid->Block()->Append(b.Return(invalid));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: functions must have type '<function>'
+%invalid = func():void {
+^^^^^^^^
+
+note: # Disassembly
+%valid = func():void {
+  $B1: {
+    ret
+  }
+}
+%invalid = func():void {
+  $B2: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Duplicate) {
+    auto* f = b.Function("my_func", ty.void_());
+    // Function would auto-push by the builder, so this adds a duplicate
+    mod.functions.Push(f);
+
+    f->SetParams({b.FunctionParam(ty.i32()), b.FunctionParam(ty.f32())});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: function %my_func added to module multiple times
+%my_func = func(%2:i32, %3:f32):void {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%2:i32, %3:f32):void {
+  $B1: {
+    ret
+  }
+}
+%my_func = func(%2:i32, %3:f32):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_DuplicateEntryPointNames) {
+    auto* c = ComputeEntryPoint("dup");
+    c->Block()->Append(b.Return(c));
+
+    auto* f = FragmentEntryPoint("dup");
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: entry point name 'dup' is not unique
+%dup_1 = @fragment func():void {  # %dup_1: 'dup'
+^^^^^^
+
+note: # Disassembly
+%dup = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    ret
+  }
+}
+%dup_1 = @fragment func():void {  # %dup_1: 'dup'
+  $B2: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_MultinBlock) {
+    auto* f = b.Function("my_func", ty.void_());
+    f->SetBlock(b.MultiInBlock());
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: root block for function cannot be a multi-in block
+%my_func = func():void {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_DeadParameter) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    p->Destroy();
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: destroyed parameter found in function parameter list
+%my_func = func(%my_param:f32):void {
+                ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:f32):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithNullFunction) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    p->SetFunction(nullptr);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: function parameter has nullptr parent function
+%my_func = func(%my_param:f32):void {
+                ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:f32):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterUsedInMultipleFunctions) {
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    auto* f1 = b.Function("my_func1", ty.void_());
+    auto* f2 = b.Function("my_func2", ty.void_());
+    f1->SetParams({p});
+    f2->SetParams({p});
+    f1->Block()->Append(b.Return(f1));
+    f2->Block()->Append(b.Return(f2));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:18 error: function parameter has incorrect parent function
+%my_func1 = func(%my_param:f32):void {
+                 ^^^^^^^^^^^^^
+
+:6:1 note: parent function declared here
+%my_func2 = func(%my_param:f32):void {
+^^^^^^^^^
+
+note: # Disassembly
+%my_func1 = func(%my_param:f32):void {
+  $B1: {
+    ret
+  }
+}
+%my_func2 = func(%my_param:f32):void {
+  $B2: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithNullType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", nullptr);
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: function parameter has nullptr type
+%my_func = func(%my_param:undef):void {
+                ^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:undef):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterDuplicated) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.u32());
+    f->SetParams({p, p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: function parameter is not unique
+%my_func = func(%my_param:u32%my_param:u32):void {
+                ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:u32%my_param:u32):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_MultipleIOAnnotations) {
+    auto* f = FragmentEntryPoint("my_func");
+
+    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
+    p->SetBuiltin(BuiltinValue::kPosition);
+    p->SetLocation(0);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:27 error: input param has more than one IO annotation, [ @location, built-in ]
+%my_func = @fragment func(%my_param:vec4<f32> [@location(0), @position]):void {
+                          ^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = @fragment func(%my_param:vec4<f32> [@location(0), @position]):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_MultipleIOAnnotations) {
+    auto* f = FragmentEntryPoint("my_func");
+
+    IOAttributes attr;
+    attr.builtin = BuiltinValue::kPosition;
+    attr.color = 0;
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
+                                               });
+    auto* p = b.FunctionParam("my_param", str_ty);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:27 error: input param struct member has more than one IO annotation, [ built-in, @color ]
+%my_func = @fragment func(%my_param:MyStruct):void {
+                          ^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  a:vec4<f32> @offset(0), @color(0), @builtin(position)
+}
+
+%my_func = @fragment func(%my_param:MyStruct):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_MissingIOAnnotations) {
+    auto* f = FragmentEntryPoint("my_func");
+
+    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:27 error: input param must have at least one IO annotation, e.g. a binding point, a location, etc
+%my_func = @fragment func(%my_param:vec4<f32>):void {
+                          ^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = @fragment func(%my_param:vec4<f32>):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_MissingIOAnnotations) {
+    auto* f = ComputeEntryPoint("my_func");
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("a"), ty.vec4<f32>(), {}},
+                                               });
+    auto* p = b.FunctionParam("my_param", str_ty);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:54 error: input param struct members must have at least one IO annotation, e.g. a binding point, a location, etc
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
+                                                     ^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  a:vec4<f32> @offset(0)
+}
+
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_DuplicateAnnotations) {
+    auto* f = ComputeEntryPoint("my_func");
+    IOAttributes attr;
+    attr.location = 0;
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
+                                               });
+    auto* p = b.FunctionParam("my_param", str_ty);
+    p->SetLocation(0);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:54 error: input param struct member has same IO annotation, as top-level struct, '@location'
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct [@location(0)]):void {
+                                                     ^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  a:vec4<f32> @offset(0), @location(0)
+}
+
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct [@location(0)]):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_WorkgroupPlusOtherIOAnnotation) {
+    auto* f = ComputeEntryPoint("my_func");
+    auto* p = b.FunctionParam("my_param", ty.ptr<workgroup, i32>());
+    p->SetLocation(0);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:54 error: input param has more than one IO annotation, [ @location, <workgroup> ]
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:ptr<workgroup, i32, read_write> [@location(0)]):void {
+                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:ptr<workgroup, i32, read_write> [@location(0)]):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_WorkgroupPlusOtherIOAnnotations) {
+    auto* f = ComputeEntryPoint("my_func");
+    IOAttributes attr;
+    attr.location = 0;
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"),
+                             {
+                                 {mod.symbols.New("a"), ty.ptr<workgroup, i32>(), attr},
+                             });
+    auto* p = b.FunctionParam("my_param", str_ty);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:54 error: input param struct member has more than one IO annotation, [ @location, <workgroup> ]
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
+                                                     ^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(1) {
+  a:ptr<workgroup, i32, read_write> @offset(0), @location(0)
+}
+
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithConstructibleType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.u32());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithPointerType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.ptr<function, i32>());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithTextureType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.external_texture());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithSamplerType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.sampler());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithVoidType) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.void_());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:17 error: function parameter type must be constructible, a pointer, a texture, or a sampler
+%my_func = func(%my_param:void):void {
+                ^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:void):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_InvariantWithPosition) {
+    auto* f = b.Function("my_func", ty.void_(), Function::PipelineStage::kFragment);
+
+    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
+    p->SetInvariant(true);
+    p->SetBuiltin(BuiltinValue::kPosition);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_InvariantWithoutPosition) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
+    p->SetInvariant(true);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:17 error: invariant can only decorate a param iff it is also decorated with position
+%my_func = func(%my_param:vec4<f32> [@invariant]):void {
+                ^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:vec4<f32> [@invariant]):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_InvariantWithPosition) {
+    auto* f = b.Function("my_func", ty.void_(), Function::PipelineStage::kFragment);
+
+    IOAttributes attr;
+    attr.invariant = true;
+    attr.builtin = BuiltinValue::kPosition;
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
+                                               });
+    auto* p = b.FunctionParam("my_param", str_ty);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_Struct_InvariantWithoutPosition) {
+    IOAttributes attr;
+    attr.invariant = true;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
+                                               });
+
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", str_ty);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:17 error: invariant can only decorate a param member iff it is also decorated with position
+%my_func = func(%my_param:MyStruct):void {
+                ^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @invariant
+}
+
+%my_func = func(%my_param:MyStruct):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Param_BindingPointWithoutCapability) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.ptr<uniform, i32>());
+    p->SetBindingPoint(0, 0);
+    f->SetParams({p});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: input param to non-entry point function has a binding point set
+%my_func = func(%my_param:ptr<uniform, i32, read> [@binding_point(0, 0)]):void {
+                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:ptr<uniform, i32, read> [@binding_point(0, 0)]):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_MultipleIOAnnotations) {
+    auto* f = VertexEntryPoint("my_func");
+    f->SetReturnLocation(0);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: return values has more than one IO annotation, [ @location, built-in ]
+%my_func = @vertex func():vec4<f32> [@location(0), @position] {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = @vertex func():vec4<f32> [@location(0), @position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_Struct_MultipleIOAnnotations) {
+    IOAttributes attr;
+    attr.builtin = BuiltinValue::kPosition;
+    attr.location = 0;
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
+                                               });
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:1 error: return values struct member has more than one IO annotation, [ @location, built-in ]
+%my_func = @vertex func():MyStruct {
+^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  a:vec4<f32> @offset(0), @location(0), @builtin(position)
+}
+
+%my_func = @vertex func():MyStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_Void_IOAnnotation) {
+    auto* f = FragmentEntryPoint();
+    f->SetReturnLocation(0);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: return values with void type should never be annotated
+%f = @fragment func():void [@location(0)] {
+^^
+
+note: # Disassembly
+%f = @fragment func():void [@location(0)] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_NonVoid_MissingIOAnnotations) {
+    auto* f = b.Function("my_func", ty.f32(), Function::PipelineStage::kFragment);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:1 error: return values must have at least one IO annotation, e.g. a binding point, a location, etc
+%my_func = @fragment func():f32 {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = @fragment func():f32 {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_NonVoid_Struct_MissingIOAnnotations) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.f32(), {}},
+                                                          });
+
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kFragment);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:1 error: return values struct members must have at least one IO annotation, e.g. a binding point, a location, etc
+%my_func = @fragment func():MyStruct {
+^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:f32 @offset(0)
+}
+
+%my_func = @fragment func():MyStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_InvariantWithPosition) {
+    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
+    f->SetReturnBuiltin(BuiltinValue::kPosition);
+    f->SetReturnInvariant(true);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_InvariantWithoutPosition) {
+    auto* f = b.Function("my_func", ty.vec4<f32>());
+    f->SetReturnInvariant(true);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: invariant can only decorate outputs iff they are also position builtins
+%my_func = func():vec4<f32> [@invariant] {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = func():vec4<f32> [@invariant] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_Struct_InvariantWithPosition) {
+    IOAttributes attr;
+    attr.invariant = true;
+    attr.builtin = BuiltinValue::kPosition;
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
+                                               });
+
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Return_Struct_InvariantWithoutPosition) {
+    IOAttributes attr;
+    attr.invariant = true;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
+                                               });
+
+    auto* f = b.Function("my_func", str_ty);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:1 error: invariant can only decorate output members iff they are also position builtins
+%my_func = func():MyStruct {
+^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @invariant
+}
+
+%my_func = func():MyStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_UnnamedEntryPoint) {
+    auto* f = b.Function(ty.void_(), ir::Function::PipelineStage::kCompute);
+    f->SetWorkgroupSize({b.Constant(1_u), b.Constant(1_u), b.Constant(1_u)});
+
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: entry points must have names
+%1 = @compute @workgroup_size(1u, 1u, 1u) func():void {
+^^
+
+note: # Disassembly
+%1 = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_NonConstructibleReturnType) {
+    auto types = Vector<const core::type::Type*, 2>{
+        ty.external_texture(),   ty.sampler(), ty.runtime_array(ty.f32()), ty.ptr<function, i32>(),
+        ty.ref<function, u32>(),
+    };
+
+    for (auto t : types) {
+        auto* f = b.Function(t);
+        b.Append(f->Block(), [&] { b.Unreachable(); });
+    }
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: function return type must be constructible
+%1 = func():texture_external {
+^^
+
+:6:1 error: function return type must be constructible
+%2 = func():sampler {
+^^
+
+:11:1 error: function return type must be constructible
+%3 = func():array<f32> {
+^^
+
+:16:1 error: function return type must be constructible
+%4 = func():ptr<function, i32, read_write> {
+^^
+
+:21:1 error: reference types are not permitted here
+%5 = func():ref<function, u32, read_write> {
+^^
+
+:21:1 error: function return type must be constructible
+%5 = func():ref<function, u32, read_write> {
+^^
+
+note: # Disassembly
+%1 = func():texture_external {
+  $B1: {
+    unreachable
+  }
+}
+%2 = func():sampler {
+  $B2: {
+    unreachable
+  }
+}
+%3 = func():array<f32> {
+  $B3: {
+    unreachable
+  }
+}
+%4 = func():ptr<function, i32, read_write> {
+  $B4: {
+    unreachable
+  }
+}
+%5 = func():ref<function, u32, read_write> {
+  $B5: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Compute_NonVoidReturn) {
+    auto* f = b.Function("my_func", ty.f32(), core::ir::Function::PipelineStage::kCompute);
+    f->SetWorkgroupSize(b.Constant(1_u), b.Constant(1_u), b.Constant(1_u));
+    f->SetReturnLocation(0);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: compute entry point must not have a return type
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func():f32 [@location(0)] {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = @compute @workgroup_size(1u, 1u, 1u) func():f32 [@location(0)] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_MissingOnCompute) {
+    auto* f = b.Function("f", ty.void_(), Function::PipelineStage::kCompute);
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: compute entry point requires @workgroup_size
+%f = @compute func():void {
+^^
+
+note: # Disassembly
+%f = @compute func():void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_NonCompute) {
+    auto* f = FragmentEntryPoint();
+    f->SetWorkgroupSize(b.Constant(1_u), b.Constant(1_u), b.Constant(1_u));
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: @workgroup_size only valid on compute entry point
+%f = @fragment @workgroup_size(1u, 1u, 1u) func():void {
+^^
+
+note: # Disassembly
+%f = @fragment @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamUndefined) {
+    auto* f = ComputeEntryPoint();
+    f->SetWorkgroupSize({nullptr, b.Constant(2_u), b.Constant(3_u)});
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: a @workgroup_size param is undefined or missing a type
+%f = @compute @workgroup_size(undef, 2u, 3u) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(undef, 2u, 3u) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamWrongType) {
+    auto* f = ComputeEntryPoint();
+    f->SetWorkgroupSize({b.Constant(1_f), b.Constant(2_u), b.Constant(3_u)});
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: @workgroup_size params must be an i32 or u32
+%f = @compute @workgroup_size(1.0f, 2u, 3u) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1.0f, 2u, 3u) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamsSameType) {
+    auto* f = ComputeEntryPoint();
+    f->SetWorkgroupSize({b.Constant(1_u), b.Constant(2_i), b.Constant(3_u)});
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: @workgroup_size params must be all i32s or all u32s
+%f = @compute @workgroup_size(1u, 2i, 3u) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(1u, 2i, 3u) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamsTooSmall) {
+    auto* f = ComputeEntryPoint();
+    f->SetWorkgroupSize({b.Constant(-1_i), b.Constant(2_i), b.Constant(3_i)});
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: @workgroup_size params must be greater than 0
+%f = @compute @workgroup_size(-1i, 2i, 3i) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(-1i, 2i, 3i) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_OverrideWithoutAllowOverrides) {
+    auto* o = b.Override(ty.u32());
+    auto* f = ComputeEntryPoint();
+    f->SetWorkgroupSize({o->Result(0), o->Result(0), o->Result(0)});
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:1 error: @workgroup_size param is not a constant value, and IR capability 'kAllowOverrides' is not set
+%f = @compute @workgroup_size(%2, %2, %2) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(%2, %2, %2) func():void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_WorkgroupSize_NonRootBlockOverride) {
+    auto* f = ComputeEntryPoint();
+    Override* o;
+    b.Append(f->Block(), [&] {
+        o = b.Override(ty.u32());
+        b.Return(f);
+    });
+    f->SetWorkgroupSize({o->Result(0), o->Result(0), o->Result(0)});
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowOverrides});
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: @workgroup_size param defined by non-module scope value
+%f = @compute @workgroup_size(%2, %2, %2) func():void {
+^^
+
+note: # Disassembly
+%f = @compute @workgroup_size(%2, %2, %2) func():void {
+  $B1: {
+    %2:u32 = override @id(0)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Vertex_BasicPosition) {
+    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
+    f->SetReturnBuiltin(BuiltinValue::kPosition);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Vertex_StructPosition) {
+    auto pos_ty = ty.vec4<f32>();
+    auto pos_attr = IOAttributes();
+    pos_attr.builtin = BuiltinValue::kPosition;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), pos_ty, pos_attr},
+                                               });
+
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Vertex_StructPositionAndClipDistances) {
+    auto pos_ty = ty.vec4<f32>();
+    auto pos_attr = IOAttributes();
+    pos_attr.builtin = BuiltinValue::kPosition;
+
+    auto clip_ty = ty.array<f32, 4>();
+    auto clip_attr = IOAttributes();
+    clip_attr.builtin = BuiltinValue::kClipDistances;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("pos"), pos_ty, pos_attr},
+                                                   {mod.symbols.New("clip"), clip_ty, clip_attr},
+                                               });
+
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Function_Vertex_StructOnlyClipDistances) {
+    auto clip_ty = ty.array<f32, 4>();
+    auto clip_attr = IOAttributes();
+    clip_attr.builtin = BuiltinValue::kClipDistances;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("clip"), clip_ty, clip_attr},
+                                               });
+
+    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:1 error: position must be declared for vertex entry point output
+%my_func = @vertex func():MyStruct {
+^^^^^^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  clip:array<f32, 4> @offset(0), @builtin(clip_distances)
+}
+
+%my_func = @vertex func():MyStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Vertex_MissingPosition) {
+    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
+    f->SetReturnLocation(0);
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: position must be declared for vertex entry point output
+%my_func = @vertex func():vec4<f32> [@location(0)] {
+^^^^^^^^
+
+note: # Disassembly
+%my_func = @vertex func():vec4<f32> [@location(0)] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_NonFragment_BoolInput) {
+    auto* f = VertexEntryPoint();
+    auto* p = b.FunctionParam("invalid", ty.bool_());
+    p->SetLocation(0);
+    f->AppendParam(p);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:19 error: entry point params can only be a bool for fragment shaders
+%f = @vertex func(%invalid:bool [@location(0)]):vec4<f32> [@position] {
+                  ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @vertex func(%invalid:bool [@location(0)]):vec4<f32> [@position] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_NonFragment_BoolOutput) {
+    auto* f = VertexEntryPoint();
+    IOAttributes attr;
+    attr.location = 0;
+    AddReturn(f, "invalid", ty.bool_(), attr);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:6:1 error: entry point return members can not be bool
+%f = @vertex func():OutputStruct {
+^^
+
+note: # Disassembly
+OutputStruct = struct @align(16) {
+  pos:vec4<f32> @offset(0), @builtin(position)
+  invalid:bool @offset(16), @location(0)
+}
+
+%f = @vertex func():OutputStruct {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Fragment_BoolInputWithoutFrontFacing) {
+    auto* f = FragmentEntryPoint();
+    auto* p = b.FunctionParam("invalid", ty.bool_());
+    p->SetLocation(0);
+    f->AppendParam(p);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:1:21 error: fragment entry point params can only be a bool if decorated with @builtin(front_facing)
+%f = @fragment func(%invalid:bool [@location(0)]):void {
+                    ^^^^^^^^^^^^^
+
+note: # Disassembly
+%f = @fragment func(%invalid:bool [@location(0)]):void {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_Fragment_BoolOutput) {
+    auto* f = FragmentEntryPoint();
+    IOAttributes attr;
+    attr.location = 0;
+    AddReturn(f, "invalid", ty.bool_(), attr);
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:1 error: entry point returns can not be bool
+%f = @fragment func():bool [@location(0)] {
+^^
+
+note: # Disassembly
+%f = @fragment func():bool [@location(0)] {
+  $B1: {
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_BoolOutput_via_MSV) {
+    auto* f = ComputeEntryPoint();
+
+    auto* v = b.Var(ty.ptr(AddressSpace::kOut, ty.bool_(), core::Access::kReadWrite));
+    IOAttributes attr;
+    attr.location = 0;
+    v->SetAttributes(attr);
+    mod.root_block->Append(v);
+
+    b.Append(f->Block(), [&] {
+        b.Append(
+            mod.CreateInstruction<ir::Store>(v->Result(0), b.Constant(b.ConstantValue(false))));
+        b.Unreachable();
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:1 error: IO address space values referenced by shader entry points can only be bool if in the input space, used only by fragment shaders and decorated with @builtin(front_facing)
+%f = @compute @workgroup_size(1u, 1u, 1u) func():void {
+^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<__out, bool, read_write> = var @location(0)
+}
+
+%f = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    store %1, false
+    unreachable
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_BoolInputWithoutFrontFacing_via_MSV) {
+    auto* f = FragmentEntryPoint();
+
+    auto* invalid = b.Var("invalid", AddressSpace::kIn, ty.bool_());
+    IOAttributes attr;
+    attr.location = 0;
+    invalid->SetAttributes(attr);
+    mod.root_block->Append(invalid);
+
+    b.Append(f->Block(), [&] {
+        auto* l = b.Load(invalid);
+        auto* v = b.Var("v", AddressSpace::kFunction, ty.bool_());
+        v->SetInitializer(l->Result(0));
+        b.Unreachable();
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:1 error: input address space values referenced by fragment shaders can only be a bool if decorated with @builtin(front_facing)
+%f = @fragment func():void {
+^^
+
+note: # Disassembly
+$B1: {  # root
+  %invalid:ptr<__in, bool, read> = var @location(0)
+}
+
+%f = @fragment func():void {
+  $B2: {
+    %3:bool = load %invalid
+    %v:ptr<function, bool, read_write> = var, %3
+    unreachable
+  }
+}
+)");
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 5490d83..0f5e595 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -25,109 +25,89 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include <string>
-#include <tuple>
-#include <utility>
+#include "src/tint/lang/core/ir/validator_test.h"
 
-#include "gmock/gmock.h"
+#include <string>
+
 #include "gtest/gtest.h"
 
-#include "src/tint/lang/core/address_space.h"
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/function_param.h"
-#include "src/tint/lang/core/ir/ir_helper_test.h"
 #include "src/tint/lang/core/ir/type/array_count.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/number.h"
-#include "src/tint/lang/core/texel_format.h"
 #include "src/tint/lang/core/type/abstract_float.h"
 #include "src/tint/lang/core/type/abstract_int.h"
 #include "src/tint/lang/core/type/manager.h"
 #include "src/tint/lang/core/type/matrix.h"
-#include "src/tint/lang/core/type/memory_view.h"
-#include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/struct.h"
 #include "src/tint/utils/text/string.h"
 
 namespace tint::core::ir {
-namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
 
-class IR_ValidatorTest : public IRTestHelper {
-  public:
-    /// Builds and returns a basic 'compute' entry point function, named @p name
-    Function* ComputeEntryPoint(const std::string& name = "f") { return b.ComputeFunction(name); }
+Function* IR_ValidatorTest::ComputeEntryPoint(const std::string& name) {
+    return b.ComputeFunction(name);
+}
 
-    /// Builds and returns a basic 'fragment' entry point function, named @p name
-    Function* FragmentEntryPoint(const std::string& name = "f") {
-        return b.Function(name, ty.void_(), Function::PipelineStage::kFragment);
+Function* IR_ValidatorTest::FragmentEntryPoint(const std::string& name) {
+    return b.Function(name, ty.void_(), Function::PipelineStage::kFragment);
+}
+
+Function* IR_ValidatorTest::VertexEntryPoint(const std::string& name) {
+    auto* f = b.Function(name, ty.vec4<f32>(), Function::PipelineStage::kVertex);
+    f->SetReturnBuiltin(BuiltinValue::kPosition);
+    return f;
+}
+
+void IR_ValidatorTest::AddBuiltinParam(Function* func,
+                                       const std::string& name,
+                                       BuiltinValue builtin,
+                                       const core::type::Type* type) {
+    auto* p = b.FunctionParam(name, type);
+    p->SetBuiltin(builtin);
+    func->AppendParam(p);
+}
+
+void IR_ValidatorTest::AddReturn(Function* func,
+                                 const std::string& name,
+                                 const core::type::Type* type,
+                                 const IOAttributes& attr) {
+    if (func->ReturnType()->Is<core::type::Struct>()) {
+        TINT_ICE() << "AddReturn does not support adding to structured returns";
     }
 
-    /// Builds and returns a basic 'vertex' entry point function, named @p name
-    Function* VertexEntryPoint(const std::string& name = "f") {
-        auto* f = b.Function(name, ty.vec4<f32>(), Function::PipelineStage::kVertex);
-        f->SetReturnBuiltin(BuiltinValue::kPosition);
-        return f;
+    if (func->ReturnType() == ty.void_()) {
+        func->SetReturnAttributes(attr);
+        func->SetReturnType(type);
+        return;
     }
 
-    /// Adds to a function an input param named @p name of type @p type, and decorated with @p
-    /// builtin
-    void AddBuiltinParam(Function* func,
-                         const std::string& name,
-                         BuiltinValue builtin,
-                         const core::type::Type* type) {
-        auto* p = b.FunctionParam(name, type);
-        p->SetBuiltin(builtin);
-        func->AppendParam(p);
-    }
+    std::string old_name =
+        func->ReturnAttributes().builtin == BuiltinValue::kPosition ? "pos" : "old_ret";
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("OutputStruct"),
+                  {
+                      {mod.symbols.New(old_name), func->ReturnType(), func->ReturnAttributes()},
+                      {mod.symbols.New(name), type, attr},
+                  });
 
-    /// Adds to a function an return value of type @p type with attributes @p attr.
-    /// If there is an already existing non-structured return, both values are moved into a
-    /// structured return using @p name as the name.
-    /// If there is an already existing structured return, then this ICEs, since that is beyond the
-    /// scope of this implementation.
-    void AddReturn(Function* func,
-                   const std::string& name,
-                   const core::type::Type* type,
-                   const IOAttributes& attr = {}) {
-        if (func->ReturnType()->Is<core::type::Struct>()) {
-            TINT_ICE() << "AddReturn does not support adding to structured returns";
-        }
+    func->SetReturnAttributes({});
+    func->SetReturnType(str_ty);
+}
 
-        if (func->ReturnType() == ty.void_()) {
-            func->SetReturnAttributes(attr);
-            func->SetReturnType(type);
-            return;
-        }
-
-        std::string old_name =
-            func->ReturnAttributes().builtin == BuiltinValue::kPosition ? "pos" : "old_ret";
-        auto* str_ty =
-            ty.Struct(mod.symbols.New("OutputStruct"),
-                      {
-                          {mod.symbols.New(old_name), func->ReturnType(), func->ReturnAttributes()},
-                          {mod.symbols.New(name), type, attr},
-                      });
-
-        func->SetReturnAttributes({});
-        func->SetReturnType(str_ty);
-    }
-
-    /// Adds to a function an return value of type @p type, and decorated with @p builtin.
-    /// See @ref AddReturn for more details
-    void AddBuiltinReturn(Function* func,
-                          const std::string& name,
-                          BuiltinValue builtin,
-                          const core::type::Type* type) {
-        IOAttributes attr;
-        attr.builtin = builtin;
-        AddReturn(func, name, type, attr);
-    }
-};
+void IR_ValidatorTest::AddBuiltinReturn(Function* func,
+                                        const std::string& name,
+                                        BuiltinValue builtin,
+                                        const core::type::Type* type) {
+    IOAttributes attr;
+    attr.builtin = builtin;
+    AddReturn(func, name, type, attr);
+}
 
 TEST_F(IR_ValidatorTest, RootBlock_Var) {
     mod.root_block->Append(b.Var(ty.ptr<private_, i32>()));
@@ -253,3349 +233,6 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, Abstract_Scalar) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Var("af", function, ty.AFloat());
-        b.Var("af", function, ty.AInt());
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: var: abstracts are not permitted
-    %af:ptr<function, abstract-float, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:4:5 error: var: abstracts are not permitted
-    %af_1:ptr<function, abstract-int, read_write> = var  # %af_1: 'af'
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %af:ptr<function, abstract-float, read_write> = var
-    %af_1:ptr<function, abstract-int, read_write> = var  # %af_1: 'af'
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Abstract_Vector) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Var("af", function, ty.vec2<AFloat>());
-        b.Var("ai", function, ty.vec3<AInt>());
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: var: abstracts are not permitted
-    %af:ptr<function, vec2<abstract-float>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:4:5 error: var: abstracts are not permitted
-    %ai:ptr<function, vec3<abstract-int>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %af:ptr<function, vec2<abstract-float>, read_write> = var
-    %ai:ptr<function, vec3<abstract-int>, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Abstract_Matrix) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Var("af", function, ty.mat2x2<AFloat>());
-        b.Var("ai", function, ty.mat3x4<AInt>());
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: var: abstracts are not permitted
-    %af:ptr<function, mat2x2<abstract-float>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:4:5 error: var: abstracts are not permitted
-    %ai:ptr<function, mat3x4<abstract-int>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %af:ptr<function, mat2x2<abstract-float>, read_write> = var
-    %ai:ptr<function, mat3x4<abstract-int>, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Abstract_Struct) {
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("af"), ty.AFloat(), {}},
-                                                   {mod.symbols.New("ai"), ty.AInt(), {}},
-                                               });
-    auto* v = b.Var(ty.ptr(private_, str_ty));
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:3 error: var: abstracts are not permitted
-  %1:ptr<private, MyStruct, read_write> = var
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:6:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-MyStruct = struct @align(1) {
-  af:abstract-float @offset(0)
-  ai:abstract-int @offset(0)
-}
-
-$B1: {  # root
-  %1:ptr<private, MyStruct, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Abstract_FunctionParam) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    f->SetParams({b.FunctionParam(ty.AFloat()), b.FunctionParam(ty.AInt())});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: abstracts are not permitted
-%my_func = func(%2:abstract-float, %3:abstract-int):void {
-                ^^^^^^^^^^^^^^^^^
-
-:1:36 error: abstracts are not permitted
-%my_func = func(%2:abstract-float, %3:abstract-int):void {
-                                   ^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%2:abstract-float, %3:abstract-int):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    f->SetParams({b.FunctionParam(ty.i32()), b.FunctionParam(ty.f32())});
-    f->Block()->Append(b.Return(f));
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_NoType) {
-    auto* valid = b.Function("valid", ty.void_());
-    valid->Block()->Append(b.Return(valid));
-
-    auto* invalid = mod.CreateValue<ir::Function>(nullptr, ty.void_());
-    invalid->SetBlock(mod.blocks.Create<ir::Block>());
-    mod.SetName(invalid, "invalid");
-    mod.functions.Push(invalid);
-    invalid->Block()->Append(b.Return(invalid));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: functions must have type '<function>'
-%invalid = func():void {
-^^^^^^^^
-
-note: # Disassembly
-%valid = func():void {
-  $B1: {
-    ret
-  }
-}
-%invalid = func():void {
-  $B2: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Duplicate) {
-    auto* f = b.Function("my_func", ty.void_());
-    // Function would auto-push by the builder, so this adds a duplicate
-    mod.functions.Push(f);
-
-    f->SetParams({b.FunctionParam(ty.i32()), b.FunctionParam(ty.f32())});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: function %my_func added to module multiple times
-%my_func = func(%2:i32, %3:f32):void {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%2:i32, %3:f32):void {
-  $B1: {
-    ret
-  }
-}
-%my_func = func(%2:i32, %3:f32):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_DuplicateEntryPointNames) {
-    auto* c = ComputeEntryPoint("dup");
-    c->Block()->Append(b.Return(c));
-
-    auto* f = FragmentEntryPoint("dup");
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: entry point name 'dup' is not unique
-%dup_1 = @fragment func():void {  # %dup_1: 'dup'
-^^^^^^
-
-note: # Disassembly
-%dup = @compute @workgroup_size(1u, 1u, 1u) func():void {
-  $B1: {
-    ret
-  }
-}
-%dup_1 = @fragment func():void {  # %dup_1: 'dup'
-  $B2: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_MultinBlock) {
-    auto* f = b.Function("my_func", ty.void_());
-    f->SetBlock(b.MultiInBlock());
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: root block for function cannot be a multi-in block
-%my_func = func():void {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_DeadParameter) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.f32());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    p->Destroy();
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: destroyed parameter found in function parameter list
-%my_func = func(%my_param:f32):void {
-                ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:f32):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithNullFunction) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.f32());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    p->SetFunction(nullptr);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: function parameter has nullptr parent function
-%my_func = func(%my_param:f32):void {
-                ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:f32):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterUsedInMultipleFunctions) {
-    auto* p = b.FunctionParam("my_param", ty.f32());
-    auto* f1 = b.Function("my_func1", ty.void_());
-    auto* f2 = b.Function("my_func2", ty.void_());
-    f1->SetParams({p});
-    f2->SetParams({p});
-    f1->Block()->Append(b.Return(f1));
-    f2->Block()->Append(b.Return(f2));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:18 error: function parameter has incorrect parent function
-%my_func1 = func(%my_param:f32):void {
-                 ^^^^^^^^^^^^^
-
-:6:1 note: parent function declared here
-%my_func2 = func(%my_param:f32):void {
-^^^^^^^^^
-
-note: # Disassembly
-%my_func1 = func(%my_param:f32):void {
-  $B1: {
-    ret
-  }
-}
-%my_func2 = func(%my_param:f32):void {
-  $B2: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithNullType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", nullptr);
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: function parameter has nullptr type
-%my_func = func(%my_param:undef):void {
-                ^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:undef):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterDuplicated) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.u32());
-    f->SetParams({p, p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: function parameter is not unique
-%my_func = func(%my_param:u32%my_param:u32):void {
-                ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:u32%my_param:u32):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_MultipleIOAnnotations) {
-    auto* f = FragmentEntryPoint("my_func");
-
-    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
-    p->SetBuiltin(BuiltinValue::kPosition);
-    p->SetLocation(0);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:27 error: input param has more than one IO annotation, [ @location, built-in ]
-%my_func = @fragment func(%my_param:vec4<f32> [@location(0), @position]):void {
-                          ^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = @fragment func(%my_param:vec4<f32> [@location(0), @position]):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_MultipleIOAnnotations) {
-    auto* f = FragmentEntryPoint("my_func");
-
-    IOAttributes attr;
-    attr.builtin = BuiltinValue::kPosition;
-    attr.color = 0;
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
-                                               });
-    auto* p = b.FunctionParam("my_param", str_ty);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:27 error: input param struct member has more than one IO annotation, [ built-in, @color ]
-%my_func = @fragment func(%my_param:MyStruct):void {
-                          ^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  a:vec4<f32> @offset(0), @color(0), @builtin(position)
-}
-
-%my_func = @fragment func(%my_param:MyStruct):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_MissingIOAnnotations) {
-    auto* f = FragmentEntryPoint("my_func");
-
-    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:27 error: input param must have at least one IO annotation, e.g. a binding point, a location, etc
-%my_func = @fragment func(%my_param:vec4<f32>):void {
-                          ^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = @fragment func(%my_param:vec4<f32>):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_MissingIOAnnotations) {
-    auto* f = ComputeEntryPoint("my_func");
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("a"), ty.vec4<f32>(), {}},
-                                               });
-    auto* p = b.FunctionParam("my_param", str_ty);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:54 error: input param struct members must have at least one IO annotation, e.g. a binding point, a location, etc
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
-                                                     ^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  a:vec4<f32> @offset(0)
-}
-
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_DuplicateAnnotations) {
-    auto* f = ComputeEntryPoint("my_func");
-    IOAttributes attr;
-    attr.location = 0;
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
-                                               });
-    auto* p = b.FunctionParam("my_param", str_ty);
-    p->SetLocation(0);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:54 error: input param struct member has same IO annotation, as top-level struct, '@location'
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct [@location(0)]):void {
-                                                     ^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  a:vec4<f32> @offset(0), @location(0)
-}
-
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct [@location(0)]):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_WorkgroupPlusOtherIOAnnotation) {
-    auto* f = ComputeEntryPoint("my_func");
-    auto* p = b.FunctionParam("my_param", ty.ptr<workgroup, i32>());
-    p->SetLocation(0);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:54 error: input param has more than one IO annotation, [ @location, <workgroup> ]
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:ptr<workgroup, i32, read_write> [@location(0)]):void {
-                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:ptr<workgroup, i32, read_write> [@location(0)]):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_WorkgroupPlusOtherIOAnnotations) {
-    auto* f = ComputeEntryPoint("my_func");
-    IOAttributes attr;
-    attr.location = 0;
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"),
-                             {
-                                 {mod.symbols.New("a"), ty.ptr<workgroup, i32>(), attr},
-                             });
-    auto* p = b.FunctionParam("my_param", str_ty);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:54 error: input param struct member has more than one IO annotation, [ @location, <workgroup> ]
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
-                                                     ^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(1) {
-  a:ptr<workgroup, i32, read_write> @offset(0), @location(0)
-}
-
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func(%my_param:MyStruct):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithConstructibleType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.u32());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithPointerType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.ptr<function, i32>());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithTextureType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.external_texture());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithSamplerType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.sampler());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_ParameterWithVoidType) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.void_());
-    f->SetParams({p});
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:17 error: function parameter type must be constructible, a pointer, a texture, or a sampler
-%my_func = func(%my_param:void):void {
-                ^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:void):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_InvariantWithPosition) {
-    auto* f = b.Function("my_func", ty.void_(), Function::PipelineStage::kFragment);
-
-    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
-    p->SetInvariant(true);
-    p->SetBuiltin(BuiltinValue::kPosition);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_InvariantWithoutPosition) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.vec4<f32>());
-    p->SetInvariant(true);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:17 error: invariant can only decorate a param iff it is also decorated with position
-%my_func = func(%my_param:vec4<f32> [@invariant]):void {
-                ^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:vec4<f32> [@invariant]):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_InvariantWithPosition) {
-    auto* f = b.Function("my_func", ty.void_(), Function::PipelineStage::kFragment);
-
-    IOAttributes attr;
-    attr.invariant = true;
-    attr.builtin = BuiltinValue::kPosition;
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
-                                               });
-    auto* p = b.FunctionParam("my_param", str_ty);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_Struct_InvariantWithoutPosition) {
-    IOAttributes attr;
-    attr.invariant = true;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
-                                               });
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", str_ty);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:17 error: invariant can only decorate a param member iff it is also decorated with position
-%my_func = func(%my_param:MyStruct):void {
-                ^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @invariant
-}
-
-%my_func = func(%my_param:MyStruct):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Param_BindingPointWithoutCapability) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* p = b.FunctionParam("my_param", ty.ptr<uniform, i32>());
-    p->SetBindingPoint(0, 0);
-    f->SetParams({p});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:17 error: input param to non-entry point function has a binding point set
-%my_func = func(%my_param:ptr<uniform, i32, read> [@binding_point(0, 0)]):void {
-                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func(%my_param:ptr<uniform, i32, read> [@binding_point(0, 0)]):void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_MultipleIOAnnotations) {
-    auto* f = VertexEntryPoint("my_func");
-    f->SetReturnLocation(0);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: return values has more than one IO annotation, [ @location, built-in ]
-%my_func = @vertex func():vec4<f32> [@location(0), @position] {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = @vertex func():vec4<f32> [@location(0), @position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_Struct_MultipleIOAnnotations) {
-    IOAttributes attr;
-    attr.builtin = BuiltinValue::kPosition;
-    attr.location = 0;
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("a"), ty.vec4<f32>(), attr},
-                                               });
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:1 error: return values struct member has more than one IO annotation, [ @location, built-in ]
-%my_func = @vertex func():MyStruct {
-^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  a:vec4<f32> @offset(0), @location(0), @builtin(position)
-}
-
-%my_func = @vertex func():MyStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_Void_IOAnnotation) {
-    auto* f = FragmentEntryPoint();
-    f->SetReturnLocation(0);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: return values with void type should never be annotated
-%f = @fragment func():void [@location(0)] {
-^^
-
-note: # Disassembly
-%f = @fragment func():void [@location(0)] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_NonVoid_MissingIOAnnotations) {
-    auto* f = b.Function("my_func", ty.f32(), Function::PipelineStage::kFragment);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:1 error: return values must have at least one IO annotation, e.g. a binding point, a location, etc
-%my_func = @fragment func():f32 {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = @fragment func():f32 {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_NonVoid_Struct_MissingIOAnnotations) {
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
-                                                              {mod.symbols.New("a"), ty.f32(), {}},
-                                                          });
-
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kFragment);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:1 error: return values struct members must have at least one IO annotation, e.g. a binding point, a location, etc
-%my_func = @fragment func():MyStruct {
-^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  a:f32 @offset(0)
-}
-
-%my_func = @fragment func():MyStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_InvariantWithPosition) {
-    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
-    f->SetReturnBuiltin(BuiltinValue::kPosition);
-    f->SetReturnInvariant(true);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_InvariantWithoutPosition) {
-    auto* f = b.Function("my_func", ty.vec4<f32>());
-    f->SetReturnInvariant(true);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: invariant can only decorate outputs iff they are also position builtins
-%my_func = func():vec4<f32> [@invariant] {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = func():vec4<f32> [@invariant] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_Struct_InvariantWithPosition) {
-    IOAttributes attr;
-    attr.invariant = true;
-    attr.builtin = BuiltinValue::kPosition;
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
-                                               });
-
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Return_Struct_InvariantWithoutPosition) {
-    IOAttributes attr;
-    attr.invariant = true;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), ty.vec4<f32>(), attr},
-                                               });
-
-    auto* f = b.Function("my_func", str_ty);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:1 error: invariant can only decorate output members iff they are also position builtins
-%my_func = func():MyStruct {
-^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @invariant
-}
-
-%my_func = func():MyStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_UnnamedEntryPoint) {
-    auto* f = b.Function(ty.void_(), ir::Function::PipelineStage::kCompute);
-    f->SetWorkgroupSize({b.Constant(1_u), b.Constant(1_u), b.Constant(1_u)});
-
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: entry points must have names
-%1 = @compute @workgroup_size(1u, 1u, 1u) func():void {
-^^
-
-note: # Disassembly
-%1 = @compute @workgroup_size(1u, 1u, 1u) func():void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_NonConstructibleReturnType) {
-    auto types = Vector<const core::type::Type*, 2>{
-        ty.external_texture(),   ty.sampler(), ty.runtime_array(ty.f32()), ty.ptr<function, i32>(),
-        ty.ref<function, u32>(),
-    };
-
-    for (auto t : types) {
-        auto* f = b.Function(t);
-        b.Append(f->Block(), [&] { b.Unreachable(); });
-    }
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: function return type must be constructible
-%1 = func():texture_external {
-^^
-
-:6:1 error: function return type must be constructible
-%2 = func():sampler {
-^^
-
-:11:1 error: function return type must be constructible
-%3 = func():array<f32> {
-^^
-
-:16:1 error: function return type must be constructible
-%4 = func():ptr<function, i32, read_write> {
-^^
-
-:21:1 error: reference types are not permitted here
-%5 = func():ref<function, u32, read_write> {
-^^
-
-:21:1 error: function return type must be constructible
-%5 = func():ref<function, u32, read_write> {
-^^
-
-note: # Disassembly
-%1 = func():texture_external {
-  $B1: {
-    unreachable
-  }
-}
-%2 = func():sampler {
-  $B2: {
-    unreachable
-  }
-}
-%3 = func():array<f32> {
-  $B3: {
-    unreachable
-  }
-}
-%4 = func():ptr<function, i32, read_write> {
-  $B4: {
-    unreachable
-  }
-}
-%5 = func():ref<function, u32, read_write> {
-  $B5: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Compute_NonVoidReturn) {
-    auto* f = b.Function("my_func", ty.f32(), core::ir::Function::PipelineStage::kCompute);
-    f->SetWorkgroupSize(b.Constant(1_u), b.Constant(1_u), b.Constant(1_u));
-    f->SetReturnLocation(0);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: compute entry point must not have a return type
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func():f32 [@location(0)] {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = @compute @workgroup_size(1u, 1u, 1u) func():f32 [@location(0)] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_MissingOnCompute) {
-    auto* f = b.Function("f", ty.void_(), Function::PipelineStage::kCompute);
-    b.Append(f->Block(), [&] { b.Return(f); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: compute entry point requires @workgroup_size
-%f = @compute func():void {
-^^
-
-note: # Disassembly
-%f = @compute func():void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_NonCompute) {
-    auto* f = FragmentEntryPoint();
-    f->SetWorkgroupSize(b.Constant(1_u), b.Constant(1_u), b.Constant(1_u));
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: @workgroup_size only valid on compute entry point
-%f = @fragment @workgroup_size(1u, 1u, 1u) func():void {
-^^
-
-note: # Disassembly
-%f = @fragment @workgroup_size(1u, 1u, 1u) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamUndefined) {
-    auto* f = ComputeEntryPoint();
-    f->SetWorkgroupSize({nullptr, b.Constant(2_u), b.Constant(3_u)});
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: a @workgroup_size param is undefined or missing a type
-%f = @compute @workgroup_size(undef, 2u, 3u) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(undef, 2u, 3u) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamWrongType) {
-    auto* f = ComputeEntryPoint();
-    f->SetWorkgroupSize({b.Constant(1_f), b.Constant(2_u), b.Constant(3_u)});
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: @workgroup_size params must be an i32 or u32
-%f = @compute @workgroup_size(1.0f, 2u, 3u) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1.0f, 2u, 3u) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamsSameType) {
-    auto* f = ComputeEntryPoint();
-    f->SetWorkgroupSize({b.Constant(1_u), b.Constant(2_i), b.Constant(3_u)});
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: @workgroup_size params must be all i32s or all u32s
-%f = @compute @workgroup_size(1u, 2i, 3u) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 2i, 3u) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_ParamsTooSmall) {
-    auto* f = ComputeEntryPoint();
-    f->SetWorkgroupSize({b.Constant(-1_i), b.Constant(2_i), b.Constant(3_i)});
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: @workgroup_size params must be greater than 0
-%f = @compute @workgroup_size(-1i, 2i, 3i) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(-1i, 2i, 3i) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_OverrideWithoutAllowOverrides) {
-    auto* o = b.Override(ty.u32());
-    auto* f = ComputeEntryPoint();
-    f->SetWorkgroupSize({o->Result(0), o->Result(0), o->Result(0)});
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:1 error: @workgroup_size param is not a constant value, and IR capability 'kAllowOverrides' is not set
-%f = @compute @workgroup_size(%2, %2, %2) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(%2, %2, %2) func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_WorkgroupSize_NonRootBlockOverride) {
-    auto* f = ComputeEntryPoint();
-    Override* o;
-    b.Append(f->Block(), [&] {
-        o = b.Override(ty.u32());
-        b.Return(f);
-    });
-    f->SetWorkgroupSize({o->Result(0), o->Result(0), o->Result(0)});
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowOverrides});
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: @workgroup_size param defined by non-module scope value
-%f = @compute @workgroup_size(%2, %2, %2) func():void {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(%2, %2, %2) func():void {
-  $B1: {
-    %2:u32 = override @id(0)
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Vertex_BasicPosition) {
-    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
-    f->SetReturnBuiltin(BuiltinValue::kPosition);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Vertex_StructPosition) {
-    auto pos_ty = ty.vec4<f32>();
-    auto pos_attr = IOAttributes();
-    pos_attr.builtin = BuiltinValue::kPosition;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), pos_ty, pos_attr},
-                                               });
-
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Vertex_StructPositionAndClipDistances) {
-    auto pos_ty = ty.vec4<f32>();
-    auto pos_attr = IOAttributes();
-    pos_attr.builtin = BuiltinValue::kPosition;
-
-    auto clip_ty = ty.array<f32, 4>();
-    auto clip_attr = IOAttributes();
-    clip_attr.builtin = BuiltinValue::kClipDistances;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("pos"), pos_ty, pos_attr},
-                                                   {mod.symbols.New("clip"), clip_ty, clip_attr},
-                                               });
-
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Function_Vertex_StructOnlyClipDistances) {
-    auto clip_ty = ty.array<f32, 4>();
-    auto clip_attr = IOAttributes();
-    clip_attr.builtin = BuiltinValue::kClipDistances;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("clip"), clip_ty, clip_attr},
-                                               });
-
-    auto* f = b.Function("my_func", str_ty, Function::PipelineStage::kVertex);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:1 error: position must be declared for vertex entry point output
-%my_func = @vertex func():MyStruct {
-^^^^^^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  clip:array<f32, 4> @offset(0), @builtin(clip_distances)
-}
-
-%my_func = @vertex func():MyStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Vertex_MissingPosition) {
-    auto* f = b.Function("my_func", ty.vec4<f32>(), Function::PipelineStage::kVertex);
-    f->SetReturnLocation(0);
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: position must be declared for vertex entry point output
-%my_func = @vertex func():vec4<f32> [@location(0)] {
-^^^^^^^^
-
-note: # Disassembly
-%my_func = @vertex func():vec4<f32> [@location(0)] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_NonFragment_BoolInput) {
-    auto* f = VertexEntryPoint();
-    auto* p = b.FunctionParam("invalid", ty.bool_());
-    p->SetLocation(0);
-    f->AppendParam(p);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: entry point params can only be a bool for fragment shaders
-%f = @vertex func(%invalid:bool [@location(0)]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%invalid:bool [@location(0)]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_NonFragment_BoolOutput) {
-    auto* f = VertexEntryPoint();
-    IOAttributes attr;
-    attr.location = 0;
-    AddReturn(f, "invalid", ty.bool_(), attr);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: entry point return members can not be bool
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  invalid:bool @offset(16), @location(0)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Fragment_BoolInputWithoutFrontFacing) {
-    auto* f = FragmentEntryPoint();
-    auto* p = b.FunctionParam("invalid", ty.bool_());
-    p->SetLocation(0);
-    f->AppendParam(p);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:21 error: fragment entry point params can only be a bool if decorated with @builtin(front_facing)
-%f = @fragment func(%invalid:bool [@location(0)]):void {
-                    ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%invalid:bool [@location(0)]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_Fragment_BoolOutput) {
-    auto* f = FragmentEntryPoint();
-    IOAttributes attr;
-    attr.location = 0;
-    AddReturn(f, "invalid", ty.bool_(), attr);
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: entry point returns can not be bool
-%f = @fragment func():bool [@location(0)] {
-^^
-
-note: # Disassembly
-%f = @fragment func():bool [@location(0)] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_BoolOutput_via_MSV) {
-    auto* f = ComputeEntryPoint();
-
-    auto* v = b.Var(ty.ptr(AddressSpace::kOut, ty.bool_(), core::Access::kReadWrite));
-    IOAttributes attr;
-    attr.location = 0;
-    v->SetAttributes(attr);
-    mod.root_block->Append(v);
-
-    b.Append(f->Block(), [&] {
-        b.Append(
-            mod.CreateInstruction<ir::Store>(v->Result(0), b.Constant(b.ConstantValue(false))));
-        b.Unreachable();
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:1 error: IO address space values referenced by shader entry points can only be bool if in the input space, used only by fragment shaders and decorated with @builtin(front_facing)
-%f = @compute @workgroup_size(1u, 1u, 1u) func():void {
-^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<__out, bool, read_write> = var @location(0)
-}
-
-%f = @compute @workgroup_size(1u, 1u, 1u) func():void {
-  $B2: {
-    store %1, false
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Function_BoolInputWithoutFrontFacing_via_MSV) {
-    auto* f = FragmentEntryPoint();
-
-    auto* invalid = b.Var("invalid", AddressSpace::kIn, ty.bool_());
-    IOAttributes attr;
-    attr.location = 0;
-    invalid->SetAttributes(attr);
-    mod.root_block->Append(invalid);
-
-    b.Append(f->Block(), [&] {
-        auto* l = b.Load(invalid);
-        auto* v = b.Var("v", AddressSpace::kFunction, ty.bool_());
-        v->SetInitializer(l->Result(0));
-        b.Unreachable();
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:1 error: input address space values referenced by fragment shaders can only be a bool if decorated with @builtin(front_facing)
-%f = @fragment func():void {
-^^
-
-note: # Disassembly
-$B1: {  # root
-  %invalid:ptr<__in, bool, read> = var @location(0)
-}
-
-%f = @fragment func():void {
-  $B2: {
-    %3:bool = load %invalid
-    %v:ptr<function, bool, read_write> = var, %3
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "size", BuiltinValue::kPointSize, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: __point_size must be used in a vertex shader entry point
-%f = @fragment func():f32 [@__point_size] {
-^^
-
-note: # Disassembly
-%f = @fragment func():f32 [@__point_size] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongIODirection) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "size", BuiltinValue::kPointSize, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: __point_size must be an output of a shader entry point
-%f = @vertex func(%size:f32 [@__point_size]):vec4<f32> [@position] {
-                  ^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%size:f32 [@__point_size]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_PointSize_WrongType) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinReturn(f, "size", BuiltinValue::kPointSize, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: __point_size must be a f32
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  size:u32 @offset(16), @builtin(__point_size)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "distances", BuiltinValue::kClipDistances, ty.array<f32, 2>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: clip_distances must be used in a vertex shader entry point
-%f = @fragment func():array<f32, 2> [@clip_distances] {
-^^
-
-note: # Disassembly
-%f = @fragment func():array<f32, 2> [@clip_distances] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongIODirection) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "distances", BuiltinValue::kClipDistances, ty.array<f32, 2>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: clip_distances must be an output of a shader entry point
-%f = @vertex func(%distances:array<f32, 2> [@clip_distances]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%distances:array<f32, 2> [@clip_distances]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_ClipDistances_WrongType) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinReturn(f, "distances", BuiltinValue::kClipDistances, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: clip_distances must be an array<f32, N>, where N <= 8
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  distances:f32 @offset(16), @builtin(clip_distances)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinReturn(f, "depth", BuiltinValue::kFragDepth, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: frag_depth must be used in a fragment shader entry point
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  depth:f32 @offset(16), @builtin(frag_depth)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongIODirection) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "depth", BuiltinValue::kFragDepth, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: frag_depth must be an output of a shader entry point
-%f = @fragment func(%depth:f32 [@frag_depth]):void {
-                    ^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%depth:f32 [@frag_depth]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FragDepth_WrongType) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "depth", BuiltinValue::kFragDepth, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: frag_depth must be a f32
-%f = @fragment func():u32 [@frag_depth] {
-^^
-
-note: # Disassembly
-%f = @fragment func():u32 [@frag_depth] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "facing", BuiltinValue::kFrontFacing, ty.bool_());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: front_facing must be used in a fragment shader entry point
-%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^
-
-:1:19 error: entry point params can only be a bool for fragment shaders
-%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%facing:bool [@front_facing]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongIODirection) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "facing", BuiltinValue::kFrontFacing, ty.bool_());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: front_facing must be an input of a shader entry point
-%f = @fragment func():bool [@front_facing] {
-^^
-
-note: # Disassembly
-%f = @fragment func():bool [@front_facing] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_FrontFacing_WrongType) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "facing", BuiltinValue::kFrontFacing, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: front_facing must be a bool
-%f = @fragment func(%facing:u32 [@front_facing]):void {
-                    ^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%facing:u32 [@front_facing]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: global_invocation_id must be used in a compute shader entry point
-%f = @fragment func(%invocation:vec3<u32> [@global_invocation_id]):void {
-                    ^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%invocation:vec3<u32> [@global_invocation_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongIODirection) {
-    // This will also trigger the compute entry points should have void returns check
-    auto* f = ComputeEntryPoint();
-    AddBuiltinReturn(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: global_invocation_id must be an input of a shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
-^^
-
-:1:1 error: compute entry point must not have a return type
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@global_invocation_id] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_GlobalInvocationId_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "invocation", BuiltinValue::kGlobalInvocationId, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: global_invocation_id must be an vec3<u32>
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%invocation:u32 [@global_invocation_id]):void {
-                                               ^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%invocation:u32 [@global_invocation_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "instance", BuiltinValue::kInstanceIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: instance_index must be used in a vertex shader entry point
-%f = @fragment func(%instance:u32 [@instance_index]):void {
-                    ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%instance:u32 [@instance_index]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongIODirection) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinReturn(f, "instance", BuiltinValue::kInstanceIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: instance_index must be an input of a shader entry point
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  instance:u32 @offset(16), @builtin(instance_index)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_InstanceIndex_WrongType) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "instance", BuiltinValue::kInstanceIndex, ty.i32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: instance_index must be an u32
-%f = @vertex func(%instance:i32 [@instance_index]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%instance:i32 [@instance_index]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kLocalInvocationId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: local_invocation_id must be used in a compute shader entry point
-%f = @fragment func(%id:vec3<u32> [@local_invocation_id]):void {
-                    ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%id:vec3<u32> [@local_invocation_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongIODirection) {
-    // This will also trigger the compute entry points should have void returns check
-    auto* f = ComputeEntryPoint();
-    AddBuiltinReturn(f, "id", BuiltinValue::kLocalInvocationId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: local_invocation_id must be an input of a shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
-^^
-
-:1:1 error: compute entry point must not have a return type
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@local_invocation_id] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationId_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kLocalInvocationId, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: local_invocation_id must be an vec3<u32>
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@local_invocation_id]):void {
-                                               ^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@local_invocation_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kLocalInvocationIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: local_invocation_index must be used in a compute shader entry point
-%f = @fragment func(%index:u32 [@local_invocation_index]):void {
-                    ^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%index:u32 [@local_invocation_index]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongIODirection) {
-    // This will also trigger the compute entry points should have void returns check
-    auto* f = ComputeEntryPoint();
-    AddBuiltinReturn(f, "index", BuiltinValue::kLocalInvocationIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: local_invocation_index must be an input of a shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
-^^
-
-:1:1 error: compute entry point must not have a return type
-%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@local_invocation_index] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_LocalInvocationIndex_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kLocalInvocationIndex, ty.i32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: local_invocation_index must be an u32
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%index:i32 [@local_invocation_index]):void {
-                                               ^^^^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%index:i32 [@local_invocation_index]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "num", BuiltinValue::kNumWorkgroups, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: num_workgroups must be used in a compute shader entry point
-%f = @fragment func(%num:vec3<u32> [@num_workgroups]):void {
-                    ^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%num:vec3<u32> [@num_workgroups]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongIODirection) {
-    // This will also trigger the compute entry points should have void returns check
-    auto* f = ComputeEntryPoint();
-    AddBuiltinReturn(f, "num", BuiltinValue::kNumWorkgroups, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: num_workgroups must be an input of a shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
-^^
-
-:1:1 error: compute entry point must not have a return type
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@num_workgroups] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_NumWorkgroups_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "num", BuiltinValue::kNumWorkgroups, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: num_workgroups must be an vec3<u32>
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%num:u32 [@num_workgroups]):void {
-                                               ^^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%num:u32 [@num_workgroups]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kSampleIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: sample_index must be used in a fragment shader entry point
-%f = @vertex func(%index:u32 [@sample_index]):vec4<f32> [@position] {
-                  ^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%index:u32 [@sample_index]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongIODirection) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "index", BuiltinValue::kSampleIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: sample_index must be an input of a shader entry point
-%f = @fragment func():u32 [@sample_index] {
-^^
-
-note: # Disassembly
-%f = @fragment func():u32 [@sample_index] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleIndex_WrongType) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kSampleIndex, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: sample_index must be an u32
-%f = @fragment func(%index:f32 [@sample_index]):void {
-                    ^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%index:f32 [@sample_index]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kVertexIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: vertex_index must be used in a vertex shader entry point
-%f = @fragment func(%index:u32 [@vertex_index]):void {
-                    ^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%index:u32 [@vertex_index]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongIODirection) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinReturn(f, "index", BuiltinValue::kVertexIndex, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:6:1 error: vertex_index must be an input of a shader entry point
-%f = @vertex func():OutputStruct {
-^^
-
-note: # Disassembly
-OutputStruct = struct @align(16) {
-  pos:vec4<f32> @offset(0), @builtin(position)
-  index:u32 @offset(16), @builtin(vertex_index)
-}
-
-%f = @vertex func():OutputStruct {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_VertexIndex_WrongType) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "index", BuiltinValue::kVertexIndex, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: vertex_index must be an u32
-%f = @vertex func(%index:f32 [@vertex_index]):vec4<f32> [@position] {
-                  ^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%index:f32 [@vertex_index]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongStage) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kWorkgroupId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: workgroup_id must be used in a compute shader entry point
-%f = @fragment func(%id:vec3<u32> [@workgroup_id]):void {
-                    ^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%id:vec3<u32> [@workgroup_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongIODirection) {
-    // This will also trigger the compute entry points should have void returns check
-    auto* f = ComputeEntryPoint();
-    AddBuiltinReturn(f, "id", BuiltinValue::kWorkgroupId, ty.vec3<u32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: workgroup_id must be an input of a shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
-^^
-
-:1:1 error: compute entry point must not have a return type
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
-^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func():vec3<u32> [@workgroup_id] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_WorkgroupId_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kWorkgroupId, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: workgroup_id must be an vec3<u32>
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@workgroup_id]):void {
-                                               ^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:u32 [@workgroup_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_Position_WrongStage) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: position must be used in a fragment or vertex shader entry point
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%pos:vec4<f32> [@position]):void {
-                                               ^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%pos:vec4<f32> [@position]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_Position_WrongIODirectionForVertex) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: position must be an output for a vertex entry point
-%f = @vertex func(%pos:vec4<f32> [@position]):vec4<f32> [@position] {
-                  ^^^^^^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%pos:vec4<f32> [@position]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_Position_WrongIODirectionForFragment) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "pos", BuiltinValue::kPosition, ty.vec4<f32>());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: position must be an input for a fragment entry point
-%f = @fragment func():vec4<f32> [@position] {
-^^
-
-note: # Disassembly
-%f = @fragment func():vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_Position_WrongType) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "pos", BuiltinValue::kPosition, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: position must be an vec4<f32>
-%f = @fragment func(%pos:f32 [@position]):void {
-                    ^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%pos:f32 [@position]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleMask_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: sample_mask must be used in a fragment entry point
-%f = @vertex func(%mask:u32 [@sample_mask]):vec4<f32> [@position] {
-                  ^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%mask:u32 [@sample_mask]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleMask_InputValid) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleMask_OutputValid) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "mask", BuiltinValue::kSampleMask, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SampleMask_WrongType) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinParam(f, "mask", BuiltinValue::kSampleMask, ty.f32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:21 error: sample_mask must be an u32
-%f = @fragment func(%mask:f32 [@sample_mask]):void {
-                    ^^^^^^^^^
-
-note: # Disassembly
-%f = @fragment func(%mask:f32 [@sample_mask]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "size", BuiltinValue::kSubgroupSize, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:19 error: subgroup_size must be used in a compute or fragment shader entry point
-%f = @vertex func(%size:u32 [@subgroup_size]):vec4<f32> [@position] {
-                  ^^^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%size:u32 [@subgroup_size]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongIODirection) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "size", BuiltinValue::kSubgroupSize, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: subgroup_size must be an input of a shader entry point
-%f = @fragment func():u32 [@subgroup_size] {
-^^
-
-note: # Disassembly
-%f = @fragment func():u32 [@subgroup_size] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupSize_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "size", BuiltinValue::kSubgroupSize, ty.i32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: subgroup_size must be an u32
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%size:i32 [@subgroup_size]):void {
-                                               ^^^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%size:i32 [@subgroup_size]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongStage) {
-    auto* f = VertexEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kSubgroupInvocationId, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:1:19 error: subgroup_invocation_id must be used in a compute or fragment shader entry point
-%f = @vertex func(%id:u32 [@subgroup_invocation_id]):vec4<f32> [@position] {
-                  ^^^^^^^
-
-note: # Disassembly
-%f = @vertex func(%id:u32 [@subgroup_invocation_id]):vec4<f32> [@position] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongIODirection) {
-    auto* f = FragmentEntryPoint();
-    AddBuiltinReturn(f, "id", BuiltinValue::kSubgroupInvocationId, ty.u32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:1 error: subgroup_invocation_id must be an input of a shader entry point
-%f = @fragment func():u32 [@subgroup_invocation_id] {
-^^
-
-note: # Disassembly
-%f = @fragment func():u32 [@subgroup_invocation_id] {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Builtin_SubgroupInvocationId_WrongType) {
-    auto* f = ComputeEntryPoint();
-    AddBuiltinParam(f, "id", BuiltinValue::kSubgroupInvocationId, ty.i32());
-
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:1:48 error: subgroup_invocation_id must be an u32
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:i32 [@subgroup_invocation_id]):void {
-                                               ^^^^^^^
-
-note: # Disassembly
-%f = @compute @workgroup_size(1u, 1u, 1u) func(%id:i32 [@subgroup_invocation_id]):void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionOutsideModule) {
-    auto* f = b.Function("f", ty.void_());
-    auto* g = b.Function("g", ty.void_());
-    mod.functions.Pop();  // Remove g
-
-    b.Append(f->Block(), [&] {
-        b.Call(g);
-        b.Return(f);
-    });
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:20 error: call: %g is not part of the module
-    %2:void = call %g
-                   ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:void = call %g
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToEntryPointFunction) {
-    auto* f = b.Function("f", ty.void_());
-    auto* g = ComputeEntryPoint("g");
-
-    b.Append(f->Block(), [&] {
-        b.Call(g);
-        b.Return(f);
-    });
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:20 error: call: call target must not have a pipeline stage
-    %2:void = call %g
-                   ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:void = call %g
-    ret
-  }
-}
-%g = @compute @workgroup_size(1u, 1u, 1u) func():void {
-  $B2: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionTooFewArguments) {
-    auto* g = b.Function("g", ty.void_());
-    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Call(g, 42_i);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:20 error: call: function has 2 parameters, but call provides 1 arguments
-    %5:void = call %g, 42i
-                   ^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func(%2:i32, %3:i32):void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %5:void = call %g, 42i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionTooManyArguments) {
-    auto* g = b.Function("g", ty.void_());
-    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Call(g, 1_i, 2_i, 3_i);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:20 error: call: function has 2 parameters, but call provides 3 arguments
-    %5:void = call %g, 1i, 2i, 3i
-                   ^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func(%2:i32, %3:i32):void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %5:void = call %g, 1i, 2i, 3i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionWrongArgType) {
-    auto* g = b.Function("g", ty.void_());
-    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>(), b.FunctionParam<i32>()});
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Call(g, 1_i, 2_f, 3_i);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:8:28 error: call: function parameter 1 is of type 'i32', but argument is of type 'f32'
-    %6:void = call %g, 1i, 2.0f, 3i
-                           ^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func(%2:i32, %3:i32, %4:i32):void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %6:void = call %g, 1i, 2.0f, 3i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionNullArg) {
-    auto* g = b.Function("g", ty.void_());
-    g->SetParams({b.FunctionParam<i32>()});
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Call(g, nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:24 error: call: operand is undefined
-    %4:void = call %g, undef
-                       ^^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func(%2:i32):void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %4:void = call %g, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToNullFunction) {
-    auto* g = b.Function("g", ty.void_());
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(g);
-        c->SetOperands(Vector{static_cast<ir::Value*>(nullptr)});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:20 error: call: operand is undefined
-    %3:void = call undef
-                   ^^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func():void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %3:void = call undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionNoResult) {
-    auto* g = b.Function("g", ty.void_());
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(g);
-        c->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:13 error: call: expected exactly 1 results, got 0
-    undef = call %g
-            ^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func():void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    undef = call %g
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToFunctionNoOperands) {
-    auto* g = b.Function("g", ty.void_());
-    b.Append(g->Block(), [&] { b.Return(g); });
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(g);
-        c->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:15 error: call: expected at least 1 operands, got 0
-    %3:void = call undef
-              ^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%g = func():void {
-  $B1: {
-    ret
-  }
-}
-%f = func():void {
-  $B2: {
-    %3:void = call undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToNonFunctionTarget) {
-    auto* g = b.Function("g", ty.void_());
-    mod.functions.Pop();  // Remove g, since it isn't actually going to be used, it is just needed
-                          // to create the UserCall before mangling it
-
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(g);
-        c->SetOperands(Vector{b.Value(0_i)});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:20 error: call: target not defined or not a function
-    %2:void = call 0i
-                   ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:void = call 0i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToBuiltin_MissingResult) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(ty.f32(), BuiltinFn::kAbs, 1_f);
-        c->SetResults(Vector{static_cast<ir::InstructionResult*>(nullptr)});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:13 error: abs: call to builtin does not have a return type
-    undef = abs 1.0f
-            ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    undef = abs 1.0f
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToBuiltin_MismatchResultType) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Call(ty.f32(), BuiltinFn::kAbs, 1_f);
-        c->Result(0)->SetType(ty.i32());
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: abs: call result type does not match builtin return type
-    %2:i32 = abs 1.0f
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:i32 = abs 1.0f
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToBuiltin_ArgNullType) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* i = b.Var<function, f32>("i");
-        i->SetInitializer(b.Constant(0_f));
-        auto* load = b.Load(i);
-        auto* load_ret = load->Result(0);
-        b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
-        load_ret->SetType(nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: load: result type is undefined
-    %3:undef = load %i
-    ^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:5:14 error: abs: argument to builtin has undefined type
-    %4:f32 = abs %3
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %i:ptr<function, f32, read_write> = var, 0.0f
-    %3:undef = load %i
-    %4:f32 = abs %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, CallToBuiltin_NonSingularResult) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* i = b.Var<function, f32>("i");
-        i->SetInitializer(b.Constant(0_f));
-        auto* load = b.Load(i);
-        auto* load_ret = load->Result(0);
-        auto* too_many_call = b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
-        too_many_call->SetResults(Vector{load_ret, load_ret});
-        auto* too_few_call = b.Call(ty.f32(), BuiltinFn::kAbs, load_ret);
-        too_few_call->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:14 error: abs: call to builtin has 2 results, when 1 is expected
-    %3:f32 = abs %3
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:6:13 error: abs: call to builtin has 0 results, when 1 is expected
-    undef = abs %3
-            ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %i:ptr<function, f32, read_write> = var, 0.0f
-    %3:f32 = load %i
-    %3:f32 = abs %3
-    undef = abs %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Bitcast_MissingArg) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* bitcast = b.Bitcast(ty.i32(), 1_u);
-        bitcast->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: bitcast: expected exactly 1 operands, got 0
-    %2:i32 = bitcast
-             ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:i32 = bitcast
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Bitcast_NullArg) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Bitcast(ty.i32(), nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:22 error: bitcast: operand is undefined
-    %2:i32 = bitcast undef
-                     ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    %2:i32 = bitcast undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Bitcast_MissingResult) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* bitcast = b.Bitcast(ty.i32(), 1_u);
-        bitcast->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:13 error: bitcast: expected exactly 1 results, got 0
-    undef = bitcast 1u
-            ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    undef = bitcast 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Bitcast_NullResult) {
-    auto* f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* c = b.Bitcast(ty.i32(), 1_u);
-        c->SetResults(Vector<InstructionResult*, 1>{nullptr});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: bitcast: result is undefined
-    undef = bitcast 1u
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%f = func():void {
-  $B1: {
-    undef = bitcast 1u
-    ret
-  }
-}
-)");
-}
-
 TEST_F(IR_ValidatorTest, Construct_Struct_ZeroValue) {
     auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
                                                               {mod.symbols.New("a"), ty.i32()},
@@ -3990,261 +627,6 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, Discard_TooManyOperands) {
-    auto* func = b.Function("foo", ty.void_());
-    b.Append(func->Block(), [&] {
-        auto* d = b.Discard();
-        d->SetOperands(Vector{b.Value(0_i)});
-        b.Return(func);
-    });
-
-    auto* ep = FragmentEntryPoint("ep");
-    b.Append(ep->Block(), [&] {
-        b.Call(func);
-        b.Return(ep);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: discard: expected exactly 0 operands, got 1
-    discard
-    ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%foo = func():void {
-  $B1: {
-    discard
-    ret
-  }
-}
-%ep = @fragment func():void {
-  $B2: {
-    %3:void = call %foo
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Discard_TooManyResults) {
-    auto* func = b.Function("foo", ty.void_());
-    b.Append(func->Block(), [&] {
-        auto* d = b.Discard();
-        d->SetResults(Vector{b.InstructionResult(ty.i32())});
-        b.Return(func);
-    });
-
-    auto* ep = FragmentEntryPoint("ep");
-    b.Append(ep->Block(), [&] {
-        b.Call(func);
-        b.Return(ep);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: discard: expected exactly 0 results, got 1
-    discard
-    ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%foo = func():void {
-  $B1: {
-    discard
-    ret
-  }
-}
-%ep = @fragment func():void {
-  $B2: {
-    %3:void = call %foo
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Discard_RootBlock) {
-    mod.root_block->Append(b.Discard());
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: discard: root block: invalid instruction: tint::core::ir::Discard
-  discard
-  ^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  discard
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Terminator_RootBlock) {
-    auto f = b.Function("f", ty.void_());
-    b.Append(f->Block(), [&] { b.Unreachable(); });
-
-    mod.root_block->Append(b.Return(f));
-    mod.root_block->Append(b.Unreachable());
-    mod.root_block->Append(b.TerminateInvocation());
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: return: root block: invalid instruction: tint::core::ir::Return
-  ret
-  ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-:3:3 error: unreachable: root block: invalid instruction: tint::core::ir::Unreachable
-  unreachable
-  ^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-:4:3 error: terminate_invocation: root block: invalid instruction: tint::core::ir::TerminateInvocation
-  terminate_invocation
-  ^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  ret
-  unreachable
-  terminate_invocation
-}
-
-%f = func():void {
-  $B2: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Terminator_HasResult) {
-    auto* ret_func = b.Function("ret_func", ty.void_());
-    b.Append(ret_func->Block(), [&] {
-        auto* r = b.Return(ret_func);
-        r->SetResults(Vector{b.InstructionResult(ty.i32())});
-    });
-
-    auto* unreachable_func = b.Function("unreachable_func", ty.void_());
-    b.Append(unreachable_func->Block(), [&] {
-        auto* r = b.Unreachable();
-        r->SetResults(Vector{b.InstructionResult(ty.i32())});
-    });
-
-    auto* terminate_func = b.Function("terminate_func", ty.void_());
-    b.Append(terminate_func->Block(), [&] {
-        auto* r = b.TerminateInvocation();
-        r->SetResults(Vector{b.InstructionResult(ty.i32())});
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: return: expected exactly 0 results, got 1
-    ret
-    ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:8:5 error: unreachable: expected exactly 0 results, got 1
-    unreachable
-    ^^^^^^^^^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-:13:5 error: terminate_invocation: expected exactly 0 results, got 1
-    terminate_invocation
-    ^^^^^^^^^^^^^^^^^^^^
-
-:12:3 note: in block
-  $B3: {
-  ^^^
-
-note: # Disassembly
-%ret_func = func():void {
-  $B1: {
-    ret
-  }
-}
-%unreachable_func = func():void {
-  $B2: {
-    unreachable
-  }
-}
-%terminate_func = func():void {
-  $B3: {
-    terminate_invocation
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Discard_NotInFragment) {
-    auto* func = b.Function("foo", ty.void_());
-    b.Append(func->Block(), [&] {
-        b.Discard();
-        b.Return(func);
-    });
-
-    auto* ep = ComputeEntryPoint("ep");
-
-    b.Append(ep->Block(), [&] {
-        b.Call(func);
-        b.Return(ep);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: discard: cannot be called in non-fragment end point
-    discard
-    ^^^^^^^
-
-note: # Disassembly
-%foo = func():void {
-  $B1: {
-    discard
-    ret
-  }
-}
-%ep = @compute @workgroup_size(1u, 1u, 1u) func():void {
-  $B2: {
-    %3:void = call %foo
-    ret
-  }
-}
-)");
-}
-
 TEST_F(IR_ValidatorTest, Block_NoTerminator) {
     b.Function("my_func", ty.void_());
 
@@ -4426,2117 +808,6 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, Access_NoOperands) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        auto* access = b.Access(ty.f32(), obj, 0_i);
-        access->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: access: expected at least 2 operands, got 0
-    %3:f32 = access
-             ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:vec3<f32>):void {
-  $B1: {
-    %3:f32 = access
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_NoIndices) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: access: expected at least 2 operands, got 1
-    %3:f32 = access %2
-             ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:vec3<f32>):void {
-  $B1: {
-    %3:f32 = access %2
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_NoResults) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        auto* access = b.Access(ty.f32(), obj, 0_i);
-        access->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:13 error: access: expected exactly 1 results, got 0
-    undef = access %2, 0i
-            ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:vec3<f32>):void {
-  $B1: {
-    undef = access %2, 0i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_NullObject) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), nullptr, 0_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:21 error: access: operand is undefined
-    %2:f32 = access undef, 0u
-                    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:f32 = access undef, 0u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_NullIndex) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:25 error: access: operand is undefined
-    %3:f32 = access %2, undef
-                        ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:vec3<f32>):void {
-  $B1: {
-    %3:f32 = access %2, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_NegativeIndex) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, -1_i);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:25 error: access: constant index must be positive, got -1
-    %3:f32 = access %2, -1i
-                        ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:vec3<f32>):void {
-  $B1: {
-    %3:f32 = access %2, -1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_OOB_Index_Value) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u, 3_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:29 error: access: index out of bounds for type 'vec2<f32>'
-    %3:f32 = access %2, 1u, 3u
-                            ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:3:29 note: acceptable range: [0..1]
-    %3:f32 = access %2, 1u, 3u
-                            ^^
-
-note: # Disassembly
-%my_func = func(%2:mat3x2<f32>):void {
-  $B1: {
-    %3:f32 = access %2, 1u, 3u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_OOB_Index_Ptr) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<private_, f32>(), obj, 1_u, 3_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:55 error: access: index out of bounds for type 'ptr<private, array<f32, 2>, read_write>'
-    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
-                                                      ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:3:55 note: acceptable range: [0..1]
-    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
-                                                      ^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
-  $B1: {
-    %3:ptr<private, f32, read_write> = access %2, 1u, 3u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_StaticallyUnindexableType_Value) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.f32());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:25 error: access: type 'f32' cannot be indexed
-    %3:f32 = access %2, 1u
-                        ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:f32):void {
-  $B1: {
-    %3:f32 = access %2, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_StaticallyUnindexableType_Ptr) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<private_, f32>(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:51 error: access: type 'ptr<private, f32, read_write>' cannot be indexed
-    %3:ptr<private, f32, read_write> = access %2, 1u
-                                                  ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, f32, read_write>):void {
-  $B1: {
-    %3:ptr<private, f32, read_write> = access %2, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_DynamicallyUnindexableType_Value) {
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
-                                                              {mod.symbols.New("a"), ty.i32()},
-                                                              {mod.symbols.New("b"), ty.i32()},
-                                                          });
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(str_ty);
-    auto* idx = b.FunctionParam(ty.i32());
-    f->SetParams({obj, idx});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.i32(), obj, idx);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:25 error: access: type 'MyStruct' cannot be dynamically indexed
-    %4:i32 = access %2, %3
-                        ^^
-
-:7:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  a:i32 @offset(0)
-  b:i32 @offset(4)
-}
-
-%my_func = func(%2:MyStruct, %3:i32):void {
-  $B1: {
-    %4:i32 = access %2, %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_DynamicallyUnindexableType_Ptr) {
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
-                                                              {mod.symbols.New("a"), ty.i32()},
-                                                              {mod.symbols.New("b"), ty.i32()},
-                                                          });
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, read_write>(str_ty));
-    auto* idx = b.FunctionParam(ty.i32());
-    f->SetParams({obj, idx});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.i32(), obj, idx);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:8:25 error: access: type 'ptr<private, MyStruct, read_write>' cannot be dynamically indexed
-    %4:i32 = access %2, %3
-                        ^^
-
-:7:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  a:i32 @offset(0)
-  b:i32 @offset(4)
-}
-
-%my_func = func(%2:ptr<private, MyStruct, read_write>, %3:i32):void {
-  $B1: {
-    %4:i32 = access %2, %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Value_Value) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.i32(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:14 error: access: result of access chain is type 'f32' but instruction type is 'i32'
-    %3:i32 = access %2, 1u, 1u
-             ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:mat3x2<f32>):void {
-  $B1: {
-    %3:i32 = access %2, 1u, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Ptr_Ptr) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<private_, i32>(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:40 error: access: result of access chain is type 'ptr<private, f32, read_write>' but instruction type is 'ptr<private, i32, read_write>'
-    %3:ptr<private, i32, read_write> = access %2, 1u, 1u
-                                       ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
-  $B1: {
-    %3:ptr<private, i32, read_write> = access %2, 1u, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_Incorrect_Type_Ptr_Value) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, array<array<f32, 2>, 3>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:14 error: access: result of access chain is type 'ptr<private, f32, read_write>' but instruction type is 'f32'
-    %3:f32 = access %2, 1u, 1u
-             ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, array<array<f32, 2>, 3>, read_write>):void {
-  $B1: {
-    %3:f32 = access %2, 1u, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVectorPtr) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, vec3<f32>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:25 error: access: cannot obtain address of vector element
-    %3:f32 = access %2, 1u
-                        ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, vec3<f32>, read_write>):void {
-  $B1: {
-    %3:f32 = access %2, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_WithCapability) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, vec3<f32>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<private_, f32>(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowVectorElementPointer});
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_ViaMatrixPtr) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:29 error: access: cannot obtain address of vector element
-    %3:f32 = access %2, 1u, 1u
-                            ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<private, mat3x2<f32>, read_write>):void {
-  $B1: {
-    %3:f32 = access %2, 1u, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVectorPtr_ViaMatrixPtr_WithCapability) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<private_, f32>(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowVectorElementPointer});
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Access_Incorrect_Ptr_AddressSpace) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<storage, array<f32, 2>, read>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<uniform, f32, read>(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:34 error: access: result of access chain is type 'ptr<storage, f32, read>' but instruction type is 'ptr<uniform, f32, read>'
-    %3:ptr<uniform, f32, read> = access %2, 1u
-                                 ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<storage, array<f32, 2>, read>):void {
-  $B1: {
-    %3:ptr<uniform, f32, read> = access %2, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_Incorrect_Ptr_Access) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.ptr<storage, array<f32, 2>, read>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.ptr<storage, f32, read_write>(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:40 error: access: result of access chain is type 'ptr<storage, f32, read>' but instruction type is 'ptr<storage, f32, read_write>'
-    %3:ptr<storage, f32, read_write> = access %2, 1u
-                                       ^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func(%2:ptr<storage, array<f32, 2>, read>):void {
-  $B1: {
-    %3:ptr<storage, f32, read_write> = access %2, 1u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVector) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.vec3<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Access_IndexVector_ViaMatrix) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(ty.mat3x2<f32>());
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ty.f32(), obj, 1_u, 1_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Access_ExtractPointerFromStruct) {
-    auto* ptr = ty.ptr<private_, i32>();
-    Vector<core::type::Manager::StructMemberDesc, 1> members{
-        core::type::Manager::StructMemberDesc{mod.symbols.New("a"), ptr},
-    };
-    auto* str = ty.Struct(mod.symbols.New("MyStruct"), std::move(members));
-    auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam("obj", str);
-    f->SetParams({obj});
-
-    b.Append(f->Block(), [&] {
-        b.Access(ptr, obj, 0_u);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Block_TerminatorInMiddle) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Return(f);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: return: must be the last instruction in the block
-    ret
-    ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    ret
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, If_RootBlock) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.Unreachable());
-    mod.root_block->Append(if_);
-
-    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: if: root block: invalid instruction: tint::core::ir::If
-  if true [t: $B2] {  # if_1
-  ^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  if true [t: $B2] {  # if_1
-    $B2: {  # true
-      unreachable
-    }
-  }
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, If_EmptyFalse) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.Return(f));
-
-    f->Block()->Append(if_);
-    f->Block()->Append(b.Return(f));
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, If_EmptyTrue) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(true);
-    if_->False()->Append(b.Return(f));
-
-    f->Block()->Append(if_);
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:7 error: block does not end in a terminator instruction
-      $B2: {  # true
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2, f: $B3] {  # if_1
-      $B2: {  # true
-      }
-      $B3: {  # false
-        ret
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, If_ConditionIsBool) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(1_i);
-    if_->True()->Append(b.Return(f));
-    if_->False()->Append(b.Return(f));
-
-    f->Block()->Append(if_);
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:8 error: if: condition type must be 'bool'
-    if 1i [t: $B2, f: $B3] {  # if_1
-       ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if 1i [t: $B2, f: $B3] {  # if_1
-      $B2: {  # true
-        ret
-      }
-      $B3: {  # false
-        ret
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, If_ConditionIsNullptr) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(nullptr);
-    if_->True()->Append(b.Return(f));
-    if_->False()->Append(b.Return(f));
-
-    f->Block()->Append(if_);
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:8 error: if: operand is undefined
-    if undef [t: $B2, f: $B3] {  # if_1
-       ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if undef [t: $B2, f: $B3] {  # if_1
-      $B2: {  # true
-        ret
-      }
-      $B3: {  # false
-        ret
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, If_NullResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.Return(f));
-    if_->False()->Append(b.Return(f));
-
-    if_->SetResults(Vector<InstructionResult*, 1>{nullptr});
-
-    f->Block()->Append(if_);
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: if: result is undefined
-    undef = if true [t: $B2, f: $B3] {  # if_1
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    undef = if true [t: $B2, f: $B3] {  # if_1
-      $B2: {  # true
-        ret
-      }
-      $B3: {  # false
-        ret
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Loop_RootBlock) {
-    auto* l = b.Loop();
-    l->Body()->Append(b.ExitLoop(l));
-    mod.root_block->Append(l);
-
-    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: loop: root block: invalid instruction: tint::core::ir::Loop
-  loop [b: $B2] {  # loop_1
-  ^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  loop [b: $B2] {  # loop_1
-    $B2: {  # body
-      exit_loop  # loop_1
-    }
-  }
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Loop_OnlyBody) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* l = b.Loop();
-    l->Body()->Append(b.ExitLoop(l));
-
-    auto sb = b.Append(f->Block());
-    sb.Append(l);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, Loop_EmptyBody) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(b.Loop());
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:7 error: block does not end in a terminator instruction
-      $B2: {  # body
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2] {  # loop_1
-      $B2: {  # body
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Switch_RootBlock) {
-    auto* switch_ = b.Switch(1_i);
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_));
-    mod.root_block->Append(switch_);
-
-    auto res = ir::Validate(mod, core::ir::Capabilities{core::ir::Capability::kAllowOverrides});
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: switch: root block: invalid instruction: tint::core::ir::Switch
-  switch 1i [c: (default, $B2)] {  # switch_1
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  switch 1i [c: (default, $B2)] {  # switch_1
-    $B2: {  # case
-      exit_switch  # switch_1
-    }
-  }
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Type_VectorElements) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Var("u32_valid", AddressSpace::kFunction, ty.vec4(ty.u32()));
-        b.Var("i32_valid", AddressSpace::kFunction, ty.vec4(ty.i32()));
-        b.Var("bool_valid", AddressSpace::kFunction, ty.vec2(ty.bool_()));
-        b.Var("f16_valid", AddressSpace::kFunction, ty.vec3(ty.f16()));
-        b.Var("f32_valid", AddressSpace::kFunction, ty.vec3(ty.f32()));
-        b.Var("void_invalid", AddressSpace::kFunction, ty.vec2(ty.void_()));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:8:5 error: var: vector elements must be scalars
-    %void_invalid:ptr<function, vec2<void>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %u32_valid:ptr<function, vec4<u32>, read_write> = var
-    %i32_valid:ptr<function, vec4<i32>, read_write> = var
-    %bool_valid:ptr<function, vec2<bool>, read_write> = var
-    %f16_valid:ptr<function, vec3<f16>, read_write> = var
-    %f32_valid:ptr<function, vec3<f32>, read_write> = var
-    %void_invalid:ptr<function, vec2<void>, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Type_MatrixElements) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Var("u32_invalid", AddressSpace::kFunction, ty.mat2x2(ty.u32()));
-        b.Var("i32_invalid", AddressSpace::kFunction, ty.mat3x2(ty.i32()));
-        b.Var("bool_invalid", AddressSpace::kFunction, ty.mat4x2(ty.bool_()));
-        b.Var("f16_valid", AddressSpace::kFunction, ty.mat2x3(ty.f16()));
-        b.Var("f32_valid", AddressSpace::kFunction, ty.mat4x4(ty.f32()));
-        b.Var("void_invalid", AddressSpace::kFunction, ty.mat3x3(ty.void_()));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: var: matrix elements must be float scalars
-    %u32_invalid:ptr<function, mat2x2<u32>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:4:5 error: var: matrix elements must be float scalars
-    %i32_invalid:ptr<function, mat3x2<i32>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:5:5 error: var: matrix elements must be float scalars
-    %bool_invalid:ptr<function, mat4x2<bool>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:8:5 error: var: matrix elements must be float scalars
-    %void_invalid:ptr<function, mat3x3<void>, read_write> = var
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %u32_invalid:ptr<function, mat2x2<u32>, read_write> = var
-    %i32_invalid:ptr<function, mat3x2<i32>, read_write> = var
-    %bool_invalid:ptr<function, mat4x2<bool>, read_write> = var
-    %f16_valid:ptr<function, mat2x3<f16>, read_write> = var
-    %f32_valid:ptr<function, mat4x4<f32>, read_write> = var
-    %void_invalid:ptr<function, mat3x3<void>, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Type_StorageTextureDimension) {
-    auto* valid =
-        b.Var("valid", AddressSpace::kStorage,
-              ty.storage_texture(core::type::TextureDimension::k2d, core::TexelFormat::kRgba32Float,
-                                 core::Access::kReadWrite),
-              read_write);
-    valid->SetBindingPoint(0, 0);
-    mod.root_block->Append(valid);
-
-    auto* cube =
-        b.Var("cube_invalid", AddressSpace::kStorage,
-              ty.storage_texture(core::type::TextureDimension::kCube,
-                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
-              read_write);
-    cube->SetBindingPoint(1, 1);
-    mod.root_block->Append(cube);
-
-    auto* cube_array =
-        b.Var("cube_array_invalid", AddressSpace::kStorage,
-              ty.storage_texture(core::type::TextureDimension::kCubeArray,
-                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
-              read_write);
-    cube_array->SetBindingPoint(2, 2);
-    mod.root_block->Append(cube_array);
-
-    auto* none =
-        b.Var("none_invalid", AddressSpace::kStorage,
-              ty.storage_texture(core::type::TextureDimension::kNone,
-                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
-              read_write);
-    none->SetBindingPoint(3, 3);
-    mod.root_block->Append(none);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:3 error: var: dimension 'cube' for storage textures does not in WGSL yet
-  %cube_invalid:ptr<storage, texture_storage_cube<rgba32float, read_write>, read_write> = var @binding_point(1, 1)
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-:4:3 error: var: dimension 'cube_array' for storage textures does not in WGSL yet
-  %cube_array_invalid:ptr<storage, texture_storage_cube_array<rgba32float, read_write>, read_write> = var @binding_point(2, 2)
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-:5:3 error: var: invalid texture dimension 'none'
-  %none_invalid:ptr<storage, texture_storage_none<rgba32float, read_write>, read_write> = var @binding_point(3, 3)
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %valid:ptr<storage, texture_storage_2d<rgba32float, read_write>, read_write> = var @binding_point(0, 0)
-  %cube_invalid:ptr<storage, texture_storage_cube<rgba32float, read_write>, read_write> = var @binding_point(1, 1)
-  %cube_array_invalid:ptr<storage, texture_storage_cube_array<rgba32float, read_write>, read_write> = var @binding_point(2, 2)
-  %none_invalid:ptr<storage, texture_storage_none<rgba32float, read_write>, read_write> = var @binding_point(3, 3)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_RootBlock_NullResult) {
-    auto* v = mod.CreateInstruction<ir::Var>(nullptr);
-    v->SetInitializer(b.Constant(0_i));
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:2:3 error: var: result is undefined
-  undef = var, 0i
-  ^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  undef = var, 0i
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_VoidType) {
-    mod.root_block->Append(b.Var(ty.ptr(private_, ty.void_())));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:2:3 error: var: pointers to void are not permitted
-  %1:ptr<private, void, read_write> = var
-  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<private, void, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Function_NullResult) {
-    auto* v = mod.CreateInstruction<ir::Var>(nullptr);
-    v->SetInitializer(b.Constant(0_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: var: result is undefined
-    undef = var, 0i
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    undef = var, 0i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Function_NoResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* v = b.Var<function, f32>();
-        v->SetInitializer(b.Constant(1_i));
-        v->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:13 error: var: expected exactly 1 results, got 0
-    undef = var, 1i
-            ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    undef = var, 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Function_NonPtrResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* v = b.Var<function, f32>();
-        v->Result(0)->SetType(ty.f32());
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: var: result type must be a pointer or a reference
-    %2:f32 = var
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:f32 = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Function_UnexpectedInputAttachmentIndex) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* v = b.Var<function, f32>();
-        v->SetInputAttachmentIndex(0);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:41 error: var: '@input_attachment_index' is not valid for non-handle var
-    %2:ptr<function, f32, read_write> = var @input_attachment_index(0)
-                                        ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, f32, read_write> = var @input_attachment_index(0)
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Function_OutsideFunctionScope) {
-    auto* v = b.Var<function, f32>();
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:39 error: var: vars in the 'function' address space must be in a function scope
-  %1:ptr<function, f32, read_write> = var
-                                      ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<function, f32, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_NonFunction_InsideFunctionScope) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Var<private_, f32>();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:40 error: var: vars in a function scope must be in the 'function' address space
-    %2:ptr<private, f32, read_write> = var
-                                       ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<private, f32, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Private_InsideFunctionScopeWithCapability) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Var<private_, f32>();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPrivateVarsInFunctions});
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Var_Private_UnexpectedInputAttachmentIndex) {
-    auto* v = b.Var<private_, f32>();
-    v->SetInputAttachmentIndex(0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
-  %1:ptr<private, f32, read_write> = var @input_attachment_index(0)
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<private, f32, read_write> = var @input_attachment_index(0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_PushConstant_UnexpectedInputAttachmentIndex) {
-    auto* v = b.Var<push_constant, f32>();
-    v->SetInputAttachmentIndex(0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
-  %1:ptr<push_constant, f32, read> = var @input_attachment_index(0)
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<push_constant, f32, read> = var @input_attachment_index(0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Storage_UnexpectedInputAttachmentIndex) {
-    auto* v = b.Var<storage, f32>();
-    v->SetBindingPoint(0, 0);
-    v->SetInputAttachmentIndex(0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
-  %1:ptr<storage, f32, read_write> = var @binding_point(0, 0) @input_attachment_index(0)
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<storage, f32, read_write> = var @binding_point(0, 0) @input_attachment_index(0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Uniform_UnexpectedInputAttachmentIndex) {
-    auto* v = b.Var<uniform, f32>();
-    v->SetBindingPoint(0, 0);
-    v->SetInputAttachmentIndex(0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:32 error: var: '@input_attachment_index' is not valid for non-handle var
-  %1:ptr<uniform, f32, read> = var @binding_point(0, 0) @input_attachment_index(0)
-                               ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<uniform, f32, read> = var @binding_point(0, 0) @input_attachment_index(0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Workgroup_UnexpectedInputAttachmentIndex) {
-    auto* v = b.Var<workgroup, f32>();
-    v->SetInputAttachmentIndex(0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:40 error: var: '@input_attachment_index' is not valid for non-handle var
-  %1:ptr<workgroup, f32, read_write> = var @input_attachment_index(0)
-                                       ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<workgroup, f32, read_write> = var @input_attachment_index(0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Init_WrongType) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* v = b.Var<function, f32>();
-        v->SetInitializer(b.Constant(1_i));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:41 error: var: initializer type 'i32' does not match store type 'f32'
-    %2:ptr<function, f32, read_write> = var, 1i
-                                        ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, f32, read_write> = var, 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Init_NullType) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* i = b.Var<function, f32>("i");
-        i->SetInitializer(b.Constant(0_f));
-        auto* load = b.Load(i);
-        auto* load_ret = load->Result(0);
-        auto* j = b.Var<function, f32>("j");
-        j->SetInitializer(load_ret);
-        load_ret->SetType(nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: load: result type is undefined
-    %3:undef = load %i
-    ^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:5:46 error: var: operand type is undefined
-    %j:ptr<function, f32, read_write> = var, %3
-                                             ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %i:ptr<function, f32, read_write> = var, 0.0f
-    %3:undef = load %i
-    %j:ptr<function, f32, read_write> = var, %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Init_FunctionTypeInit) {
-    auto* invalid = b.Function("invalid_init", ty.void_());
-    b.Append(invalid->Block(), [&] { b.Return(invalid); });
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* i = b.Var<function, f32>("i");
-        i->SetInitializer(invalid);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:41 error: var: initializer type '<function>' does not match store type 'f32'
-    %i:ptr<function, f32, read_write> = var, %invalid_init
-                                        ^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%invalid_init = func():void {
-  $B1: {
-    ret
-  }
-}
-%my_func = func():void {
-  $B2: {
-    %i:ptr<function, f32, read_write> = var, %invalid_init
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Init_InvalidAddressSpace) {
-    auto* p = b.Var<private_, f32>("p");
-    p->SetInitializer(b.Constant(1_f));
-    mod.root_block->Append(p);
-    auto* s = b.Var<storage, f32>("s");
-    s->SetInitializer(b.Constant(1_f));
-    mod.root_block->Append(s);
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* v = b.Var<function, f32>("v");
-        v->SetInitializer(b.Constant(1_f));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:38 error: var: only variables in the function or private address space may be initialized
-  %s:ptr<storage, f32, read_write> = var, 1.0f
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %p:ptr<private, f32, read_write> = var, 1.0f
-  %s:ptr<storage, f32, read_write> = var, 1.0f
-}
-
-%my_func = func():void {
-  $B2: {
-    %v:ptr<function, f32, read_write> = var, 1.0f
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_HandleMissingBindingPoint) {
-    auto* v = b.Var(ty.ptr<handle, i32>());
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:31 error: var: a resource variable is missing binding point
-  %1:ptr<handle, i32, read> = var
-                              ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<handle, i32, read> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_StorageMissingBindingPoint) {
-    auto* v = b.Var(ty.ptr<storage, i32>());
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:38 error: var: a resource variable is missing binding point
-  %1:ptr<storage, i32, read_write> = var
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<storage, i32, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_UniformMissingBindingPoint) {
-    auto* v = b.Var(ty.ptr<uniform, i32>());
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:32 error: var: a resource variable is missing binding point
-  %1:ptr<uniform, i32, read> = var
-                               ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<uniform, i32, read> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_NonResourceWithBindingPoint) {
-    auto* v = b.Var(ty.ptr<private_, i32>());
-    v->SetBindingPoint(0, 0);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:38 error: var: a non-resource variable has binding point
-  %1:ptr<private, i32, read_write> = var @binding_point(0, 0)
-                                     ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<private, i32, read_write> = var @binding_point(0, 0)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_MultipleIOAnnotations) {
-    auto* v = b.Var<AddressSpace::kIn, vec4<f32>>();
-    IOAttributes attr;
-    attr.builtin = BuiltinValue::kPosition;
-    attr.location = 0;
-    v->SetAttributes(attr);
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:2:35 error: var: module scope variable has more than one IO annotation, [ @location, built-in ]
-  %1:ptr<__in, vec4<f32>, read> = var @location(0) @builtin(position)
-                                  ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<__in, vec4<f32>, read> = var @location(0) @builtin(position)
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Struct_MultipleIOAnnotations) {
-    IOAttributes attr;
-    attr.builtin = BuiltinValue::kPosition;
-    attr.color = 0;
-
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.New("a"), ty.f32(), attr},
-                                               });
-    auto* v = b.Var(ty.ptr(AddressSpace::kOut, str_ty, read_write));
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:6:41 error: var: module scope variable struct member has more than one IO annotation, [ built-in, @color ]
-  %1:ptr<__out, MyStruct, read_write> = var
-                                        ^^^
-
-:5:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  a:f32 @offset(0), @color(0), @builtin(position)
-}
-
-$B1: {  # root
-  %1:ptr<__out, MyStruct, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_MissingIOAnnotations) {
-    auto* v = b.Var<AddressSpace::kIn, vec4<f32>>();
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:2:35 error: var: module scope variable must have at least one IO annotation, e.g. a binding point, a location, etc
-  %1:ptr<__in, vec4<f32>, read> = var
-                                  ^^^
-
-:1:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-$B1: {  # root
-  %1:ptr<__in, vec4<f32>, read> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Var_Struct_MissingIOAnnotations) {
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
-                                                              {mod.symbols.New("a"), ty.f32(), {}},
-                                                          });
-    auto* v = b.Var(ty.ptr(AddressSpace::kOut, str_ty, read_write));
-    mod.root_block->Append(v);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:6:41 error: var: module scope variable struct members must have at least one IO annotation, e.g. a binding point, a location, etc
-  %1:ptr<__out, MyStruct, read_write> = var
-                                        ^^^
-
-:5:1 note: in block
-$B1: {  # root
-^^^
-
-note: # Disassembly
-MyStruct = struct @align(4) {
-  a:f32 @offset(0)
-}
-
-$B1: {  # root
-  %1:ptr<__out, MyStruct, read_write> = var
-}
-
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_NullResult) {
-    auto* v = mod.CreateInstruction<ir::Let>(nullptr, b.Constant(1_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: let: result is undefined
-    undef = let 1i
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    undef = let 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_EmptyResults) {
-    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Constant(1_i));
-    v->ClearResults();
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:13 error: let: expected exactly 1 results, got 0
-    undef = let 1i
-            ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    undef = let 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_NullValue) {
-    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.f32()), nullptr);
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:18 error: let: operand is undefined
-    %2:f32 = let undef
-                 ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:f32 = let undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_EmptyValue) {
-    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Constant(1_i));
-    v->ClearOperands();
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:14 error: let: expected exactly 1 operands, got 0
-    %2:i32 = let
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32 = let
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_WrongType) {
-    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.f32()), b.Constant(1_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto sb = b.Append(f->Block());
-    sb.Append(v);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:14 error: let: result type 'f32' does not match value type 'i32'
-    %2:f32 = let 1i
-             ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:f32 = let 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_VoidResult) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* l = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.void_()), b.Constant(1_i));
-        b.Append(l);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:15 error: let: result type cannot be void
-    %2:void = let 1i
-              ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:void = let 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Let_VoidValue) {
-    auto* v = b.Function("void_func", ty.void_());
-    b.Append(v->Block(), [&] { b.Return(v); });
-
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* l = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Value(b.Call(v)));
-        b.Append(l);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:9:14 error: let: value type cannot be void
-    %4:i32 = let %3
-             ^^^
-
-:7:3 note: in block
-  $B2: {
-  ^^^
-
-note: # Disassembly
-%void_func = func():void {
-  $B1: {
-    ret
-  }
-}
-%my_func = func():void {
-  $B2: {
-    %3:void = call %void_func
-    %4:i32 = let %3
-    ret
-  }
-}
-)");
-}
-
 TEST_F(IR_ValidatorTest, Instruction_AppendedDead) {
     auto* f = b.Function("my_func", ty.void_());
 
@@ -6784,7 +1055,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: binary: expected at least 2 operands, got 0
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: binary: expected at least 2 operands, got 0
     %2:i32 = add
     ^^^^^^^^^^^^
 
@@ -6812,7 +1084,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: binary: expected exactly 1 results, got 0
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: binary: expected exactly 1 results, got 0
     undef = add 1i, 2i
     ^^^^^^^^^^^^^^^^^^
 
@@ -6977,3312 +1250,6 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, ExitIf) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.ExitIf(if_));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_NullIf) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(mod.CreateInstruction<ExitIf>(nullptr));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:5:9 error: exit_if: has no parent control instruction
-        exit_if  # undef
-        ^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # true
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2] {  # if_1
-      $B2: {  # true
-        exit_if  # undef
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_LessOperandsThenIfParams) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.ExitIf(if_, 1_i));
-
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    if_->SetResults(Vector{r1, r2});
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_if: provides 1 value but 'if' expects 2 values
-        exit_if 1i  # if_1
-        ^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # true
-      ^^^
-
-:3:5 note: 'if' declared here
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-      $B2: {  # true
-        exit_if 1i  # if_1
-      }
-      # implicit false block: exit_if undef, undef
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_MoreOperandsThenIfParams) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.ExitIf(if_, 1_i, 2_f, 3_i));
-
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    if_->SetResults(Vector{r1, r2});
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_if: provides 3 values but 'if' expects 2 values
-        exit_if 1i, 2.0f, 3i  # if_1
-        ^^^^^^^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # true
-      ^^^
-
-:3:5 note: 'if' declared here
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-      $B2: {  # true
-        exit_if 1i, 2.0f, 3i  # if_1
-      }
-      # implicit false block: exit_if undef, undef
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_WithResult) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.ExitIf(if_, 1_i, 2_f));
-
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    if_->SetResults(Vector{r1, r2});
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_IncorrectResultType) {
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.ExitIf(if_, 1_i, 2_i));
-
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    if_->SetResults(Vector{r1, r2});
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:21 error: exit_if: operand with type 'i32' does not match 'if' target type 'f32'
-        exit_if 1i, 2i  # if_1
-                    ^^
-
-:4:7 note: in block
-      $B2: {  # true
-      ^^^
-
-:3:13 note: %3 declared here
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-            ^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-      $B2: {  # true
-        exit_if 1i, 2i  # if_1
-      }
-      # implicit false block: exit_if undef, undef
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_NotInParentIf) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_ = b.If(true);
-    if_->True()->Append(b.Return(f));
-
-    auto sb = b.Append(f->Block());
-    sb.Append(if_);
-    sb.ExitIf(if_);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:5 error: exit_if: found outside all control instructions
-    exit_if  # if_1
-    ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2] {  # if_1
-      $B2: {  # true
-        ret
-      }
-    }
-    exit_if  # if_1
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpsOverIf) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* if_inner = b.If(true);
-
-    auto* if_outer = b.If(true);
-    b.Append(if_outer->True(), [&] {
-        b.Append(if_inner);
-        b.ExitIf(if_outer);
-    });
-
-    b.Append(if_inner->True(), [&] { b.ExitIf(if_outer); });
-
-    b.Append(f->Block(), [&] {
-        b.Append(if_outer);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_if: if target jumps over other control instructions
-            exit_if  # if_1
-            ^^^^^^^
-
-:6:11 note: in block
-          $B3: {  # true
-          ^^^
-
-:5:9 note: first control instruction jumped
-        if true [t: $B3] {  # if_2
-        ^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2] {  # if_1
-      $B2: {  # true
-        if true [t: $B3] {  # if_2
-          $B3: {  # true
-            exit_if  # if_1
-          }
-        }
-        exit_if  # if_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpOverSwitch) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* switch_inner = b.Switch(1_i);
-
-    auto* if_outer = b.If(true);
-    b.Append(if_outer->True(), [&] {
-        b.Append(switch_inner);
-        b.ExitIf(if_outer);
-    });
-
-    auto* c = b.Case(switch_inner, {b.Constant(1_i), nullptr});
-    b.Append(c, [&] { b.ExitIf(if_outer); });
-
-    b.Append(f->Block(), [&] {
-        b.Append(if_outer);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_if: if target jumps over other control instructions
-            exit_if  # if_1
-            ^^^^^^^
-
-:6:11 note: in block
-          $B3: {  # case
-          ^^^
-
-:5:9 note: first control instruction jumped
-        switch 1i [c: (1i default, $B3)] {  # switch_1
-        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2] {  # if_1
-      $B2: {  # true
-        switch 1i [c: (1i default, $B3)] {  # switch_1
-          $B3: {  # case
-            exit_if  # if_1
-          }
-        }
-        exit_if  # if_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitIf_InvalidJumpOverLoop) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* loop = b.Loop();
-
-    auto* if_outer = b.If(true);
-    b.Append(if_outer->True(), [&] {
-        b.Append(loop);
-        b.ExitIf(if_outer);
-    });
-
-    b.Append(loop->Body(), [&] { b.ExitIf(if_outer); });
-
-    b.Append(f->Block(), [&] {
-        b.Append(if_outer);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_if: if target jumps over other control instructions
-            exit_if  # if_1
-            ^^^^^^^
-
-:6:11 note: in block
-          $B3: {  # body
-          ^^^
-
-:5:9 note: first control instruction jumped
-        loop [b: $B3] {  # loop_1
-        ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    if true [t: $B2] {  # if_1
-      $B2: {  # true
-        loop [b: $B3] {  # loop_1
-          $B3: {  # body
-            exit_if  # if_1
-          }
-        }
-        exit_if  # if_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_NullSwitch) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(mod.CreateInstruction<ExitSwitch>(nullptr));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_switch: has no parent control instruction
-        exit_switch  # undef
-        ^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # case
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch  # undef
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_LessOperandsThenSwitchParams) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    switch_->SetResults(Vector{r1, r2});
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_, 1_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_switch: provides 1 value but 'switch' expects 2 values
-        exit_switch 1i  # switch_1
-        ^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # case
-      ^^^
-
-:3:5 note: 'switch' declared here
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch 1i  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_MoreOperandsThenSwitchParams) {
-    auto* switch_ = b.Switch(1_i);
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    switch_->SetResults(Vector{r1, r2});
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_, 1_i, 2_f, 3_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_switch: provides 3 values but 'switch' expects 2 values
-        exit_switch 1i, 2.0f, 3i  # switch_1
-        ^^^^^^^^^^^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # case
-      ^^^
-
-:3:5 note: 'switch' declared here
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch 1i, 2.0f, 3i  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_WithResult) {
-    auto* switch_ = b.Switch(1_i);
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    switch_->SetResults(Vector{r1, r2});
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_, 1_i, 2_f));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_IncorrectResultType) {
-    auto* switch_ = b.Switch(1_i);
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    switch_->SetResults(Vector{r1, r2});
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.ExitSwitch(switch_, 1_i, 2_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:25 error: exit_switch: operand with type 'i32' does not match 'switch' target type 'f32'
-        exit_switch 1i, 2i  # switch_1
-                        ^^
-
-:4:7 note: in block
-      $B2: {  # case
-      ^^^
-
-:3:13 note: %3 declared here
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-            ^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch 1i, 2i  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_NotInParentSwitch) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* def = b.DefaultCase(switch_);
-    def->Append(b.Return(f));
-
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-
-    auto* if_ = sb.Append(b.If(true));
-    b.Append(if_->True(), [&] { b.ExitSwitch(switch_); });
-    sb.Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:10:9 error: exit_switch: switch not found in parent control instructions
-        exit_switch  # switch_1
-        ^^^^^^^^^^^
-
-:9:7 note: in block
-      $B3: {  # true
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        ret
-      }
-    }
-    if true [t: $B3] {  # if_1
-      $B3: {  # true
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_JumpsOverIfs) {
-    // switch(true) {
-    //   default: {
-    //     if (true) {
-    //      if (false) {
-    //         break;
-    //       }
-    //     }
-    //     break;
-    //  }
-    auto* switch_ = b.Switch(1_i);
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* def = b.DefaultCase(switch_);
-    b.Append(def, [&] {
-        auto* if_ = b.If(true);
-        b.Append(if_->True(), [&] {
-            auto* inner_if_ = b.If(false);
-            b.Append(inner_if_->True(), [&] { b.ExitSwitch(switch_); });
-            b.Return(f);
-        });
-        b.ExitSwitch(switch_);
-    });
-
-    auto sb = b.Append(f->Block());
-    sb.Append(switch_);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverSwitch) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* def = b.DefaultCase(switch_);
-    b.Append(def, [&] {
-        auto* inner = b.Switch(0_i);
-        b.ExitSwitch(switch_);
-
-        auto* inner_def = b.DefaultCase(inner);
-        b.Append(inner_def, [&] { b.ExitSwitch(switch_); });
-    });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(switch_);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
-            exit_switch  # switch_1
-            ^^^^^^^^^^^
-
-:6:11 note: in block
-          $B3: {  # case
-          ^^^
-
-:5:9 note: first control instruction jumped
-        switch 0i [c: (default, $B3)] {  # switch_2
-        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        switch 0i [c: (default, $B3)] {  # switch_2
-          $B3: {  # case
-            exit_switch  # switch_1
-          }
-        }
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverLoop) {
-    auto* switch_ = b.Switch(1_i);
-
-    auto* def = b.DefaultCase(switch_);
-    b.Append(def, [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitSwitch(switch_); });
-        b.ExitSwitch(switch_);
-    });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(switch_);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
-            exit_switch  # switch_1
-            ^^^^^^^^^^^
-
-:6:11 note: in block
-          $B3: {  # body
-          ^^^
-
-:5:9 note: first control instruction jumped
-        loop [b: $B3] {  # loop_1
-        ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        loop [b: $B3] {  # loop_1
-          $B3: {  # body
-            exit_switch  # switch_1
-          }
-        }
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_OutsideOfLoop) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Continue(loop);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:5 error: continue: called outside of associated loop
-    continue  # -> $B3
-    ^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2] {  # loop_1
-      $B2: {  # body
-        exit_loop  # loop_1
-      }
-    }
-    continue  # -> $B3
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_InLoopInit) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Initializer(), [&] { b.Continue(loop); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: continue: must only be called from loop body
-        continue  # -> $B4
-        ^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3] {  # loop_1
-      $B2: {  # initializer
-        continue  # -> $B4
-      }
-      $B3: {  # body
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_InLoopBody) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, Continue_InLoopContinuing) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Append(loop->Continuing(), [&] { b.Continue(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:9 error: continue: must only be called from loop body
-        continue  # -> $B3
-        ^^^^^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        exit_loop  # loop_1
-      }
-      $B3: {  # continuing
-        continue  # -> $B3
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_UnexpectedValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: continue: provides 2 values but 'loop' block $B3 expects 0 values
-        continue 1i, 2.0f  # -> $B3
-        ^^^^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:7:7 note: 'loop' block $B3 declared here
-      $B3: {  # continuing
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue 1i, 2.0f  # -> $B3
-      }
-      $B3: {  # continuing
-        break_if true  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_MissingValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Continuing()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: continue: provides 0 values but 'loop' block $B3 expects 2 values
-        continue  # -> $B3
-        ^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:7:7 note: 'loop' block $B3 declared here
-      $B3 (%2:i32, %3:i32): {  # continuing
-      ^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3 (%2:i32, %3:i32): {  # continuing
-        break_if true  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_MismatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Continuing()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_i, 3_f, false); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:22 error: continue: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
-        continue 1i, 2i, 3.0f, false  # -> $B3
-                     ^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:7:20 note: %3 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
-                   ^^
-
-:5:26 error: continue: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
-        continue 1i, 2i, 3.0f, false  # -> $B3
-                         ^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:7:28 note: %4 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
-                           ^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue 1i, 2i, 3.0f, false  # -> $B3
-      }
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
-        break_if true  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Continue_MatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Continuing()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f, 3_u, false); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_OutsideOfLoop) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.NextIteration(loop);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:5 error: next_iteration: called outside of associated loop
-    next_iteration  # -> $B2
-    ^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2] {  # loop_1
-      $B2: {  # body
-        exit_loop  # loop_1
-      }
-    }
-    next_iteration  # -> $B2
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_InLoopInit) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_InLoopBody) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.NextIteration(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: next_iteration: must only be called from loop initializer or continuing
-        next_iteration  # -> $B2
-        ^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2] {  # loop_1
-      $B2: {  # body
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_InLoopContinuing) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Append(loop->Continuing(), [&] { b.NextIteration(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_UnexpectedValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: next_iteration: provides 2 values but 'loop' block $B3 expects 0 values
-        next_iteration 1i, 2.0f  # -> $B3
-        ^^^^^^^^^^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-:7:7 note: 'loop' block $B3 declared here
-      $B3: {  # body
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3] {  # loop_1
-      $B2: {  # initializer
-        next_iteration 1i, 2.0f  # -> $B3
-      }
-      $B3: {  # body
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_MissingValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: next_iteration: provides 0 values but 'loop' block $B3 expects 2 values
-        next_iteration  # -> $B3
-        ^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-:7:7 note: 'loop' block $B3 declared here
-      $B3 (%2:i32, %3:i32): {  # body
-      ^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3] {  # loop_1
-      $B2: {  # initializer
-        next_iteration  # -> $B3
-      }
-      $B3 (%2:i32, %3:i32): {  # body
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_MismatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_i, 3_f, false); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:28 error: next_iteration: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
-        next_iteration 1i, 2i, 3.0f, false  # -> $B3
-                           ^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-:7:20 note: %3 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-                   ^^
-
-:5:32 error: next_iteration: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
-        next_iteration 1i, 2i, 3.0f, false  # -> $B3
-                               ^^^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-:7:28 note: %4 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-                           ^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3] {  # loop_1
-      $B2: {  # initializer
-        next_iteration 1i, 2i, 3.0f, false  # -> $B3
-      }
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, NextIteration_MatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f, 3_u, false); });
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, LoopBodyParamsWithoutInitializer) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
-        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: loop: loop with body block parameters must have an initializer
-    loop [b: $B2] {  # loop_1
-    ^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2] {  # loop_1
-      $B2 (%2:i32, %3:i32): {  # body
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ContinuingUseValueBeforeContinue) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* value = b.Let("value", 1_i);
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] {
-            b.Append(value);
-            b.Append(b.If(true)->True(), [&] { b.Continue(loop); });
-            b.ExitLoop(loop);
-        });
-        b.Append(loop->Continuing(), [&] {
-            b.Let("use", value);
-            b.NextIteration(loop);
-        });
-        b.Return(f);
-    });
-
-    ASSERT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ContinuingUseValueAfterContinue) {
-    auto* f = b.Function("my_func", ty.void_());
-    auto* value = b.Let("value", 1_i);
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] {
-            b.Append(b.If(true)->True(), [&] { b.Continue(loop); });
-            b.Append(value);
-            b.ExitLoop(loop);
-        });
-        b.Append(loop->Continuing(), [&] {
-            b.Let("use", value);
-            b.NextIteration(loop);
-        });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:14:24 error: let: %value cannot be used in continuing block as it is declared after the first 'continue' in the loop's body
-        %use:i32 = let %value
-                       ^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:10:9 note: %value declared here
-        %value:i32 = let 1i
-        ^^^^^^^^^^
-
-:7:13 note: loop body's first 'continue'
-            continue  # -> $B3
-            ^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        if true [t: $B4] {  # if_1
-          $B4: {  # true
-            continue  # -> $B3
-          }
-        }
-        %value:i32 = let 1i
-        exit_loop  # loop_1
-      }
-      $B3: {  # continuing
-        %use:i32 = let %value
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_NextIterUnexpectedValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i), Empty); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:9 error: break_if: provides 2 values but 'loop' block $B2 expects 0 values
-        break_if true next_iteration: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
-        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-:4:7 note: 'loop' block $B2 declared here
-      $B2: {  # body
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        break_if true next_iteration: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_NextIterMissingValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
-        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, nullptr, nullptr); });
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:11:9 error: break_if: provides 0 values but 'loop' block $B3 expects 2 values
-        break_if true  # -> [t: exit_loop loop_1, f: $B3]
-        ^^^^^^^^^^^^^
-
-:10:7 note: in block
-      $B4: {  # continuing
-      ^^^
-
-:7:7 note: 'loop' block $B3 declared here
-      $B3 (%2:i32, %3:i32): {  # body
-      ^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
-      $B2: {  # initializer
-        next_iteration undef, undef  # -> $B3
-      }
-      $B3 (%2:i32, %3:i32): {  # body
-        continue  # -> $B4
-      }
-      $B4: {  # continuing
-        break_if true  # -> [t: exit_loop loop_1, f: $B3]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_NextIterMismatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Initializer(),
-                 [&] { b.NextIteration(loop, nullptr, nullptr, nullptr, nullptr); });
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(),
-                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i, 3_f, false), Empty); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:11:45 error: break_if: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
-        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
-                                            ^^
-
-:10:7 note: in block
-      $B4: {  # continuing
-      ^^^
-
-:7:20 note: %3 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-                   ^^
-
-:11:49 error: break_if: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
-        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
-                                                ^^^^
-
-:10:7 note: in block
-      $B4: {  # continuing
-      ^^^
-
-:7:28 note: %4 declared here
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-                           ^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
-      $B2: {  # initializer
-        next_iteration undef, undef, undef, undef  # -> $B3
-      }
-      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
-        continue  # -> $B4
-      }
-      $B4: {  # continuing
-        break_if true next_iteration: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B3]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_NextIterMatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Body()->SetParams(
-            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
-        b.Append(loop->Initializer(),
-                 [&] { b.NextIteration(loop, nullptr, nullptr, nullptr, nullptr); });
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(),
-                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_f, 3_u, false), Empty); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_ExitUnexpectedValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i)); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:9 error: break_if: provides 2 values but 'loop' expects 0 values
-        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
-        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-:3:5 note: 'loop' declared here
-    loop [b: $B2, c: $B3] {  # loop_1
-    ^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_ExitMissingValues) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<i32>());
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:9 error: break_if: provides 0 values but 'loop' expects 2 values
-        break_if true  # -> [t: exit_loop loop_1, f: $B2]
-        ^^^^^^^^^^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-:3:5 note: 'loop' declared here
-    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        break_if true  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_ExitMismatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
-                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(),
-                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i, 3_f, false)); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:8:40 error: break_if: operand with type 'i32' does not match 'loop' target type 'f32'
-        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
-                                       ^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-:3:13 note: %3 declared here
-    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
-            ^^^^^^
-
-:8:44 error: break_if: operand with type 'f32' does not match 'loop' target type 'u32'
-        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
-                                           ^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-:3:21 note: %4 declared here
-    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
-                    ^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, BreakIf_ExitMatchedTypes) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
-                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
-        b.Append(loop->Body(), [&] { b.Continue(loop); });
-        b.Append(loop->Continuing(),
-                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_f, 3_u, false)); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop) {
-    auto* loop = b.Loop();
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.ExitLoop(loop));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_NullLoop) {
-    auto* loop = b.Loop();
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(mod.CreateInstruction<ExitLoop>(nullptr));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_loop: has no parent control instruction
-        exit_loop  # undef
-        ^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        exit_loop  # undef
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_LessOperandsThenLoopParams) {
-    auto* loop = b.Loop();
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    loop->SetResults(Vector{r1, r2});
-
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.ExitLoop(loop, 1_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_loop: provides 1 value but 'loop' expects 2 values
-        exit_loop 1i  # loop_1
-        ^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:3:5 note: 'loop' declared here
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        exit_loop 1i  # loop_1
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_MoreOperandsThenLoopParams) {
-    auto* loop = b.Loop();
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    loop->SetResults(Vector{r1, r2});
-
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_f, 3_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_loop: provides 3 values but 'loop' expects 2 values
-        exit_loop 1i, 2.0f, 3i  # loop_1
-        ^^^^^^^^^^^^^^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:3:5 note: 'loop' declared here
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        exit_loop 1i, 2.0f, 3i  # loop_1
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_WithResult) {
-    auto* loop = b.Loop();
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    loop->SetResults(Vector{r1, r2});
-
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_f));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_EQ(res, Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_IncorrectResultType) {
-    auto* loop = b.Loop();
-    auto* r1 = b.InstructionResult(ty.i32());
-    auto* r2 = b.InstructionResult(ty.f32());
-    loop->SetResults(Vector{r1, r2});
-
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.ExitLoop(loop, 1_i, 2_i));
-
-    auto* f = b.Function("my_func", ty.void_());
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:23 error: exit_loop: operand with type 'i32' does not match 'loop' target type 'f32'
-        exit_loop 1i, 2i  # loop_1
-                      ^^
-
-:4:7 note: in block
-      $B2: {  # body
-      ^^^
-
-:3:13 note: %3 declared here
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-            ^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        exit_loop 1i, 2i  # loop_1
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_NotInParentLoop) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* loop = b.Loop();
-    loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Body()->Append(b.Return(f));
-
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-
-    auto* if_ = sb.Append(b.If(true));
-    b.Append(if_->True(), [&] { b.ExitLoop(loop); });
-    sb.Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:13:9 error: exit_loop: loop not found in parent control instructions
-        exit_loop  # loop_1
-        ^^^^^^^^^
-
-:12:7 note: in block
-      $B4: {  # true
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        ret
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    if true [t: $B4] {  # if_1
-      $B4: {  # true
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_JumpsOverIfs) {
-    // loop {
-    //   if (true) {
-    //    if (false) {
-    //       break;
-    //     }
-    //   }
-    //   break;
-    // }
-    auto* loop = b.Loop();
-    loop->Continuing()->Append(b.NextIteration(loop));
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(loop->Body(), [&] {
-        auto* if_ = b.If(true);
-        b.Append(if_->True(), [&] {
-            auto* inner_if_ = b.If(false);
-            b.Append(inner_if_->True(), [&] { b.ExitLoop(loop); });
-            b.Return(f);
-        });
-        b.ExitLoop(loop);
-    });
-
-    auto sb = b.Append(f->Block());
-    sb.Append(loop);
-    sb.Return(f);
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidJumpOverSwitch) {
-    auto* loop = b.Loop();
-    loop->Continuing()->Append(b.NextIteration(loop));
-
-    b.Append(loop->Body(), [&] {
-        auto* inner = b.Switch(1_i);
-        b.ExitLoop(loop);
-
-        auto* inner_def = b.DefaultCase(inner);
-        b.Append(inner_def, [&] { b.ExitLoop(loop); });
-    });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
-            exit_loop  # loop_1
-            ^^^^^^^^^
-
-:6:11 note: in block
-          $B4: {  # case
-          ^^^
-
-:5:9 note: first control instruction jumped
-        switch 1i [c: (default, $B4)] {  # switch_1
-        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        switch 1i [c: (default, $B4)] {  # switch_1
-          $B4: {  # case
-            exit_loop  # loop_1
-          }
-        }
-        exit_loop  # loop_1
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidJumpOverLoop) {
-    auto* outer_loop = b.Loop();
-
-    outer_loop->Continuing()->Append(b.NextIteration(outer_loop));
-
-    b.Append(outer_loop->Body(), [&] {
-        auto* loop = b.Loop();
-        b.Append(loop->Body(), [&] { b.ExitLoop(outer_loop); });
-        b.ExitLoop(outer_loop);
-    });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(outer_loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
-            exit_loop  # loop_1
-            ^^^^^^^^^
-
-:6:11 note: in block
-          $B4: {  # body
-          ^^^
-
-:5:9 note: first control instruction jumped
-        loop [b: $B4] {  # loop_2
-        ^^^^^^^^^^^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        loop [b: $B4] {  # loop_2
-          $B4: {  # body
-            exit_loop  # loop_1
-          }
-        }
-        exit_loop  # loop_1
-      }
-      $B3: {  # continuing
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideContinuing) {
-    auto* loop = b.Loop();
-
-    loop->Continuing()->Append(b.ExitLoop(loop));
-    loop->Body()->Append(b.Continue(loop));
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:8:9 error: exit_loop: loop exit jumps out of continuing block
-        exit_loop  # loop_1
-        ^^^^^^^^^
-
-:7:7 note: in block
-      $B3: {  # continuing
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        exit_loop  # loop_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideContinuingNested) {
-    auto* loop = b.Loop();
-
-    b.Append(loop->Continuing(), [&]() {
-        auto* if_ = b.If(true);
-        b.Append(if_->True(), [&]() { b.ExitLoop(loop); });
-        b.NextIteration(loop);
-    });
-
-    b.Append(loop->Body(), [&] { b.Continue(loop); });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:10:13 error: exit_loop: loop exit jumps out of continuing block
-            exit_loop  # loop_1
-            ^^^^^^^^^
-
-:9:11 note: in block
-          $B4: {  # true
-          ^^^
-
-:7:7 note: in continuing block
-      $B3: {  # continuing
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [b: $B2, c: $B3] {  # loop_1
-      $B2: {  # body
-        continue  # -> $B3
-      }
-      $B3: {  # continuing
-        if true [t: $B4] {  # if_1
-          $B4: {  # true
-            exit_loop  # loop_1
-          }
-        }
-        next_iteration  # -> $B2
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideInitializer) {
-    auto* loop = b.Loop();
-
-    loop->Initializer()->Append(b.ExitLoop(loop));
-    loop->Continuing()->Append(b.NextIteration(loop));
-
-    b.Append(loop->Body(), [&] { b.Continue(loop); });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:9 error: exit_loop: loop exit not permitted in loop initializer
-        exit_loop  # loop_1
-        ^^^^^^^^^
-
-:4:7 note: in block
-      $B2: {  # initializer
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
-      $B2: {  # initializer
-        exit_loop  # loop_1
-      }
-      $B3: {  # body
-        continue  # -> $B4
-      }
-      $B4: {  # continuing
-        next_iteration  # -> $B3
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, ExitLoop_InvalidInsideInitializerNested) {
-    auto* loop = b.Loop();
-
-    b.Append(loop->Initializer(), [&]() {
-        auto* if_ = b.If(true);
-        b.Append(if_->True(), [&]() { b.ExitLoop(loop); });
-        b.NextIteration(loop);
-    });
-    loop->Continuing()->Append(b.NextIteration(loop));
-
-    b.Append(loop->Body(), [&] { b.Continue(loop); });
-
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(loop);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:7:13 error: exit_loop: loop exit not permitted in loop initializer
-            exit_loop  # loop_1
-            ^^^^^^^^^
-
-:6:11 note: in block
-          $B5: {  # true
-          ^^^
-
-:4:7 note: in initializer block
-      $B2: {  # initializer
-      ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
-      $B2: {  # initializer
-        if true [t: $B5] {  # if_1
-          $B5: {  # true
-            exit_loop  # loop_1
-          }
-        }
-        next_iteration  # -> $B3
-      }
-      $B3: {  # body
-        continue  # -> $B4
-      }
-      $B4: {  # continuing
-        next_iteration  # -> $B3
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {  //
-        b.Return(f);
-    });
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, Return_WithValue) {
-    auto* f = b.Function("my_func", ty.i32());
-    b.Append(f->Block(), [&] {  //
-        b.Return(f, 42_i);
-    });
-
-    EXPECT_EQ(ir::Validate(mod), Success);
-}
-
-TEST_F(IR_ValidatorTest, Return_UnexpectedResult) {
-    auto* f = b.Function("my_func", ty.i32());
-    b.Append(f->Block(), [&] {  //
-        auto* r = b.Return(f, 42_i);
-        r->SetResults(Vector{b.InstructionResult(ty.i32())});
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: return: expected exactly 0 results, got 1
-    ret 42i
-    ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():i32 {
-  $B1: {
-    ret 42i
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return_NotFunction) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {  //
-        auto* var = b.Var(ty.ptr<function, f32>());
-        auto* r = b.Return(nullptr);
-        r->SetOperand(0, var->Result(0));
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:5 error: return: expected function for first operand
-    ret
-    ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, f32, read_write> = var
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return_MissingFunction) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* r = b.Return(f);
-        r->ClearOperands();
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: return: expected between 1 and 2 operands, got 0
-    ret
-    ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return_UnexpectedValue) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {  //
-        b.Return(f, 42_i);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: return: unexpected return value
-    ret 42i
-    ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    ret 42i
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return_MissingValue) {
-    auto* f = b.Function("my_func", ty.i32());
-    b.Append(f->Block(), [&] {  //
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: return: expected return value
-    ret
-    ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():i32 {
-  $B1: {
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Return_WrongValueType) {
-    auto* f = b.Function("my_func", ty.i32());
-    b.Append(f->Block(), [&] {  //
-        b.Return(f, 42_f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:3:5 error: return: return value type 'f32' does not match function return type 'i32'
-    ret 42.0f
-    ^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():i32 {
-  $B1: {
-    ret 42.0f
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Unreachable_UnexpectedResult) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {  //
-        auto* u = b.Unreachable();
-        u->SetResults(Vector{b.InstructionResult(ty.i32())});
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: unreachable: expected exactly 0 results, got 1
-    unreachable
-    ^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Unreachable_UnexpectedOperand) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {  //
-        auto* u = b.Unreachable();
-        u->SetOperands(Vector{b.Value(0_i)});
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: unreachable: expected exactly 0 operands, got 1
-    unreachable
-    ^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    unreachable
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Load_NullFrom) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.i32()), nullptr));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:19 error: load: operand is undefined
-    %2:i32 = load undef
-                  ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32 = load undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Load_SourceNotMemoryView) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* let = b.Let("l", 1_i);
-        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.f32()), let->Result(0)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:19 error: load: load source operand is not a memory view
-    %3:f32 = load %l
-                  ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %l:i32 = let 1i
-    %3:f32 = load %l
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Load_TypeMismatch) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.f32()), var->Result(0)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:19 error: load: result type 'f32' does not match source store type 'i32'
-    %3:f32 = load %2
-                  ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    %3:f32 = load %2
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Load_MissingResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        auto* load = mod.CreateInstruction<ir::Load>(nullptr, var->Result(0));
-        load->ClearResults();
-        b.Append(load);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:13 error: load: expected exactly 1 results, got 0
-    undef = load %2
-            ^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    undef = load %2
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Load_NonReadableSource) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32, core::Access::kWrite>());
-        b.Append(mod.CreateInstruction<ir::Load>(b.InstructionResult(ty.i32()), var->Result(0)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:19 error: load: load source operand has a non-readable access type, 'write'
-    %3:i32 = load %2
-                  ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, write> = var
-    %3:i32 = load %2
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NullTo) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(mod.CreateInstruction<ir::Store>(nullptr, b.Constant(42_i)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:11 error: store: operand is undefined
-    store undef, 42i
-          ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    store undef, 42i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NullFrom) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), nullptr));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:15 error: store: operand is undefined
-    store %2, undef
-              ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    store %2, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NullToAndFrom) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(mod.CreateInstruction<ir::Store>(nullptr, nullptr));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:11 error: store: operand is undefined
-    store undef, undef
-          ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:3:18 error: store: operand is undefined
-    store undef, undef
-                 ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    store undef, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NonEmptyResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        auto* store = mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_i));
-        store->SetResults(Vector{b.InstructionResult(ty.i32())});
-        b.Append(store);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:5 error: store: expected exactly 0 results, got 1
-    store %2, 42i
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    store %2, 42i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_TargetNotMemoryView) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* let = b.Let("l", 1_i);
-        b.Append(mod.CreateInstruction<ir::Store>(let->Result(0), b.Constant(42_u)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:11 error: store: store target operand is not a memory view
-    store %l, 42u
-          ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %l:i32 = let 1i
-    store %l, 42u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_TypeMismatch) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_u)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:15 error: store: value type 'u32' does not match store type 'i32'
-    store %2, 42u
-              ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    store %2, 42u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NoStoreType) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* result = b.InstructionResult(ty.u32());
-        result->SetType(nullptr);
-        b.Append(mod.CreateInstruction<ir::Store>(result, b.Constant(42_u)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:11 error: store: operand type is undefined
-    store %2, 42u
-          ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    store %2, 42u
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NoValueType) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32>());
-        auto* val = b.Construct(ty.u32(), 42_u);
-        val->Result(0)->SetType(nullptr);
-
-        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), val->Result(0)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: construct: result type is undefined
-    %3:undef = construct 42u
-    ^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-:5:15 error: store: operand type is undefined
-    store %2, %3
-              ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read_write> = var
-    %3:undef = construct 42u
-    store %2, %3
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Store_NonWriteableTarget) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, i32, core::Access::kRead>());
-        b.Append(mod.CreateInstruction<ir::Store>(var->Result(0), b.Constant(42_i)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:11 error: store: store target operand has a non-writeable access type, 'read'
-    store %2, 42i
-          ^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, i32, read> = var
-    store %2, 42i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, LoadVectorElement_NullResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(
-            mod.CreateInstruction<ir::LoadVectorElement>(nullptr, var->Result(0), b.Constant(1_i)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: load_vector_element: result is undefined
-    undef = load_vector_element %2, 1i
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    undef = load_vector_element %2, 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, LoadVectorElement_NullFrom) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(mod.CreateInstruction<ir::LoadVectorElement>(b.InstructionResult(ty.f32()),
-                                                              nullptr, b.Constant(1_i)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:34 error: load_vector_element: operand is undefined
-    %2:f32 = load_vector_element undef, 1i
-                                 ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:f32 = load_vector_element undef, 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, LoadVectorElement_NullIndex) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.CreateInstruction<ir::LoadVectorElement>(b.InstructionResult(ty.f32()),
-                                                              var->Result(0), nullptr));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:38 error: load_vector_element: operand is undefined
-    %3:f32 = load_vector_element %2, undef
-                                     ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    %3:f32 = load_vector_element %2, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, LoadVectorElement_MissingResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        auto* load = b.LoadVectorElement(var, b.Constant(1_i));
-        load->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:13 error: load_vector_element: expected exactly 1 results, got 0
-    undef = load_vector_element %2, 1i
-            ^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    undef = load_vector_element %2, 1i
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, LoadVectorElement_MissingOperands) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        auto* load = b.LoadVectorElement(var, b.Constant(1_i));
-        load->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:14 error: load_vector_element: expected exactly 2 operands, got 0
-    %3:f32 = load_vector_element
-             ^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    %3:f32 = load_vector_element
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, StoreVectorElement_NullTo) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(nullptr, b.Constant(1_i),
-                                                               b.Constant(2_f)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:26 error: store_vector_element: operand is undefined
-    store_vector_element undef, 1i, 2.0f
-                         ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    store_vector_element undef, 1i, 2.0f
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, StoreVectorElement_NullIndex) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(var->Result(0), nullptr,
-                                                               b.Constant(2_f)));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:30 error: store_vector_element: operand is undefined
-    store_vector_element %2, undef, 2.0f
-                             ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    store_vector_element %2, undef, 2.0f
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, StoreVectorElement_NullValue) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.CreateInstruction<ir::StoreVectorElement>(var->Result(0), b.Constant(1_i),
-                                                               nullptr));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(:4:34 error: store_vector_element: operand is undefined
-    store_vector_element %2, 1i, undef
-                                 ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    store_vector_element %2, 1i, undef
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, StoreVectorElement_MissingOperands) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        auto* store = b.StoreVectorElement(var, b.Constant(1_i), b.Constant(2_f));
-        store->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: store_vector_element: expected exactly 3 operands, got 0
-    store_vector_element
-    ^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    store_vector_element
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, StoreVectorElement_UnexpectedResult) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        auto* store = b.StoreVectorElement(var, b.Constant(1_i), b.Constant(2_f));
-        store->SetResults(Vector{b.InstructionResult(ty.f32())});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: store_vector_element: expected exactly 0 results, got 1
-    store_vector_element %2, 1i, 2.0f
-    ^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec3<f32>, read_write> = var
-    store_vector_element %2, 1i, 2.0f
-    ret
-  }
-}
-)");
-}
-
 TEST_F(IR_ValidatorTest, Scoping_UseBeforeDecl) {
     auto* f = b.Function("my_func", ty.void_());
 
@@ -10319,732 +1286,6 @@
 )");
 }
 
-template <typename T>
-static const core::type::Type* TypeBuilder(core::type::Manager& m) {
-    return m.Get<T>();
-}
-template <typename T>
-static const core::type::Type* RefTypeBuilder(core::type::Manager& m) {
-    return m.ref<AddressSpace::kFunction, T>();
-}
-using TypeBuilderFn = decltype(&TypeBuilder<i32>);
-
-using IR_ValidatorRefTypeTest = IRTestParamHelper<std::tuple</* holds_ref */ bool,
-                                                             /* refs_allowed */ bool,
-                                                             /* type_builder */ TypeBuilderFn>>;
-
-TEST_P(IR_ValidatorRefTypeTest, Var) {
-    bool holds_ref = std::get<0>(GetParam());
-    bool refs_allowed = std::get<1>(GetParam());
-    auto* type = std::get<2>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        if (auto* view = type->As<core::type::MemoryView>()) {
-            b.Var(view);
-        } else {
-            b.Var(ty.ptr<function>(type));
-        }
-
-        b.Return(fn);
-    });
-
-    Capabilities caps;
-    if (refs_allowed) {
-        caps.Add(Capability::kAllowRefTypes);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (!holds_ref || refs_allowed) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("3:5 error: var: reference types are not permitted"));
-    }
-}
-
-TEST_P(IR_ValidatorRefTypeTest, FnParam) {
-    bool holds_ref = std::get<0>(GetParam());
-    bool refs_allowed = std::get<1>(GetParam());
-    auto* type = std::get<2>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    fn->SetParams(Vector{b.FunctionParam(type)});
-    b.Append(fn->Block(), [&] { b.Return(fn); });
-
-    Capabilities caps;
-    if (refs_allowed) {
-        caps.Add(Capability::kAllowRefTypes);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (!holds_ref) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("reference types are not permitted"));
-    }
-}
-
-TEST_P(IR_ValidatorRefTypeTest, FnRet) {
-    bool holds_ref = std::get<0>(GetParam());
-    bool refs_allowed = std::get<1>(GetParam());
-    auto* type = std::get<2>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", type);
-    b.Append(fn->Block(), [&] { b.Unreachable(); });
-
-    Capabilities caps;
-    if (refs_allowed) {
-        caps.Add(Capability::kAllowRefTypes);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (!holds_ref) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("reference types are not permitted"));
-    }
-}
-
-TEST_P(IR_ValidatorRefTypeTest, BlockParam) {
-    bool holds_ref = std::get<0>(GetParam());
-    bool refs_allowed = std::get<1>(GetParam());
-    auto* type = std::get<2>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Continuing()->SetParams({b.BlockParam(type)});
-        b.Append(loop->Body(), [&] {  //
-            b.Continue(loop, nullptr);
-        });
-        b.Append(loop->Continuing(), [&] {  //
-            b.NextIteration(loop);
-        });
-        b.Unreachable();
-    });
-
-    Capabilities caps;
-    if (refs_allowed) {
-        caps.Add(Capability::kAllowRefTypes);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (!holds_ref) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("reference types are not permitted"));
-    }
-}
-
-INSTANTIATE_TEST_SUITE_P(NonRefTypes,
-                         IR_ValidatorRefTypeTest,
-                         testing::Combine(/* holds_ref */ testing::Values(false),
-                                          /* refs_allowed */ testing::Values(false, true),
-                                          /* type_builder */
-                                          testing::Values(TypeBuilder<i32>,
-                                                          TypeBuilder<bool>,
-                                                          TypeBuilder<vec4<f32>>,
-                                                          TypeBuilder<array<f32, 3>>)));
-
-INSTANTIATE_TEST_SUITE_P(RefTypes,
-                         IR_ValidatorRefTypeTest,
-                         testing::Combine(/* holds_ref */ testing::Values(true),
-                                          /* refs_allowed */ testing::Values(false, true),
-                                          /* type_builder */
-                                          testing::Values(RefTypeBuilder<i32>,
-                                                          RefTypeBuilder<bool>,
-                                                          RefTypeBuilder<vec4<f32>>)));
-
-TEST_F(IR_ValidatorTest, PointerToPointer) {
-    auto* type = ty.ptr<function, ptr<function, i32>>();
-    auto* fn = b.Function("my_func", ty.void_());
-    fn->SetParams(Vector{b.FunctionParam(type)});
-    b.Append(fn->Block(), [&] {  //
-        b.Return(fn);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_THAT(res.Failure().reason.Str(),
-                testing::HasSubstr("nested pointer types are not permitted"));
-}
-
-TEST_F(IR_ValidatorTest, PointerToVoid) {
-    auto* type = ty.ptr(AddressSpace::kFunction, ty.void_());
-    auto* fn = b.Function("my_func", ty.void_());
-    fn->SetParams(Vector{b.FunctionParam(type)});
-    b.Append(fn->Block(), [&] {  //
-        b.Return(fn);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_THAT(res.Failure().reason.Str(),
-                testing::HasSubstr("pointers to void are not permitted"));
-}
-
-TEST_F(IR_ValidatorTest, ReferenceToReference) {
-    auto* type = ty.ref<function>(ty.ref<function, i32>());
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {  //
-        b.Var(type);
-        b.Return(fn);
-    });
-
-    Capabilities caps;
-    caps.Add(Capability::kAllowRefTypes);
-
-    auto res = ir::Validate(mod, caps);
-    ASSERT_NE(res, Success);
-    EXPECT_THAT(res.Failure().reason.Str(),
-                testing::HasSubstr("nested reference types are not permitted"));
-}
-
-TEST_F(IR_ValidatorTest, ReferenceToVoid) {
-    auto* type = ty.ref(AddressSpace::kFunction, ty.void_());
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {  //
-        b.Var(type);
-        b.Return(fn);
-    });
-
-    Capabilities caps;
-    caps.Add(Capability::kAllowRefTypes);
-
-    auto res = ir::Validate(mod, caps);
-    ASSERT_NE(res, Success);
-    EXPECT_THAT(res.Failure().reason.Str(),
-                testing::HasSubstr("references to void are not permitted"));
-}
-
-TEST_F(IR_ValidatorTest, PointerInStructure_WithoutCapability) {
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("S"), {
-                                            {mod.symbols.New("a"), ty.ptr<private_, i32>()},
-                                        });
-    mod.root_block->Append(b.Var("my_struct", private_, str_ty));
-
-    auto* fn = b.Function("F", ty.void_());
-    b.Append(fn->Block(), [&] { b.Return(fn); });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_THAT(res.Failure().reason.Str(),
-                testing::HasSubstr("nested pointer types are not permitted"));
-}
-
-TEST_F(IR_ValidatorTest, PointerInStructure_WithCapability) {
-    auto* str_ty =
-        ty.Struct(mod.symbols.New("S"), {
-                                            {mod.symbols.New("a"), ty.ptr<private_, i32>()},
-                                        });
-
-    auto* fn = b.Function("F", ty.void_());
-    auto* param = b.FunctionParam("param", str_ty);
-    fn->SetParams({param});
-    b.Append(fn->Block(), [&] { b.Return(fn); });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
-    EXPECT_EQ(res, Success) << res.Failure();
-}
-
-using IR_Validator8BitIntTypeTest = IRTestParamHelper<std::tuple<
-    /* int8_allowed */ bool,
-    /* type_builder */ TypeBuilderFn>>;
-
-TEST_P(IR_Validator8BitIntTypeTest, Var) {
-    bool int8_allowed = std::get<0>(GetParam());
-    auto* type = std::get<1>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        b.Var(ty.ptr<function>(type));
-        b.Return(fn);
-    });
-
-    Capabilities caps;
-    if (int8_allowed) {
-        caps.Add(Capability::kAllow8BitIntegers);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (int8_allowed) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("3:5 error: var: 8-bit integer types are not permitted"));
-    }
-}
-
-TEST_P(IR_Validator8BitIntTypeTest, FnParam) {
-    bool int8_allowed = std::get<0>(GetParam());
-    auto* type = std::get<1>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    fn->SetParams(Vector{b.FunctionParam(type)});
-    b.Append(fn->Block(), [&] { b.Return(fn); });
-
-    Capabilities caps;
-    if (int8_allowed) {
-        caps.Add(Capability::kAllow8BitIntegers);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (int8_allowed) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("8-bit integer types are not permitted"));
-    }
-}
-
-TEST_P(IR_Validator8BitIntTypeTest, FnRet) {
-    bool int8_allowed = std::get<0>(GetParam());
-    auto* type = std::get<1>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", type);
-    b.Append(fn->Block(), [&] { b.Unreachable(); });
-
-    Capabilities caps;
-    if (int8_allowed) {
-        caps.Add(Capability::kAllow8BitIntegers);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (int8_allowed) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("8-bit integer types are not permitted"));
-    }
-}
-
-TEST_P(IR_Validator8BitIntTypeTest, BlockParam) {
-    bool int8_allowed = std::get<0>(GetParam());
-    auto* type = std::get<1>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        auto* loop = b.Loop();
-        loop->Continuing()->SetParams({b.BlockParam(type)});
-        b.Append(loop->Body(), [&] {  //
-            b.Continue(loop, nullptr);
-        });
-        b.Append(loop->Continuing(), [&] {  //
-            b.NextIteration(loop);
-        });
-        b.Unreachable();
-    });
-
-    Capabilities caps;
-    if (int8_allowed) {
-        caps.Add(Capability::kAllow8BitIntegers);
-    }
-    auto res = ir::Validate(mod, caps);
-    if (int8_allowed) {
-        ASSERT_EQ(res, Success) << res.Failure();
-    } else {
-        ASSERT_NE(res, Success);
-        EXPECT_THAT(res.Failure().reason.Str(),
-                    testing::HasSubstr("8-bit integer types are not permitted"));
-    }
-}
-
-INSTANTIATE_TEST_SUITE_P(Int8Types,
-                         IR_Validator8BitIntTypeTest,
-                         testing::Combine(
-                             /* int8_allowed */ testing::Values(false, true),
-                             /* type_builder */
-                             testing::Values(TypeBuilder<i8>,
-                                             TypeBuilder<u8>,
-                                             TypeBuilder<vec4<i8>>,
-                                             TypeBuilder<array<u8, 4>>)));
-
-TEST_F(IR_ValidatorTest, Int8Type_InstructionOperand_NotAllowed) {
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        b.Convert(ty.i32(), u8(1));
-        b.Return(fn);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:22 error: convert: 8-bit integer types are not permitted
-    %2:i32 = convert 1u8
-                     ^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:i32 = convert 1u8
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Int8Type_InstructionOperand_Allowed) {
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        b.Convert(ty.i32(), u8(1));
-        b.Return(fn);
-    });
-
-    auto res = ir::Validate(mod, Capabilities{Capability::kAllow8BitIntegers});
-    ASSERT_EQ(res, Success) << res.Failure();
-}
-
-TEST_F(IR_ValidatorTest, Switch_NoCondition) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    auto* s = b.ir.CreateInstruction<ir::Switch>();
-    f->Block()->Append(s);
-    b.Append(b.DefaultCase(s), [&] { b.ExitSwitch(s); });
-    f->Block()->Append(b.Return(f));
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(error: switch: operand is undefined
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch undef [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Switch_ConditionPointer) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* s = b.Switch(b.Var("a", b.Zero<i32>()));
-        b.Append(b.DefaultCase(s), [&] { b.ExitSwitch(s); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(error: switch: condition type must be an integer scalar
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %a:ptr<function, i32, read_write> = var, 0i
-    switch %a [c: (default, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Switch_NoCases) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        b.Switch(1_i);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: switch: missing default case for switch
-    switch 1i [] {  # switch_1
-    ^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [] {  # switch_1
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Switch_NoDefaultCase) {
-    auto* f = b.Function("my_func", ty.void_());
-
-    b.Append(f->Block(), [&] {
-        auto* s = b.Switch(1_i);
-        b.Append(b.Case(s, {b.Constant(0_i)}), [&] { b.ExitSwitch(s); });
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: switch: missing default case for switch
-    switch 1i [c: (0i, $B2)] {  # switch_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    switch 1i [c: (0i, $B2)] {  # switch_1
-      $B2: {  # case
-        exit_switch  # switch_1
-      }
-    }
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_MissingValue) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        swizzle->ClearOperands();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:20 error: swizzle: expected exactly 1 operands, got 0
-    %3:vec4<f32> = swizzle undef, wzyx
-                   ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    %3:vec4<f32> = swizzle undef, wzyx
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_NullValue) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        swizzle->SetOperand(0, nullptr);
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(error: swizzle: operand is undefined
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    %3:vec4<f32> = swizzle undef, wzyx
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_MissingResult) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        swizzle->ClearResults();
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:13 error: swizzle: expected exactly 1 results, got 0
-    undef = swizzle %2, wzyx
-            ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    undef = swizzle %2, wzyx
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_NullResult) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        swizzle->SetResults(Vector<ir::InstructionResult*, 1>{nullptr});
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:5 error: swizzle: result is undefined
-    undef = swizzle %2, wzyx
-    ^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    undef = swizzle %2, wzyx
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_NoIndices) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        auto indices = Vector<uint32_t, 0>();
-        swizzle->SetIndices(std::move(indices));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:20 error: swizzle: expected at least 1 indices
-    %3:vec4<f32> = swizzle %2,
-                   ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    %3:vec4<f32> = swizzle %2,
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_TooManyIndices) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        auto indices = Vector<uint32_t, 5>{1, 1, 1, 1, 1};
-        swizzle->SetIndices(std::move(indices));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:20 error: swizzle: expected at most 4 indices
-    %3:vec4<f32> = swizzle %2, yyyyy
-                   ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    %3:vec4<f32> = swizzle %2, yyyyy
-    ret
-  }
-}
-)");
-}
-
-TEST_F(IR_ValidatorTest, Swizzle_InvalidIndices) {
-    auto* f = b.Function("my_func", ty.void_());
-    b.Append(f->Block(), [&] {
-        auto* var = b.Var(ty.ptr(function, ty.vec4<f32>()));
-        auto* swizzle = b.Swizzle(ty.vec4<f32>(), var, {3, 2, 1, 0});
-        auto indices = Vector<uint32_t, 4>{4, 3, 2, 1};
-        swizzle->SetIndices(std::move(indices));
-        b.Return(f);
-    });
-
-    auto res = ir::Validate(mod);
-    ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:20 error: swizzle: invalid index value
-    %3:vec4<f32> = swizzle %2, wzy
-                   ^^^^^^^
-
-:2:3 note: in block
-  $B1: {
-  ^^^
-
-note: # Disassembly
-%my_func = func():void {
-  $B1: {
-    %2:ptr<function, vec4<f32>, read_write> = var
-    %3:vec4<f32> = swizzle %2, wzy
-    ret
-  }
-}
-)");
-}
-
 TEST_F(IR_ValidatorTest, OverrideWithoutCapability) {
     b.Append(mod.root_block, [&] { b.Override("a", 1_u); });
 
@@ -11260,92 +1501,4 @@
 )");
 }
 
-using BitcastTypeTest = IRTestParamHelper<std::tuple<
-    /* bitcast allowed */ bool,
-    /* src type_builder */ TypeBuilderFn,
-    /* dest type_builder */ TypeBuilderFn>>;
-
-TEST_P(BitcastTypeTest, Check) {
-    bool bitcast_allowed = std::get<0>(GetParam());
-    auto* src_ty = std::get<1>(GetParam())(ty);
-    auto* dest_ty = std::get<2>(GetParam())(ty);
-
-    auto* fn = b.Function("my_func", ty.void_());
-    b.Append(fn->Block(), [&] {
-        b.Bitcast(dest_ty, b.Zero(src_ty));
-        b.Return(fn);
-    });
-
-    auto res = ir::Validate(mod);
-    if (bitcast_allowed) {
-        ASSERT_EQ(res, Success) << "Bitcast should be defined for '" << src_ty->FriendlyName()
-                                << "' -> '" << dest_ty->FriendlyName() << "': " << res.Failure();
-    } else {
-        ASSERT_NE(res, Success) << "Bitcast should NOT be defined for '" << src_ty->FriendlyName()
-                                << "' -> '" << dest_ty->FriendlyName() << "'";
-        EXPECT_THAT(res.Failure().reason.Str(), testing::HasSubstr("bitcast is not defined"));
-    }
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    IR_ValidatorTest,
-    BitcastTypeTest,
-    testing::Values(
-        // Scalar identity
-        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<u32>),
-        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<i32>),
-        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<f32>),
-        std::make_tuple(true, TypeBuilder<f16>, TypeBuilder<f16>),
-
-        // Scalar reinterpretation
-        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<i32>),
-        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<f32>),
-        std::make_tuple(true, TypeBuilder<u32>, TypeBuilder<vec2<f16>>),
-        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<u32>),
-        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<f32>),
-        std::make_tuple(true, TypeBuilder<i32>, TypeBuilder<vec2<f16>>),
-        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<u32>),
-        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<i32>),
-        std::make_tuple(true, TypeBuilder<f32>, TypeBuilder<vec2<f16>>),
-        std::make_tuple(false, TypeBuilder<u32>, TypeBuilder<f16>),
-        std::make_tuple(false, TypeBuilder<i32>, TypeBuilder<f16>),
-        std::make_tuple(false, TypeBuilder<f32>, TypeBuilder<f16>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<u32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<i32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<f32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<vec2<f16>>),
-
-        // Component-wise identity, sparsely (non-exhaustively) covering types and vector sizes
-        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec2<u32>>),
-        std::make_tuple(true, TypeBuilder<vec3<i32>>, TypeBuilder<vec3<i32>>),
-        std::make_tuple(true, TypeBuilder<vec4<f32>>, TypeBuilder<vec4<f32>>),
-        std::make_tuple(true, TypeBuilder<vec3<f16>>, TypeBuilder<vec3<f16>>),
-        std::make_tuple(false, TypeBuilder<vec2<u32>>, TypeBuilder<vec3<u32>>),
-        std::make_tuple(false, TypeBuilder<vec2<u32>>, TypeBuilder<vec4<u32>>),
-        std::make_tuple(false, TypeBuilder<vec3<i32>>, TypeBuilder<vec2<i32>>),
-        std::make_tuple(false, TypeBuilder<vec3<i32>>, TypeBuilder<vec4<i32>>),
-        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec2<f32>>),
-        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec3<f32>>),
-
-        // Component-wise reinterpretation, sparsely (non-exhaustively) covering types and vector
-        // sizes
-        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec2<i32>>),
-        std::make_tuple(true, TypeBuilder<vec3<u32>>, TypeBuilder<vec3<f32>>),
-        std::make_tuple(true, TypeBuilder<vec4<i32>>, TypeBuilder<vec4<u32>>),
-        std::make_tuple(true, TypeBuilder<vec3<i32>>, TypeBuilder<vec3<f32>>),
-        std::make_tuple(true, TypeBuilder<vec3<f32>>, TypeBuilder<vec3<u32>>),
-        std::make_tuple(true, TypeBuilder<vec2<f32>>, TypeBuilder<vec2<i32>>),
-        std::make_tuple(true, TypeBuilder<vec2<u32>>, TypeBuilder<vec4<f16>>),
-        std::make_tuple(true, TypeBuilder<vec2<i32>>, TypeBuilder<vec4<f16>>),
-        std::make_tuple(true, TypeBuilder<vec2<f32>>, TypeBuilder<vec4<f16>>),
-        std::make_tuple(false, TypeBuilder<vec4<u32>>, TypeBuilder<vec4<f16>>),
-        std::make_tuple(false, TypeBuilder<vec2<i32>>, TypeBuilder<vec2<f16>>),
-        std::make_tuple(false, TypeBuilder<vec4<f32>>, TypeBuilder<vec4<f16>>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<u32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<i32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<f32>),
-        std::make_tuple(false, TypeBuilder<f16>, TypeBuilder<vec2<f16>>)
-        ));
-
-}  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_test.h b/src/tint/lang/core/ir/validator_test.h
new file mode 100644
index 0000000..9d647b3
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_test.h
@@ -0,0 +1,89 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_VALIDATOR_TEST_H_
+#define SRC_TINT_LANG_CORE_IR_VALIDATOR_TEST_H_
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/core/io_attributes.h"
+#include "src/tint/lang/core/ir/ir_helper_test.h"
+
+namespace tint::core {
+
+namespace type {
+class Type;
+}  // namespace type
+
+namespace ir {
+
+class Function;
+
+class IR_ValidatorTest : public IRTestHelper {
+  public:
+    /// Builds and returns a basic 'compute' entry point function, named @p name
+    Function* ComputeEntryPoint(const std::string& name = "f");
+
+    /// Builds and returns a basic 'fragment' entry point function, named @p name
+    Function* FragmentEntryPoint(const std::string& name = "f");
+
+    /// Builds and returns a basic 'vertex' entry point function, named @p name
+    Function* VertexEntryPoint(const std::string& name = "f");
+
+    /// Adds to a function an input param named @p name of type @p type, and decorated with @p
+    /// builtin
+    void AddBuiltinParam(Function* func,
+                         const std::string& name,
+                         BuiltinValue builtin,
+                         const core::type::Type* type);
+
+    /// Adds to a function an return value of type @p type with attributes @p attr.
+    /// If there is an already existing non-structured return, both values are moved into a
+    /// structured return using @p name as the name.
+    /// If there is an already existing structured return, then this ICEs, since that is beyond the
+    /// scope of this implementation.
+    void AddReturn(Function* func,
+                   const std::string& name,
+                   const core::type::Type* type,
+                   const IOAttributes& attr = {});
+
+    /// Adds to a function an return value of type @p type, and decorated with @p builtin.
+    /// See @ref AddReturn for more details
+    void AddBuiltinReturn(Function* func,
+                          const std::string& name,
+                          BuiltinValue builtin,
+                          const core::type::Type* type);
+};
+
+}  // namespace ir
+}  // namespace tint::core
+
+#endif  // SRC_TINT_LANG_CORE_IR_VALIDATOR_TEST_H_
diff --git a/src/tint/lang/core/ir/validator_type_test.cc b/src/tint/lang/core/ir/validator_type_test.cc
new file mode 100644
index 0000000..ff9bde3
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_type_test.cc
@@ -0,0 +1,787 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/ir/ir_helper_test.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/texel_format.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/memory_view.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Abstract_Scalar) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Var("af", function, ty.AFloat());
+        b.Var("af", function, ty.AInt());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: var: abstracts are not permitted
+    %af:ptr<function, abstract-float, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:4:5 error: var: abstracts are not permitted
+    %af_1:ptr<function, abstract-int, read_write> = var  # %af_1: 'af'
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %af:ptr<function, abstract-float, read_write> = var
+    %af_1:ptr<function, abstract-int, read_write> = var  # %af_1: 'af'
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Abstract_Vector) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Var("af", function, ty.vec2<AFloat>());
+        b.Var("ai", function, ty.vec3<AInt>());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: var: abstracts are not permitted
+    %af:ptr<function, vec2<abstract-float>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:4:5 error: var: abstracts are not permitted
+    %ai:ptr<function, vec3<abstract-int>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %af:ptr<function, vec2<abstract-float>, read_write> = var
+    %ai:ptr<function, vec3<abstract-int>, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Abstract_Matrix) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Var("af", function, ty.mat2x2<AFloat>());
+        b.Var("ai", function, ty.mat3x4<AInt>());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: var: abstracts are not permitted
+    %af:ptr<function, mat2x2<abstract-float>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:4:5 error: var: abstracts are not permitted
+    %ai:ptr<function, mat3x4<abstract-int>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %af:ptr<function, mat2x2<abstract-float>, read_write> = var
+    %ai:ptr<function, mat3x4<abstract-int>, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Abstract_Struct) {
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("af"), ty.AFloat(), {}},
+                                                   {mod.symbols.New("ai"), ty.AInt(), {}},
+                                               });
+    auto* v = b.Var(ty.ptr(private_, str_ty));
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:7:3 error: var: abstracts are not permitted
+  %1:ptr<private, MyStruct, read_write> = var
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:6:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+MyStruct = struct @align(1) {
+  af:abstract-float @offset(0)
+  ai:abstract-int @offset(0)
+}
+
+$B1: {  # root
+  %1:ptr<private, MyStruct, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Abstract_FunctionParam) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    f->SetParams({b.FunctionParam(ty.AFloat()), b.FunctionParam(ty.AInt())});
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: abstracts are not permitted
+%my_func = func(%2:abstract-float, %3:abstract-int):void {
+                ^^^^^^^^^^^^^^^^^
+
+:1:36 error: abstracts are not permitted
+%my_func = func(%2:abstract-float, %3:abstract-int):void {
+                                   ^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%2:abstract-float, %3:abstract-int):void {
+  $B1: {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Type_VectorElements) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Var("u32_valid", AddressSpace::kFunction, ty.vec4(ty.u32()));
+        b.Var("i32_valid", AddressSpace::kFunction, ty.vec4(ty.i32()));
+        b.Var("bool_valid", AddressSpace::kFunction, ty.vec2(ty.bool_()));
+        b.Var("f16_valid", AddressSpace::kFunction, ty.vec3(ty.f16()));
+        b.Var("f32_valid", AddressSpace::kFunction, ty.vec3(ty.f32()));
+        b.Var("void_invalid", AddressSpace::kFunction, ty.vec2(ty.void_()));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:8:5 error: var: vector elements must be scalars
+    %void_invalid:ptr<function, vec2<void>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %u32_valid:ptr<function, vec4<u32>, read_write> = var
+    %i32_valid:ptr<function, vec4<i32>, read_write> = var
+    %bool_valid:ptr<function, vec2<bool>, read_write> = var
+    %f16_valid:ptr<function, vec3<f16>, read_write> = var
+    %f32_valid:ptr<function, vec3<f32>, read_write> = var
+    %void_invalid:ptr<function, vec2<void>, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Type_MatrixElements) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Var("u32_invalid", AddressSpace::kFunction, ty.mat2x2(ty.u32()));
+        b.Var("i32_invalid", AddressSpace::kFunction, ty.mat3x2(ty.i32()));
+        b.Var("bool_invalid", AddressSpace::kFunction, ty.mat4x2(ty.bool_()));
+        b.Var("f16_valid", AddressSpace::kFunction, ty.mat2x3(ty.f16()));
+        b.Var("f32_valid", AddressSpace::kFunction, ty.mat4x4(ty.f32()));
+        b.Var("void_invalid", AddressSpace::kFunction, ty.mat3x3(ty.void_()));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:5 error: var: matrix elements must be float scalars
+    %u32_invalid:ptr<function, mat2x2<u32>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:4:5 error: var: matrix elements must be float scalars
+    %i32_invalid:ptr<function, mat3x2<i32>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:5:5 error: var: matrix elements must be float scalars
+    %bool_invalid:ptr<function, mat4x2<bool>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:8:5 error: var: matrix elements must be float scalars
+    %void_invalid:ptr<function, mat3x3<void>, read_write> = var
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %u32_invalid:ptr<function, mat2x2<u32>, read_write> = var
+    %i32_invalid:ptr<function, mat3x2<i32>, read_write> = var
+    %bool_invalid:ptr<function, mat4x2<bool>, read_write> = var
+    %f16_valid:ptr<function, mat2x3<f16>, read_write> = var
+    %f32_valid:ptr<function, mat4x4<f32>, read_write> = var
+    %void_invalid:ptr<function, mat3x3<void>, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Type_StorageTextureDimension) {
+    auto* valid =
+        b.Var("valid", AddressSpace::kStorage,
+              ty.storage_texture(core::type::TextureDimension::k2d, core::TexelFormat::kRgba32Float,
+                                 core::Access::kReadWrite),
+              read_write);
+    valid->SetBindingPoint(0, 0);
+    mod.root_block->Append(valid);
+
+    auto* cube =
+        b.Var("cube_invalid", AddressSpace::kStorage,
+              ty.storage_texture(core::type::TextureDimension::kCube,
+                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
+              read_write);
+    cube->SetBindingPoint(1, 1);
+    mod.root_block->Append(cube);
+
+    auto* cube_array =
+        b.Var("cube_array_invalid", AddressSpace::kStorage,
+              ty.storage_texture(core::type::TextureDimension::kCubeArray,
+                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
+              read_write);
+    cube_array->SetBindingPoint(2, 2);
+    mod.root_block->Append(cube_array);
+
+    auto* none =
+        b.Var("none_invalid", AddressSpace::kStorage,
+              ty.storage_texture(core::type::TextureDimension::kNone,
+                                 core::TexelFormat::kRgba32Float, core::Access::kReadWrite),
+              read_write);
+    none->SetBindingPoint(3, 3);
+    mod.root_block->Append(none);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:3 error: var: dimension 'cube' for storage textures does not in WGSL yet
+  %cube_invalid:ptr<storage, texture_storage_cube<rgba32float, read_write>, read_write> = var @binding_point(1, 1)
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+:4:3 error: var: dimension 'cube_array' for storage textures does not in WGSL yet
+  %cube_array_invalid:ptr<storage, texture_storage_cube_array<rgba32float, read_write>, read_write> = var @binding_point(2, 2)
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+:5:3 error: var: invalid texture dimension 'none'
+  %none_invalid:ptr<storage, texture_storage_none<rgba32float, read_write>, read_write> = var @binding_point(3, 3)
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %valid:ptr<storage, texture_storage_2d<rgba32float, read_write>, read_write> = var @binding_point(0, 0)
+  %cube_invalid:ptr<storage, texture_storage_cube<rgba32float, read_write>, read_write> = var @binding_point(1, 1)
+  %cube_array_invalid:ptr<storage, texture_storage_cube_array<rgba32float, read_write>, read_write> = var @binding_point(2, 2)
+  %none_invalid:ptr<storage, texture_storage_none<rgba32float, read_write>, read_write> = var @binding_point(3, 3)
+}
+
+)");
+}
+
+namespace {
+template <typename T>
+static const core::type::Type* TypeBuilder(core::type::Manager& m) {
+    return m.Get<T>();
+}
+
+template <typename T>
+static const core::type::Type* RefTypeBuilder(core::type::Manager& m) {
+    return m.ref<AddressSpace::kFunction, T>();
+}
+
+using TypeBuilderFn = decltype(&TypeBuilder<i32>);
+}  // namespace
+
+using IR_ValidatorRefTypeTest = IRTestParamHelper<std::tuple</* holds_ref */ bool,
+                                                             /* refs_allowed */ bool,
+                                                             /* type_builder */ TypeBuilderFn>>;
+
+TEST_P(IR_ValidatorRefTypeTest, Var) {
+    bool holds_ref = std::get<0>(GetParam());
+    bool refs_allowed = std::get<1>(GetParam());
+    auto* type = std::get<2>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        if (auto* view = type->As<core::type::MemoryView>()) {
+            b.Var(view);
+        } else {
+            b.Var(ty.ptr<function>(type));
+        }
+
+        b.Return(fn);
+    });
+
+    Capabilities caps;
+    if (refs_allowed) {
+        caps.Add(Capability::kAllowRefTypes);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (!holds_ref || refs_allowed) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("3:5 error: var: reference types are not permitted"));
+    }
+}
+
+TEST_P(IR_ValidatorRefTypeTest, FnParam) {
+    bool holds_ref = std::get<0>(GetParam());
+    bool refs_allowed = std::get<1>(GetParam());
+    auto* type = std::get<2>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    fn->SetParams(Vector{b.FunctionParam(type)});
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    Capabilities caps;
+    if (refs_allowed) {
+        caps.Add(Capability::kAllowRefTypes);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (!holds_ref) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("reference types are not permitted"));
+    }
+}
+
+TEST_P(IR_ValidatorRefTypeTest, FnRet) {
+    bool holds_ref = std::get<0>(GetParam());
+    bool refs_allowed = std::get<1>(GetParam());
+    auto* type = std::get<2>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", type);
+    b.Append(fn->Block(), [&] { b.Unreachable(); });
+
+    Capabilities caps;
+    if (refs_allowed) {
+        caps.Add(Capability::kAllowRefTypes);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (!holds_ref) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("reference types are not permitted"));
+    }
+}
+
+TEST_P(IR_ValidatorRefTypeTest, BlockParam) {
+    bool holds_ref = std::get<0>(GetParam());
+    bool refs_allowed = std::get<1>(GetParam());
+    auto* type = std::get<2>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams({b.BlockParam(type)});
+        b.Append(loop->Body(), [&] {  //
+            b.Continue(loop, nullptr);
+        });
+        b.Append(loop->Continuing(), [&] {  //
+            b.NextIteration(loop);
+        });
+        b.Unreachable();
+    });
+
+    Capabilities caps;
+    if (refs_allowed) {
+        caps.Add(Capability::kAllowRefTypes);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (!holds_ref) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("reference types are not permitted"));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(NonRefTypes,
+                         IR_ValidatorRefTypeTest,
+                         testing::Combine(/* holds_ref */ testing::Values(false),
+                                          /* refs_allowed */ testing::Values(false, true),
+                                          /* type_builder */
+                                          testing::Values(TypeBuilder<i32>,
+                                                          TypeBuilder<bool>,
+                                                          TypeBuilder<vec4<f32>>,
+                                                          TypeBuilder<array<f32, 3>>)));
+
+INSTANTIATE_TEST_SUITE_P(RefTypes,
+                         IR_ValidatorRefTypeTest,
+                         testing::Combine(/* holds_ref */ testing::Values(true),
+                                          /* refs_allowed */ testing::Values(false, true),
+                                          /* type_builder */
+                                          testing::Values(RefTypeBuilder<i32>,
+                                                          RefTypeBuilder<bool>,
+                                                          RefTypeBuilder<vec4<f32>>)));
+
+TEST_F(IR_ValidatorTest, PointerToPointer) {
+    auto* type = ty.ptr<function, ptr<function, i32>>();
+    auto* fn = b.Function("my_func", ty.void_());
+    fn->SetParams(Vector{b.FunctionParam(type)});
+    b.Append(fn->Block(), [&] {  //
+        b.Return(fn);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason.Str(),
+                testing::HasSubstr("nested pointer types are not permitted"));
+}
+
+TEST_F(IR_ValidatorTest, PointerToVoid) {
+    auto* type = ty.ptr(AddressSpace::kFunction, ty.void_());
+    auto* fn = b.Function("my_func", ty.void_());
+    fn->SetParams(Vector{b.FunctionParam(type)});
+    b.Append(fn->Block(), [&] {  //
+        b.Return(fn);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason.Str(),
+                testing::HasSubstr("pointers to void are not permitted"));
+}
+
+TEST_F(IR_ValidatorTest, ReferenceToReference) {
+    auto* type = ty.ref<function>(ty.ref<function, i32>());
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {  //
+        b.Var(type);
+        b.Return(fn);
+    });
+
+    Capabilities caps;
+    caps.Add(Capability::kAllowRefTypes);
+
+    auto res = ir::Validate(mod, caps);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason.Str(),
+                testing::HasSubstr("nested reference types are not permitted"));
+}
+
+TEST_F(IR_ValidatorTest, ReferenceToVoid) {
+    auto* type = ty.ref(AddressSpace::kFunction, ty.void_());
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {  //
+        b.Var(type);
+        b.Return(fn);
+    });
+
+    Capabilities caps;
+    caps.Add(Capability::kAllowRefTypes);
+
+    auto res = ir::Validate(mod, caps);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason.Str(),
+                testing::HasSubstr("references to void are not permitted"));
+}
+
+TEST_F(IR_ValidatorTest, PointerInStructure_WithoutCapability) {
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("S"), {
+                                            {mod.symbols.New("a"), ty.ptr<private_, i32>()},
+                                        });
+    mod.root_block->Append(b.Var("my_struct", private_, str_ty));
+
+    auto* fn = b.Function("F", ty.void_());
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason.Str(),
+                testing::HasSubstr("nested pointer types are not permitted"));
+}
+
+TEST_F(IR_ValidatorTest, PointerInStructure_WithCapability) {
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("S"), {
+                                            {mod.symbols.New("a"), ty.ptr<private_, i32>()},
+                                        });
+
+    auto* fn = b.Function("F", ty.void_());
+    auto* param = b.FunctionParam("param", str_ty);
+    fn->SetParams({param});
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPointersAndHandlesInStructures});
+    EXPECT_EQ(res, Success) << res.Failure();
+}
+
+using IR_Validator8BitIntTypeTest = IRTestParamHelper<std::tuple<
+    /* int8_allowed */ bool,
+    /* type_builder */ TypeBuilderFn>>;
+
+TEST_P(IR_Validator8BitIntTypeTest, Var) {
+    bool int8_allowed = std::get<0>(GetParam());
+    auto* type = std::get<1>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Var(ty.ptr<function>(type));
+        b.Return(fn);
+    });
+
+    Capabilities caps;
+    if (int8_allowed) {
+        caps.Add(Capability::kAllow8BitIntegers);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (int8_allowed) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("3:5 error: var: 8-bit integer types are not permitted"));
+    }
+}
+
+TEST_P(IR_Validator8BitIntTypeTest, FnParam) {
+    bool int8_allowed = std::get<0>(GetParam());
+    auto* type = std::get<1>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    fn->SetParams(Vector{b.FunctionParam(type)});
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    Capabilities caps;
+    if (int8_allowed) {
+        caps.Add(Capability::kAllow8BitIntegers);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (int8_allowed) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("8-bit integer types are not permitted"));
+    }
+}
+
+TEST_P(IR_Validator8BitIntTypeTest, FnRet) {
+    bool int8_allowed = std::get<0>(GetParam());
+    auto* type = std::get<1>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", type);
+    b.Append(fn->Block(), [&] { b.Unreachable(); });
+
+    Capabilities caps;
+    if (int8_allowed) {
+        caps.Add(Capability::kAllow8BitIntegers);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (int8_allowed) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("8-bit integer types are not permitted"));
+    }
+}
+
+TEST_P(IR_Validator8BitIntTypeTest, BlockParam) {
+    bool int8_allowed = std::get<0>(GetParam());
+    auto* type = std::get<1>(GetParam())(ty);
+
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams({b.BlockParam(type)});
+        b.Append(loop->Body(), [&] {  //
+            b.Continue(loop, nullptr);
+        });
+        b.Append(loop->Continuing(), [&] {  //
+            b.NextIteration(loop);
+        });
+        b.Unreachable();
+    });
+
+    Capabilities caps;
+    if (int8_allowed) {
+        caps.Add(Capability::kAllow8BitIntegers);
+    }
+    auto res = ir::Validate(mod, caps);
+    if (int8_allowed) {
+        ASSERT_EQ(res, Success) << res.Failure();
+    } else {
+        ASSERT_NE(res, Success);
+        EXPECT_THAT(res.Failure().reason.Str(),
+                    testing::HasSubstr("8-bit integer types are not permitted"));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(Int8Types,
+                         IR_Validator8BitIntTypeTest,
+                         testing::Combine(
+                             /* int8_allowed */ testing::Values(false, true),
+                             /* type_builder */
+                             testing::Values(TypeBuilder<i8>,
+                                             TypeBuilder<u8>,
+                                             TypeBuilder<vec4<i8>>,
+                                             TypeBuilder<array<u8, 4>>)));
+
+TEST_F(IR_ValidatorTest, Int8Type_InstructionOperand_NotAllowed) {
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Convert(ty.i32(), u8(1));
+        b.Return(fn);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:22 error: convert: 8-bit integer types are not permitted
+    %2:i32 = convert 1u8
+                     ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32 = convert 1u8
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Int8Type_InstructionOperand_Allowed) {
+    auto* fn = b.Function("my_func", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Convert(ty.i32(), u8(1));
+        b.Return(fn);
+    });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllow8BitIntegers});
+    ASSERT_EQ(res, Success) << res.Failure();
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator_value_test.cc b/src/tint/lang/core/ir/validator_value_test.cc
new file mode 100644
index 0000000..556464a
--- /dev/null
+++ b/src/tint/lang/core/ir/validator_value_test.cc
@@ -0,0 +1,982 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/validator_test.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/number.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::core::ir {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+TEST_F(IR_ValidatorTest, Var_RootBlock_NullResult) {
+    auto* v = mod.CreateInstruction<ir::Var>(nullptr);
+    v->SetInitializer(b.Constant(0_i));
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:2:3 error: var: result is undefined
+  undef = var, 0i
+  ^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  undef = var, 0i
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_VoidType) {
+    mod.root_block->Append(b.Var(ty.ptr(private_, ty.void_())));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:2:3 error: var: pointers to void are not permitted
+  %1:ptr<private, void, read_write> = var
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<private, void, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Function_NullResult) {
+    auto* v = mod.CreateInstruction<ir::Var>(nullptr);
+    v->SetInitializer(b.Constant(0_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: var: result is undefined
+    undef = var, 0i
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    undef = var, 0i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Function_NoResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* v = b.Var<function, f32>();
+        v->SetInitializer(b.Constant(1_i));
+        v->ClearResults();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:13 error: var: expected exactly 1 results, got 0
+    undef = var, 1i
+            ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    undef = var, 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Function_NonPtrResult) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* v = b.Var<function, f32>();
+        v->Result(0)->SetType(ty.f32());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: var: result type must be a pointer or a reference
+    %2:f32 = var
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:f32 = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Function_UnexpectedInputAttachmentIndex) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* v = b.Var<function, f32>();
+        v->SetInputAttachmentIndex(0);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:41 error: var: '@input_attachment_index' is not valid for non-handle var
+    %2:ptr<function, f32, read_write> = var @input_attachment_index(0)
+                                        ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, f32, read_write> = var @input_attachment_index(0)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Function_OutsideFunctionScope) {
+    auto* v = b.Var<function, f32>();
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:39 error: var: vars in the 'function' address space must be in a function scope
+  %1:ptr<function, f32, read_write> = var
+                                      ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<function, f32, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_NonFunction_InsideFunctionScope) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Var<private_, f32>();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:40 error: var: vars in a function scope must be in the 'function' address space
+    %2:ptr<private, f32, read_write> = var
+                                       ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<private, f32, read_write> = var
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Private_InsideFunctionScopeWithCapability) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        b.Var<private_, f32>();
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod, Capabilities{Capability::kAllowPrivateVarsInFunctions});
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, Var_Private_UnexpectedInputAttachmentIndex) {
+    auto* v = b.Var<private_, f32>();
+    v->SetInputAttachmentIndex(0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
+  %1:ptr<private, f32, read_write> = var @input_attachment_index(0)
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<private, f32, read_write> = var @input_attachment_index(0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_PushConstant_UnexpectedInputAttachmentIndex) {
+    auto* v = b.Var<push_constant, f32>();
+    v->SetInputAttachmentIndex(0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
+  %1:ptr<push_constant, f32, read> = var @input_attachment_index(0)
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<push_constant, f32, read> = var @input_attachment_index(0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Storage_UnexpectedInputAttachmentIndex) {
+    auto* v = b.Var<storage, f32>();
+    v->SetBindingPoint(0, 0);
+    v->SetInputAttachmentIndex(0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:38 error: var: '@input_attachment_index' is not valid for non-handle var
+  %1:ptr<storage, f32, read_write> = var @binding_point(0, 0) @input_attachment_index(0)
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<storage, f32, read_write> = var @binding_point(0, 0) @input_attachment_index(0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Uniform_UnexpectedInputAttachmentIndex) {
+    auto* v = b.Var<uniform, f32>();
+    v->SetBindingPoint(0, 0);
+    v->SetInputAttachmentIndex(0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:32 error: var: '@input_attachment_index' is not valid for non-handle var
+  %1:ptr<uniform, f32, read> = var @binding_point(0, 0) @input_attachment_index(0)
+                               ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<uniform, f32, read> = var @binding_point(0, 0) @input_attachment_index(0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Workgroup_UnexpectedInputAttachmentIndex) {
+    auto* v = b.Var<workgroup, f32>();
+    v->SetInputAttachmentIndex(0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:40 error: var: '@input_attachment_index' is not valid for non-handle var
+  %1:ptr<workgroup, f32, read_write> = var @input_attachment_index(0)
+                                       ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<workgroup, f32, read_write> = var @input_attachment_index(0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Init_WrongType) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* v = b.Var<function, f32>();
+        v->SetInitializer(b.Constant(1_i));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:41 error: var: initializer type 'i32' does not match store type 'f32'
+    %2:ptr<function, f32, read_write> = var, 1i
+                                        ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:ptr<function, f32, read_write> = var, 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Init_NullType) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* i = b.Var<function, f32>("i");
+        i->SetInitializer(b.Constant(0_f));
+        auto* load = b.Load(i);
+        auto* load_ret = load->Result(0);
+        auto* j = b.Var<function, f32>("j");
+        j->SetInitializer(load_ret);
+        load_ret->SetType(nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:5 error: load: result type is undefined
+    %3:undef = load %i
+    ^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:5:46 error: var: operand type is undefined
+    %j:ptr<function, f32, read_write> = var, %3
+                                             ^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %i:ptr<function, f32, read_write> = var, 0.0f
+    %3:undef = load %i
+    %j:ptr<function, f32, read_write> = var, %3
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Init_FunctionTypeInit) {
+    auto* invalid = b.Function("invalid_init", ty.void_());
+    b.Append(invalid->Block(), [&] { b.Return(invalid); });
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* i = b.Var<function, f32>("i");
+        i->SetInitializer(invalid);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:41 error: var: initializer type '<function>' does not match store type 'f32'
+    %i:ptr<function, f32, read_write> = var, %invalid_init
+                                        ^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%invalid_init = func():void {
+  $B1: {
+    ret
+  }
+}
+%my_func = func():void {
+  $B2: {
+    %i:ptr<function, f32, read_write> = var, %invalid_init
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Init_InvalidAddressSpace) {
+    auto* p = b.Var<private_, f32>("p");
+    p->SetInitializer(b.Constant(1_f));
+    mod.root_block->Append(p);
+    auto* s = b.Var<storage, f32>("s");
+    s->SetInitializer(b.Constant(1_f));
+    mod.root_block->Append(s);
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* v = b.Var<function, f32>("v");
+        v->SetInitializer(b.Constant(1_f));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:3:38 error: var: only variables in the function or private address space may be initialized
+  %s:ptr<storage, f32, read_write> = var, 1.0f
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %p:ptr<private, f32, read_write> = var, 1.0f
+  %s:ptr<storage, f32, read_write> = var, 1.0f
+}
+
+%my_func = func():void {
+  $B2: {
+    %v:ptr<function, f32, read_write> = var, 1.0f
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_HandleMissingBindingPoint) {
+    auto* v = b.Var(ty.ptr<handle, i32>());
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:31 error: var: a resource variable is missing binding point
+  %1:ptr<handle, i32, read> = var
+                              ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<handle, i32, read> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_StorageMissingBindingPoint) {
+    auto* v = b.Var(ty.ptr<storage, i32>());
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:38 error: var: a resource variable is missing binding point
+  %1:ptr<storage, i32, read_write> = var
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<storage, i32, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_UniformMissingBindingPoint) {
+    auto* v = b.Var(ty.ptr<uniform, i32>());
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:32 error: var: a resource variable is missing binding point
+  %1:ptr<uniform, i32, read> = var
+                               ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<uniform, i32, read> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_NonResourceWithBindingPoint) {
+    auto* v = b.Var(ty.ptr<private_, i32>());
+    v->SetBindingPoint(0, 0);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:2:38 error: var: a non-resource variable has binding point
+  %1:ptr<private, i32, read_write> = var @binding_point(0, 0)
+                                     ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<private, i32, read_write> = var @binding_point(0, 0)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_MultipleIOAnnotations) {
+    auto* v = b.Var<AddressSpace::kIn, vec4<f32>>();
+    IOAttributes attr;
+    attr.builtin = BuiltinValue::kPosition;
+    attr.location = 0;
+    v->SetAttributes(attr);
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:2:35 error: var: module scope variable has more than one IO annotation, [ @location, built-in ]
+  %1:ptr<__in, vec4<f32>, read> = var @location(0) @builtin(position)
+                                  ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<__in, vec4<f32>, read> = var @location(0) @builtin(position)
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Struct_MultipleIOAnnotations) {
+    IOAttributes attr;
+    attr.builtin = BuiltinValue::kPosition;
+    attr.color = 0;
+
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("a"), ty.f32(), attr},
+                                               });
+    auto* v = b.Var(ty.ptr(AddressSpace::kOut, str_ty, read_write));
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:6:41 error: var: module scope variable struct member has more than one IO annotation, [ built-in, @color ]
+  %1:ptr<__out, MyStruct, read_write> = var
+                                        ^^^
+
+:5:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:f32 @offset(0), @color(0), @builtin(position)
+}
+
+$B1: {  # root
+  %1:ptr<__out, MyStruct, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_MissingIOAnnotations) {
+    auto* v = b.Var<AddressSpace::kIn, vec4<f32>>();
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:2:35 error: var: module scope variable must have at least one IO annotation, e.g. a binding point, a location, etc
+  %1:ptr<__in, vec4<f32>, read> = var
+                                  ^^^
+
+:1:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+$B1: {  # root
+  %1:ptr<__in, vec4<f32>, read> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Var_Struct_MissingIOAnnotations) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.f32(), {}},
+                                                          });
+    auto* v = b.Var(ty.ptr(AddressSpace::kOut, str_ty, read_write));
+    mod.root_block->Append(v);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:6:41 error: var: module scope variable struct members must have at least one IO annotation, e.g. a binding point, a location, etc
+  %1:ptr<__out, MyStruct, read_write> = var
+                                        ^^^
+
+:5:1 note: in block
+$B1: {  # root
+^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:f32 @offset(0)
+}
+
+$B1: {  # root
+  %1:ptr<__out, MyStruct, read_write> = var
+}
+
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_NullResult) {
+    auto* v = mod.CreateInstruction<ir::Let>(nullptr, b.Constant(1_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:5 error: let: result is undefined
+    undef = let 1i
+    ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    undef = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_EmptyResults) {
+    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Constant(1_i));
+    v->ClearResults();
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:13 error: let: expected exactly 1 results, got 0
+    undef = let 1i
+            ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    undef = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_NullValue) {
+    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.f32()), nullptr);
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(), R"(:3:18 error: let: operand is undefined
+    %2:f32 = let undef
+                 ^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:f32 = let undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_EmptyValue) {
+    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Constant(1_i));
+    v->ClearOperands();
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: let: expected exactly 1 operands, got 0
+    %2:i32 = let
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32 = let
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_WrongType) {
+    auto* v = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.f32()), b.Constant(1_i));
+
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    sb.Append(v);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:14 error: let: result type 'f32' does not match value type 'i32'
+    %2:f32 = let 1i
+             ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:f32 = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_VoidResult) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* l = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.void_()), b.Constant(1_i));
+        b.Append(l);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:3:15 error: let: result type cannot be void
+    %2:void = let 1i
+              ^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:void = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Let_VoidValue) {
+    auto* v = b.Function("void_func", ty.void_());
+    b.Append(v->Block(), [&] { b.Return(v); });
+
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* l = mod.CreateInstruction<ir::Let>(b.InstructionResult(ty.i32()), b.Value(b.Call(v)));
+        b.Append(l);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:9:14 error: let: value type cannot be void
+    %4:i32 = let %3
+             ^^^
+
+:7:3 note: in block
+  $B2: {
+  ^^^
+
+note: # Disassembly
+%void_func = func():void {
+  $B1: {
+    ret
+  }
+}
+%my_func = func():void {
+  $B2: {
+    %3:void = call %void_func
+    %4:i32 = let %3
+    ret
+  }
+}
+)");
+}
+
+}  // namespace tint::core::ir