[spirv-reader] Emit struct types and constants

Add a map to uniquify type declarations.

Empty structures are disallowed, as per WGSL.

Test OpConstantNull too.

Bug: tint:1907, tint:2123
Change-Id: I20c73d441f89ca13d2d18019863b4fbc0d2b0084
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/168205
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index 1d0b9ee..6990fb1 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -82,6 +82,7 @@
     "constant_test.cc",
     "function_test.cc",
     "helper_test.h",
+    "struct_test.cc",
     "var_test.cc",
   ],
   deps = [
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index 524b09d..ac508eb 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -88,6 +88,7 @@
   lang/spirv/reader/parser/constant_test.cc
   lang/spirv/reader/parser/function_test.cc
   lang/spirv/reader/parser/helper_test.h
+  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 fa9f30c..8ffc520 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -89,6 +89,7 @@
         "constant_test.cc",
         "function_test.cc",
         "helper_test.h",
+        "struct_test.cc",
         "var_test.cc",
       ]
       deps = [
diff --git a/src/tint/lang/spirv/reader/parser/constant_test.cc b/src/tint/lang/spirv/reader/parser/constant_test.cc
index 7d3542b..862db10 100644
--- a/src/tint/lang/spirv/reader/parser/constant_test.cc
+++ b/src/tint/lang/spirv/reader/parser/constant_test.cc
@@ -624,5 +624,106 @@
 )");
 }
 
+TEST_F(SpirvParserTest, Constant_Struct) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+        %f32 = OpTypeFloat 32
+        %str = OpTypeStruct %i32 %f32
+     %i32_42 = OpConstant %i32 42
+     %f32_n1 = OpConstant %f32 -1
+  %str_const = OpConstantComposite %str %i32_42 %f32_n1
+       %null = OpConstantNull %str
+    %void_fn = OpTypeFunction %void
+    %fn_type = OpTypeFunction %str %str
+
+       %main = OpFunction %void None %void_fn
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+        %foo = OpFunction %str None %fn_type
+      %param = OpFunctionParameter %str
+  %foo_start = OpLabel
+               OpReturnValue %param
+               OpFunctionEnd
+
+        %bar = OpFunction %void None %void_fn
+  %bar_start = OpLabel
+          %1 = OpFunctionCall %str %foo %str_const
+          %2 = OpFunctionCall %str %foo %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%4 = func():void -> %b3 {
+  %b3 = block {
+    %5:tint_symbol_2 = call %2, tint_symbol_2(42i, -1.0f)
+    %6:tint_symbol_2 = call %2, tint_symbol_2(0i, 0.0f)
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Constant_Struct_Nested) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+        %f32 = OpTypeFloat 32
+      %i32_2 = OpConstant %i32 2
+      %inner = OpTypeStruct %i32 %f32
+        %arr = OpTypeArray %inner %i32_2
+      %outer = OpTypeStruct %arr %arr
+     %i32_42 = OpConstant %i32 42
+     %i32_n1 = OpConstant %i32 -1
+     %f32_n1 = OpConstant %f32 -1
+     %f32_42 = OpConstant %f32 42
+%inner_const_0 = OpConstantComposite %inner %i32_42 %f32_n1
+%inner_const_1 = OpConstantComposite %inner %i32_n1 %f32_42
+  %arr_const_0 = OpConstantComposite %arr %inner_const_0 %inner_const_1
+  %arr_const_1 = OpConstantComposite %arr %inner_const_1 %inner_const_0
+  %outer_const = OpConstantComposite %outer %arr_const_0 %arr_const_1
+       %null = OpConstantNull %outer
+    %void_fn = OpTypeFunction %void
+    %fn_type = OpTypeFunction %outer %outer
+
+       %main = OpFunction %void None %void_fn
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+        %foo = OpFunction %outer None %fn_type
+      %param = OpFunctionParameter %outer
+  %foo_start = OpLabel
+               OpReturnValue %param
+               OpFunctionEnd
+
+        %bar = OpFunction %void None %void_fn
+  %bar_start = OpLabel
+          %1 = OpFunctionCall %outer %foo %outer_const
+          %2 = OpFunctionCall %outer %foo %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%4 = func():void -> %b3 {
+  %b3 = block {
+    %5:tint_symbol_5 = call %2, tint_symbol_5(array<tint_symbol_2, 2>(tint_symbol_2(42i, -1.0f), tint_symbol_2(-1i, 42.0f)), array<tint_symbol_2, 2>(tint_symbol_2(-1i, 42.0f), tint_symbol_2(42i, -1.0f)))
+    %6:tint_symbol_5 = call %2, tint_symbol_5(array<tint_symbol_2, 2>(tint_symbol_2(0i, 0.0f)))
+    ret
+  }
+}
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/parser/helper_test.h b/src/tint/lang/spirv/reader/parser/helper_test.h
index af16699..489484e 100644
--- a/src/tint/lang/spirv/reader/parser/helper_test.h
+++ b/src/tint/lang/spirv/reader/parser/helper_test.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_LANG_SPIRV_READER_PARSER_HELPER_TEST_H_
 #define SRC_TINT_LANG_SPIRV_READER_PARSER_HELPER_TEST_H_
 
+#include <iostream>
 #include <string>
 #include <vector>
 
@@ -53,11 +54,10 @@
 template <typename BASE>
 class SpirvParserTestHelperBase : public BASE {
   protected:
-    /// Run the parser on a SPIR-V module and return the Tint IR or an error string.
-    /// @param spirv_asm the SPIR-V assembly to parse
-    /// @returns the disassembled Tint IR
-    std::string Run(std::string spirv_asm) {
-        // Assemble the SPIR-V input.
+    /// Assemble a textual SPIR-V module into a SPIR-V binary.
+    /// @param spirv_asm the textual SPIR-V assembly
+    /// @returns the SPIR-V binary data, or an error string
+    static Result<std::vector<uint32_t>, std::string> Assemble(std::string spirv_asm) {
         StringStream err;
         std::vector<uint32_t> binary;
         spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
@@ -65,14 +65,24 @@
             [&err](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
                 err << "SPIR-V assembly failed:" << pos.line << ":" << pos.column << ": " << msg;
             });
-        auto assembled =
-            tools.Assemble(spirv_asm, &binary, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
-        if (!assembled) {
+        if (!tools.Assemble(spirv_asm, &binary, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
             return err.str();
         }
+        return binary;
+    }
+
+    /// Run the parser on a SPIR-V module and return the Tint IR or an error string.
+    /// @param spirv_asm the SPIR-V assembly to parse
+    /// @returns the disassembled Tint IR
+    std::string Run(std::string spirv_asm) {
+        // Assemble the SPIR-V input.
+        auto binary = Assemble(spirv_asm);
+        if (binary != Success) {
+            return binary.Failure();
+        }
 
         // Parse the SPIR-V to produce an IR module.
-        auto parsed = Parse(Slice(binary.data(), binary.size()));
+        auto parsed = Parse(Slice(binary.Get().data(), binary.Get().size()));
         if (parsed != Success) {
             return parsed.Failure().reason.str();
         }
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index e19d9f1..5b0d9be 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -27,6 +27,7 @@
 
 #include "src/tint/lang/spirv/reader/parser/parser.h"
 
+#include <algorithm>
 #include <memory>
 #include <utility>
 #include <vector>
@@ -105,53 +106,58 @@
     /// @param type a SPIR-V type object
     /// @returns a Tint type object
     const core::type::Type* Type(const spvtools::opt::analysis::Type* type) {
-        switch (type->kind()) {
-            case spvtools::opt::analysis::Type::kVoid:
-                return ty_.void_();
-            case spvtools::opt::analysis::Type::kBool:
-                return ty_.bool_();
-            case spvtools::opt::analysis::Type::kInteger: {
-                auto* int_ty = type->AsInteger();
-                TINT_ASSERT_OR_RETURN_VALUE(int_ty->width() == 32, ty_.void_());
-                if (int_ty->IsSigned()) {
-                    return ty_.i32();
-                } else {
-                    return ty_.u32();
-                }
-            }
-            case spvtools::opt::analysis::Type::kFloat: {
-                auto* float_ty = type->AsFloat();
-                if (float_ty->width() == 16) {
-                    return ty_.f16();
-                } else if (float_ty->width() == 32) {
-                    return ty_.f32();
-                } else {
-                    TINT_UNREACHABLE()
-                        << "unsupported floating point type width: " << float_ty->width();
+        return types_.GetOrCreate(type, [&]() -> const core::type::Type* {
+            switch (type->kind()) {
+                case spvtools::opt::analysis::Type::kVoid:
                     return ty_.void_();
+                case spvtools::opt::analysis::Type::kBool:
+                    return ty_.bool_();
+                case spvtools::opt::analysis::Type::kInteger: {
+                    auto* int_ty = type->AsInteger();
+                    TINT_ASSERT_OR_RETURN_VALUE(int_ty->width() == 32, ty_.void_());
+                    if (int_ty->IsSigned()) {
+                        return ty_.i32();
+                    } else {
+                        return ty_.u32();
+                    }
                 }
+                case spvtools::opt::analysis::Type::kFloat: {
+                    auto* float_ty = type->AsFloat();
+                    if (float_ty->width() == 16) {
+                        return ty_.f16();
+                    } else if (float_ty->width() == 32) {
+                        return ty_.f32();
+                    } else {
+                        TINT_UNREACHABLE()
+                            << "unsupported floating point type width: " << float_ty->width();
+                        return ty_.void_();
+                    }
+                }
+                case spvtools::opt::analysis::Type::kVector: {
+                    auto* vec_ty = type->AsVector();
+                    TINT_ASSERT_OR_RETURN_VALUE(vec_ty->element_count() <= 4, ty_.void_());
+                    return ty_.vec(Type(vec_ty->element_type()), vec_ty->element_count());
+                }
+                case spvtools::opt::analysis::Type::kMatrix: {
+                    auto* mat_ty = type->AsMatrix();
+                    TINT_ASSERT_OR_RETURN_VALUE(mat_ty->element_count() <= 4, ty_.void_());
+                    return ty_.mat(As<core::type::Vector>(Type(mat_ty->element_type())),
+                                   mat_ty->element_count());
+                }
+                case spvtools::opt::analysis::Type::kArray:
+                    return EmitArray(type->AsArray());
+                case spvtools::opt::analysis::Type::kStruct:
+                    return EmitStruct(type->AsStruct());
+                case spvtools::opt::analysis::Type::kPointer: {
+                    auto* ptr_ty = type->AsPointer();
+                    return ty_.ptr(AddressSpace(ptr_ty->storage_class()),
+                                   Type(ptr_ty->pointee_type()));
+                }
+                default:
+                    TINT_UNIMPLEMENTED() << "unhandled SPIR-V type: " << type->str();
+                    return ty_.void_();
             }
-            case spvtools::opt::analysis::Type::kVector: {
-                auto* vec_ty = type->AsVector();
-                TINT_ASSERT_OR_RETURN_VALUE(vec_ty->element_count() <= 4, ty_.void_());
-                return ty_.vec(Type(vec_ty->element_type()), vec_ty->element_count());
-            }
-            case spvtools::opt::analysis::Type::kMatrix: {
-                auto* mat_ty = type->AsMatrix();
-                TINT_ASSERT_OR_RETURN_VALUE(mat_ty->element_count() <= 4, ty_.void_());
-                return ty_.mat(As<core::type::Vector>(Type(mat_ty->element_type())),
-                               mat_ty->element_count());
-            }
-            case spvtools::opt::analysis::Type::kArray:
-                return EmitArray(type->AsArray());
-            case spvtools::opt::analysis::Type::kPointer: {
-                auto* ptr_ty = type->AsPointer();
-                return ty_.ptr(AddressSpace(ptr_ty->storage_class()), Type(ptr_ty->pointee_type()));
-            }
-            default:
-                TINT_UNIMPLEMENTED() << "unhandled SPIR-V type: " << type->str();
-                return ty_.void_();
-        }
+        });
     }
 
     /// @param id a SPIR-V result ID for a type declaration instruction
@@ -182,6 +188,48 @@
         return ty_.array(Type(arr_ty->element_type()), static_cast<uint32_t>(count_val));
     }
 
+    /// @param struct_ty a SPIR-V struct object
+    /// @returns a Tint struct object
+    const core::type::Type* EmitStruct(const spvtools::opt::analysis::Struct* struct_ty) {
+        if (struct_ty->NumberOfComponents() == 0) {
+            TINT_ICE() << "empty structures are not supported";
+            return ty_.void_();
+        }
+
+        // Build a list of struct members.
+        uint32_t current_size = 0u;
+        Vector<core::type::StructMember*, 4> members;
+        for (uint32_t i = 0; i < struct_ty->NumberOfComponents(); i++) {
+            auto* member_ty = Type(struct_ty->element_types()[i]);
+            uint32_t align = std::max<uint32_t>(member_ty->Align(), 1u);
+            uint32_t offset = tint::RoundUp(align, current_size);
+            core::type::StructMemberAttributes attributes;
+
+            // Handle member decorations that affect layout or attributes.
+            if (struct_ty->element_decorations().count(i)) {
+                for (auto& deco : struct_ty->element_decorations().at(i)) {
+                    switch (spv::Decoration(deco[0])) {
+                        case spv::Decoration::Offset:
+                            offset = deco[1];
+                            break;
+                        default:
+                            TINT_UNIMPLEMENTED() << "unhandled member decoration: " << deco[0];
+                            break;
+                    }
+                }
+            }
+
+            // 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)));
+
+            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));
+    }
+
     /// @param id a SPIR-V result ID for a function declaration instruction
     /// @returns a Tint function object
     core::ir::Function* Function(uint32_t id) {
@@ -255,6 +303,13 @@
             }
             return ir_.constant_values.Composite(Type(a->type()), std::move(elements));
         }
+        if (auto* s = constant->AsStructConstant()) {
+            Vector<const core::constant::Value*, 16> elements;
+            for (auto& el : s->GetComponents()) {
+                elements.Push(Constant(el));
+            }
+            return ir_.constant_values.Composite(Type(s->type()), std::move(elements));
+        }
         TINT_UNIMPLEMENTED() << "unhandled constant type";
         return nullptr;
     }
@@ -389,6 +444,8 @@
 
     /// The Tint IR function that is currently being emitted.
     core::ir::Function* current_function_ = nullptr;
+    /// A map from a SPIR-V type declaration result ID to the corresponding Tint type object.
+    Hashmap<const spvtools::opt::analysis::Type*, const core::type::Type*, 16> types_;
     /// A map from a SPIR-V function definition result ID to the corresponding Tint function object.
     Hashmap<uint32_t, core::ir::Function*, 8> functions_;
     /// A map from a SPIR-V result ID to the corresponding Tint value object.
diff --git a/src/tint/lang/spirv/reader/parser/struct_test.cc b/src/tint/lang/spirv/reader/parser/struct_test.cc
new file mode 100644
index 0000000..afbaa39
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/struct_test.cc
@@ -0,0 +1,232 @@
+// Copyright 2024 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"
+
+#include "gtest/gtest-spi.h"
+
+namespace tint::spirv::reader {
+
+TEST_F(SpirvParserTest, Struct_Empty) {
+    EXPECT_FATAL_FAILURE(  //
+        {
+            auto assembly = Assemble(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %str = OpTypeStruct
+    %ep_type = OpTypeFunction %void
+    %fn_type = OpTypeFunction %void %str
+
+        %foo = OpFunction %void None %fn_type
+      %param = OpFunctionParameter %str
+  %foo_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)");
+            auto parsed = Parse(Slice(assembly.Get().data(), assembly.Get().size()));
+            EXPECT_EQ(parsed, Success);
+        },
+        "empty structures are not supported");
+}
+
+TEST_F(SpirvParserTest, Struct_BasicDecl) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+        %str = OpTypeStruct %i32 %i32
+    %ep_type = OpTypeFunction %void
+    %fn_type = OpTypeFunction %void %str
+
+        %foo = OpFunction %void None %fn_type
+      %param = OpFunctionParameter %str
+  %foo_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+tint_symbol_2 = struct @align(4) {
+  tint_symbol:i32 @offset(0)
+  tint_symbol_1:i32 @offset(4)
+}
+
+%1 = func(%2:tint_symbol_2):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Struct_MultipleUses) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+        %str = OpTypeStruct %i32 %i32
+    %ep_type = OpTypeFunction %void
+    %fn_type = OpTypeFunction %str %str %str
+
+        %foo = OpFunction %str None %fn_type
+    %param_1 = OpFunctionParameter %str
+    %param_2 = OpFunctionParameter %str
+  %foo_start = OpLabel
+               OpReturnValue %param_1
+               OpFunctionEnd
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+tint_symbol_2 = struct @align(4) {
+  tint_symbol:i32 @offset(0)
+  tint_symbol_1:i32 @offset(4)
+}
+
+%1 = func(%2:tint_symbol_2, %3:tint_symbol_2):tint_symbol_2 -> %b1 {
+  %b1 = block {
+    ret %2
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Struct_Nested) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+      %inner = OpTypeStruct %i32 %i32
+     %middle = OpTypeStruct %inner %inner
+      %outer = OpTypeStruct %inner %middle %inner
+    %ep_type = OpTypeFunction %void
+    %fn_type = OpTypeFunction %void %outer
+
+        %foo = OpFunction %void None %fn_type
+      %param = OpFunctionParameter %outer
+  %foo_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+tint_symbol_2 = struct @align(4) {
+  tint_symbol:i32 @offset(0)
+  tint_symbol_1:i32 @offset(4)
+}
+
+tint_symbol_6 = struct @align(4) {
+  tint_symbol_4:tint_symbol_2 @offset(0)
+  tint_symbol_5:tint_symbol_2 @offset(8)
+}
+
+tint_symbol_9 = struct @align(4) {
+  tint_symbol_3:tint_symbol_2 @offset(0)
+  tint_symbol_7:tint_symbol_6 @offset(8)
+  tint_symbol_8:tint_symbol_2 @offset(24)
+}
+
+%1 = func(%2:tint_symbol_9):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Struct_Offset) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpMemberDecorate %str 0 Offset 0
+               OpMemberDecorate %str 1 Offset 4
+               OpMemberDecorate %str 2 Offset 32
+               OpMemberDecorate %str 3 Offset 64
+       %void = OpTypeVoid
+        %i32 = OpTypeInt 32 1
+        %str = OpTypeStruct %i32 %i32 %i32 %i32
+    %ep_type = OpTypeFunction %void
+    %fn_type = OpTypeFunction %void %str
+
+        %foo = OpFunction %void None %fn_type
+      %param = OpFunctionParameter %str
+  %foo_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+tint_symbol_4 = struct @align(4) {
+  tint_symbol:i32 @offset(0)
+  tint_symbol_1:i32 @offset(4)
+  tint_symbol_2:i32 @offset(32)
+  tint_symbol_3:i32 @offset(64)
+}
+
+%1 = func(%2:tint_symbol_4):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+}  // namespace tint::spirv::reader