[spirv-reader][ir] Support names.

Add support for `OpName` and `OpMemberName` decorations.

Bug: 391482385, 391482466
Change-Id: I5640494ebd30100d0f6141b99c1df1ca7ca9666c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/226157
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index 165aba1..17609ee 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -91,6 +91,7 @@
     "import_test.cc",
     "memory_test.cc",
     "misc_test.cc",
+    "name_test.cc",
     "struct_test.cc",
     "var_test.cc",
   ],
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index c9d1414..9fedf1b 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -100,6 +100,7 @@
   lang/spirv/reader/parser/import_test.cc
   lang/spirv/reader/parser/memory_test.cc
   lang/spirv/reader/parser/misc_test.cc
+  lang/spirv/reader/parser/name_test.cc
   lang/spirv/reader/parser/struct_test.cc
   lang/spirv/reader/parser/var_test.cc
 )
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.gn b/src/tint/lang/spirv/reader/parser/BUILD.gn
index 55175db..937008a 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -99,6 +99,7 @@
         "import_test.cc",
         "memory_test.cc",
         "misc_test.cc",
+        "name_test.cc",
         "struct_test.cc",
         "var_test.cc",
       ]
diff --git a/src/tint/lang/spirv/reader/parser/name_test.cc b/src/tint/lang/spirv/reader/parser/name_test.cc
new file mode 100644
index 0000000..9bc1f4b3
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/name_test.cc
@@ -0,0 +1,314 @@
+// 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/spirv/reader/parser/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+TEST_F(SpirvParserTest, Name_Set) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %1 "my_name"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+    %ep_type = OpTypeFunction %void
+        %one = OpConstant %i32 1
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %1 = OpCopyObject %i32 %one
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %my_name:i32 = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_IgnoreEmptyName) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %1 ""
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+    %ep_type = OpTypeFunction %void
+        %one = OpConstant %i32 1
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %1 = OpCopyObject %i32 %one
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = let 1i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_Duplicate) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %1 "vanilla"
+               OpName %2 "vanilla"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+    %ep_type = OpTypeFunction %void
+        %one = OpConstant %i32 1
+        %two = OpConstant %i32 2
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %1 = OpCopyObject %i32 %one
+          %2 = OpCopyObject %i32 %two
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %vanilla:i32 = let 1i
+    %vanilla_1:i32 = let 2i  # %vanilla_1: 'vanilla'
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_MemberName) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %S "Desert"
+               OpMemberName %S 0 "strawberry"
+               OpMemberName %S 1 "vanilla"
+               OpMemberName %S 2 "chocolate"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+          %S = OpTypeStruct %i32 %i32 %i32
+        %one = OpConstant %i32 1
+          %d = OpConstantComposite %S %one %one %one
+    %ep_type = OpTypeFunction %void
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %2 = OpCopyObject %S %d
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+Desert = struct @align(4) {
+  strawberry:i32 @offset(0)
+  vanilla:i32 @offset(4)
+  chocolate:i32 @offset(8)
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:Desert = let Desert(1i)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_IgnoreEmptyMemberName) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %S "Desert"
+               OpMemberName %S 0 "strawberry"
+               OpMemberName %S 1 ""
+               OpMemberName %S 2 "chocolate"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+          %S = OpTypeStruct %i32 %i32 %i32
+        %one = OpConstant %i32 1
+          %d = OpConstantComposite %S %one %one %one
+    %ep_type = OpTypeFunction %void
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %2 = OpCopyObject %S %d
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+Desert = struct @align(4) {
+  strawberry:i32 @offset(0)
+  tint_symbol:i32 @offset(4)
+  chocolate:i32 @offset(8)
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:Desert = let Desert(1i)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_StructSkipFirstMember) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %S "Desert"
+               OpMemberName %S 1 "strawberry"
+               OpMemberName %S 2 "chocolate"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+          %S = OpTypeStruct %i32 %i32 %i32
+        %one = OpConstant %i32 1
+          %d = OpConstantComposite %S %one %one %one
+    %ep_type = OpTypeFunction %void
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %2 = OpCopyObject %S %d
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+Desert = struct @align(4) {
+  tint_symbol:i32 @offset(0)
+  strawberry:i32 @offset(4)
+  chocolate:i32 @offset(8)
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:Desert = let Desert(1i)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_StructSkipMember) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %S "Desert"
+               OpMemberName %S 0 "strawberry"
+               OpMemberName %S 2 "chocolate"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+          %S = OpTypeStruct %i32 %i32 %i32
+        %one = OpConstant %i32 1
+          %d = OpConstantComposite %S %one %one %one
+    %ep_type = OpTypeFunction %void
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %2 = OpCopyObject %S %d
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+Desert = struct @align(4) {
+  strawberry:i32 @offset(0)
+  tint_symbol:i32 @offset(4)
+  chocolate:i32 @offset(8)
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:Desert = let Desert(1i)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Name_StructSkipLast) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %S "Desert"
+               OpMemberName %S 0 "strawberry"
+               OpMemberName %S 1 "chocolate"
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+          %S = OpTypeStruct %i32 %i32 %i32
+        %one = OpConstant %i32 1
+          %d = OpConstantComposite %S %one %one %one
+    %ep_type = OpTypeFunction %void
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+          %2 = OpCopyObject %S %d
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+Desert = struct @align(4) {
+  strawberry:i32 @offset(0)
+  chocolate:i32 @offset(4)
+  tint_symbol:i32 @offset(8)
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:Desert = let Desert(1i)
+    ret
+  }
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index 3323527..6e81010 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -30,6 +30,7 @@
 #include <algorithm>
 #include <memory>
 #include <string>
+#include <unordered_map>
 #include <unordered_set>
 #include <utility>
 #include <vector>
@@ -112,15 +113,48 @@
             EmitModuleScopeVariables();
         }
 
+        RegisterNames();
+
         EmitFunctions();
         EmitEntryPointAttributes();
 
         // TODO(crbug.com/tint/1907): Handle annotation instructions.
-        // TODO(crbug.com/tint/1907): Handle names.
 
         return std::move(ir_);
     }
 
+    void RegisterNames() {
+        // Register names from OpName
+        for (const auto& inst : spirv_context_->debugs2()) {
+            switch (inst.opcode()) {
+                case spv::Op::OpName: {
+                    const auto name = inst.GetInOperand(1).AsString();
+                    if (!name.empty()) {
+                        id_to_name_[inst.GetSingleWordInOperand(0)] = name;
+                    }
+                    break;
+                }
+                case spv::Op::OpMemberName: {
+                    const auto name = inst.GetInOperand(2).AsString();
+                    if (!name.empty()) {
+                        uint32_t struct_id = inst.GetSingleWordInOperand(0);
+                        uint32_t member_idx = inst.GetSingleWordInOperand(1);
+                        auto iter = struct_to_member_names_.insert({struct_id, {}});
+                        auto& members = (*(iter.first)).second;
+
+                        if (members.size() < (member_idx + 1)) {
+                            members.resize(member_idx + 1);
+                        }
+                        members[member_idx] = name;
+                    }
+                    break;
+                }
+                default:
+                    break;
+            }
+        }
+    }
+
     /// @param sc a SPIR-V storage class
     /// @returns the Tint address space for a SPIR-V storage class
     core::AddressSpace AddressSpace(spv::StorageClass sc) {
@@ -282,6 +316,15 @@
             TINT_ICE() << "empty structures are not supported";
         }
 
+        auto* type_mgr = spirv_context_->get_type_mgr();
+        auto struct_id = type_mgr->GetId(struct_ty);
+
+        std::vector<std::string>* member_names = nullptr;
+        auto struct_to_member_iter = struct_to_member_names_.find(struct_id);
+        if (struct_to_member_iter != struct_to_member_names_.end()) {
+            member_names = &((*struct_to_member_iter).second);
+        }
+
         // Build a list of struct members.
         uint32_t current_size = 0u;
         Vector<core::type::StructMember*, 4> members;
@@ -335,15 +378,28 @@
                 }
             }
 
-            // TODO(crbug.com/tint/1907): Use OpMemberName to name it.
-            members.Push(ty_.Get<core::type::StructMember>(ir_.symbols.New(), member_ty, i, offset,
-                                                           align, member_ty->Size(),
-                                                           std::move(attributes)));
+            Symbol name;
+            if (member_names && member_names->size() > i) {
+                auto n = (*member_names)[i];
+                name = ir_.symbols.New(n);
+            } else {
+                name = ir_.symbols.New();
+            }
+
+            members.Push(ty_.Get<core::type::StructMember>(
+                name, member_ty, i, offset, align, member_ty->Size(), std::move(attributes)));
 
             current_size = offset + member_ty->Size();
         }
-        // TODO(crbug.com/tint/1907): Use OpName to name it.
-        return ty_.Struct(ir_.symbols.New(), std::move(members));
+
+        Symbol name;
+        auto iter = id_to_name_.find(struct_id);
+        if (iter != id_to_name_.end()) {
+            name = ir_.symbols.New(iter->second);
+        } else {
+            name = ir_.symbols.New();
+        }
+        return ty_.Struct(name, std::move(members));
     }
 
     /// @param id a SPIR-V result ID for a function declaration instruction
@@ -437,6 +493,11 @@
         current_block_->Append(inst);
         TINT_ASSERT(inst->Results().Length() == 1u);
         AddValue(result_id, inst->Result(0));
+
+        auto iter = id_to_name_.find(result_id);
+        if (iter != id_to_name_.end()) {
+            ir_.SetName(inst, iter->second);
+        }
     }
 
     /// Emit an instruction to the current block.
@@ -1191,6 +1252,11 @@
     // The set of IDs of imports that are ignored. For example, any "NonSemanticInfo." import is
     // ignored.
     std::unordered_set<uint32_t> ignored_imports_;
+
+    // Map of SPIR-V IDs to string names
+    std::unordered_map<uint32_t, std::string> id_to_name_;
+    // Map of SPIR-V Struct IDs to a list of member string names
+    std::unordered_map<uint32_t, std::vector<std::string>> struct_to_member_names_;
 };
 
 }  // namespace
diff --git a/src/tint/lang/spirv/reader/reader_test.cc b/src/tint/lang/spirv/reader/reader_test.cc
index eb351b2..3b475a3 100644
--- a/src/tint/lang/spirv/reader/reader_test.cc
+++ b/src/tint/lang/spirv/reader/reader_test.cc
@@ -317,15 +317,15 @@
 )");
     ASSERT_EQ(got, Success);
     EXPECT_EQ(got, R"(
-tint_symbol_2 = struct @align(16) {
-  tint_symbol:vec4<f32> @offset(0)
-  tint_symbol_1:array<f32, 1> @offset(16)
+VertexOutputs = struct @align(16) {
+  position:vec4<f32> @offset(0)
+  clipDistance:array<f32, 1> @offset(16)
 }
 
-tint_symbol_6 = struct @align(16) {
-  tint_symbol_3:vec4<f32> @offset(0), @builtin(position)
-  tint_symbol_4:array<f32, 1> @offset(16), @builtin(clip_distances)
-  tint_symbol_5:f32 @offset(20), @builtin(__point_size)
+tint_symbol_3 = struct @align(16) {
+  tint_symbol:vec4<f32> @offset(0), @builtin(position)
+  tint_symbol_1:array<f32, 1> @offset(16), @builtin(clip_distances)
+  tint_symbol_2:f32 @offset(20), @builtin(__point_size)
 }
 
 $B1: {  # root
@@ -334,14 +334,14 @@
   %3:ptr<private, f32, read_write> = var
 }
 
-%4 = func():tint_symbol_2 {
+%4 = func():VertexOutputs {
   $B2: {
-    ret tint_symbol_2(vec4<f32>(0.0f), array<f32, 1>(0.0f))
+    ret VertexOutputs(vec4<f32>(0.0f), array<f32, 1>(0.0f))
   }
 }
 %main_inner = func():void {
   $B3: {
-    %6:tint_symbol_2 = call %4
+    %6:VertexOutputs = call %4
     %7:vec4<f32> = access %6, 0u
     store %1, %7
     %8:array<f32, 1> = access %6, 1u
@@ -350,13 +350,13 @@
     ret
   }
 }
-%main = @vertex func():tint_symbol_6 {
+%main = @vertex func():tint_symbol_3 {
   $B4: {
     %10:void = call %main_inner
     %11:vec4<f32> = load %1
     %12:array<f32, 1> = load %2
     %13:f32 = load %3
-    %14:tint_symbol_6 = construct %11, %12, %13
+    %14:tint_symbol_3 = construct %11, %12, %13
     ret %14
   }
 }
@@ -568,14 +568,14 @@
 
     ASSERT_EQ(got, Success);
     EXPECT_EQ(got, R"(
-tint_symbol_2 = struct @align(16) {
-  tint_symbol:vec4<f32> @offset(0)
-  tint_symbol_1:vec4<f32> @offset(16)
+FragOutput = struct @align(16) {
+  color:vec4<f32> @offset(0)
+  blend:vec4<f32> @offset(16)
 }
 
-tint_symbol_5 = struct @align(16) {
-  tint_symbol_3:vec4<f32> @offset(0), @location(0), @blend_src(0)
-  tint_symbol_4:vec4<f32> @offset(16), @location(0), @blend_src(1)
+tint_symbol_2 = struct @align(16) {
+  tint_symbol:vec4<f32> @offset(0), @location(0), @blend_src(0)
+  tint_symbol_1:vec4<f32> @offset(16), @location(0), @blend_src(1)
 }
 
 $B1: {  # root
@@ -583,20 +583,20 @@
   %2:ptr<private, vec4<f32>, read_write> = var
 }
 
-%3 = func():tint_symbol_2 {
+%3 = func():FragOutput {
   $B2: {
-    %4:ptr<function, tint_symbol_2, read_write> = var, tint_symbol_2(vec4<f32>(0.0f))
-    %5:ptr<function, vec4<f32>, read_write> = access %4, 0u
+    %output:ptr<function, FragOutput, read_write> = var, FragOutput(vec4<f32>(0.0f))
+    %5:ptr<function, vec4<f32>, read_write> = access %output, 0u
     store %5, vec4<f32>(0.5f, 0.5f, 0.5f, 1.0f)
-    %6:ptr<function, vec4<f32>, read_write> = access %4, 1u
+    %6:ptr<function, vec4<f32>, read_write> = access %output, 1u
     store %6, vec4<f32>(0.5f, 0.5f, 0.5f, 1.0f)
-    %7:tint_symbol_2 = load %4
+    %7:FragOutput = load %output
     ret %7
   }
 }
 %frag_main_inner = func():void {
   $B3: {
-    %9:tint_symbol_2 = call %3
+    %9:FragOutput = call %3
     %10:vec4<f32> = access %9, 0u
     store %1, %10
     %11:vec4<f32> = access %9, 1u
@@ -604,12 +604,12 @@
     ret
   }
 }
-%frag_main = @fragment func():tint_symbol_5 {
+%frag_main = @fragment func():tint_symbol_2 {
   $B4: {
     %13:void = call %frag_main_inner
     %14:vec4<f32> = load %1
     %15:vec4<f32> = load %2
-    %16:tint_symbol_5 = construct %14, %15
+    %16:tint_symbol_2 = construct %14, %15
     ret %16
   }
 }