tint: Add StructMember attributes to sem.

Removes the need to examine AST attributes.

Change-Id: Iaaa6b10fd56baf732057c4c3960c1bfc5bbdeaa6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/129621
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 70b3d97..56c2031 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -747,6 +747,7 @@
     "builtin/extension.h",
     "builtin/function.cc",
     "builtin/function.h",
+    "builtin/interpolation.h",
     "builtin/interpolation_sampling.cc",
     "builtin/interpolation_sampling.h",
     "builtin/interpolation_type.cc",
diff --git a/src/tint/builtin/interpolation.h b/src/tint/builtin/interpolation.h
new file mode 100644
index 0000000..ad73c30
--- /dev/null
+++ b/src/tint/builtin/interpolation.h
@@ -0,0 +1,33 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_BUILTIN_INTERPOLATION_H_
+#define SRC_TINT_BUILTIN_INTERPOLATION_H_
+
+#include "src/tint/builtin/interpolation_sampling.h"
+#include "src/tint/builtin/interpolation_type.h"
+
+namespace tint::builtin {
+
+/// The values of an `@interpolate` attribute
+struct Interpolation {
+    /// The first argument of a `@interpolate` attribute
+    builtin::InterpolationType type = builtin::InterpolationType::kUndefined;
+    /// The second argument of a `@interpolate` attribute
+    builtin::InterpolationSampling sampling = builtin::InterpolationSampling::kUndefined;
+};
+
+}  // namespace tint::builtin
+
+#endif  // SRC_TINT_BUILTIN_INTERPOLATION_H_
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index 246943d..adafe3a 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -621,8 +621,8 @@
         // Recurse into members.
         for (auto* member : struct_ty->Members()) {
             AddEntryPointInOutVariables(name + "." + member->Name().Name(), member->Type(),
-                                        member->Declaration()->attributes, member->Location(),
-                                        variables);
+                                        member->Declaration()->attributes,
+                                        member->Attributes().location, variables);
         }
         return;
     }
diff --git a/src/tint/resolver/builtin_structs.cc b/src/tint/resolver/builtin_structs.cc
index ce4f38c..f035f0c 100644
--- a/src/tint/resolver/builtin_structs.cc
+++ b/src/tint/resolver/builtin_structs.cc
@@ -53,7 +53,7 @@
             /* offset */ offset,
             /* align */ align,
             /* size */ size,
-            /* location */ std::nullopt));
+            /* attributes */ type::StructMemberAttributes{}));
         offset += size;
     }
     uint32_t size_without_padding = offset;
diff --git a/src/tint/resolver/inferred_type_test.cc b/src/tint/resolver/inferred_type_test.cc
index ab2478b..2fb8446 100644
--- a/src/tint/resolver/inferred_type_test.cc
+++ b/src/tint/resolver/inferred_type_test.cc
@@ -150,11 +150,12 @@
     auto* member = Member("x", ty.i32());
     auto* str = Structure("S", utils::Vector{member});
 
-    auto* expected_type = create<sem::Struct>(
-        str, str->source, str->name->symbol,
-        utils::Vector{create<sem::StructMember>(member, member->source, member->name->symbol,
-                                                create<type::I32>(), 0u, 0u, 0u, 4u, std::nullopt)},
-        0u, 4u, 4u);
+    auto* expected_type =
+        create<sem::Struct>(str, str->source, str->name->symbol,
+                            utils::Vector{create<sem::StructMember>(
+                                member, member->source, member->name->symbol, create<type::I32>(),
+                                0u, 0u, 0u, 4u, type::StructMemberAttributes{})},
+                            0u, 4u, 4u);
 
     auto* ctor_expr = Call(ty.Of(str));
 
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 638cac0..322b3ee 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3721,14 +3721,22 @@
     return true;
 }
 
-bool Resolver::InterpolateAttribute(const ast::InterpolateAttribute* attr) {
-    if (!InterpolationType(attr->type)) {
-        return false;
+utils::Result<builtin::Interpolation> Resolver::InterpolateAttribute(
+    const ast::InterpolateAttribute* attr) {
+    builtin::Interpolation out;
+    auto* type = InterpolationType(attr->type);
+    if (!type) {
+        return utils::Failure;
     }
-    if (attr->sampling && !InterpolationSampling(attr->sampling)) {
-        return false;
+    out.type = type->Value();
+    if (attr->sampling) {
+        auto* sampling = InterpolationSampling(attr->sampling);
+        if (!sampling) {
+            return utils::Failure;
+        }
+        out.sampling = sampling->Value();
     }
-    return true;
+    return out;
 }
 
 bool Resolver::InternalAttribute(const ast::InternalAttribute* attr) {
@@ -4021,7 +4029,7 @@
         bool has_offset_attr = false;
         bool has_align_attr = false;
         bool has_size_attr = false;
-        std::optional<uint32_t> location;
+        type::StructMemberAttributes attributes;
         for (auto* attribute : member->attributes) {
             Mark(attribute);
             bool ok = Switch(
@@ -4122,12 +4130,32 @@
                     if (!value) {
                         return false;
                     }
-                    location = value.Get();
+                    attributes.location = value.Get();
                     return true;
                 },
-                [&](const ast::BuiltinAttribute* attr) -> bool { return BuiltinAttribute(attr); },
-                [&](const ast::InterpolateAttribute* attr) { return InterpolateAttribute(attr); },
-                [&](const ast::InvariantAttribute* attr) { return InvariantAttribute(attr); },
+                [&](const ast::BuiltinAttribute* attr) {
+                    auto value = BuiltinAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.builtin = value.Get();
+                    return true;
+                },
+                [&](const ast::InterpolateAttribute* attr) {
+                    auto value = InterpolateAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.interpolation = value.Get();
+                    return true;
+                },
+                [&](const ast::InvariantAttribute* attr) {
+                    if (!InvariantAttribute(attr)) {
+                        return false;
+                    }
+                    attributes.invariant = true;
+                    return true;
+                },
                 [&](const ast::StrideAttribute* attr) {
                     if (validator_.IsValidationEnabled(
                             member->attributes, ast::DisabledValidation::kIgnoreStrideAttribute)) {
@@ -4163,7 +4191,7 @@
         auto* sem_member = builder_->create<sem::StructMember>(
             member, member->source, member->name->symbol, type,
             static_cast<uint32_t>(sem_members.Length()), static_cast<uint32_t>(offset),
-            static_cast<uint32_t>(align), static_cast<uint32_t>(size), location);
+            static_cast<uint32_t>(align), static_cast<uint32_t>(size), attributes);
         builder_->Sem().Add(member, sem_member);
         sem_members.Push(sem_member);
 
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index b26ad96..7432f28 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -354,7 +354,8 @@
 
     /// Resolves the `@interpolate` attribute @p attr
     /// @returns true on success, false on failure
-    bool InterpolateAttribute(const ast::InterpolateAttribute* attr);
+    utils::Result<builtin::Interpolation> InterpolateAttribute(
+        const ast::InterpolateAttribute* attr);
 
     /// Resolves the internal attribute @p attr
     /// @returns true on success, false on failure
diff --git a/src/tint/resolver/struct_pipeline_stage_use_test.cc b/src/tint/resolver/struct_pipeline_stage_use_test.cc
index 4707650..529f1a6 100644
--- a/src/tint/resolver/struct_pipeline_stage_use_test.cc
+++ b/src/tint/resolver/struct_pipeline_stage_use_test.cc
@@ -187,7 +187,7 @@
     auto* sem = TypeOf(s)->As<sem::Struct>();
     ASSERT_NE(sem, nullptr);
     ASSERT_EQ(1u, sem->Members().Length());
-    EXPECT_EQ(3u, sem->Members()[0]->Location());
+    EXPECT_EQ(3u, sem->Members()[0]->Attributes().location);
 }
 
 TEST_F(ResolverPipelineStageUseTest, StructUsedAsShaderReturnTypeViaAlias) {
@@ -217,7 +217,7 @@
     auto* sem = TypeOf(s)->As<sem::Struct>();
     ASSERT_NE(sem, nullptr);
     ASSERT_EQ(1u, sem->Members().Length());
-    EXPECT_EQ(3u, sem->Members()[0]->Location());
+    EXPECT_EQ(3u, sem->Members()[0]->Attributes().location);
 }
 
 }  // namespace
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index c1ba335..fafcfdf 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -1215,7 +1215,7 @@
                     if (!validate_entry_point_attributes_inner(
                             member->Declaration()->attributes, member->Type(), member->Source(),
                             param_or_ret,
-                            /*is_struct_member*/ true, member->Location())) {
+                            /*is_struct_member*/ true, member->Attributes().location)) {
                         AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
                                 decl->source);
                         return false;
@@ -2105,9 +2105,9 @@
                 },
                 [&](const ast::LocationAttribute* location) {
                     has_location = true;
-                    TINT_ASSERT(Resolver, member->Location().has_value());
-                    if (!LocationAttribute(location, member->Location().value(), member->Type(),
-                                           locations, stage, member->Source())) {
+                    TINT_ASSERT(Resolver, member->Attributes().location.has_value());
+                    if (!LocationAttribute(location, member->Attributes().location.value(),
+                                           member->Type(), locations, stage, member->Source())) {
                         return false;
                     }
                     return true;
diff --git a/src/tint/sem/struct.cc b/src/tint/sem/struct.cc
index 413231a..138671e 100644
--- a/src/tint/sem/struct.cc
+++ b/src/tint/sem/struct.cc
@@ -40,8 +40,8 @@
                            uint32_t offset,
                            uint32_t align,
                            uint32_t size,
-                           std::optional<uint32_t> location)
-    : Base(source, name, type, index, offset, align, size, location), declaration_(declaration) {}
+                           const type::StructMemberAttributes& attributes)
+    : Base(source, name, type, index, offset, align, size, attributes), declaration_(declaration) {}
 
 StructMember::~StructMember() = default;
 
diff --git a/src/tint/sem/struct.h b/src/tint/sem/struct.h
index 2b437af..7362f0f 100644
--- a/src/tint/sem/struct.h
+++ b/src/tint/sem/struct.h
@@ -84,7 +84,7 @@
     /// @param offset the byte offset from the base of the structure
     /// @param align the byte alignment of the member
     /// @param size the byte size of the member
-    /// @param location the location attribute, if present
+    /// @param attributes the optional attributes
     StructMember(const ast::StructMember* declaration,
                  tint::Source source,
                  Symbol name,
@@ -93,7 +93,7 @@
                  uint32_t offset,
                  uint32_t align,
                  uint32_t size,
-                 std::optional<uint32_t> location);
+                 const type::StructMemberAttributes& attributes);
 
     /// Destructor
     ~StructMember() override;
diff --git a/src/tint/transform/canonicalize_entry_point_io.cc b/src/tint/transform/canonicalize_entry_point_io.cc
index 840f8ff..3bcd8d1 100644
--- a/src/tint/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/transform/canonicalize_entry_point_io.cc
@@ -380,8 +380,8 @@
 
             auto attributes =
                 CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate);
-            auto* input_expr =
-                AddInput(name, member->Type(), member->Location(), std::move(attributes));
+            auto* input_expr = AddInput(name, member->Type(), member->Attributes().location,
+                                        std::move(attributes));
             inner_struct_values.Push(input_expr);
         }
 
@@ -410,8 +410,8 @@
                     CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate);
 
                 // Extract the original structure member.
-                AddOutput(name, member->Type(), member->Location(), std::move(attributes),
-                          ctx.dst->MemberAccessor(original_result, name));
+                AddOutput(name, member->Type(), member->Attributes().location,
+                          std::move(attributes), ctx.dst->MemberAccessor(original_result, name));
             }
         } else if (!inner_ret_type->Is<type::Void>()) {
             auto attributes =
diff --git a/src/tint/transform/truncate_interstage_variables.cc b/src/tint/transform/truncate_interstage_variables.cc
index 33c5cf7..1fc0050 100644
--- a/src/tint/transform/truncate_interstage_variables.cc
+++ b/src/tint/transform/truncate_interstage_variables.cc
@@ -83,6 +83,8 @@
         auto* func_sem = sem.Get(func_ast);
         auto* str = func_sem->ReturnType()->As<sem::Struct>();
 
+        // This transform is run after CanonicalizeEntryPointIO transform,
+        // So it is guaranteed that entry point inputs are already grouped in a struct.
         if (TINT_UNLIKELY(!str)) {
             TINT_ICE(Transform, ctx.dst->Diagnostics())
                 << "Entrypoint function return type is non-struct.\n"
@@ -91,20 +93,14 @@
             continue;
         }
 
-        // This transform is run after CanonicalizeEntryPointIO transform,
-        // So it is guaranteed that entry point inputs are already grouped in a struct.
-        const ast::Struct* struct_ty = str->Declaration();
-
         // A prepass to check if any interstage variable locations in the entry point needs
         // truncating. If not we don't really need to handle this entry point.
         utils::Hashset<const sem::StructMember*, 16u> omit_members;
 
-        for (auto* member : struct_ty->members) {
-            if (ast::GetAttribute<ast::LocationAttribute>(member->attributes)) {
-                auto* m = sem.Get(member);
-                uint32_t location = m->Location().value();
-                if (!data->interstage_locations.test(location)) {
-                    omit_members.Add(m);
+        for (auto* member : str->Members()) {
+            if (auto location = member->Attributes().location) {
+                if (!data->interstage_locations.test(location.value())) {
+                    omit_members.Add(member);
                 }
             }
         }
diff --git a/src/tint/transform/vertex_pulling.cc b/src/tint/transform/vertex_pulling.cc
index 064be00..5f704de 100644
--- a/src/tint/transform/vertex_pulling.cc
+++ b/src/tint/transform/vertex_pulling.cc
@@ -826,8 +826,8 @@
                 auto* sem = src->Sem().Get(member);
                 info.type = sem->Type();
 
-                TINT_ASSERT(Transform, sem->Location().has_value());
-                location_info[sem->Location().value()] = info;
+                TINT_ASSERT(Transform, sem->Attributes().location.has_value());
+                location_info[sem->Attributes().location.value()] = info;
                 has_locations = true;
             } else {
                 auto* builtin_attr = ast::GetAttribute<ast::BuiltinAttribute>(member->attributes);
diff --git a/src/tint/type/struct.cc b/src/tint/type/struct.cc
index e5c8d4c..58d58e6 100644
--- a/src/tint/type/struct.cc
+++ b/src/tint/type/struct.cc
@@ -179,7 +179,7 @@
                            uint32_t offset,
                            uint32_t align,
                            uint32_t size,
-                           std::optional<uint32_t> location)
+                           const StructMemberAttributes& attributes)
     : source_(source),
       name_(name),
       type_(type),
@@ -187,7 +187,7 @@
       offset_(offset),
       align_(align),
       size_(size),
-      location_(location) {}
+      attributes_(attributes) {}
 
 StructMember::~StructMember() = default;
 
@@ -195,7 +195,7 @@
     auto sym = ctx.dst.st->Register(name_.Name());
     auto* ty = type_->Clone(ctx);
     return ctx.dst.mgr->Get<StructMember>(source_, sym, ty, index_, offset_, align_, size_,
-                                          location_);
+                                          attributes_);
 }
 
 }  // namespace tint::type
diff --git a/src/tint/type/struct.h b/src/tint/type/struct.h
index ea459f5..fc214ba 100644
--- a/src/tint/type/struct.h
+++ b/src/tint/type/struct.h
@@ -22,6 +22,7 @@
 #include <unordered_set>
 
 #include "src/tint/builtin/address_space.h"
+#include "src/tint/builtin/interpolation.h"
 #include "src/tint/symbol.h"
 #include "src/tint/type/node.h"
 #include "src/tint/type/type.h"
@@ -163,6 +164,18 @@
     utils::Vector<const Struct*, 2> concrete_types_;
 };
 
+/// Attributes that can be applied to the StructMember
+struct StructMemberAttributes {
+    /// The value of a `@location` attribute
+    std::optional<uint32_t> location;
+    /// The value of a `@builtin` attribute
+    std::optional<builtin::BuiltinValue> builtin;
+    /// The values of a `@interpolate` attribute
+    std::optional<builtin::Interpolation> interpolation;
+    /// True if the member was annotated with `@invariant`
+    bool invariant = false;
+};
+
 /// StructMember holds the type information for structure members.
 class StructMember : public utils::Castable<StructMember, Node> {
   public:
@@ -174,7 +187,7 @@
     /// @param offset the byte offset from the base of the structure
     /// @param align the byte alignment of the member
     /// @param size the byte size of the member
-    /// @param location the location attribute, if present
+    /// @param attributes the optional attributes
     StructMember(tint::Source source,
                  Symbol name,
                  const type::Type* type,
@@ -182,7 +195,7 @@
                  uint32_t offset,
                  uint32_t align,
                  uint32_t size,
-                 std::optional<uint32_t> location);
+                 const StructMemberAttributes& attributes);
 
     /// Destructor
     ~StructMember() override;
@@ -215,8 +228,8 @@
     /// @returns byte size
     uint32_t Size() const { return size_; }
 
-    /// @returns the location, if set
-    std::optional<uint32_t> Location() const { return location_; }
+    /// @returns the optional attributes
+    const StructMemberAttributes& Attributes() const { return attributes_; }
 
     /// @param ctx the clone context
     /// @returns a clone of this struct member
@@ -231,7 +244,7 @@
     const uint32_t offset_;
     const uint32_t align_;
     const uint32_t size_;
-    const std::optional<uint32_t> location_;
+    const StructMemberAttributes attributes_;
 };
 
 }  // namespace tint::type
diff --git a/src/tint/type/struct_test.cc b/src/tint/type/struct_test.cc
index 7bfb437..d7f68a1 100644
--- a/src/tint/type/struct_test.cc
+++ b/src/tint/type/struct_test.cc
@@ -101,10 +101,8 @@
     auto* sem = p.Sem().Get(st);
     ASSERT_EQ(2u, sem->Members().Length());
 
-    EXPECT_TRUE(sem->Members()[0]->Location().has_value());
-    EXPECT_EQ(sem->Members()[0]->Location().value(), 1u);
-
-    EXPECT_FALSE(sem->Members()[1]->Location().has_value());
+    EXPECT_EQ(sem->Members()[0]->Attributes().location, 1u);
+    EXPECT_FALSE(sem->Members()[1]->Attributes().location.has_value());
 }
 
 TEST_F(TypeStructTest, IsConstructable) {
@@ -207,12 +205,15 @@
 }
 
 TEST_F(TypeStructTest, Clone) {
+    type::StructMemberAttributes attrs_location_2;
+    attrs_location_2.location = 2;
+
     auto* s = create<Struct>(
         Source{}, Sym("my_struct"),
         utils::Vector{create<StructMember>(Source{}, Sym("b"), create<Vector>(create<F32>(), 3u),
-                                           0u, 0u, 16u, 12u, std::optional<uint32_t>{2}),
+                                           0u, 0u, 16u, 12u, attrs_location_2),
                       create<StructMember>(Source{}, Sym("a"), create<I32>(), 1u, 16u, 4u, 4u,
-                                           std::optional<uint32_t>())},
+                                           type::StructMemberAttributes{})},
         4u /* align */, 8u /* size */, 16u /* size_no_padding */);
 
     ProgramID id;
diff --git a/src/tint/type/type_test.cc b/src/tint/type/type_test.cc
index 1565e10..a09ab85 100644
--- a/src/tint/type/type_test.cc
+++ b/src/tint/type/type_test.cc
@@ -56,7 +56,7 @@
                                                    /* offset */ 0u,
                                                    /* align */ 4u,
                                                    /* size */ 4u,
-                                                   /* location */ std::nullopt),
+                                                   /* attributes */ type::StructMemberAttributes{}),
                                            },
                                            /* align*/ 4u,
                                            /* size*/ 4u,
@@ -72,7 +72,7 @@
                                                    /* offset */ 0u,
                                                    /* align */ 4u,
                                                    /* size */ 4u,
-                                                   /* location */ std::nullopt),
+                                                   /* attributes */ type::StructMemberAttributes{}),
                                            },
                                            /* align*/ 4u,
                                            /* size*/ 4u,
@@ -88,7 +88,7 @@
                                             /* offset */ 0u,
                                             /* align */ 4u,
                                             /* size */ 4u,
-                                            /* location */ std::nullopt),
+                                            /* attributes */ type::StructMemberAttributes{}),
                                     },
                                     /* align*/ 4u,
                                     /* size*/ 4u,
diff --git a/src/tint/writer/glsl/generator_impl.h b/src/tint/writer/glsl/generator_impl.h
index 0698d4f..060ea39 100644
--- a/src/tint/writer/glsl/generator_impl.h
+++ b/src/tint/writer/glsl/generator_impl.h
@@ -399,7 +399,7 @@
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
     void EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     void EmitLet(const ast::Let* let);
     /// Handles generating a module-scope 'let' declaration
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 5cce6a1..1d19efe 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -4216,73 +4216,48 @@
             auto* ty = mem->Type();
             auto out = line(b);
             std::string pre, post;
-            if (auto* decl = mem->Declaration()) {
-                for (auto* attr : decl->attributes) {
-                    if (attr->Is<ast::LocationAttribute>()) {
-                        auto& pipeline_stage_uses = str->PipelineStageUses();
-                        if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                            TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
-                        }
 
-                        auto loc = mem->Location().value();
-                        if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kVertexOutput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentInput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (TINT_LIKELY(pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentOutput))) {
-                            post += " : SV_Target" + std::to_string(loc);
-                        } else {
-                            TINT_ICE(Writer, diagnostics_) << "invalid use of location attribute";
-                        }
-                    } else if (auto* builtin_attr = attr->As<ast::BuiltinAttribute>()) {
-                        auto builtin = program_->Sem().Get(builtin_attr)->Value();
-                        auto name = builtin_to_attribute(builtin);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer, "unsupported builtin");
-                            return false;
-                        }
-                        post += " : " + name;
-                    } else if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
-                        auto& sem = program_->Sem();
-                        auto i_type =
-                            sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationType>>(
-                                   interpolate->type)
-                                ->Value();
+            auto& attributes = mem->Attributes();
 
-                        auto i_smpl = builtin::InterpolationSampling::kUndefined;
-                        if (interpolate->sampling) {
-                            i_smpl =
-                                sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationSampling>>(
-                                       interpolate->sampling)
-                                    ->Value();
-                        }
-
-                        auto mod = interpolation_to_modifiers(i_type, i_smpl);
-                        if (mod.empty()) {
-                            diagnostics_.add_error(diag::System::Writer,
-                                                   "unsupported interpolation");
-                            return false;
-                        }
-                        pre += mod;
-
-                    } else if (attr->Is<ast::InvariantAttribute>()) {
-                        // Note: `precise` is not exactly the same as `invariant`, but is
-                        // stricter and therefore provides the necessary guarantees.
-                        // See discussion here: https://github.com/gpuweb/gpuweb/issues/893
-                        pre += "precise ";
-                    } else if (TINT_UNLIKELY((!attr->IsAnyOf<ast::StructMemberAlignAttribute,
-                                                             ast::StructMemberOffsetAttribute,
-                                                             ast::StructMemberSizeAttribute>()))) {
-                        TINT_ICE(Writer, diagnostics_)
-                            << "unhandled struct member attribute: " << attr->Name();
-                        return false;
-                    }
+            if (auto location = attributes.location) {
+                auto& pipeline_stage_uses = str->PipelineStageUses();
+                if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                    TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
                 }
+                if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexOutput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentInput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (TINT_LIKELY(pipeline_stage_uses.count(
+                               type::PipelineStageUsage::kFragmentOutput))) {
+                    post += " : SV_Target" + std::to_string(location.value());
+                } else {
+                    TINT_ICE(Writer, diagnostics_) << "invalid use of location attribute";
+                }
+            }
+            if (auto builtin = attributes.builtin) {
+                auto name = builtin_to_attribute(builtin.value());
+                if (name.empty()) {
+                    diagnostics_.add_error(diag::System::Writer, "unsupported builtin");
+                    return false;
+                }
+                post += " : " + name;
+            }
+            if (auto interpolation = attributes.interpolation) {
+                auto mod = interpolation_to_modifiers(interpolation->type, interpolation->sampling);
+                if (mod.empty()) {
+                    diagnostics_.add_error(diag::System::Writer, "unsupported interpolation");
+                    return false;
+                }
+                pre += mod;
+            }
+            if (attributes.invariant) {
+                // Note: `precise` is not exactly the same as `invariant`, but is
+                // stricter and therefore provides the necessary guarantees.
+                // See discussion here: https://github.com/gpuweb/gpuweb/issues/893
+                pre += "precise ";
             }
 
             out << pre;
diff --git a/src/tint/writer/hlsl/generator_impl.h b/src/tint/writer/hlsl/generator_impl.h
index 103e355..9a330df 100644
--- a/src/tint/writer/hlsl/generator_impl.h
+++ b/src/tint/writer/hlsl/generator_impl.h
@@ -470,7 +470,7 @@
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
     bool EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     /// @returns true if the variable was emitted
     bool EmitLet(const ast::Let* let);
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 0435918..c0ba336 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -2852,88 +2852,51 @@
 
         out << " " << mem_name;
         // Emit attributes
-        if (auto* decl = mem->Declaration()) {
-            for (auto* attr : decl->attributes) {
-                bool ok = Switch(
-                    attr,
-                    [&](const ast::BuiltinAttribute* builtin_attr) {
-                        auto builtin = program_->Sem().Get(builtin_attr)->Value();
-                        auto name = builtin_to_attribute(builtin);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer, "unknown builtin");
-                            return false;
-                        }
-                        out << " [[" << name << "]]";
-                        return true;
-                    },
-                    [&](const ast::LocationAttribute*) {
-                        auto& pipeline_stage_uses = str->PipelineStageUses();
-                        if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                            TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
-                            return false;
-                        }
+        auto& attributes = mem->Attributes();
 
-                        uint32_t loc = mem->Location().value();
-                        if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
-                            out << " [[attribute(" + std::to_string(loc) + ")]]";
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kVertexOutput)) {
-                            out << " [[user(locn" + std::to_string(loc) + ")]]";
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentInput)) {
-                            out << " [[user(locn" + std::to_string(loc) + ")]]";
-                        } else if (TINT_LIKELY(pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentOutput))) {
-                            out << " [[color(" + std::to_string(loc) + ")]]";
-                        } else {
-                            TINT_ICE(Writer, diagnostics_) << "invalid use of location decoration";
-                            return false;
-                        }
-                        return true;
-                    },
-                    [&](const ast::InterpolateAttribute* interpolate) {
-                        auto& sem = program_->Sem();
-                        auto i_type =
-                            sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationType>>(
-                                   interpolate->type)
-                                ->Value();
-
-                        auto i_smpl = builtin::InterpolationSampling::kUndefined;
-                        if (interpolate->sampling) {
-                            i_smpl =
-                                sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationSampling>>(
-                                       interpolate->sampling)
-                                    ->Value();
-                        }
-
-                        auto name = interpolation_to_attribute(i_type, i_smpl);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer,
-                                                   "unknown interpolation attribute");
-                            return false;
-                        }
-                        out << " [[" << name << "]]";
-                        return true;
-                    },
-                    [&](const ast::InvariantAttribute*) {
-                        if (invariant_define_name_.empty()) {
-                            invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
-                        }
-                        out << " " << invariant_define_name_;
-                        return true;
-                    },
-                    [&](const ast::StructMemberOffsetAttribute*) { return true; },
-                    [&](const ast::StructMemberAlignAttribute*) { return true; },
-                    [&](const ast::StructMemberSizeAttribute*) { return true; },
-                    [&](Default) {
-                        TINT_ICE(Writer, diagnostics_)
-                            << "unhandled struct member attribute: " << attr->Name();
-                        return false;
-                    });
-                if (!ok) {
-                    return false;
-                }
+        if (auto builtin = attributes.builtin) {
+            auto name = builtin_to_attribute(builtin.value());
+            if (name.empty()) {
+                diagnostics_.add_error(diag::System::Writer, "unknown builtin");
+                return false;
             }
+            out << " [[" << name << "]]";
+        }
+
+        if (auto location = attributes.location) {
+            auto& pipeline_stage_uses = str->PipelineStageUses();
+            if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
+                return false;
+            }
+
+            if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
+                out << " [[attribute(" + std::to_string(location.value()) + ")]]";
+            } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexOutput)) {
+                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+            } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentInput)) {
+                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+            } else if (TINT_LIKELY(
+                           pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentOutput))) {
+                out << " [[color(" + std::to_string(location.value()) + ")]]";
+            } else {
+                TINT_ICE(Writer, diagnostics_) << "invalid use of location decoration";
+                return false;
+            }
+        }
+
+        if (auto interpolation = attributes.interpolation) {
+            auto name = interpolation_to_attribute(interpolation->type, interpolation->sampling);
+            if (name.empty()) {
+                diagnostics_.add_error(diag::System::Writer, "unknown interpolation attribute");
+                return false;
+            }
+            out << " [[" << name << "]]";
+        }
+
+        if (attributes.invariant) {
+            invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
+            out << " " << invariant_define_name_;
         }
 
         out << ";";
diff --git a/src/tint/writer/msl/generator_impl.h b/src/tint/writer/msl/generator_impl.h
index 5699242..db5f310 100644
--- a/src/tint/writer/msl/generator_impl.h
+++ b/src/tint/writer/msl/generator_impl.h
@@ -350,7 +350,7 @@
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
     bool EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     /// @returns true if the variable was emitted
     bool EmitLet(const ast::Let* let);