validation: Fix array storage class validation

We weren't recursing into array element types, which meant that we
weren't validating nested arrays or buffers whose top-level store-type
was an array.

Change-Id: Ib897b36e0b5c3de3dc67c4f60805411c014cd914
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/77561
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index 89c2808..309cb8a 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -108,8 +108,8 @@
   /// Describes the context in which a variable is declared
   enum class VariableKind { kParameter, kLocal, kGlobal };
 
-  std::set<std::pair<const sem::Struct*, ast::StorageClass>>
-      valid_struct_storage_layouts_;
+  std::set<std::pair<const sem::Type*, ast::StorageClass>>
+      valid_type_storage_layouts_;
 
   /// Structure holding semantic information about a block (i.e. scope), such as
   /// parent block and variables declared in the block.
@@ -292,9 +292,9 @@
                                       const sem::Array* arr_type);
   bool ValidateTextureIntrinsicFunction(const sem::Call* call);
   bool ValidateNoDuplicateDecorations(const ast::DecorationList& decorations);
-  // sem::Struct is assumed to have at least one member
-  bool ValidateStorageClassLayout(const sem::Struct* type,
-                                  ast::StorageClass sc);
+  bool ValidateStorageClassLayout(const sem::Type* type,
+                                  ast::StorageClass sc,
+                                  Source source);
   bool ValidateStorageClassLayout(const sem::Variable* var);
 
   /// @returns true if the decoration list contains a
diff --git a/src/resolver/resolver_validation.cc b/src/resolver/resolver_validation.cc
index 8864a26..f3e8302 100644
--- a/src/resolver/resolver_validation.cc
+++ b/src/resolver/resolver_validation.cc
@@ -229,8 +229,9 @@
   return true;
 }
 
-bool Resolver::ValidateStorageClassLayout(const sem::Struct* str,
-                                          ast::StorageClass sc) {
+bool Resolver::ValidateStorageClassLayout(const sem::Type* store_ty,
+                                          ast::StorageClass sc,
+                                          Source source) {
   // https://gpuweb.github.io/gpuweb/wgsl/#storage-class-layout-constraints
 
   auto is_uniform_struct_or_array = [sc](const sem::Type* ty) {
@@ -255,110 +256,125 @@
     return builder_->Symbols().NameFor(sm->Declaration()->symbol);
   };
 
+  // Cache result of type + storage class pair.
+  if (!valid_type_storage_layouts_.emplace(store_ty, sc).second) {
+    return true;
+  }
+
   if (!ast::IsHostShareable(sc)) {
     return true;
   }
 
-  for (size_t i = 0; i < str->Members().size(); ++i) {
-    auto* const m = str->Members()[i];
-    uint32_t required_align = required_alignment_of(m->Type());
+  if (auto* str = store_ty->As<sem::Struct>()) {
+    for (size_t i = 0; i < str->Members().size(); ++i) {
+      auto* const m = str->Members()[i];
+      uint32_t required_align = required_alignment_of(m->Type());
 
-    // Validate that member is at a valid byte offset
-    if (m->Offset() % required_align != 0) {
-      AddError("the offset of a struct member of type '" +
-                   m->Type()->UnwrapRef()->FriendlyName(builder_->Symbols()) +
-                   "' in storage class '" + ast::ToString(sc) +
-                   "' must be a multiple of " + std::to_string(required_align) +
-                   " bytes, but '" + member_name_of(m) +
-                   "' is currently at offset " + std::to_string(m->Offset()) +
-                   ". Consider setting @align(" +
-                   std::to_string(required_align) + ") on this member",
-               m->Declaration()->source);
-
-      AddNote("see layout of struct:\n" + str->Layout(builder_->Symbols()),
-              str->Declaration()->source);
-
-      if (auto* member_str = m->Type()->As<sem::Struct>()) {
-        AddNote("and layout of struct member:\n" +
-                    member_str->Layout(builder_->Symbols()),
-                member_str->Declaration()->source);
+      // Recurse into the member type.
+      if (!ValidateStorageClassLayout(m->Type(), sc,
+                                      m->Declaration()->type->source)) {
+        AddNote("see layout of struct:\n" + str->Layout(builder_->Symbols()),
+                str->Declaration()->source);
+        return false;
       }
 
-      return false;
-    }
-
-    // For uniform buffers, validate that the number of bytes between the
-    // previous member of type struct and the current is a multiple of 16 bytes.
-    auto* const prev_member = (i == 0) ? nullptr : str->Members()[i - 1];
-    if (prev_member && is_uniform_struct(prev_member->Type())) {
-      const uint32_t prev_to_curr_offset = m->Offset() - prev_member->Offset();
-      if (prev_to_curr_offset % 16 != 0) {
-        AddError(
-            "uniform storage requires that the number of bytes between the "
-            "start of the previous member of type struct and the current "
-            "member be a multiple of 16 bytes, but there are currently " +
-                std::to_string(prev_to_curr_offset) + " bytes between '" +
-                member_name_of(prev_member) + "' and '" + member_name_of(m) +
-                "'. Consider setting @align(16) on this member",
-            m->Declaration()->source);
+      // Validate that member is at a valid byte offset
+      if (m->Offset() % required_align != 0) {
+        AddError("the offset of a struct member of type '" +
+                     m->Type()->UnwrapRef()->FriendlyName(builder_->Symbols()) +
+                     "' in storage class '" + ast::ToString(sc) +
+                     "' must be a multiple of " +
+                     std::to_string(required_align) + " bytes, but '" +
+                     member_name_of(m) + "' is currently at offset " +
+                     std::to_string(m->Offset()) +
+                     ". Consider setting @align(" +
+                     std::to_string(required_align) + ") on this member",
+                 m->Declaration()->source);
 
         AddNote("see layout of struct:\n" + str->Layout(builder_->Symbols()),
                 str->Declaration()->source);
 
-        auto* prev_member_str = prev_member->Type()->As<sem::Struct>();
-        AddNote("and layout of previous member struct:\n" +
-                    prev_member_str->Layout(builder_->Symbols()),
-                prev_member_str->Declaration()->source);
+        if (auto* member_str = m->Type()->As<sem::Struct>()) {
+          AddNote("and layout of struct member:\n" +
+                      member_str->Layout(builder_->Symbols()),
+                  member_str->Declaration()->source);
+        }
+
         return false;
       }
-    }
 
-    // For uniform buffer array members, validate that array elements are
-    // aligned to 16 bytes
-    if (auto* arr = m->Type()->As<sem::Array>()) {
-      if (sc == ast::StorageClass::kUniform) {
-        // We already validated that this array member is itself aligned to 16
-        // bytes above, so we only need to validate that stride is a multiple of
-        // 16 bytes.
-        if (arr->Stride() % 16 != 0) {
-          // Since WGSL has no stride attribute, try to provide a useful hint
-          // for how the shader author can resolve the issue.
-          std::string hint;
-          if (arr->ElemType()->is_scalar()) {
-            hint =
-                "Consider using a vector or struct as the element type "
-                "instead.";
-          } else if (auto* vec = arr->ElemType()->As<sem::Vector>();
-                     vec && vec->type()->Size() == 4) {
-            hint = "Consider using a vec4 instead.";
-          } else if (arr->ElemType()->Is<sem::Struct>()) {
-            hint =
-                "Consider using the @size attribute on the last struct member.";
-          } else {
-            hint =
-                "Consider wrapping the element type in a struct and using the "
-                "@size attribute.";
-          }
+      // For uniform buffers, validate that the number of bytes between the
+      // previous member of type struct and the current is a multiple of 16
+      // bytes.
+      auto* const prev_member = (i == 0) ? nullptr : str->Members()[i - 1];
+      if (prev_member && is_uniform_struct(prev_member->Type())) {
+        const uint32_t prev_to_curr_offset =
+            m->Offset() - prev_member->Offset();
+        if (prev_to_curr_offset % 16 != 0) {
           AddError(
-              "uniform storage requires that array elements be aligned to 16 "
-              "bytes, but array element alignment of '" +
-                  member_name_of(m) + "' is currently " +
-                  std::to_string(arr->Stride()) + ". " + hint,
-              m->Declaration()->type->source);
+              "uniform storage requires that the number of bytes between the "
+              "start of the previous member of type struct and the current "
+              "member be a multiple of 16 bytes, but there are currently " +
+                  std::to_string(prev_to_curr_offset) + " bytes between '" +
+                  member_name_of(prev_member) + "' and '" + member_name_of(m) +
+                  "'. Consider setting @align(16) on this member",
+              m->Declaration()->source);
+
           AddNote("see layout of struct:\n" + str->Layout(builder_->Symbols()),
                   str->Declaration()->source);
+
+          auto* prev_member_str = prev_member->Type()->As<sem::Struct>();
+          AddNote("and layout of previous member struct:\n" +
+                      prev_member_str->Layout(builder_->Symbols()),
+                  prev_member_str->Declaration()->source);
           return false;
         }
       }
     }
+  }
 
-    // If member is struct, recurse
-    if (auto* str_member = m->Type()->As<sem::Struct>()) {
-      // Cache result of struct + storage class pair
-      if (valid_struct_storage_layouts_.emplace(str_member, sc).second) {
-        if (!ValidateStorageClassLayout(str_member, sc)) {
-          return false;
+  // For uniform buffer array members, validate that array elements are
+  // aligned to 16 bytes
+  if (auto* arr = store_ty->As<sem::Array>()) {
+    // Recurse into the element type.
+    // TODO(crbug.com/tint/1388): Ideally we'd pass the source for nested
+    // element type here, but we can't easily get that from the semantic node.
+    // We should consider recursing through the AST type nodes instead.
+    if (!ValidateStorageClassLayout(arr->ElemType(), sc, source)) {
+      return false;
+    }
+
+    if (sc == ast::StorageClass::kUniform) {
+      // We already validated that this array member is itself aligned to 16
+      // bytes above, so we only need to validate that stride is a multiple
+      // of 16 bytes.
+      if (arr->Stride() % 16 != 0) {
+        // Since WGSL has no stride attribute, try to provide a useful hint
+        // for how the shader author can resolve the issue.
+        std::string hint;
+        if (arr->ElemType()->is_scalar()) {
+          hint =
+              "Consider using a vector or struct as the element type "
+              "instead.";
+        } else if (auto* vec = arr->ElemType()->As<sem::Vector>();
+                   vec && vec->type()->Size() == 4) {
+          hint = "Consider using a vec4 instead.";
+        } else if (arr->ElemType()->Is<sem::Struct>()) {
+          hint =
+              "Consider using the @size attribute on the last struct "
+              "member.";
+        } else {
+          hint =
+              "Consider wrapping the element type in a struct and using "
+              "the "
+              "@size attribute.";
         }
+        AddError(
+            "uniform storage requires that array elements be aligned to 16 "
+            "bytes, but array element alignment is currently " +
+                std::to_string(arr->Stride()) + ". " + hint,
+            source);
+        return false;
       }
     }
   }
@@ -368,10 +384,20 @@
 
 bool Resolver::ValidateStorageClassLayout(const sem::Variable* var) {
   if (auto* str = var->Type()->UnwrapRef()->As<sem::Struct>()) {
-    if (!ValidateStorageClassLayout(str, var->StorageClass())) {
+    if (!ValidateStorageClassLayout(str, var->StorageClass(),
+                                    str->Declaration()->source)) {
       AddNote("see declaration of variable", var->Declaration()->source);
       return false;
     }
+  } else {
+    Source source = var->Declaration()->source;
+    if (var->Declaration()->type) {
+      source = var->Declaration()->type->source;
+    }
+    if (!ValidateStorageClassLayout(var->Type()->UnwrapRef(),
+                                    var->StorageClass(), source)) {
+      return false;
+    }
   }
 
   return true;
diff --git a/src/resolver/storage_class_layout_validation_test.cc b/src/resolver/storage_class_layout_validation_test.cc
index a8e7056..9355082 100644
--- a/src/resolver/storage_class_layout_validation_test.cc
+++ b/src/resolver/storage_class_layout_validation_test.cc
@@ -405,7 +405,7 @@
   ASSERT_FALSE(r()->Resolve());
   EXPECT_EQ(
       r()->error(),
-      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment of 'inner' is currently 4. Consider using a vector or struct as the element type instead.
+      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment is currently 4. Consider using a vector or struct as the element type instead.
 12:34 note: see layout of struct:
 /*            align(4) size(44) */ struct Outer {
 /* offset( 0) align(4) size(40) */   inner : array<f32, 10>;
@@ -442,7 +442,7 @@
   ASSERT_FALSE(r()->Resolve());
   EXPECT_EQ(
       r()->error(),
-      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment of 'inner' is currently 8. Consider using a vec4 instead.
+      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment is currently 8. Consider using a vec4 instead.
 12:34 note: see layout of struct:
 /*            align(8) size(88) */ struct Outer {
 /* offset( 0) align(8) size(80) */   inner : array<vec2<f32>, 10>;
@@ -488,7 +488,7 @@
   ASSERT_FALSE(r()->Resolve());
   EXPECT_EQ(
       r()->error(),
-      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment of 'inner' is currently 8. Consider using the @size attribute on the last struct member.
+      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment is currently 8. Consider using the @size attribute on the last struct member.
 12:34 note: see layout of struct:
 /*            align(4) size(84) */ struct Outer {
 /* offset( 0) align(4) size(80) */   inner : array<ArrayElem, 10>;
@@ -498,6 +498,49 @@
 }
 
 TEST_F(ResolverStorageClassLayoutValidationTest,
+       UniformBuffer_InvalidArrayStride_TopLevelArray) {
+  // @group(0) @binding(0)
+  // var<uniform> a : array<f32, 4>;
+  Global(Source{{78, 90}}, "a", ty.array(Source{{34, 56}}, ty.f32(), 4),
+         ast::StorageClass::kUniform, GroupAndBinding(0, 0));
+
+  ASSERT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment is currently 4. Consider using a vector or struct as the element type instead.)");
+}
+
+TEST_F(ResolverStorageClassLayoutValidationTest,
+       UniformBuffer_InvalidArrayStride_NestedArray) {
+  // struct Outer {
+  //   inner : array<array<f32, 4>, 4>
+  // };
+  //
+  // @group(0) @binding(0)
+  // var<uniform> a : array<Outer, 4>;
+
+  Structure(
+      Source{{12, 34}}, "Outer",
+      {
+          Member("inner", ty.array(Source{{34, 56}}, ty.array(ty.f32(), 4), 4)),
+      },
+      {StructBlock()});
+
+  Global(Source{{78, 90}}, "a", ty.type_name("Outer"),
+         ast::StorageClass::kUniform, GroupAndBinding(0, 0));
+
+  ASSERT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(34:56 error: uniform storage requires that array elements be aligned to 16 bytes, but array element alignment is currently 4. Consider using a vector or struct as the element type instead.
+12:34 note: see layout of struct:
+/*            align(4) size(64) */ struct Outer {
+/* offset( 0) align(4) size(64) */   inner : array<array<f32, 4>, 4>;
+/*                              */ };
+78:90 note: see declaration of variable)");
+}
+
+TEST_F(ResolverStorageClassLayoutValidationTest,
        UniformBuffer_InvalidArrayStride_SuggestedFix) {
   // type Inner = @stride(16) array<f32, 10>;
   //
diff --git a/src/resolver/storage_class_validation_test.cc b/src/resolver/storage_class_validation_test.cc
index 9965e08..63fdcac 100644
--- a/src/resolver/storage_class_validation_test.cc
+++ b/src/resolver/storage_class_validation_test.cc
@@ -302,8 +302,11 @@
 }
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferArray) {
+  // struct S {
+  //   @size(16) f : f32;
+  // }
   // var<uniform> g : array<S, 3>;
-  auto* s = Structure("S", {Member("a", ty.f32())});
+  auto* s = Structure("S", {Member("a", ty.f32(), {MemberSize(16)})});
   auto* a = ty.array(ty.Of(s), 3);
   Global(Source{{56, 78}}, "g", a, ast::StorageClass::kUniform,
          ast::DecorationList{