[spirv-reader] Support row-major matrices

Apply the `@row_major` attribute when a `RowMajor` decoration is seen,
and then use the `TransposeRowMajor` transform to transpose the
matrices.

Add unit tests that cover the full SPIR-V -> WGSL translation.

Bug: 364267168
Change-Id: I1f7d0746a9ab23c425a41a0d7c9224f6a7d41280
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/207375
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
index 5ebf2be..174fbd3 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
@@ -138,6 +138,7 @@
     "named_types_test.cc",
     "namer_test.cc",
     "parser_test.cc",
+    "row_major_matrix_test.cc",
     "spirv_tools_helpers_test.cc",
     "spirv_tools_helpers_test.h",
     "type_test.cc",
@@ -155,6 +156,7 @@
     "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
+    "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/generator",
@@ -186,6 +188,7 @@
     "//conditions:default": [],
   }) + select({
     ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
       "//src/tint/lang/wgsl/writer/ast_printer",
     ],
     "//conditions:default": [],
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
index 76e2429..6708a76 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
@@ -146,6 +146,7 @@
   lang/spirv/reader/ast_parser/named_types_test.cc
   lang/spirv/reader/ast_parser/namer_test.cc
   lang/spirv/reader/ast_parser/parser_test.cc
+  lang/spirv/reader/ast_parser/row_major_matrix_test.cc
   lang/spirv/reader/ast_parser/spirv_tools_helpers_test.cc
   lang/spirv/reader/ast_parser/spirv_tools_helpers_test.h
   lang/spirv/reader/ast_parser/type_test.cc
@@ -164,6 +165,7 @@
   tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
+  tint_lang_wgsl_writer_ir_to_program
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_generator
@@ -202,6 +204,7 @@
 
 if(TINT_BUILD_WGSL_WRITER)
   tint_target_add_dependencies(tint_lang_spirv_reader_ast_parser_test test
+    tint_lang_wgsl_writer
     tint_lang_wgsl_writer_ast_printer
   )
 endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
index 7a5c65c..4125ab5 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
@@ -146,6 +146,7 @@
         "named_types_test.cc",
         "namer_test.cc",
         "parser_test.cc",
+        "row_major_matrix_test.cc",
         "spirv_tools_helpers_test.cc",
         "spirv_tools_helpers_test.h",
         "type_test.cc",
@@ -165,6 +166,7 @@
         "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
+        "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/generator",
@@ -200,7 +202,10 @@
       }
 
       if (tint_build_wgsl_writer) {
-        deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer" ]
+        deps += [
+          "${tint_src_dir}/lang/wgsl/writer",
+          "${tint_src_dir}/lang/wgsl/writer/ast_printer",
+        ]
       }
       public_configs = [ "${tint_spirv_tools_dir}/:spvtools_internal_config" ]
     }
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
index b79fb8c..a14ccaa 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
@@ -42,6 +42,7 @@
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/ast/id_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
+#include "src/tint/lang/wgsl/ast/row_major_attribute.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/containers/unique_vector.h"
@@ -506,9 +507,12 @@
         case spv::Decoration::RelaxedPrecision:  // WGSL doesn't support relaxed precision.
             break;
         case spv::Decoration::RowMajor:
-            Fail() << "WGSL does not support row-major matrices: can't "
-                      "translate member "
-                   << member_index << " of " << ShowType(struct_type_id);
+            if (!member_ty->Is<Matrix>()) {
+                Fail() << "row-major matrix layout not currently supported on type "
+                       << member_ty->String();
+                break;
+            }
+            out.Add(create<ast::RowMajorAttribute>(Source{}));
             break;
         case spv::Decoration::MatrixStride: {
             if (decoration.size() != 2) {
diff --git a/src/tint/lang/spirv/reader/ast_parser/module_var_test.cc b/src/tint/lang/spirv/reader/ast_parser/module_var_test.cc
index f92a464..77f0d06 100644
--- a/src/tint/lang/spirv/reader/ast_parser/module_var_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/module_var_test.cc
@@ -1341,6 +1341,38 @@
 )")) << module_str;
 }
 
+TEST_F(SpvModuleScopeVarParserTest, MatrixStrideDecoration_Natural_RowMajor) {
+    auto p = parser(test::Assemble(Preamble() + FragMain() + R"(
+     OpName %myvar "myvar"
+     OpDecorate %myvar DescriptorSet 0
+     OpDecorate %myvar Binding 0
+     OpDecorate %s Block
+     OpMemberDecorate %s 0 MatrixStride 8
+     OpMemberDecorate %s 0 Offset 0
+     OpMemberDecorate %s 0 RowMajor
+     %void = OpTypeVoid
+     %voidfn = OpTypeFunction %void
+     %float = OpTypeFloat 32
+     %v3float = OpTypeVector %float 3
+     %m2v3float = OpTypeMatrix %v3float 2
+
+     %s = OpTypeStruct %m2v3float
+     %ptr_sb_s = OpTypePointer StorageBuffer %s
+     %myvar = OpVariable %ptr_sb_s StorageBuffer
+  )" + MainBody()));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+    EXPECT_TRUE(p->error().empty());
+    const auto module_str = test::ToString(p->program());
+    EXPECT_THAT(module_str, HasSubstr(R"(struct S {
+  /* @offset(0) */
+  @stride(8) @internal(disable_validation__ignore_stride) @row_major
+  field0 : mat2x3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> myvar : S;
+)")) << module_str;
+}
+
 TEST_F(SpvModuleScopeVarParserTest, MatrixStrideDecoration) {
     auto p = parser(test::Assemble(Preamble() + FragMain() + R"(
      OpName %myvar "myvar"
@@ -1373,7 +1405,7 @@
 )")) << module_str;
 }
 
-TEST_F(SpvModuleScopeVarParserTest, RowMajorDecoration_IsError) {
+TEST_F(SpvModuleScopeVarParserTest, RowMajorDecoration) {
     auto p = parser(test::Assemble(Preamble() + FragMain() + R"(
      OpName %myvar "myvar"
      OpDecorate %s Block
@@ -1389,11 +1421,17 @@
      %ptr_sb_s = OpTypePointer StorageBuffer %s
      %myvar = OpVariable %ptr_sb_s StorageBuffer
   )" + MainBody()));
-    EXPECT_FALSE(p->BuildAndParseInternalModuleExceptFunctions());
-    EXPECT_THAT(
-        p->error(),
-        Eq(R"(WGSL does not support row-major matrices: can't translate member 0 of %3 = OpTypeStruct %8)"))
-        << p->error();
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+    EXPECT_TRUE(p->error().empty());
+    const auto module_str = test::ToString(p->program());
+    EXPECT_THAT(module_str, HasSubstr(R"(struct S {
+  /* @offset(0) */
+  @row_major
+  field0 : mat3x2f,
+}
+
+var<storage, read_write> myvar : S;
+)")) << module_str;
 }
 
 TEST_F(SpvModuleScopeVarParserTest, StorageBuffer_NonWritable_Var) {
diff --git a/src/tint/lang/spirv/reader/ast_parser/parse.cc b/src/tint/lang/spirv/reader/ast_parser/parse.cc
index 48f9443..9106cbb 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parse.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parse.cc
@@ -34,6 +34,7 @@
 #include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h"
 #include "src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h"
 #include "src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h"
+#include "src/tint/lang/spirv/reader/ast_lower/transpose_row_major.h"
 #include "src/tint/lang/spirv/reader/ast_parser/ast_parser.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h"
@@ -115,6 +116,7 @@
     manager.Add<ast::transform::SimplifyPointers>();
     manager.Add<FoldTrivialLets>();
     manager.Add<PassWorkgroupIdAsArgument>();
+    manager.Add<TransposeRowMajor>();
     manager.Add<DecomposeStridedMatrix>();
     manager.Add<DecomposeStridedArray>();
     manager.Add<ast::transform::RemoveUnreachableStatements>();
diff --git a/src/tint/lang/spirv/reader/ast_parser/row_major_matrix_test.cc b/src/tint/lang/spirv/reader/ast_parser/row_major_matrix_test.cc
new file mode 100644
index 0000000..ddb2ec5
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_parser/row_major_matrix_test.cc
@@ -0,0 +1,374 @@
+// 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/ast_parser/parse.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/tint/lang/spirv/reader/ast_parser/spirv_tools_helpers_test.h"
+#include "src/tint/lang/wgsl/writer/writer.h"
+
+namespace tint::spirv::reader::ast_parser {
+namespace {
+
+class SpirvReaderRowMajorMatrixTest : public testing::Test {
+  public:
+    std::string Run(std::string decorations, std::string types, std::string body) {
+        auto spv = test::Assemble(R"(
+               OpCapability Shader
+               OpExtension "SPV_KHR_storage_buffer_storage_class"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %foo "foo"
+               OpExecutionMode %foo LocalSize 1 1 1
+               OpDecorate %buffer DescriptorSet 0
+               OpDecorate %buffer Binding 0
+               OpDecorate %S Block
+
+          )" + decorations + R"(
+
+         %u32 = OpTypeInt 32 0
+       %u32_0 = OpConstant %u32 0
+       %u32_1 = OpConstant %u32 1
+       %u32_2 = OpConstant %u32 2
+
+         %f32 = OpTypeFloat 32
+       %f32_2 = OpConstant %f32 2.0
+
+       %vec3f = OpTypeVector %f32 3
+     %mat2x3f = OpTypeMatrix %vec3f 2
+
+           )" + types + R"(
+
+    %_ptr_Storage_f32 = OpTypePointer StorageBuffer %f32
+  %_ptr_Storage_vec3f = OpTypePointer StorageBuffer %vec3f
+%_ptr_Storage_mat2x3f = OpTypePointer StorageBuffer %mat2x3f
+%_ptr_Storage_S = OpTypePointer StorageBuffer %S
+      %buffer = OpVariable %_ptr_Storage_S StorageBuffer
+
+        %void = OpTypeVoid
+   %func_type = OpTypeFunction %void
+         %foo = OpFunction %void None %func_type
+  %func_start = OpLabel
+
+           )" + body + R"(
+
+               OpReturn
+               OpFunctionEnd
+)");
+        auto program = Parse(spv, {});
+        auto errs = program.Diagnostics().Str();
+        EXPECT_TRUE(program.IsValid()) << errs;
+        EXPECT_EQ(program.Diagnostics().Count(), 0u) << errs;
+        auto result = wgsl::writer::Generate(program, {});
+        EXPECT_EQ(result, Success);
+        return "\n" + result->wgsl;
+    }
+};
+
+TEST_F(SpirvReaderRowMajorMatrixTest, LoadMatrix_DefaultStride) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %m_ptr = OpAccessChain %_ptr_Storage_mat2x3f %buffer %u32_0
+          %m = OpLoad %mat2x3f %m_ptr)");
+
+    EXPECT_EQ(result, R"(
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  let x_20 = transpose(x_2.field0);
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, LoadMatrix_CustomStride) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 32
+               OpMemberDecorate %S 1 Offset 128)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %m_ptr = OpAccessChain %_ptr_Storage_mat2x3f %buffer %u32_0
+          %m = OpLoad %mat2x3f %m_ptr)");
+
+    EXPECT_EQ(result, R"(
+struct strided_arr {
+  @size(32)
+  el : vec2<f32>,
+}
+
+struct S {
+  /* @offset(0) */
+  field0 : array<strided_arr, 3u>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(128) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn arr_to_mat3x2_stride_32(arr : array<strided_arr, 3u>) -> mat3x2<f32> {
+  return mat3x2<f32>(arr[0u].el, arr[1u].el, arr[2u].el);
+}
+
+fn foo_1() {
+  let x_20 = transpose(arr_to_mat3x2_stride_32(x_2.field0));
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, LoadColumn) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %c_ptr = OpAccessChain %_ptr_Storage_vec3f %buffer %u32_0 %u32_1
+          %c = OpLoad %vec3f %c_ptr)");
+
+    EXPECT_EQ(result, R"(
+fn tint_load_row_major_column(tint_from : ptr<storage, mat3x2<f32>, read_write>, tint_idx : u32) -> vec3<f32> {
+  return vec3<f32>(tint_from[0][tint_idx], tint_from[1][tint_idx], tint_from[2][tint_idx]);
+}
+
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  let x_20 = tint_load_row_major_column(&(x_2.field0), u32(1u));
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, LoadElement) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %e_ptr = OpAccessChain %_ptr_Storage_f32 %buffer %u32_0 %u32_1 %u32_2
+          %e = OpLoad %f32 %e_ptr)");
+
+    EXPECT_EQ(result, R"(
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  let x_20 = x_2.field0[2u][1u];
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, StoreMatrix) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %m_ptr = OpAccessChain %_ptr_Storage_mat2x3f %buffer %u32_0
+          %m = OpLoad %mat2x3f %m_ptr
+        %mul = OpMatrixTimesScalar %mat2x3f %m %f32_2
+               OpStore %m_ptr %mul)");
+
+    EXPECT_EQ(result, R"(
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  x_2.field0 = transpose((transpose(x_2.field0) * 2.0f));
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, StoreColumn) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %c_ptr = OpAccessChain %_ptr_Storage_vec3f %buffer %u32_0 %u32_1
+          %c = OpLoad %vec3f %c_ptr
+        %mul = OpVectorTimesScalar %vec3f %c %f32_2
+               OpStore %c_ptr %mul)");
+
+    EXPECT_EQ(result, R"(
+fn tint_load_row_major_column(tint_from : ptr<storage, mat3x2<f32>, read_write>, tint_idx : u32) -> vec3<f32> {
+  return vec3<f32>(tint_from[0][tint_idx], tint_from[1][tint_idx], tint_from[2][tint_idx]);
+}
+
+fn tint_store_row_major_column(tint_to : ptr<storage, mat3x2<f32>, read_write>, tint_idx : u32, tint_col : vec3<f32>) {
+  tint_to[0][tint_idx] = tint_col[0];
+  tint_to[1][tint_idx] = tint_col[1];
+  tint_to[2][tint_idx] = tint_col[2];
+}
+
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  tint_store_row_major_column(&(x_2.field0), u32(1u), (tint_load_row_major_column(&(x_2.field0), u32(1u)) * 2.0f));
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+TEST_F(SpirvReaderRowMajorMatrixTest, StoreElement) {
+    auto result = Run(R"(
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 0 RowMajor
+               OpMemberDecorate %S 0 MatrixStride 8
+               OpMemberDecorate %S 1 Offset 64)",
+                      R"(
+          %S = OpTypeStruct %mat2x3f %vec3f)",
+                      R"(
+      %e_ptr = OpAccessChain %_ptr_Storage_f32 %buffer %u32_0 %u32_1 %u32_2
+          %e = OpLoad %f32 %e_ptr
+        %mul = OpFMul %f32 %e %f32_2
+               OpStore %e_ptr %mul)");
+
+    EXPECT_EQ(result, R"(
+struct S {
+  /* @offset(0) */
+  field0 : mat3x2<f32>,
+  @size(32)
+  padding_0 : u32,
+  /* @offset(64) */
+  field1 : vec3f,
+}
+
+@group(0) @binding(0) var<storage, read_write> x_2 : S;
+
+fn foo_1() {
+  x_2.field0[2u][1u] = (x_2.field0[2u][1u] * 2.0f);
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn foo() {
+  foo_1();
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader::ast_parser