[ir][spirv-writer] Rework unit testing

Add a `Generate()` method to the base test helper that runs the SPIR-V
generator and validates the result with spirv-val. The full
disassembled module is then stored to `output_`.

Rewrite the `access` tests to use this method, instead of manually
invoking the PIMPL methods. Attach friendly names to variables and
results to allow the test to only check for a specific instruction.

Bug: tint:1906
Change-Id: I679ecbff46598d50777f5663d36c6c1235a3f5ff
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/139480
Reviewed-by: dan sinclair <dsinclair@google.com>
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index c4f7557..1fd5db9 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -636,6 +636,13 @@
                 TINT_ICE(Writer, diagnostics_)
                     << "unimplemented instruction: " << inst->TypeInfo().name;
             });
+
+        // Set the name for the SPIR-V result ID if provided in the module.
+        if (inst->Result() && !inst->Is<ir::Var>()) {
+            if (auto name = ir_->NameOf(inst)) {
+                module_.PushDebug(spv::Op::OpName, {Value(inst), Operand(name.Name())});
+            }
+        }
     }
 
     if (block->IsEmpty()) {
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
index a6ed2f1..5b0d3f6 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "gmock/gmock.h"
 #include "src/tint/writer/spirv/ir/test_helper_ir.h"
 
 namespace tint::writer::spirv {
@@ -20,454 +21,229 @@
 using namespace tint::builtin::fluent_types;  // NOLINT
 using namespace tint::number_suffixes;        // NOLINT
 
-using SpvGeneratorImplTest_Access = SpvGeneratorImplTest;
-
-TEST_F(SpvGeneratorImplTest_Access, Array_Value_ConstantIndex) {
-    auto* arr_val = b.FunctionParam(ty.array(ty.i32(), 4));
+TEST_F(SpvGeneratorImplTest, Access_Array_Value_ConstantIndex) {
+    auto* arr_val = b.FunctionParam("arr", ty.array(ty.i32(), 4));
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({arr_val});
-
     b.With(func->Block(), [&] {
-        b.Access(ty.i32(), arr_val, 1_u);
+        auto* result = b.Access(ty.i32(), arr_val, 1_u);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpDecorate %3 ArrayStride 4
-%2 = OpTypeVoid
-%4 = OpTypeInt 32 1
-%6 = OpTypeInt 32 0
-%5 = OpConstant %6 4
-%3 = OpTypeArray %4 %5
-%8 = OpTypeFunction %2 %3
-%1 = OpFunction %2 None %8
-%7 = OpFunctionParameter %3
-%9 = OpLabel
-%10 = OpCompositeExtract %4 %7 1
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result = OpCompositeExtract %int %arr 1)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Array_Pointer_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Array_Pointer_ConstantIndex) {
     auto* func = b.Function("foo", ty.void_());
-
     b.With(func->Block(), [&] {
-        auto* arr_var = b.Var(ty.ptr<function, array<i32, 4>>());
-        b.Access(ty.ptr<function, i32>(), arr_var, 1_u);
+        auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
+        auto* result = b.Access(ty.ptr<function, i32>(), arr_var, 1_u);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpDecorate %7 ArrayStride 4
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%8 = OpTypeInt 32 1
-%10 = OpTypeInt 32 0
-%9 = OpConstant %10 4
-%7 = OpTypeArray %8 %9
-%6 = OpTypePointer Function %7
-%12 = OpTypePointer Function %8
-%13 = OpConstant %10 1
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%11 = OpAccessChain %12 %5 %13
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result = OpAccessChain %_ptr_Function_int %arr %uint_1)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Array_Pointer_DynamicIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Array_Pointer_DynamicIndex) {
+    auto* idx = b.FunctionParam("idx", ty.i32());
     auto* func = b.Function("foo", ty.void_());
-
+    func->SetParams({idx});
     b.With(func->Block(), [&] {
-        auto* idx_var = b.Var(ty.ptr<function, i32>());
-        auto* idx = b.Load(idx_var);
-        auto* arr_var = b.Var(ty.ptr<function, array<i32, 4>>());
-        b.Access(ty.ptr<function, i32>(), arr_var, idx);
+        auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
+        auto* result = b.Access(ty.ptr<function, i32>(), arr_var, idx);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpDecorate %11 ArrayStride 4
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%7 = OpTypeInt 32 1
-%6 = OpTypePointer Function %7
-%13 = OpTypeInt 32 0
-%12 = OpConstant %13 4
-%11 = OpTypeArray %7 %12
-%10 = OpTypePointer Function %11
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%9 = OpVariable %10 Function
-%8 = OpLoad %7 %5
-%14 = OpAccessChain %6 %9 %8
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result = OpAccessChain %_ptr_Function_int %arr %idx)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Matrix_Value_ConstantIndex) {
-    auto* mat_val = b.FunctionParam(ty.mat2x2(ty.f32()));
+TEST_F(SpvGeneratorImplTest, Access_Matrix_Value_ConstantIndex) {
+    auto* mat_val = b.FunctionParam("mat", ty.mat2x2(ty.f32()));
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({mat_val});
-
     b.With(func->Block(), [&] {
-        b.Access(ty.vec2(ty.f32()), mat_val, 1_u);
-        b.Access(ty.f32(), mat_val, 1_u, 0_u);
+        auto* result_vector = b.Access(ty.vec2(ty.f32()), mat_val, 1_u);
+        auto* result_scalar = b.Access(ty.f32(), mat_val, 1_u, 0_u);
         b.Return(func);
+        mod.SetName(result_vector, "result_vector");
+        mod.SetName(result_scalar, "result_scalar");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%5 = OpTypeFloat 32
-%4 = OpTypeVector %5 2
-%3 = OpTypeMatrix %4 2
-%7 = OpTypeFunction %2 %3
-%1 = OpFunction %2 None %7
-%6 = OpFunctionParameter %3
-%8 = OpLabel
-%9 = OpCompositeExtract %4 %6 1
-%10 = OpCompositeExtract %5 %6 1 0
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result_vector = OpCompositeExtract %v2float %mat 1)"));
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result_scalar = OpCompositeExtract %float %mat 1 0)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Matrix_Pointer_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Matrix_Pointer_ConstantIndex) {
     auto* func = b.Function("foo", ty.void_());
-
     b.With(func->Block(), [&] {
-        auto* mat_var = b.Var(ty.ptr<function, mat2x2<f32>>());
-        b.Access(ty.ptr<function, vec2<f32>>(), mat_var, 1_u);
-        b.Access(ty.ptr<function, f32>(), mat_var, 1_u, 0_u);
+        auto* mat_var = b.Var("mat", ty.ptr<function, mat2x2<f32>>());
+        auto* result_vector = b.Access(ty.ptr<function, vec2<f32>>(), mat_var, 1_u);
+        auto* result_scalar = b.Access(ty.ptr<function, f32>(), mat_var, 1_u, 0_u);
         b.Return(func);
+        mod.SetName(result_vector, "result_vector");
+        mod.SetName(result_scalar, "result_scalar");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%9 = OpTypeFloat 32
-%8 = OpTypeVector %9 2
-%7 = OpTypeMatrix %8 2
-%6 = OpTypePointer Function %7
-%11 = OpTypePointer Function %8
-%13 = OpTypeInt 32 0
-%12 = OpConstant %13 1
-%15 = OpTypePointer Function %9
-%16 = OpConstant %13 0
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%10 = OpAccessChain %11 %5 %12
-%14 = OpAccessChain %15 %5 %12 %16
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(
+                    R"(%result_vector = OpAccessChain %_ptr_Function_v2float %mat %uint_1)"));
+    EXPECT_THAT(output_,
+                testing::HasSubstr(
+                    R"(%result_scalar = OpAccessChain %_ptr_Function_float %mat %uint_1 %uint_0)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Matrix_Pointer_DynamicIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Matrix_Pointer_DynamicIndex) {
+    auto* idx = b.FunctionParam("idx", ty.i32());
     auto* func = b.Function("foo", ty.void_());
-
+    func->SetParams({idx});
     b.With(func->Block(), [&] {
-        auto* idx_var = b.Var(ty.ptr<function, i32>());
-        auto* idx = b.Load(idx_var);
-        auto* mat_var = b.Var(ty.ptr<function, mat2x2<f32>>());
-        b.Access(ty.ptr<function, vec2<f32>>(), mat_var, idx);
-        b.Access(ty.ptr<function, f32>(), mat_var, idx, idx);
+        auto* mat_var = b.Var("mat", ty.ptr<function, mat2x2<f32>>());
+        auto* result_vector = b.Access(ty.ptr<function, vec2<f32>>(), mat_var, idx);
+        auto* result_scalar = b.Access(ty.ptr<function, f32>(), mat_var, idx, idx);
         b.Return(func);
+        mod.SetName(result_vector, "result_vector");
+        mod.SetName(result_scalar, "result_scalar");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%7 = OpTypeInt 32 1
-%6 = OpTypePointer Function %7
-%13 = OpTypeFloat 32
-%12 = OpTypeVector %13 2
-%11 = OpTypeMatrix %12 2
-%10 = OpTypePointer Function %11
-%15 = OpTypePointer Function %12
-%17 = OpTypePointer Function %13
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%9 = OpVariable %10 Function
-%8 = OpLoad %7 %5
-%14 = OpAccessChain %15 %9 %8
-%16 = OpAccessChain %17 %9 %8 %8
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(
+                             R"(%result_vector = OpAccessChain %_ptr_Function_v2float %mat %idx)"));
+    EXPECT_THAT(output_,
+                testing::HasSubstr(
+                    R"(%result_scalar = OpAccessChain %_ptr_Function_float %mat %idx %idx)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Vector_Value_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Vector_Value_ConstantIndex) {
+    auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
     auto* func = b.Function("foo", ty.void_());
-    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
     func->SetParams({vec_val});
-
     b.With(func->Block(), [&] {
-        b.Access(ty.i32(), vec_val, 1_u);
+        auto* result = b.Access(ty.i32(), vec_val, 1_u);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%4 = OpTypeInt 32 1
-%3 = OpTypeVector %4 4
-%6 = OpTypeFunction %2 %3
-%1 = OpFunction %2 None %6
-%5 = OpFunctionParameter %3
-%7 = OpLabel
-%8 = OpCompositeExtract %4 %5 1
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result = OpCompositeExtract %int %vec 1)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Vector_Value_DynamicIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Vector_Value_DynamicIndex) {
+    auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
+    auto* idx = b.FunctionParam("idx", ty.i32());
     auto* func = b.Function("foo", ty.void_());
-    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
-    func->SetParams({vec_val});
-
+    func->SetParams({vec_val, idx});
     b.With(func->Block(), [&] {
-        auto* idx_var = b.Var(ty.ptr<function, i32>());
-        auto* idx = b.Load(idx_var);
-        b.Access(ty.i32(), vec_val, idx);
+        auto* result = b.Access(ty.i32(), vec_val, idx);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%4 = OpTypeInt 32 1
-%3 = OpTypeVector %4 4
-%6 = OpTypeFunction %2 %3
-%9 = OpTypePointer Function %4
-%1 = OpFunction %2 None %6
-%5 = OpFunctionParameter %3
-%7 = OpLabel
-%8 = OpVariable %9 Function
-%10 = OpLoad %4 %8
-%11 = OpVectorExtractDynamic %4 %5 %10
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result = OpVectorExtractDynamic %int %vec %idx)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Vector_Pointer_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Vector_Pointer_ConstantIndex) {
     auto* func = b.Function("foo", ty.void_());
-
     b.With(func->Block(), [&] {
-        auto* vec_var = b.Var(ty.ptr<function, vec4<i32>>());
-        b.Access(ty.ptr<function, i32>(), vec_var, 1_u);
+        auto* vec_var = b.Var("vec", ty.ptr<function, vec4<i32>>());
+        auto* result = b.Access(ty.ptr<function, i32>(), vec_var, 1_u);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%8 = OpTypeInt 32 1
-%7 = OpTypeVector %8 4
-%6 = OpTypePointer Function %7
-%10 = OpTypePointer Function %8
-%12 = OpTypeInt 32 0
-%11 = OpConstant %12 1
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%9 = OpAccessChain %10 %5 %11
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result = OpAccessChain %_ptr_Function_int %vec %uint_1)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Vector_Pointer_DynamicIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Vector_Pointer_DynamicIndex) {
+    auto* idx = b.FunctionParam("idx", ty.i32());
     auto* func = b.Function("foo", ty.void_());
-
+    func->SetParams({idx});
     b.With(func->Block(), [&] {
-        auto* idx_var = b.Var(ty.ptr<function, i32>());
-        auto* idx = b.Load(idx_var);
-        auto* vec_var = b.Var(ty.ptr<function, vec4<i32>>());
-        b.Access(ty.ptr<function, i32>(), vec_var, idx);
+        auto* vec_var = b.Var("vec", ty.ptr<function, vec4<i32>>());
+        auto* result = b.Access(ty.ptr<function, i32>(), vec_var, idx);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%7 = OpTypeInt 32 1
-%6 = OpTypePointer Function %7
-%11 = OpTypeVector %7 4
-%10 = OpTypePointer Function %11
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%9 = OpVariable %10 Function
-%8 = OpLoad %7 %5
-%12 = OpAccessChain %6 %9 %8
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_,
+                testing::HasSubstr(R"(%result = OpAccessChain %_ptr_Function_int %vec %idx)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, NestedVector_Value_DynamicIndex) {
-    auto* val = b.FunctionParam(ty.array(ty.array(ty.vec4(ty.i32()), 4), 4));
+TEST_F(SpvGeneratorImplTest, Access_NestedVector_Value_DynamicIndex) {
+    auto* val = b.FunctionParam("arr", ty.array(ty.array(ty.vec4(ty.i32()), 4), 4));
+    auto* idx = b.FunctionParam("idx", ty.i32());
     auto* func = b.Function("foo", ty.void_());
-    func->SetParams({val});
-
+    func->SetParams({val, idx});
     b.With(func->Block(), [&] {
-        auto* idx_var = b.Var(ty.ptr<function, i32>());
-        auto* idx = b.Load(idx_var);
-        b.Access(ty.i32(), val, 1_u, 2_u, idx);
+        auto* result = b.Access(ty.i32(), val, 1_u, 2_u, idx);
         b.Return(func);
+        mod.SetName(result, "result");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpDecorate %4 ArrayStride 16
-OpDecorate %3 ArrayStride 64
-%2 = OpTypeVoid
-%6 = OpTypeInt 32 1
-%5 = OpTypeVector %6 4
-%8 = OpTypeInt 32 0
-%7 = OpConstant %8 4
-%4 = OpTypeArray %5 %7
-%3 = OpTypeArray %4 %7
-%10 = OpTypeFunction %2 %3
-%13 = OpTypePointer Function %6
-%1 = OpFunction %2 None %10
-%9 = OpFunctionParameter %3
-%11 = OpLabel
-%12 = OpVariable %13 Function
-%14 = OpLoad %6 %12
-%16 = OpCompositeExtract %5 %9 1 2
-%15 = OpVectorExtractDynamic %6 %16 %14
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%14 = OpCompositeExtract %v4int %arr 1 2)"));
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result = OpVectorExtractDynamic %int %14 %idx)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Struct_Value_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Struct_Value_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
                                                    {mod.symbols.Register("a"), ty.f32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
-    auto* str_val = b.FunctionParam(str);
+    auto* str_val = b.FunctionParam("str", str);
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({str_val});
-
     b.With(func->Block(), [&] {
-        b.Access(ty.f32(), str_val, 0_u);
-        b.Access(ty.i32(), str_val, 1_u, 2_u);
+        auto* result_a = b.Access(ty.f32(), str_val, 0_u);
+        auto* result_b = b.Access(ty.i32(), str_val, 1_u, 2_u);
         b.Return(func);
+        mod.SetName(result_a, "result_a");
+        mod.SetName(result_b, "result_b");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpMemberName %3 0 "a"
-OpMemberName %3 1 "b"
-OpName %3 "MyStruct"
-OpMemberDecorate %3 0 Offset 0
-OpMemberDecorate %3 1 Offset 16
-%2 = OpTypeVoid
-%4 = OpTypeFloat 32
-%6 = OpTypeInt 32 1
-%5 = OpTypeVector %6 4
-%3 = OpTypeStruct %4 %5
-%8 = OpTypeFunction %2 %3
-%1 = OpFunction %2 None %8
-%7 = OpFunctionParameter %3
-%9 = OpLabel
-%10 = OpCompositeExtract %4 %7 0
-%11 = OpCompositeExtract %6 %7 1 2
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result_a = OpCompositeExtract %float %str 0)"));
+    EXPECT_THAT(output_, testing::HasSubstr(R"(%result_b = OpCompositeExtract %int %str 1 2)"));
 }
 
-TEST_F(SpvGeneratorImplTest_Access, Struct_Pointer_ConstantIndex) {
+TEST_F(SpvGeneratorImplTest, Access_Struct_Pointer_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
                                                    {mod.symbols.Register("a"), ty.f32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
     auto* func = b.Function("foo", ty.void_());
-
     b.With(func->Block(), [&] {
-        auto* str_var = b.Var(ty.ptr(function, str, read_write));
-        b.Access(ty.ptr<function, f32>(), str_var, 0_u);
-        b.Access(ty.ptr<function, i32>(), str_var, 1_u, 2_u);
+        auto* str_var = b.Var("str", ty.ptr(function, str, read_write));
+        auto* result_a = b.Access(ty.ptr<function, f32>(), str_var, 0_u);
+        auto* result_b = b.Access(ty.ptr<function, i32>(), str_var, 1_u, 2_u);
         b.Return(func);
+        mod.SetName(result_a, "result_a");
+        mod.SetName(result_b, "result_b");
     });
 
-    ASSERT_TRUE(IRIsValid()) << Error();
-
-    generator_.EmitFunction(func);
-    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-OpMemberName %7 0 "a"
-OpMemberName %7 1 "b"
-OpName %7 "MyStruct"
-OpMemberDecorate %7 0 Offset 0
-OpMemberDecorate %7 1 Offset 16
-%2 = OpTypeVoid
-%3 = OpTypeFunction %2
-%8 = OpTypeFloat 32
-%10 = OpTypeInt 32 1
-%9 = OpTypeVector %10 4
-%7 = OpTypeStruct %8 %9
-%6 = OpTypePointer Function %7
-%12 = OpTypePointer Function %8
-%14 = OpTypeInt 32 0
-%13 = OpConstant %14 0
-%16 = OpTypePointer Function %10
-%17 = OpConstant %14 1
-%18 = OpConstant %14 2
-%1 = OpFunction %2 None %3
-%4 = OpLabel
-%5 = OpVariable %6 Function
-%11 = OpAccessChain %12 %5 %13
-%15 = OpAccessChain %16 %5 %17 %18
-OpReturn
-OpFunctionEnd
-)");
+    ASSERT_TRUE(Generate()) << Error();
+    EXPECT_THAT(output_, testing::HasSubstr(
+                             R"(%result_a = OpAccessChain %_ptr_Function_float %str %uint_0)"));
+    EXPECT_THAT(
+        output_,
+        testing::HasSubstr(R"(%result_b = OpAccessChain %_ptr_Function_int %str %uint_1 %uint_2)"));
 }
 
 }  // namespace
diff --git a/src/tint/writer/spirv/ir/test_helper_ir.h b/src/tint/writer/spirv/ir/test_helper_ir.h
index 08f8246..debe5d8 100644
--- a/src/tint/writer/spirv/ir/test_helper_ir.h
+++ b/src/tint/writer/spirv/ir/test_helper_ir.h
@@ -16,8 +16,10 @@
 #define SRC_TINT_WRITER_SPIRV_IR_TEST_HELPER_IR_H_
 
 #include <string>
+#include <utility>
 
 #include "gtest/gtest.h"
+#include "spirv-tools/libspirv.hpp"
 #include "src/tint/ir/builder.h"
 #include "src/tint/ir/validate.h"
 #include "src/tint/writer/spirv/ir/generator_impl_ir.h"
@@ -54,6 +56,9 @@
     /// Validation errors
     std::string err_;
 
+    /// SPIR-V output.
+    std::string output_;
+
     /// @returns the error string from the validation
     std::string Error() const { return err_; }
 
@@ -67,6 +72,57 @@
         return true;
     }
 
+    /// Run the generator on the IR module and validate the result.
+    /// @returns true if generation and validation succeeded
+    bool Generate() {
+        if (!generator_.Generate()) {
+            err_ = generator_.Diagnostics().str();
+            return false;
+        }
+        if (!Validate()) {
+            return false;
+        }
+
+        output_ = Disassemble(generator_.Result(), SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+        return true;
+    }
+
+    /// Validate the generated SPIR-V using the SPIR-V Tools Validator.
+    /// @returns true if validation succeeded, false otherwise
+    bool Validate() {
+        auto binary = generator_.Result();
+
+        std::string spv_errors;
+        auto msg_consumer = [&spv_errors](spv_message_level_t level, const char*,
+                                          const spv_position_t& position, const char* message) {
+            switch (level) {
+                case SPV_MSG_FATAL:
+                case SPV_MSG_INTERNAL_ERROR:
+                case SPV_MSG_ERROR:
+                    spv_errors +=
+                        "error: line " + std::to_string(position.index) + ": " + message + "\n";
+                    break;
+                case SPV_MSG_WARNING:
+                    spv_errors +=
+                        "warning: line " + std::to_string(position.index) + ": " + message + "\n";
+                    break;
+                case SPV_MSG_INFO:
+                    spv_errors +=
+                        "info: line " + std::to_string(position.index) + ": " + message + "\n";
+                    break;
+                case SPV_MSG_DEBUG:
+                    break;
+            }
+        };
+
+        spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
+        tools.SetMessageConsumer(msg_consumer);
+
+        auto result = tools.Validate(binary);
+        err_ = std::move(spv_errors);
+        return result;
+    }
+
     /// @returns the disassembled types from the generated module.
     std::string DumpTypes() { return DumpInstructions(generator_.Module().Types()); }
 
diff --git a/src/tint/writer/spirv/spv_dump.cc b/src/tint/writer/spirv/spv_dump.cc
index da24446..6ded6ff 100644
--- a/src/tint/writer/spirv/spv_dump.cc
+++ b/src/tint/writer/spirv/spv_dump.cc
@@ -19,7 +19,7 @@
 
 namespace tint::writer::spirv {
 
-std::string Disassemble(const std::vector<uint32_t>& data) {
+std::string Disassemble(const std::vector<uint32_t>& data, uint32_t options /* = 0u */) {
     std::string spv_errors;
     spv_target_env target_env = SPV_ENV_UNIVERSAL_1_0;
 
@@ -49,7 +49,7 @@
     tools.SetMessageConsumer(msg_consumer);
 
     std::string result;
-    if (!tools.Disassemble(data, &result, SPV_BINARY_TO_TEXT_OPTION_NO_HEADER)) {
+    if (!tools.Disassemble(data, &result, SPV_BINARY_TO_TEXT_OPTION_NO_HEADER | options)) {
         return "*** Invalid SPIR-V ***\n" + spv_errors;
     }
     return result;
diff --git a/src/tint/writer/spirv/spv_dump.h b/src/tint/writer/spirv/spv_dump.h
index 359f0cb..b9030a4 100644
--- a/src/tint/writer/spirv/spv_dump.h
+++ b/src/tint/writer/spirv/spv_dump.h
@@ -24,8 +24,9 @@
 
 /// Disassembles SPIR-V binary data into its textual form.
 /// @param data the SPIR-V binary data
+/// @param options the additional SPIR-V disassembler options to use
 /// @returns the disassembled SPIR-V string
-std::string Disassemble(const std::vector<uint32_t>& data);
+std::string Disassemble(const std::vector<uint32_t>& data, uint32_t options = 0u);
 
 /// Dumps the given builder to a SPIR-V disassembly string
 /// @param builder the builder to convert