Import Tint changes from Dawn

Changes:
  - e50f779c2dd2cebf696366812dcaf984208a53fd [tint][resolver] Fix dual source blending validation by Ben Clayton <bclayton@google.com>
  - e30eacb0b6e8725e79c283908b7cb0c7f219ac44 [tint][resolver] Tweak diagnostic messages by Ben Clayton <bclayton@google.com>
  - 65e3824a1fc0401a10b3c8a26a461d0613bfdffc [tint][sem] Move variable attributes to separate structs by Ben Clayton <bclayton@google.com>
  - 3e56a384d541a9340164373c5640585401933b14 [tint] Add chromium_experimental_framebuffer_fetch by Ben Clayton <bclayton@google.com>
  - 9cd1619dd2d3f4290127cad201795e9325f257dc [tint] Comment the fields of core::type::StructMemberAttr... by Ben Clayton <bclayton@google.com>
  - de3f95a16bca110f452acdc80296dc9a27ab84cb [tint][resolver] Fix @index validation, tweak diagnostics by Ben Clayton <bclayton@google.com>
  - 3f8fb00ff16b80d8b7881d586cd3729640f67b1e [tint][resolver] Standardise attribute diagnostics by Ben Clayton <bclayton@google.com>
  - 0e29fcb8d4fb5820e1aab9cae8dfb48747c92570 [tint][resolver] Clean up attribute validation tests by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: e50f779c2dd2cebf696366812dcaf984208a53fd
Change-Id: I35111336b878abc204403fbc0a592c19d6a7e7a1
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/160240
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/cmd/common/generate_external_texture_bindings.cc b/src/tint/cmd/common/generate_external_texture_bindings.cc
index 7002faa..fdb7704 100644
--- a/src/tint/cmd/common/generate_external_texture_bindings.cc
+++ b/src/tint/cmd/common/generate_external_texture_bindings.cc
@@ -48,7 +48,7 @@
     std::vector<tint::BindingPoint> ext_tex_bps;
     for (auto* var : program.AST().GlobalVariables()) {
         if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-            if (auto bp = sem_var->BindingPoint()) {
+            if (auto bp = sem_var->Attributes().binding_point) {
                 auto& n = group_to_next_binding_number[bp->group];
                 n = std::max(n, bp->binding + 1);
 
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 8f36a89..e6ae17b 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -284,7 +284,7 @@
         std::vector<BindingPoint> ext_tex_bps;
         for (auto* var : program.AST().GlobalVariables()) {
             if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-                if (auto bp = sem_var->BindingPoint()) {
+                if (auto bp = sem_var->Attributes().binding_point) {
                     auto& n = group_to_next_binding_number[bp->group];
                     n = std::max(n, bp->binding + 1);
 
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index 0fd0ac8..368dcf0 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -119,7 +119,13 @@
         if (func->Stage() == Function::PipelineStage::kVertex && backend->NeedsVertexPointSize()) {
             vertex_point_size_index =
                 backend->AddOutput(ir.symbols.New("vertex_point_size"), ty.f32(),
-                                   {{}, {}, {BuiltinValue::kPointSize}, {}, false});
+                                   core::type::StructMemberAttributes{
+                                       /* location */ {},
+                                       /* index */ {},
+                                       /* builtin */ core::BuiltinValue::kPointSize,
+                                       /* interpolation */ {},
+                                       /* invariant */ false,
+                                   });
         }
 
         auto new_params = backend->FinalizeInputs();
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
index 0cf6dc4..e16f35e 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
@@ -1486,20 +1486,31 @@
 TEST_F(IR_ZeroInitWorkgroupMemoryTest, ExistingLocalInvocationIndexInStruct) {
     auto* var = MakeVar("wgvar", ty.bool_());
 
-    auto* structure =
-        ty.Struct(mod.symbols.New("MyStruct"),
-                  {
-                      {
-                          mod.symbols.New("global_id"),
-                          ty.vec3<u32>(),
-                          {{}, {}, core::BuiltinValue::kGlobalInvocationId, {}, false},
-                      },
-                      {
-                          mod.symbols.New("index"),
-                          ty.u32(),
-                          {{}, {}, core::BuiltinValue::kLocalInvocationIndex, {}, false},
-                      },
-                  });
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"),
+                                {
+                                    {
+                                        mod.symbols.New("global_id"),
+                                        ty.vec3<u32>(),
+                                        core::type::StructMemberAttributes{
+                                            /* location */ {},
+                                            /* index */ {},
+                                            /* builtin */ core::BuiltinValue::kGlobalInvocationId,
+                                            /* interpolation */ {},
+                                            /* invariant */ false,
+                                        },
+                                    },
+                                    {
+                                        mod.symbols.New("index"),
+                                        ty.u32(),
+                                        core::type::StructMemberAttributes{
+                                            /* location */ {},
+                                            /* index */ {},
+                                            /* builtin */ core::BuiltinValue::kLocalInvocationIndex,
+                                            /* interpolation */ {},
+                                            /* invariant */ false,
+                                        },
+                                    },
+                                });
     auto* func = MakeEntryPoint("main", 1, 1, 1);
     func->SetParams({b.FunctionParam("params", structure)});
     b.Append(func->Block(), [&] {  //
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index b0c7219..738844e 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -1929,7 +1929,7 @@
         TINT_ICE() << "storage variable must be of struct type";
         return;
     }
-    auto bp = *sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto bp = *sem->As<sem::GlobalVariable>()->Attributes().binding_point;
     {
         auto out = Line();
         out << "layout(binding = " << bp.binding << ", std140";
@@ -1948,7 +1948,7 @@
         TINT_ICE() << "storage variable must be of struct type";
         return;
     }
-    auto bp = *sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto bp = *sem->As<sem::GlobalVariable>()->Attributes().binding_point;
     Line() << "layout(binding = " << bp.binding << ", std430) buffer "
            << UniqueIdentifier(StructName(str) + "_ssbo") << " {";
     EmitStructMembers(current_buffer_, str);
@@ -2080,7 +2080,7 @@
     }
 
     auto out = Line();
-    EmitAttributes(out, var, decl->attributes);
+    EmitAttributes(out, var);
     EmitInterpolationQualifiers(out, decl->attributes);
 
     auto name = decl->name->symbol.Name();
@@ -2130,23 +2130,17 @@
     }
 }
 
-void ASTPrinter::EmitAttributes(StringStream& out,
-                                const sem::GlobalVariable* var,
-                                VectorRef<const ast::Attribute*> attributes) {
-    if (attributes.IsEmpty()) {
-        return;
-    }
+void ASTPrinter::EmitAttributes(StringStream& out, const sem::GlobalVariable* var) {
+    auto& attrs = var->Attributes();
 
     bool first = true;
-    for (auto* attr : attributes) {
-        if (attr->As<ast::LocationAttribute>()) {
-            out << (first ? "layout(" : ", ");
-            out << "location = " << std::to_string(var->Location().value());
-            first = false;
-        }
-        if (attr->As<ast::IndexAttribute>()) {
-            out << ", index = " << std::to_string(var->Index().value());
-        }
+    if (attrs.location.has_value()) {
+        out << (first ? "layout(" : ", ");
+        out << "location = " << std::to_string(attrs.location.value());
+        first = false;
+    }
+    if (attrs.index.has_value()) {
+        out << ", index = " << std::to_string(attrs.index.value());
     }
     if (!first) {
         out << ") ";
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
index 519bdf8..f5cc633 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
@@ -317,10 +317,7 @@
     /// Handles emitting attributes
     /// @param out the output of the expression stream
     /// @param var the global variable semantics
-    /// @param attrs the attributes
-    void EmitAttributes(StringStream& out,
-                        const sem::GlobalVariable* var,
-                        VectorRef<const ast::Attribute*> attrs);
+    void EmitAttributes(StringStream& out, const sem::GlobalVariable* var);
     /// Handles emitting the entry point function
     /// @param func the entry point
     void EmitEntryPointFunction(const ast::Function* func);
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
index b41e960..b056310 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
@@ -118,10 +118,10 @@
                                               std::string name) {
         SamplerTexturePair bp_pair;
         bp_pair.texture_binding_point =
-            texture_var ? *texture_var->As<sem::GlobalVariable>()->BindingPoint()
+            texture_var ? *texture_var->As<sem::GlobalVariable>()->Attributes().binding_point
                         : binding_info->placeholder_binding_point;
         bp_pair.sampler_binding_point =
-            sampler_var ? *sampler_var->As<sem::GlobalVariable>()->BindingPoint()
+            sampler_var ? *sampler_var->As<sem::GlobalVariable>()->Attributes().binding_point
                         : binding_info->placeholder_binding_point;
         auto it = binding_info->binding_map.find(bp_pair);
         if (it != binding_info->binding_map.end()) {
@@ -232,7 +232,7 @@
             if (tint::IsAnyOf<core::type::Texture, core::type::Sampler>(type) &&
                 !type->Is<core::type::StorageTexture>()) {
                 ctx.Remove(ctx.src->AST().GlobalDeclarations(), global);
-            } else if (auto binding_point = global_sem->BindingPoint()) {
+            } else if (auto binding_point = global_sem->Attributes().binding_point) {
                 if (binding_point->group == 0 && binding_point->binding == 0) {
                     auto* attribute =
                         ctx.dst->Disable(ast::DisabledValidation::kBindingPointCollision);
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
index 67eb338..bc40549 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
@@ -366,7 +366,7 @@
                 auto* global_sem = sem.Get<sem::GlobalVariable>(var);
 
                 // The original binding point
-                BindingPoint binding_point = *global_sem->BindingPoint();
+                BindingPoint binding_point = *global_sem->Attributes().binding_point;
 
                 if (binding_point == cfg->ubo_binding) {
                     // This ubo_binding struct already exists.
@@ -423,7 +423,7 @@
     /// @returns binding of the global variable.
     BindingPoint GetAndRecordGlobalBinding(const sem::GlobalVariable* global,
                                            TextureBuiltinsFromUniformOptions::Field field) {
-        auto binding = global->BindingPoint().value();
+        auto binding = global->Attributes().binding_point.value();
         auto iter = bindpoint_to_data.find(binding);
         if (iter == bindpoint_to_data.end()) {
             // First visit, recording the binding.
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index b23c19f..6ee9b5a 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -3331,7 +3331,7 @@
 }
 
 bool ASTPrinter::EmitUniformVariable(const ast::Var* var, const sem::Variable* sem) {
-    auto binding_point = *sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto binding_point = *sem->As<sem::GlobalVariable>()->Attributes().binding_point;
     auto* type = sem->Type()->UnwrapRef();
     auto name = var->name->symbol.Name();
     Line() << "cbuffer cbuffer_" << name << RegisterAndSpace('b', binding_point) << " {";
@@ -3360,7 +3360,7 @@
 
     auto* global_sem = sem->As<sem::GlobalVariable>();
     out << RegisterAndSpace(sem->Access() == core::Access::kRead ? 't' : 'u',
-                            *global_sem->BindingPoint())
+                            *global_sem->Attributes().binding_point)
         << ";";
 
     return true;
@@ -3389,7 +3389,7 @@
     }
 
     if (register_space) {
-        auto bp = sem->As<sem::GlobalVariable>()->BindingPoint();
+        auto bp = sem->As<sem::GlobalVariable>()->Attributes().binding_point;
         out << " : register(" << register_space << bp->binding;
         // Omit the space if it's 0, as it's the default.
         // SM 5.0 doesn't support spaces, so we don't emit them if group is 0 for better
diff --git a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
index 31590d9..22a8ff3 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
@@ -165,7 +165,7 @@
 
                 for (auto* global : src.AST().GlobalVariables()) {
                     auto* global_sem = src.Sem().Get<sem::GlobalVariable>(global);
-                    if (auto bp = global_sem->BindingPoint()) {
+                    if (auto bp = global_sem->Attributes().binding_point) {
                         if (bp->group >= group) {
                             group = bp->group + 1;
                         }
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index aed7504..6ef2020 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -1970,10 +1970,9 @@
             return kInvalidBindingIndex;
         }
         auto* param_sem = builder_.Sem().Get<sem::Parameter>(param);
-        auto bp = param_sem->BindingPoint();
+        auto bp = param_sem->Attributes().binding_point;
         if (TINT_UNLIKELY(bp->group != 0)) {
-            TINT_ICE() << "encountered non-zero resource group index (use "
-                          "BindingRemapper to fix)";
+            TINT_ICE() << "encountered non-zero resource group index (use BindingRemapper to fix)";
             return kInvalidBindingIndex;
         }
         return bp->binding;
diff --git a/src/tint/lang/msl/writer/writer_bench.cc b/src/tint/lang/msl/writer/writer_bench.cc
index 2705e93..0af1f7b 100644
--- a/src/tint/lang/msl/writer/writer_bench.cc
+++ b/src/tint/lang/msl/writer/writer_bench.cc
@@ -65,7 +65,7 @@
     uint32_t next_binding_point = 0;
     for (auto* var : program.AST().GlobalVariables()) {
         if (auto* var_sem = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-            if (auto bp = var_sem->BindingPoint()) {
+            if (auto bp = var_sem->Attributes().binding_point) {
                 gen_options.binding_remapper_options.binding_points[*bp] = BindingPoint{
                     0,                     // group
                     next_binding_point++,  // binding
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 734c380..a75d703 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -806,13 +806,13 @@
             [&](const ast::LocationAttribute*) {
                 module_.PushAnnot(spv::Op::OpDecorate,
                                   {Operand(var_id), U32Operand(SpvDecorationLocation),
-                                   Operand(sem->Location().value())});
+                                   Operand(sem->Attributes().location.value())});
                 return true;
             },
             [&](const ast::IndexAttribute*) {
                 module_.PushAnnot(spv::Op::OpDecorate,
                                   {Operand(var_id), U32Operand(SpvDecorationIndex),
-                                   Operand(sem->Index().value())});
+                                   Operand(sem->Attributes().index.value())});
                 return true;
             },
             [&](const ast::InterpolateAttribute* interpolate) {
@@ -837,14 +837,14 @@
                 return true;
             },
             [&](const ast::BindingAttribute*) {
-                auto bp = sem->BindingPoint();
+                auto bp = sem->Attributes().binding_point;
                 module_.PushAnnot(
                     spv::Op::OpDecorate,
                     {Operand(var_id), U32Operand(SpvDecorationBinding), Operand(bp->binding)});
                 return true;
             },
             [&](const ast::GroupAttribute*) {
-                auto bp = sem->BindingPoint();
+                auto bp = sem->Attributes().binding_point;
                 module_.PushAnnot(
                     spv::Op::OpDecorate,
                     {Operand(var_id), U32Operand(SpvDecorationDescriptorSet), Operand(bp->group)});
diff --git a/src/tint/lang/spirv/writer/function_test.cc b/src/tint/lang/spirv/writer/function_test.cc
index 890ddff..fc66154 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -347,11 +347,31 @@
 }
 
 TEST_F(SpirvWriterTest, Function_ShaderIO_DualSourceBlend) {
-    auto* outputs = ty.Struct(mod.symbols.New("Outputs"),
-                              {
-                                  {mod.symbols.Register("a"), ty.f32(), {0u, 0u, {}, {}, false}},
-                                  {mod.symbols.Register("b"), ty.f32(), {0u, 1u, {}, {}, false}},
-                              });
+    auto* outputs =
+        ty.Struct(mod.symbols.New("Outputs"), {
+                                                  {
+                                                      mod.symbols.Register("a"),
+                                                      ty.f32(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 0u,
+                                                          /* index */ 0u,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                                  {
+                                                      mod.symbols.Register("b"),
+                                                      ty.f32(),
+                                                      core::type::StructMemberAttributes{
+                                                          /* location */ 0u,
+                                                          /* index */ 1u,
+                                                          /* builtin */ std::nullopt,
+                                                          /* interpolation */ std::nullopt,
+                                                          /* invariant */ false,
+                                                      },
+                                                  },
+                                              });
 
     auto* func = b.Function("main", outputs, core::ir::Function::PipelineStage::kFragment);
     b.Append(func->Block(), [&] {  //
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
index 21ef88b..076a594 100644
--- a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
@@ -55,7 +55,7 @@
     Vector<tint::BindingPoint, 4> ext_tex_bps;
     for (auto* var : program.AST().GlobalVariables()) {
         if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-            if (auto bp = sem_var->BindingPoint()) {
+            if (auto bp = sem_var->Attributes().binding_point) {
                 if (auto val = group_to_next_binding_number.Find(bp->group)) {
                     *val = std::max(*val, bp->binding + 1);
                 } else {
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index 4dcd4a3..edbde41 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -151,27 +151,50 @@
                                  {
                                      mod.symbols.New("front_facing"),
                                      ty.bool_(),
-                                     {{}, {}, core::BuiltinValue::kFrontFacing, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kFrontFacing,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("position"),
                                      ty.vec4<f32>(),
-                                     {{}, {}, core::BuiltinValue::kPosition, {}, true},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kPosition,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ true,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color1"),
                                      ty.f32(),
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color2"),
                                      ty.f32(),
-                                     {1u,
-                                      {},
-                                      {},
-                                      core::Interpolation{core::InterpolationType::kLinear,
-                                                          core::InterpolationSampling::kSample},
-                                      false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 1u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */
+                                         core::Interpolation{
+                                             core::InterpolationType::kLinear,
+                                             core::InterpolationSampling::kSample,
+                                         },
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -276,12 +299,24 @@
                                  {
                                      mod.symbols.New("position"),
                                      ty.vec4<f32>(),
-                                     {{}, {}, core::BuiltinValue::kPosition, {}, true},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kPosition,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ true,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color1"),
                                      ty.f32(),
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -476,22 +511,39 @@
                                  {
                                      mod.symbols.New("position"),
                                      ty.vec4<f32>(),
-                                     {{}, {}, core::BuiltinValue::kPosition, {}, true},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kPosition,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ true,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color1"),
                                      ty.f32(),
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color2"),
                                      ty.f32(),
-                                     {1u,
-                                      {},
-                                      {},
-                                      core::Interpolation{core::InterpolationType::kLinear,
-                                                          core::InterpolationSampling::kSample},
-                                      false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 1u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */
+                                         core::Interpolation{
+                                             core::InterpolationType::kLinear,
+                                             core::InterpolationSampling::kSample,
+                                         },
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -561,18 +613,31 @@
 }
 
 TEST_F(SpirvWriter_ShaderIOTest, ReturnValue_DualSourceBlending) {
-    auto* str_ty = ty.Struct(mod.symbols.New("Output"), {
-                                                            {
-                                                                mod.symbols.New("color1"),
-                                                                ty.f32(),
-                                                                {0u, 0u, {}, {}, false},
-                                                            },
-                                                            {
-                                                                mod.symbols.New("color2"),
-                                                                ty.f32(),
-                                                                {0u, 1u, {}, {}, false},
-                                                            },
-                                                        });
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("Output"), {
+                                                 {
+                                                     mod.symbols.New("color1"),
+                                                     ty.f32(),
+                                                     core::type::StructMemberAttributes{
+                                                         /* location */ 0u,
+                                                         /* index */ 0u,
+                                                         /* builtin */ std::nullopt,
+                                                         /* interpolation */ std::nullopt,
+                                                         /* invariant */ false,
+                                                     },
+                                                 },
+                                                 {
+                                                     mod.symbols.New("color2"),
+                                                     ty.f32(),
+                                                     core::type::StructMemberAttributes{
+                                                         /* location */ 0u,
+                                                         /* index */ 1u,
+                                                         /* builtin */ std::nullopt,
+                                                         /* interpolation */ std::nullopt,
+                                                         /* invariant */ false,
+                                                     },
+                                                 },
+                                             });
 
     auto* ep = b.Function("foo", str_ty);
     ep->SetStage(core::ir::Function::PipelineStage::kFragment);
@@ -639,12 +704,24 @@
                                  {
                                      mod.symbols.New("position"),
                                      vec4f,
-                                     {{}, {}, core::BuiltinValue::kPosition, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kPosition,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color"),
                                      vec4f,
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -766,12 +843,24 @@
                                  {
                                      mod.symbols.New("position"),
                                      vec4f,
-                                     {{}, {}, core::BuiltinValue::kPosition, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kPosition,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("color"),
                                      vec4f,
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -847,12 +936,24 @@
                                  {
                                      mod.symbols.New("color"),
                                      ty.f32(),
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("mask"),
                                      ty.u32(),
-                                     {{}, {}, core::BuiltinValue::kSampleMask, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kSampleMask,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
@@ -924,19 +1025,24 @@
 
 // Test that interpolation attributes are stripped from vertex inputs and fragment outputs.
 TEST_F(SpirvWriter_ShaderIOTest, InterpolationOnVertexInputOrFragmentOutput) {
-    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"),
-                             {
-                                 {
-                                     mod.symbols.New("color"),
-                                     ty.f32(),
-                                     {1u,
-                                      {},
-                                      {},
-                                      core::Interpolation{core::InterpolationType::kLinear,
-                                                          core::InterpolationSampling::kSample},
-                                      false},
-                                 },
-                             });
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {
+                                                       mod.symbols.New("color"),
+                                                       ty.f32(),
+                                                       core::type::StructMemberAttributes{
+                                                           /* location */ 1u,
+                                                           /* index */ std::nullopt,
+                                                           /* builtin */ std::nullopt,
+                                                           /* interpolation */
+                                                           core::Interpolation{
+                                                               core::InterpolationType::kLinear,
+                                                               core::InterpolationSampling::kSample,
+                                                           },
+                                                           /* invariant */ false,
+                                                       },
+                                                   },
+                                               });
 
     // Vertex shader.
     {
@@ -1071,12 +1177,24 @@
                                  {
                                      mod.symbols.New("color"),
                                      ty.f32(),
-                                     {0u, {}, {}, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ 0u,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ std::nullopt,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                                  {
                                      mod.symbols.New("depth"),
                                      ty.f32(),
-                                     {{}, {}, core::BuiltinValue::kFragDepth, {}, false},
+                                     core::type::StructMemberAttributes{
+                                         /* location */ std::nullopt,
+                                         /* index */ std::nullopt,
+                                         /* builtin */ core::BuiltinValue::kFragDepth,
+                                         /* interpolation */ std::nullopt,
+                                         /* invariant */ false,
+                                     },
                                  },
                              });
 
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
index 2f617c1..d98068d 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
@@ -100,7 +100,7 @@
 
         IterateArrayLengthOnStorageVar(
             [&](const CallExpression*, const sem::VariableUser*, const sem::GlobalVariable* var) {
-                if (auto binding = var->BindingPoint()) {
+                if (auto binding = var->Attributes().binding_point) {
                     auto idx_itr = cfg->bindpoint_to_size_index.find(*binding);
                     if (idx_itr == cfg->bindpoint_to_size_index.end()) {
                         return;
@@ -138,7 +138,7 @@
         IterateArrayLengthOnStorageVar([&](const CallExpression* call_expr,
                                            const sem::VariableUser* storage_buffer_sem,
                                            const sem::GlobalVariable* var) {
-            auto binding = var->BindingPoint();
+            auto binding = var->Attributes().binding_point;
             if (!binding) {
                 return;
             }
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
index 3ff5e0a..b2ec92a 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
@@ -89,7 +89,7 @@
             auto* func = src.Sem().Get(func_ast);
             std::unordered_map<BindingPoint, int> binding_point_counts;
             for (auto* global : func->TransitivelyReferencedGlobals()) {
-                if (auto from = global->BindingPoint()) {
+                if (auto from = global->Attributes().binding_point) {
                     auto bp_it = remappings->binding_points.find(*from);
                     if (bp_it != remappings->binding_points.end()) {
                         // Remapped
@@ -113,7 +113,7 @@
             auto* global_sem = src.Sem().Get<sem::GlobalVariable>(var);
 
             // The original binding point
-            BindingPoint from = *global_sem->BindingPoint();
+            BindingPoint from = *global_sem->Attributes().binding_point;
 
             // The binding point after remapping
             BindingPoint bp = from;
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 4643176..45c270c 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -428,7 +428,8 @@
         }
 
         auto name = param->Declaration()->name->symbol.Name();
-        auto* input_expr = AddInput(name, param->Type(), param->Location(), std::move(attributes));
+        auto* input_expr =
+            AddInput(name, param->Type(), param->Attributes().location, std::move(attributes));
         inner_call_parameters.Push(input_expr);
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
index c463dbc..0a5f85e 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
@@ -131,7 +131,7 @@
             // The binding points for the newly introduced bindings must have been provided to this
             // transform. We fetch the new binding points by providing the original texture_external
             // binding points into the passed map.
-            BindingPoint bp = *sem_var->BindingPoint();
+            BindingPoint bp = *sem_var->Attributes().binding_point;
 
             BindingsMap::const_iterator it = new_binding_points->bindings_map.find(bp);
             if (it == new_binding_points->bindings_map.end()) {
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.cc b/src/tint/lang/wgsl/ast/transform/robustness.cc
index c984b29..0bc1575 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness.cc
@@ -699,11 +699,11 @@
         if (globalVariable == nullptr) {
             return false;
         }
-        if (!globalVariable->BindingPoint().has_value()) {
+        auto binding_point = globalVariable->Attributes().binding_point;
+        if (!binding_point.has_value()) {
             return false;
         }
-        BindingPoint bindingPoint = *globalVariable->BindingPoint();
-        return cfg.bindings_ignored.find(bindingPoint) != cfg.bindings_ignored.cend();
+        return cfg.bindings_ignored.find(*binding_point) != cfg.bindings_ignored.cend();
     }
 
     /// @returns true if expr is an IndexAccessorExpression whose object is a runtime-sized array.
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index daae6f8..3311ce4 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -105,7 +105,7 @@
                         // so that its allocated ID so that it won't be affected by other
                         // stripped away overrides
                         auto* global = sem.Get(override);
-                        const auto* id = b.Id(global->OverrideId());
+                        const auto* id = b.Id(global->Attributes().override_id.value());
                         ctx.InsertFront(override->attributes, id);
                     }
                     b.AST().AddGlobalVariable(ctx.Clone(override));
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.cc b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
index 75dd9df..a36bc65 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.cc
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
@@ -87,7 +87,7 @@
         Type ty = w->type ? ctx.Clone(w->type) : Type{};
 
         // No replacement provided, just clone the override node as a const.
-        auto iter = data->map.find(sem->OverrideId());
+        auto iter = data->map.find(sem->Attributes().override_id.value());
         if (iter == data->map.end()) {
             if (!w->initializer) {
                 b.Diagnostics().add_error(
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index 145d053..0ebf6fa 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -796,11 +796,11 @@
             auto* sem = src.Sem().Get<sem::Parameter>(param);
             info.type = sem->Type();
 
-            if (TINT_UNLIKELY(!sem->Location().has_value())) {
+            if (TINT_UNLIKELY(!sem->Attributes().location.has_value())) {
                 TINT_ICE() << "Location missing value";
                 return;
             }
-            location_info[sem->Location().value()] = info;
+            location_info[sem->Attributes().location.value()] = info;
         } else {
             auto* builtin_attr = GetAttribute<BuiltinAttribute>(param->attributes);
             if (TINT_UNLIKELY(!builtin_attr)) {
diff --git a/src/tint/lang/wgsl/extension.cc b/src/tint/lang/wgsl/extension.cc
index a9f363c..e30d7b2 100644
--- a/src/tint/lang/wgsl/extension.cc
+++ b/src/tint/lang/wgsl/extension.cc
@@ -48,6 +48,9 @@
     if (str == "chromium_experimental_dp4a") {
         return Extension::kChromiumExperimentalDp4A;
     }
+    if (str == "chromium_experimental_framebuffer_fetch") {
+        return Extension::kChromiumExperimentalFramebufferFetch;
+    }
     if (str == "chromium_experimental_full_ptr_parameters") {
         return Extension::kChromiumExperimentalFullPtrParameters;
     }
@@ -83,6 +86,8 @@
             return "chromium_disable_uniformity_analysis";
         case Extension::kChromiumExperimentalDp4A:
             return "chromium_experimental_dp4a";
+        case Extension::kChromiumExperimentalFramebufferFetch:
+            return "chromium_experimental_framebuffer_fetch";
         case Extension::kChromiumExperimentalFullPtrParameters:
             return "chromium_experimental_full_ptr_parameters";
         case Extension::kChromiumExperimentalPixelLocal:
diff --git a/src/tint/lang/wgsl/extension.h b/src/tint/lang/wgsl/extension.h
index 931c1ad..0e259f1 100644
--- a/src/tint/lang/wgsl/extension.h
+++ b/src/tint/lang/wgsl/extension.h
@@ -48,6 +48,7 @@
     kUndefined,
     kChromiumDisableUniformityAnalysis,
     kChromiumExperimentalDp4A,
+    kChromiumExperimentalFramebufferFetch,
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPixelLocal,
     kChromiumExperimentalPushConstant,
@@ -76,17 +77,24 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr std::string_view kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",      "chromium_experimental_dp4a",
-    "chromium_experimental_full_ptr_parameters", "chromium_experimental_pixel_local",
-    "chromium_experimental_push_constant",       "chromium_experimental_read_write_storage_texture",
-    "chromium_experimental_subgroups",           "chromium_internal_dual_source_blending",
-    "chromium_internal_relaxed_uniform_layout",  "f16",
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_dp4a",
+    "chromium_experimental_framebuffer_fetch",
+    "chromium_experimental_full_ptr_parameters",
+    "chromium_experimental_pixel_local",
+    "chromium_experimental_push_constant",
+    "chromium_experimental_read_write_storage_texture",
+    "chromium_experimental_subgroups",
+    "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout",
+    "f16",
 };
 
 /// All extensions
 static constexpr Extension kAllExtensions[] = {
     Extension::kChromiumDisableUniformityAnalysis,
     Extension::kChromiumExperimentalDp4A,
+    Extension::kChromiumExperimentalFramebufferFetch,
     Extension::kChromiumExperimentalFullPtrParameters,
     Extension::kChromiumExperimentalPixelLocal,
     Extension::kChromiumExperimentalPushConstant,
diff --git a/src/tint/lang/wgsl/extension_bench.cc b/src/tint/lang/wgsl/extension_bench.cc
index 5d7a6d9..06b7f67 100644
--- a/src/tint/lang/wgsl/extension_bench.cc
+++ b/src/tint/lang/wgsl/extension_bench.cc
@@ -59,62 +59,69 @@
         "chromium_exverimentiil_dp4a",
         "chro8ium_experimenWWal_dp4a",
         "chromiMm_eperimxxntal_dp4a",
-        "chromium_expeggimeXtal_full_ptr_paraeters",
-        "chromium_expVrimental_full_ptr_puraXeer",
-        "chromium_experimental_full_ptr3parameters",
+        "cXromium_expermental_framggbuffer_fetch",
+        "chVomiu_experimntal_fXauebuffer_fetch",
+        "chromium_experimental_fr3mebuffer_fetch",
+        "chromium_experimental_framebuffer_fetch",
+        "chromium_experimental_fraEebuffer_fetch",
+        "chromTTum_experimental_fraebuffePP_fetch",
+        "chromum_experiddental_framebxxffer_fetch",
+        "chromium_experimental_full_ptr_p44rameters",
+        "SShromium_experimental_full_ptr_parameVVers",
+        "chroRium_expRrimental_fu22l_ptr_prameters",
         "chromium_experimental_full_ptr_parameters",
-        "chromium_experimentalEfull_ptr_parameters",
-        "chromium_experimentalfull_ptr_PPaTTameters",
-        "chromium_ddxperimental_fullptrxxparameters",
-        "chromium_experi44ental_pixel_local",
-        "chromium_experimental_VVSixel_local",
-        "chroRium_experimental_pix22Rlocal",
+        "chromium_experimFntal_full_ptr_paramet9r",
+        "chromium_experimentl_full_ptr_parameters",
+        "chromiuHexperimental_fulOO_pVr_paramRRters",
+        "chromium_experimenya_pixel_local",
+        "chrromium_exp77rimentnnl_pixellllocGl",
+        "chromium_exper4mental_pixel_lo00al",
         "chromium_experimental_pixel_local",
-        "chromiuF_experiment9lpixel_local",
-        "chromium_experimental_pixel_loca",
-        "Vhromium_expeOOimentalHpixRRl_lcal",
-        "chromiym_experimental_push_contant",
-        "nnhro77ium_experimenGal_push_conrrllant",
-        "chromium_experimental_push_c4nstan00",
+        "chooomium_eperimenal_pxel_local",
+        "chromium_experzzental_pixel_ocal",
+        "chiiomiu_epperimental_pix11l_local",
+        "chromXXum_experimental_push_constant",
+        "chromII9um_experinnental_push_c55nstant",
+        "chSSomium_experiHHental_pusrraaconstaYt",
         "chromium_experimental_push_constant",
-        "chooomum_experimental_ush_constat",
-        "chromium_xperimntal_zzush_constant",
-        "chromi11m_experimepptal_psh_ciistant",
-        "chromium_experimental_read_writeXXstorage_texture",
-        "chromium_exII55rimental_read_write99storagnn_texture",
-        "chromiumSSexperaamental_read_wYitrr_storage_HHexture",
+        "chromium_epHrimentkk_psh_constant",
+        "chromium_expegimenja_puRRh_costant",
+        "chromium_bxperimental_push_contan",
+        "chromium_experimental_read_wjite_storage_texture",
+        "chromium_expeimental_read_write_storage_texture",
+        "chqomium_experimentl_rad_write_storage_texture",
         "chromium_experimental_read_write_storage_texture",
-        "Hhromium_experimeta_rkkad_write_strage_texture",
-        "chromium_experijental_red_wrRgte_storag_texture",
-        "chromium_exbeimentalread_write_storage_texture",
-        "chromium_experimental_sjbgroups",
-        "chromium_experimental_sbgroups",
-        "cromum_experimentalqsubgroups",
+        "chromium_experimental_read_writNN_storage_texure",
+        "chromiumexperimental_read_write_stovvage_texure",
+        "chromium_xperimental_read_write_storage_textQQre",
+        "cffromium_experimenralsubgrous",
+        "chromium_experimentjl_subgroups",
+        "chromiu2ww8NNperimental_subgoups",
         "chromium_experimental_subgroups",
-        "chromium_expNNrimental_subgoups",
-        "chromium_experimetal_svvbgrous",
-        "chromium_experiQental_subgroups",
-        "chrorum_internal_dal_source_bleffding",
-        "chromium_internal_dual_source_jlending",
-        "chromiNNm_internal_dua8_sourwwe_blening",
+        "chromium_experimental_subgroup",
+        "chromiurr_experimental_subgroups",
+        "cGromium_experimental_subgroups",
+        "chromium_internFFl_dual_source_blending",
+        "cEromum_internl_dual_source_bleding",
+        "chromium_internal_dual_source_brrendin",
         "chromium_internal_dual_source_blending",
-        "chromium_internal_dual_soure_blending",
-        "chromium_irrternal_dual_source_blending",
-        "chromium_internal_duaG_source_blending",
-        "chromium_internalFFrelaxed_uniform_layout",
-        "chromEum_internal_relaxed_unifrmlyout",
-        "chromium_internalrrrelaxd_uniform_layout",
+        "chromium_internal_dual_surce_blendin",
+        "chromium_iXterJJa_dual_souDce_blending",
+        "chromi8m_ineral_dual_source_blendin",
+        "chromu11_internal_relaed_uniform_kayou",
+        "chromium_internal_relaxd_uniform_layout",
+        "chromium_internal_elaxed_uJiform_layout",
         "chromium_internal_relaxed_uniform_layout",
-        "chromiuminternal_relaxed_uniform_layut",
-        "cXroDium_internal_rJJlaed_uniform_layout",
-        "chromium_int8nal_relaed_uniform_layut",
-        "k",
-        "16",
-        "J1",
+        "chromium_intcrnal_relaxed_uniform_layout",
+        "chromiOm_internal_relaxed_uniform_layout",
+        "chromiutt_intervva_KK_relaxed_uniform_layout",
+        "xx8",
+        "__F",
+        "f1q",
         "f16",
-        "c16",
-        "fO6",
-        "_KKttvv",
+        "331O",
+        "ftt6QQ",
+        "666",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/wgsl/extension_test.cc b/src/tint/lang/wgsl/extension_test.cc
index 648baea..029deb2 100644
--- a/src/tint/lang/wgsl/extension_test.cc
+++ b/src/tint/lang/wgsl/extension_test.cc
@@ -59,6 +59,7 @@
 static constexpr Case kValidCases[] = {
     {"chromium_disable_uniformity_analysis", Extension::kChromiumDisableUniformityAnalysis},
     {"chromium_experimental_dp4a", Extension::kChromiumExperimentalDp4A},
+    {"chromium_experimental_framebuffer_fetch", Extension::kChromiumExperimentalFramebufferFetch},
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
     {"chromium_experimental_pixel_local", Extension::kChromiumExperimentalPixelLocal},
@@ -78,30 +79,33 @@
     {"chro1ium_experimental_dp4a", Extension::kUndefined},
     {"chrJmium_experiqqetal_dp4a", Extension::kUndefined},
     {"chromium_experimenll77l_dp4a", Extension::kUndefined},
-    {"chroium_experimental_full_ptr_paqqppmetHHrs", Extension::kUndefined},
-    {"chrium_evperiental_full_ptr_paraceters", Extension::kUndefined},
-    {"chromium_expGimental_fullbptr_parameters", Extension::kUndefined},
-    {"vhromium_experimental_pixel_liical", Extension::kUndefined},
-    {"chromium_experiment8l_pixel_lWWcal", Extension::kUndefined},
-    {"chromium_expeimentMl_xxixel_local", Extension::kUndefined},
-    {"chrXmium_experimeggtal_ush_constant", Extension::kUndefined},
-    {"chromiu_experVmentalpusX_constant", Extension::kUndefined},
-    {"chro3ium_experimental_push_constant", Extension::kUndefined},
-    {"chromium_experimentEl_read_write_storage_texture", Extension::kUndefined},
-    {"chromium_experimePPtTT_read_write_storage_texture", Extension::kUndefined},
-    {"chromium_expeimental_read_write_stoddagexxtexture", Extension::kUndefined},
-    {"chromium_experimental_44ubgroups", Extension::kUndefined},
-    {"cSSromVVum_experimental_subgroups", Extension::kUndefined},
-    {"chrmium_e22perimental_suRgrRups", Extension::kUndefined},
-    {"chroFium_internal_dual_source_bl9ndig", Extension::kUndefined},
-    {"chrmium_internal_dual_source_blending", Extension::kUndefined},
-    {"cVromium_interHal_dualOOsouRRce_blening", Extension::kUndefined},
-    {"chromium_internl_relaxyd_uniform_layout", Extension::kUndefined},
-    {"chromnnum_internrr77_Gelaxell_uniform_layout", Extension::kUndefined},
-    {"chromium_intern4l_relaxe00_uniform_layout", Extension::kUndefined},
-    {"5", Extension::kUndefined},
-    {"u16", Extension::kUndefined},
-    {"f", Extension::kUndefined},
+    {"cqqromium_eppperimental_framebuffe_fetcHH", Extension::kUndefined},
+    {"chrmium_experimvntal_frcmebufer_ftch", Extension::kUndefined},
+    {"chromium_expebimental_framGbufer_fetch", Extension::kUndefined},
+    {"chvomium_exiierimental_full_ptr_parameters", Extension::kUndefined},
+    {"chromium_WWxperimental_full8ptr_parameters", Extension::kUndefined},
+    {"chromxxum_Mperimental_full_ptr_parameters", Extension::kUndefined},
+    {"chromum_experimental_pixeX_loggal", Extension::kUndefined},
+    {"chromium_expVrXmntal_ixel_local", Extension::kUndefined},
+    {"3hromium_experimental_pixel_local", Extension::kUndefined},
+    {"chromium_experEmental_push_constant", Extension::kUndefined},
+    {"chPPomiumexperimental_push_conTTtant", Extension::kUndefined},
+    {"chromixxm_experimentddl_push_constnt", Extension::kUndefined},
+    {"44hromium_experimental_read_write_storage_texture", Extension::kUndefined},
+    {"chromium_experimental_reaSS_wriVVe_storage_texture", Extension::kUndefined},
+    {"chro22ium_eRperimental_read_Rrite_storag_texture", Extension::kUndefined},
+    {"chromium_experimental_sbgroup9", Extension::kUndefined},
+    {"cromium_experimental_subgroups", Extension::kUndefined},
+    {"VhrHium_experimental_subOOrouRRs", Extension::kUndefined},
+    {"chromium_internay_dual_sorce_blending", Extension::kUndefined},
+    {"chrnnmium_internal_duGrr_source_bllend77ng", Extension::kUndefined},
+    {"chromiu4_inter00al_dual_source_blending", Extension::kUndefined},
+    {"chrmoom_internal_relaxed_uniform_lyout", Extension::kUndefined},
+    {"chroium_internal_rlaxed_uniform_layzzut", Extension::kUndefined},
+    {"chromium_internaii_r11axed_uppifor_layout", Extension::kUndefined},
+    {"f1XX", Extension::kUndefined},
+    {"55199II", Extension::kUndefined},
+    {"frSSHHa", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
index b395664..003ef72 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
@@ -81,18 +81,18 @@
 
     auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[0]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint()->group, 0u);
-    EXPECT_EQ(sem->BindingPoint()->binding, 0u);
+    EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+    EXPECT_EQ(sem->Attributes().binding_point->binding, 0u);
 
     sem = flattened->Sem().Get<sem::GlobalVariable>(vars[1]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint()->group, 0u);
-    EXPECT_EQ(sem->BindingPoint()->binding, 1u);
+    EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+    EXPECT_EQ(sem->Attributes().binding_point->binding, 1u);
 
     sem = flattened->Sem().Get<sem::GlobalVariable>(vars[2]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint()->group, 0u);
-    EXPECT_EQ(sem->BindingPoint()->binding, 2u);
+    EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+    EXPECT_EQ(sem->Attributes().binding_point->binding, 2u);
 }
 
 TEST_F(FlattenBindingsTest, NotFlat_MultipleNamespaces) {
@@ -144,20 +144,20 @@
     for (size_t i = 0; i < num_buffers; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint()->group, 0u);
-        EXPECT_EQ(sem->BindingPoint()->binding, i);
+        EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+        EXPECT_EQ(sem->Attributes().binding_point->binding, i);
     }
     for (size_t i = 0; i < num_samplers; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i + num_buffers]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint()->group, 0u);
-        EXPECT_EQ(sem->BindingPoint()->binding, i);
+        EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+        EXPECT_EQ(sem->Attributes().binding_point->binding, i);
     }
     for (size_t i = 0; i < num_textures; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i + num_buffers + num_samplers]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint()->group, 0u);
-        EXPECT_EQ(sem->BindingPoint()->binding, i);
+        EXPECT_EQ(sem->Attributes().binding_point->group, 0u);
+        EXPECT_EQ(sem->Attributes().binding_point->binding, i);
     }
 }
 
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index 278773a..7f49f66 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -177,7 +177,7 @@
     for (auto* param : sem->Parameters()) {
         AddEntryPointInOutVariables(param->Declaration()->name->symbol.Name(),
                                     param->Declaration()->name->symbol.Name(), param->Type(),
-                                    param->Declaration()->attributes, param->Location(),
+                                    param->Declaration()->attributes, param->Attributes().location,
                                     entry_point.input_variables);
 
         entry_point.input_position_used |= ContainsBuiltin(
@@ -212,10 +212,10 @@
         auto name = decl->name->symbol.Name();
 
         auto* global = var->As<sem::GlobalVariable>();
-        if (global && global->Declaration()->Is<ast::Override>()) {
+        if (auto override_id = global->Attributes().override_id) {
             Override override;
             override.name = name;
-            override.id = global->OverrideId();
+            override.id = override_id.value();
             auto* type = var->Type();
             TINT_ASSERT(type->Is<core::type::Scalar>());
             if (type->is_bool_scalar_or_vector()) {
@@ -279,7 +279,7 @@
         // WGSL, so the resolver should catch it. Thus here the inspector just
         // assumes all definitions of the override id are the same, so only needs
         // to find the first reference to override id.
-        OverrideId override_id = global->OverrideId();
+        auto override_id = global->Attributes().override_id.value();
         if (result.find(override_id) != result.end()) {
             continue;
         }
@@ -311,9 +311,9 @@
     std::map<std::string, OverrideId> result;
     for (auto* var : program_.AST().GlobalVariables()) {
         auto* global = program_.Sem().Get<sem::GlobalVariable>(var);
-        if (global && global->Declaration()->Is<ast::Override>()) {
+        if (auto override_id = global->Attributes().override_id) {
             auto name = var->name->symbol.Name();
-            result[name] = global->OverrideId();
+            result[name] = override_id.value();
         }
     }
     return result;
@@ -527,8 +527,9 @@
         auto* texture = pair.first->As<sem::GlobalVariable>();
         auto* sampler = pair.second ? pair.second->As<sem::GlobalVariable>() : nullptr;
         SamplerTexturePair new_pair;
-        new_pair.sampler_binding_point = sampler ? *sampler->BindingPoint() : placeholder;
-        new_pair.texture_binding_point = *texture->BindingPoint();
+        new_pair.sampler_binding_point =
+            sampler ? *sampler->Attributes().binding_point : placeholder;
+        new_pair.texture_binding_point = *texture->Attributes().binding_point;
         new_pairs.push_back(new_pair);
     }
     return new_pairs;
@@ -818,18 +819,18 @@
         auto* t = c->args[static_cast<size_t>(texture_index)];
         auto* s = c->args[static_cast<size_t>(sampler_index)];
 
-        GetOriginatingResources(std::array<const ast::Expression*, 2>{t, s},
-                                [&](std::array<const sem::GlobalVariable*, 2> globals) {
-                                    auto texture_binding_point = *globals[0]->BindingPoint();
-                                    auto sampler_binding_point = *globals[1]->BindingPoint();
+        GetOriginatingResources(
+            std::array<const ast::Expression*, 2>{t, s},
+            [&](std::array<const sem::GlobalVariable*, 2> globals) {
+                auto texture_binding_point = *globals[0]->Attributes().binding_point;
+                auto sampler_binding_point = *globals[1]->Attributes().binding_point;
 
-                                    for (auto* entry_point : entry_points) {
-                                        const auto& ep_name =
-                                            entry_point->Declaration()->name->symbol.Name();
-                                        (*sampler_targets_)[ep_name].Add(
-                                            {sampler_binding_point, texture_binding_point});
-                                    }
-                                });
+                for (auto* entry_point : entry_points) {
+                    const auto& ep_name = entry_point->Declaration()->name->symbol.Name();
+                    (*sampler_targets_)[ep_name].Add(
+                        {sampler_binding_point, texture_binding_point});
+                }
+            });
     }
 }
 
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 052c464..0eb5c8e 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -1879,15 +1879,15 @@
 
     ASSERT_TRUE(result.count("a"));
     ASSERT_TRUE(program_->Sem().Get(a));
-    EXPECT_EQ(result["a"], program_->Sem().Get(a)->OverrideId());
+    EXPECT_EQ(result["a"], program_->Sem().Get(a)->Attributes().override_id);
 
     ASSERT_TRUE(result.count("b"));
     ASSERT_TRUE(program_->Sem().Get(b));
-    EXPECT_EQ(result["b"], program_->Sem().Get(b)->OverrideId());
+    EXPECT_EQ(result["b"], program_->Sem().Get(b)->Attributes().override_id);
 
     ASSERT_TRUE(result.count("c"));
     ASSERT_TRUE(program_->Sem().Get(c));
-    EXPECT_EQ(result["c"], program_->Sem().Get(c)->OverrideId());
+    EXPECT_EQ(result["c"], program_->Sem().Get(c)->Attributes().override_id);
 }
 
 TEST_F(InspectorGetResourceBindingsTest, Empty) {
diff --git a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
index 26bd8dc..893573c 100644
--- a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
@@ -205,7 +205,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index a0c10a8..5e99225 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -456,12 +456,8 @@
                         }
                     });
 
-                if (param_sem->Location().has_value()) {
-                    param->SetLocation(param_sem->Location().value(), interpolation);
-                }
-                if (param_sem->BindingPoint().has_value()) {
-                    param->SetBindingPoint(param_sem->BindingPoint()->group,
-                                           param_sem->BindingPoint()->binding);
+                if (param_sem->Attributes().location.has_value()) {
+                    param->SetLocation(param_sem->Attributes().location.value(), interpolation);
                 }
             }
 
@@ -1324,8 +1320,8 @@
                 current_block_->Append(val);
 
                 if (auto* gv = sem->As<sem::GlobalVariable>(); gv && var->HasBindingPoint()) {
-                    val->SetBindingPoint(gv->BindingPoint().value().group,
-                                         gv->BindingPoint().value().binding);
+                    val->SetBindingPoint(gv->Attributes().binding_point->group,
+                                         gv->Attributes().binding_point->binding);
                 }
 
                 // Store the declaration so we can get the instruction to store too
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index 64e2bc7..d88daa5 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -31,6 +31,8 @@
 #include "src/tint/lang/wgsl/ast/transform/add_block_attribute.h"
 #include "src/tint/lang/wgsl/resolver/resolver.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/text/string_stream.h"
 
 #include "gmock/gmock.h"
@@ -57,7 +59,7 @@
 enum class AttributeKind {
     kAlign,
     kBinding,
-    kBuiltin,
+    kBuiltinPosition,
     kDiagnostic,
     kGroup,
     kId,
@@ -68,18 +70,53 @@
     kMustUse,
     kOffset,
     kSize,
-    kStage,
+    kStageCompute,
     kStride,
-    kWorkgroup,
-
-    kBindingAndGroup,
+    kWorkgroupSize,
 };
+static std::ostream& operator<<(std::ostream& o, AttributeKind k) {
+    switch (k) {
+        case AttributeKind::kAlign:
+            return o << "@align";
+        case AttributeKind::kBinding:
+            return o << "@binding";
+        case AttributeKind::kBuiltinPosition:
+            return o << "@builtin(position)";
+        case AttributeKind::kDiagnostic:
+            return o << "@diagnostic";
+        case AttributeKind::kGroup:
+            return o << "@group";
+        case AttributeKind::kId:
+            return o << "@id";
+        case AttributeKind::kIndex:
+            return o << "@index";
+        case AttributeKind::kInterpolate:
+            return o << "@interpolate";
+        case AttributeKind::kInvariant:
+            return o << "@invariant";
+        case AttributeKind::kLocation:
+            return o << "@location";
+        case AttributeKind::kOffset:
+            return o << "@offset";
+        case AttributeKind::kMustUse:
+            return o << "@must_use";
+        case AttributeKind::kSize:
+            return o << "@size";
+        case AttributeKind::kStageCompute:
+            return o << "@stage(compute)";
+        case AttributeKind::kStride:
+            return o << "@stride";
+        case AttributeKind::kWorkgroupSize:
+            return o << "@workgroup_size";
+    }
+    TINT_UNREACHABLE();
+    return o << "<unknown>";
+}
 
 static bool IsBindingAttribute(AttributeKind kind) {
     switch (kind) {
         case AttributeKind::kBinding:
         case AttributeKind::kGroup:
-        case AttributeKind::kBindingAndGroup:
             return true;
         default:
             return false;
@@ -87,195 +124,500 @@
 }
 
 struct TestParams {
-    AttributeKind kind;
-    bool should_pass;
+    Vector<AttributeKind, 2> attributes;
+    std::string error;  // empty string (Pass) is an expected pass
 };
+
+static constexpr const char* Pass = "";
+
+static std::vector<TestParams> OnlyDiagnosticValidFor(std::string thing) {
+    return {TestParams{
+                {AttributeKind::kAlign},
+                "1:2 error: @align is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kBinding},
+                "1:2 error: @binding is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kBuiltinPosition},
+                "1:2 error: @builtin is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kDiagnostic},
+                Pass,
+            },
+            TestParams{
+                {AttributeKind::kGroup},
+                "1:2 error: @group is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kId},
+                "1:2 error: @id is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kIndex},
+                "1:2 error: @index is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kInterpolate},
+                "1:2 error: @interpolate is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kInvariant},
+                "1:2 error: @invariant is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kLocation},
+                "1:2 error: @location is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kMustUse},
+                "1:2 error: @must_use is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kOffset},
+                "1:2 error: @offset is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kSize},
+                "1:2 error: @size is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kStageCompute},
+                "1:2 error: @stage is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kStride},
+                "1:2 error: @stride is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kWorkgroupSize},
+                "1:2 error: @workgroup_size is not valid for " + thing,
+            },
+            TestParams{
+                {AttributeKind::kBinding, AttributeKind::kGroup},
+                "1:2 error: @binding is not valid for " + thing,
+            }};
+}
+
+static std::ostream& operator<<(std::ostream& o, const TestParams& c) {
+    return o << "attributes: " << c.attributes << ", expect pass: " << c.error.empty();
+}
+
+const ast::Attribute* CreateAttribute(const Source& source,
+                                      ProgramBuilder& builder,
+                                      AttributeKind kind) {
+    switch (kind) {
+        case AttributeKind::kAlign:
+            return builder.MemberAlign(source, 4_i);
+        case AttributeKind::kBinding:
+            return builder.Binding(source, 1_a);
+        case AttributeKind::kBuiltinPosition:
+            return builder.Builtin(source, core::BuiltinValue::kPosition);
+        case AttributeKind::kDiagnostic:
+            return builder.DiagnosticAttribute(source, wgsl::DiagnosticSeverity::kInfo, "chromium",
+                                               "unreachable_code");
+        case AttributeKind::kGroup:
+            return builder.Group(source, 1_a);
+        case AttributeKind::kId:
+            return builder.Id(source, 0_a);
+        case AttributeKind::kIndex:
+            return builder.Index(source, 0_a);
+        case AttributeKind::kInterpolate:
+            return builder.Interpolate(source, core::InterpolationType::kLinear,
+                                       core::InterpolationSampling::kCenter);
+        case AttributeKind::kInvariant:
+            return builder.Invariant(source);
+        case AttributeKind::kLocation:
+            return builder.Location(source, 0_a);
+        case AttributeKind::kOffset:
+            return builder.MemberOffset(source, 4_a);
+        case AttributeKind::kMustUse:
+            return builder.MustUse(source);
+        case AttributeKind::kSize:
+            return builder.MemberSize(source, 16_a);
+        case AttributeKind::kStageCompute:
+            return builder.Stage(source, ast::PipelineStage::kCompute);
+        case AttributeKind::kStride:
+            return builder.create<ast::StrideAttribute>(source, 4u);
+        case AttributeKind::kWorkgroupSize:
+            return builder.create<ast::WorkgroupAttribute>(source, builder.Expr(1_i));
+    }
+    TINT_UNREACHABLE() << kind;
+    return nullptr;
+}
+
 struct TestWithParams : ResolverTestWithParam<TestParams> {
-    void EnableExtensionIfNecessary(AttributeKind attributeKind) {
-        if (attributeKind == AttributeKind::kIndex) {
+    void EnableExtensionIfNecessary(AttributeKind attribute) {
+        if (attribute == AttributeKind::kIndex) {
             Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
         }
     }
+
+    void EnableRequiredExtensions() {
+        for (auto attribute : GetParam().attributes) {
+            EnableExtensionIfNecessary(attribute);
+        }
+    }
+
+    Vector<const ast::Attribute*, 2> CreateAttributes(ProgramBuilder& builder,
+                                                      VectorRef<AttributeKind> kinds) {
+        return Transform<2>(kinds, [&](AttributeKind kind, size_t index) {
+            return CreateAttribute(Source{{static_cast<uint32_t>(index) * 2 + 1,
+                                           static_cast<uint32_t>(index) * 2 + 2}},
+                                   builder, kind);
+        });
+    }
+
+    Vector<const ast::Attribute*, 2> CreateAttributes() {
+        return CreateAttributes(*this, GetParam().attributes);
+    }
 };
 
-static Vector<const ast::Attribute*, 2> createAttributes(const Source& source,
-                                                         ProgramBuilder& builder,
-                                                         AttributeKind kind) {
-    switch (kind) {
-        case AttributeKind::kAlign:
-            return {builder.MemberAlign(source, 4_i)};
-        case AttributeKind::kBinding:
-            return {builder.Binding(source, 1_a)};
-        case AttributeKind::kBuiltin:
-            return {builder.Builtin(source, core::BuiltinValue::kPosition)};
-        case AttributeKind::kDiagnostic:
-            return {builder.DiagnosticAttribute(source, wgsl::DiagnosticSeverity::kInfo, "chromium",
-                                                "unreachable_code")};
-        case AttributeKind::kGroup:
-            return {builder.Group(source, 1_a)};
-        case AttributeKind::kId:
-            return {builder.Id(source, 0_a)};
-        case AttributeKind::kIndex:
-            return {builder.Index(source, 0_a)};
-        case AttributeKind::kInterpolate:
-            return {builder.Interpolate(source, core::InterpolationType::kLinear,
-                                        core::InterpolationSampling::kCenter)};
-        case AttributeKind::kInvariant:
-            return {builder.Invariant(source)};
-        case AttributeKind::kLocation:
-            return {builder.Location(source, 1_a)};
-        case AttributeKind::kOffset:
-            return {builder.MemberOffset(source, 4_a)};
-        case AttributeKind::kMustUse:
-            return {builder.MustUse(source)};
-        case AttributeKind::kSize:
-            return {builder.MemberSize(source, 16_a)};
-        case AttributeKind::kStage:
-            return {builder.Stage(source, ast::PipelineStage::kCompute)};
-        case AttributeKind::kStride:
-            return {builder.create<ast::StrideAttribute>(source, 4u)};
-        case AttributeKind::kWorkgroup:
-            return {builder.create<ast::WorkgroupAttribute>(source, builder.Expr(1_i))};
-        case AttributeKind::kBindingAndGroup:
-            return {builder.Binding(source, 1_a), builder.Group(source, 1_a)};
-    }
-    return {};
-}
+#define CHECK()                                      \
+    if (GetParam().error.empty()) {                  \
+        EXPECT_TRUE(r()->Resolve()) << r()->error(); \
+    } else {                                         \
+        EXPECT_FALSE(r()->Resolve());                \
+        EXPECT_EQ(GetParam().error, r()->error());   \
+    }                                                \
+    TINT_REQUIRE_SEMICOLON
 
-static std::string name(AttributeKind kind) {
-    switch (kind) {
-        case AttributeKind::kAlign:
-            return "@align";
-        case AttributeKind::kBinding:
-            return "@binding";
-        case AttributeKind::kBuiltin:
-            return "@builtin";
-        case AttributeKind::kDiagnostic:
-            return "@diagnostic";
-        case AttributeKind::kGroup:
-            return "@group";
-        case AttributeKind::kId:
-            return "@id";
-        case AttributeKind::kIndex:
-            return "@index";
-        case AttributeKind::kInterpolate:
-            return "@interpolate";
-        case AttributeKind::kInvariant:
-            return "@invariant";
-        case AttributeKind::kLocation:
-            return "@location";
-        case AttributeKind::kOffset:
-            return "@offset";
-        case AttributeKind::kMustUse:
-            return "@must_use";
-        case AttributeKind::kSize:
-            return "@size";
-        case AttributeKind::kStage:
-            return "@stage";
-        case AttributeKind::kStride:
-            return "@stride";
-        case AttributeKind::kWorkgroup:
-            return "@workgroup_size";
-        case AttributeKind::kBindingAndGroup:
-            return "@binding";
-    }
-    return "<unknown>";
+namespace FunctionTests {
+using VoidFunctionAttributeTest = TestWithParams;
+TEST_P(VoidFunctionAttributeTest, IsValid) {
+    EnableRequiredExtensions();
+
+    Func(Source{{9, 9}}, "main", Empty, ty.void_(), Empty, CreateAttributes());
+
+    CHECK();
 }
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    VoidFunctionAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use can only be applied to functions that return a value)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(9:9 error: a compute shader must include 'workgroup_size' in its attributes)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute, AttributeKind::kWorkgroupSize},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for functions)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is only valid for compute stages)",
+        }));
+
+using NonVoidFunctionAttributeTest = TestWithParams;
+TEST_P(NonVoidFunctionAttributeTest, IsValid) {
+    EnableRequiredExtensions();
+
+    Func(Source{{9, 9}}, "main", Empty, ty.i32(), Vector{Return(1_i)}, CreateAttributes());
+
+    CHECK();
+}
+INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
+                         NonVoidFunctionAttributeTest,
+                         testing::Values(
+                             TestParams{
+                                 {AttributeKind::kAlign},
+                                 R"(1:2 error: @align is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBinding},
+                                 R"(1:2 error: @binding is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBuiltinPosition},
+                                 R"(1:2 error: @builtin is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kDiagnostic},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kGroup},
+                                 R"(1:2 error: @group is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kId},
+                                 R"(1:2 error: @id is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kIndex},
+                                 R"(1:2 error: @index is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInterpolate},
+                                 R"(1:2 error: @interpolate is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInvariant},
+                                 R"(1:2 error: @invariant is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kLocation},
+                                 R"(1:2 error: @location is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kMustUse},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kOffset},
+                                 R"(1:2 error: @offset is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kSize},
+                                 R"(1:2 error: @size is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStageCompute},
+                                 R"(9:9 error: missing entry point IO attribute on return type)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStageCompute, AttributeKind::kWorkgroupSize},
+                                 R"(9:9 error: missing entry point IO attribute on return type)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStride},
+                                 R"(1:2 error: @stride is not valid for functions)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kWorkgroupSize},
+                                 R"(1:2 error: @workgroup_size is only valid for compute stages)",
+                             }));
+}  // namespace FunctionTests
+
 namespace FunctionInputAndOutputTests {
 using FunctionParameterAttributeTest = TestWithParams;
 TEST_P(FunctionParameterAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
     Func("main",
          Vector{
-             Param("a", ty.vec4<f32>(), createAttributes({}, *this, params.kind)),
+             Param("a", ty.vec4<f32>(), CreateAttributes()),
          },
          ty.void_(), tint::Empty);
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else if (params.kind == AttributeKind::kLocation || params.kind == AttributeKind::kBuiltin ||
-               params.kind == AttributeKind::kInvariant ||
-               params.kind == AttributeKind::kInterpolate) {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "error: " + name(params.kind) +
-                                    " is not valid for non-entry point function parameters");
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "error: " + name(params.kind) + " is not valid for function parameters");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         FunctionParameterAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    FunctionParameterAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for non-entry point function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for non-entry point function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for non-entry point function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for non-entry point function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+        }));
 
 using FunctionReturnTypeAttributeTest = TestWithParams;
 TEST_P(FunctionReturnTypeAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
     Func("main", tint::Empty, ty.f32(),
          Vector{
              Return(1_f),
          },
-         tint::Empty, createAttributes({}, *this, params.kind));
+         tint::Empty, CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "error: " + name(params.kind) +
-                                    " is not valid for non-entry point function "
-                                    "return types");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         FunctionReturnTypeAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    FunctionReturnTypeAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for non-entry point function return types)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for non-entry point function return types)",
+        }));
 }  // namespace FunctionInputAndOutputTests
 
 namespace EntryPointInputAndOutputTests {
 using ComputeShaderParameterAttributeTest = TestWithParams;
 TEST_P(ComputeShaderParameterAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
     Func("main",
          Vector{
-             Param("a", ty.vec4<f32>(), createAttributes(Source{{12, 34}}, *this, params.kind)),
+             Param("a", ty.vec4<f32>(), CreateAttributes()),
          },
          ty.void_(), tint::Empty,
          Vector{
@@ -283,97 +625,174 @@
              WorkgroupSize(1_i),
          });
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (params.kind == AttributeKind::kBuiltin) {
-            EXPECT_EQ(
-                r()->error(),
-                R"(12:34 error: @builtin(position) cannot be used in input of compute pipeline stage)");
-        } else if (params.kind == AttributeKind::kInterpolate ||
-                   params.kind == AttributeKind::kLocation ||
-                   params.kind == AttributeKind::kInvariant) {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for compute shader inputs");
-        } else {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for function parameters");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         ComputeShaderParameterAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    ComputeShaderParameterAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin(position) cannot be used for compute shader input)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+        }));
 
 using FragmentShaderParameterAttributeTest = TestWithParams;
 TEST_P(FragmentShaderParameterAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
-    auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
-    if (params.kind != AttributeKind::kBuiltin && params.kind != AttributeKind::kLocation) {
-        attrs.Push(Builtin(Source{{34, 56}}, core::BuiltinValue::kPosition));
-    }
-    auto* p = Param("a", ty.vec4<f32>(), attrs);
+    EnableRequiredExtensions();
+    auto* p = Param(Source{{9, 9}}, "a", ty.vec4<f32>(), CreateAttributes());
     Func("frag_main", Vector{p}, ty.void_(), tint::Empty,
          Vector{
              Stage(ast::PipelineStage::kFragment),
          });
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for function parameters");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         FragmentShaderParameterAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, true},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         // kInterpolate tested separately (requires @location)
-                                         TestParams{AttributeKind::kInvariant, true},
-                                         TestParams{AttributeKind::kLocation, true},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    FragmentShaderParameterAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(9:9 error: missing entry point IO attribute on parameter)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @interpolate can only be used with @location)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(9:9 error: missing entry point IO attribute on parameter)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+        }));
 
 using VertexShaderParameterAttributeTest = TestWithParams;
 TEST_P(VertexShaderParameterAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
-    if (params.kind != AttributeKind::kLocation) {
-        attrs.Push(Location(Source{{34, 56}}, 2_a));
-    }
-    auto* p = Param("a", ty.vec4<f32>(), attrs);
+    auto* p = Param(Source{{9, 9}}, "a", ty.vec4<f32>(), CreateAttributes());
     Func("vertex_main", Vector{p}, ty.vec4<f32>(),
          Vector{
              Return(Call<vec4<f32>>()),
@@ -385,48 +804,96 @@
              Builtin(core::BuiltinValue::kPosition),
          });
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (params.kind == AttributeKind::kBuiltin) {
-            EXPECT_EQ(
-                r()->error(),
-                R"(12:34 error: @builtin(position) cannot be used in input of vertex pipeline stage)");
-        } else if (params.kind == AttributeKind::kInvariant) {
-            EXPECT_EQ(r()->error(),
-                      "12:34 error: invariant attribute must only be applied to a "
-                      "position builtin");
-        } else {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for function parameters");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         VertexShaderParameterAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, true},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, true},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    VertexShaderParameterAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin(position) cannot be used for vertex shader input)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(9:9 error: missing entry point IO attribute on parameter)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kBuiltinPosition},
+            R"(3:4 error: @builtin(position) cannot be used for vertex shader input)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(9:9 error: missing entry point IO attribute on parameter)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant, AttributeKind::kLocation},
+            R"(1:2 error: @invariant must be applied to a position builtin)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
+            R"(3:4 error: @builtin(position) cannot be used for vertex shader input)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for function parameters)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+        }));
 
 using ComputeShaderReturnTypeAttributeTest = TestWithParams;
 TEST_P(ComputeShaderReturnTypeAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
     Func("main", tint::Empty, ty.vec4<f32>(),
          Vector{
@@ -436,112 +903,188 @@
              Stage(ast::PipelineStage::kCompute),
              WorkgroupSize(1_i),
          },
-         createAttributes(Source{{12, 34}}, *this, params.kind));
+         CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (params.kind == AttributeKind::kBuiltin) {
-            EXPECT_EQ(
-                r()->error(),
-                R"(12:34 error: @builtin(position) cannot be used in output of compute pipeline stage)");
-        } else if (params.kind == AttributeKind::kInterpolate ||
-                   params.kind == AttributeKind::kLocation ||
-                   params.kind == AttributeKind::kInvariant ||
-                   params.kind == AttributeKind::kIndex) {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for compute shader output");
-        } else {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for entry point return "
-                                        "types");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         ComputeShaderReturnTypeAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    ComputeShaderReturnTypeAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin(position) cannot be used for compute shader output)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index can only be used for fragment shader output)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location cannot be used by compute shaders)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+        }));
 
 using FragmentShaderReturnTypeAttributeTest = TestWithParams;
 TEST_P(FragmentShaderReturnTypeAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
-    attrs.Push(Location(Source{{34, 56}}, 2_a));
-    Func("frag_main", tint::Empty, ty.vec4<f32>(), Vector{Return(Call<vec4<f32>>())},
+    Func(Source{{9, 9}}, "frag_main", tint::Empty, ty.vec4<f32>(),
+         Vector{Return(Call<vec4<f32>>())},
          Vector{
              Stage(ast::PipelineStage::kFragment),
          },
-         attrs);
+         CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (params.kind == AttributeKind::kBuiltin) {
-            EXPECT_EQ(
-                r()->error(),
-                R"(12:34 error: @builtin(position) cannot be used in output of fragment pipeline stage)");
-        } else if (params.kind == AttributeKind::kInvariant) {
-            EXPECT_EQ(
-                r()->error(),
-                R"(12:34 error: invariant attribute must only be applied to a position builtin)");
-        } else if (params.kind == AttributeKind::kLocation) {
-            EXPECT_EQ(r()->error(),
-                      R"(34:56 error: duplicate location attribute
-12:34 note: first attribute declared here)");
-        } else {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for entry point return types");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         FragmentShaderReturnTypeAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, true},
-                                         TestParams{AttributeKind::kInterpolate, true},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    FragmentShaderReturnTypeAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin(position) cannot be used for fragment shader output)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(9:9 error: missing entry point IO attribute on return type)",
+        },
+        TestParams{
+            {AttributeKind::kIndex, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(9:9 error: missing entry point IO attribute on return type)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(9:9 error: missing entry point IO attribute on return type)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant, AttributeKind::kLocation},
+            R"(1:2 error: @invariant must be applied to a position builtin)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: @binding is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kIndex, AttributeKind::kLocation},
+            Pass,
+        }));
 
 using VertexShaderReturnTypeAttributeTest = TestWithParams;
 TEST_P(VertexShaderReturnTypeAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
-    auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
+    EnableRequiredExtensions();
+    auto attrs = CreateAttributes();
     // a vertex shader must include the 'position' builtin in its return type
-    if (params.kind != AttributeKind::kBuiltin) {
-        attrs.Push(Builtin(Source{{34, 56}}, core::BuiltinValue::kPosition));
+    if (!GetParam().attributes.Any([](auto b) { return b == AttributeKind::kBuiltinPosition; })) {
+        attrs.Push(Builtin(Source{{9, 9}}, core::BuiltinValue::kPosition));
     }
     Func("vertex_main", tint::Empty, ty.vec4<f32>(),
          Vector{
@@ -550,64 +1093,90 @@
          Vector{
              Stage(ast::PipelineStage::kVertex),
          },
-         attrs);
+         std::move(attrs));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (params.kind == AttributeKind::kLocation) {
-            EXPECT_EQ(r()->error(),
-                      R"(34:56 error: multiple entry point IO attributes
-12:34 note: previously consumed @location)");
-        } else if (params.kind == AttributeKind::kIndex) {
-            EXPECT_EQ(r()->error(), R"(12:34 error: @index is not valid for vertex shader output)");
-        } else {
-            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
-                                        " is not valid for entry point return types");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         VertexShaderReturnTypeAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, true},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         // kInterpolate tested separately (requires @location)
-                                         TestParams{AttributeKind::kInvariant, true},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    VertexShaderReturnTypeAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index can only be used for fragment shader output)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate can only be used with @location)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(9:9 error: multiple entry point IO attributes
+1:2 note: previously consumed @location)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: @binding is not valid for entry point return types)",
+        },
+        TestParams{
+            {AttributeKind::kLocation, AttributeKind::kLocation},
+            R"(3:4 error: duplicate location attribute
+1:2 note: first attribute declared here)",
+        }));
 
 using EntryPointParameterAttributeTest = TestWithParams;
-TEST_F(EntryPointParameterAttributeTest, DuplicateAttribute) {
-    Func("main", tint::Empty, ty.f32(),
-         Vector{
-             Return(1_f),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kFragment),
-         },
-         Vector{
-             Location(Source{{12, 34}}, 2_a),
-             Location(Source{{56, 78}}, 3_a),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate location attribute
-12:34 note: first attribute declared here)");
-}
-
 TEST_F(EntryPointParameterAttributeTest, DuplicateInternalAttribute) {
     auto* s = Param("s", ty.sampler(core::type::SamplerKind::kSampler),
                     Vector{
@@ -625,25 +1194,6 @@
 }
 
 using EntryPointReturnTypeAttributeTest = ResolverTest;
-TEST_F(EntryPointReturnTypeAttributeTest, DuplicateAttribute) {
-    Func("main", tint::Empty, ty.f32(),
-         Vector{
-             Return(1_f),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kFragment),
-         },
-         Vector{
-             Location(Source{{12, 34}}, 2_a),
-             Location(Source{{56, 78}}, 3_a),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate location attribute
-12:34 note: first attribute declared here)");
-}
-
 TEST_F(EntryPointReturnTypeAttributeTest, DuplicateInternalAttribute) {
     Func("f", tint::Empty, ty.i32(), Vector{Return(1_i)},
          Vector{
@@ -662,114 +1212,176 @@
 using StructAttributeTest = TestWithParams;
 using SpirvBlockAttribute = ast::transform::AddBlockAttribute::BlockAttribute;
 TEST_P(StructAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    Structure("mystruct", Vector{Member("a", ty.f32())},
-              createAttributes(Source{{12, 34}}, *this, params.kind));
+    Structure("S", Vector{Member("a", ty.f32())}, CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for struct declarations");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         StructAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    StructAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for struct declarations)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: @binding is not valid for struct declarations)",
+        }));
 
 using StructMemberAttributeTest = TestWithParams;
 TEST_P(StructMemberAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
-    Vector<const ast::StructMember*, 1> members;
-    if (params.kind == AttributeKind::kBuiltin) {
-        members.Push(
-            Member("a", ty.vec4<f32>(), createAttributes(Source{{12, 34}}, *this, params.kind)));
-    } else {
-        members.Push(Member("a", ty.f32(), createAttributes(Source{{12, 34}}, *this, params.kind)));
-    }
-    Structure("mystruct", members);
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for struct members");
-    }
+    EnableRequiredExtensions();
+    Structure("S", Vector{Member("a", ty.vec4<f32>(), CreateAttributes())});
+
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          StructMemberAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, true},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, true},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         // kIndex tested separately (requires @location)
-                                         // kInterpolate tested separately (requires @location)
-                                         // kInvariant tested separately (requires position builtin)
-                                         TestParams{AttributeKind::kLocation, true},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, true},
-                                         TestParams{AttributeKind::kSize, true},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
-TEST_F(StructMemberAttributeTest, DuplicateAttribute) {
-    Structure("mystruct", Vector{
-                              Member("a", ty.i32(),
-                                     Vector{
-                                         MemberAlign(Source{{12, 34}}, 4_i),
-                                         MemberAlign(Source{{56, 78}}, 8_i),
-                                     }),
-                          });
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate align attribute
-12:34 note: first attribute declared here)");
-}
-TEST_F(StructMemberAttributeTest, InvariantAttributeWithPosition) {
-    Structure("mystruct", Vector{
-                              Member("a", ty.vec4<f32>(),
-                                     Vector{
-                                         Invariant(),
-                                         Builtin(core::BuiltinValue::kPosition),
-                                     }),
-                          });
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-TEST_F(StructMemberAttributeTest, InvariantAttributeWithoutPosition) {
-    Structure("mystruct", Vector{
-                              Member("a", ty.vec4<f32>(),
-                                     Vector{
-                                         Invariant(Source{{12, 34}}),
-                                     }),
-                          });
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: invariant attribute must only be applied to a "
-              "position builtin");
-}
+                         testing::Values(
+                             TestParams{
+                                 {AttributeKind::kAlign},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kBinding},
+                                 R"(1:2 error: @binding is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBuiltinPosition},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kDiagnostic},
+                                 R"(1:2 error: @diagnostic is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kGroup},
+                                 R"(1:2 error: @group is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kId},
+                                 R"(1:2 error: @id is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kIndex},
+                                 R"(1:2 error: @index can only be used with @location(0))",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInterpolate},
+                                 R"(1:2 error: @interpolate can only be used with @location)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInterpolate, AttributeKind::kLocation},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kInvariant},
+                                 R"(1:2 error: @invariant must be applied to a position builtin)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kLocation},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kMustUse},
+                                 R"(1:2 error: @must_use is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kOffset},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kSize},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kStageCompute},
+                                 R"(1:2 error: @stage is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStride},
+                                 R"(1:2 error: @stride is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kWorkgroupSize},
+                                 R"(1:2 error: @workgroup_size is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBinding, AttributeKind::kGroup},
+                                 R"(1:2 error: @binding is not valid for struct members)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kAlign, AttributeKind::kAlign},
+                                 R"(3:4 error: duplicate align attribute
+1:2 note: first attribute declared here)",
+                             }));
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Const) {
     GlobalConst("val", ty.i32(), Expr(1_i));
@@ -945,93 +1557,182 @@
 
 using ArrayAttributeTest = TestWithParams;
 TEST_P(ArrayAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    auto arr = ty.array(ty.f32(), createAttributes(Source{{12, 34}}, *this, params.kind));
-    Structure("mystruct", Vector{
-                              Member("a", arr),
-                          });
+    auto arr = ty.array(ty.f32(), CreateAttributes());
+    Structure("S", Vector{
+                       Member("a", arr),
+                   });
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for array types");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          ArrayAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, true},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::Values(
+                             TestParams{
+                                 {AttributeKind::kAlign},
+                                 R"(1:2 error: @align is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBinding},
+                                 R"(1:2 error: @binding is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBuiltinPosition},
+                                 R"(1:2 error: @builtin is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kDiagnostic},
+                                 R"(1:2 error: @diagnostic is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kGroup},
+                                 R"(1:2 error: @group is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kId},
+                                 R"(1:2 error: @id is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kIndex},
+                                 R"(1:2 error: @index is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInterpolate},
+                                 R"(1:2 error: @interpolate is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kInvariant},
+                                 R"(1:2 error: @invariant is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kLocation},
+                                 R"(1:2 error: @location is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kMustUse},
+                                 R"(1:2 error: @must_use is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kOffset},
+                                 R"(1:2 error: @offset is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kSize},
+                                 R"(1:2 error: @size is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStageCompute},
+                                 R"(1:2 error: @stage is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStride},
+                                 Pass,
+                             },
+                             TestParams{
+                                 {AttributeKind::kWorkgroupSize},
+                                 R"(1:2 error: @workgroup_size is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kBinding, AttributeKind::kGroup},
+                                 R"(1:2 error: @binding is not valid for array types)",
+                             },
+                             TestParams{
+                                 {AttributeKind::kStride, AttributeKind::kStride},
+                                 R"(3:4 error: duplicate stride attribute
+1:2 note: first attribute declared here)",
+                             }));
 
 using VariableAttributeTest = TestWithParams;
 TEST_P(VariableAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
-    if (IsBindingAttribute(params.kind)) {
-        GlobalVar("a", ty.sampler(core::type::SamplerKind::kSampler), attrs);
+    if (GetParam().attributes.Any(IsBindingAttribute)) {
+        GlobalVar(Source{{9, 9}}, "a", ty.sampler(core::type::SamplerKind::kSampler),
+                  CreateAttributes());
     } else {
-        GlobalVar("a", ty.f32(), core::AddressSpace::kPrivate, attrs);
+        GlobalVar(Source{{9, 9}}, "a", ty.f32(), core::AddressSpace::kPrivate, CreateAttributes());
     }
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        if (!IsBindingAttribute(params.kind)) {
-            EXPECT_EQ(r()->error(),
-                      "12:34 error: " + name(params.kind) + " is not valid for module-scope 'var'");
-        }
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         VariableAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, true}));
-
-TEST_F(VariableAttributeTest, DuplicateAttribute) {
-    GlobalVar("a", ty.sampler(core::type::SamplerKind::kSampler), Binding(Source{{12, 34}}, 2_a),
-              Group(2_a), Binding(Source{{56, 78}}, 3_a));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate binding attribute
-12:34 note: first attribute declared here)");
-}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    VariableAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(9:9 error: resource variables require @group and @binding attributes)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(9:9 error: resource variables require @group and @binding attributes)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for module-scope 'var')",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup, AttributeKind::kBinding},
+            R"(5:6 error: duplicate binding attribute
+1:2 note: first attribute declared here)",
+        }));
 
 TEST_F(VariableAttributeTest, LocalVar) {
     auto* v = Var("a", ty.f32(), Vector{Binding(Source{{12, 34}}, 2_a)});
@@ -1053,420 +1754,319 @@
 
 using ConstantAttributeTest = TestWithParams;
 TEST_P(ConstantAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    GlobalConst("a", ty.f32(), Expr(1.23_f),
-                createAttributes(Source{{12, 34}}, *this, params.kind));
+    GlobalConst("a", ty.f32(), Expr(1.23_f), CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for 'const' declaration");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         ConstantAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
-
-TEST_F(ConstantAttributeTest, InvalidAttribute) {
-    GlobalConst("a", ty.f32(), Expr(1.23_f),
-                Vector{
-                    Id(Source{{12, 34}}, 0_a),
-                });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @id is not valid for 'const' declaration");
-}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    ConstantAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: @id is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for 'const' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: @binding is not valid for 'const' declaration)",
+        }));
 
 using OverrideAttributeTest = TestWithParams;
 TEST_P(OverrideAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    Override("a", ty.f32(), Expr(1.23_f), createAttributes(Source{{12, 34}}, *this, params.kind));
+    Override("a", ty.f32(), Expr(1.23_f), CreateAttributes());
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for 'override' declaration");
-    }
+    CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         OverrideAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, false},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kId, true},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
-
-TEST_F(OverrideAttributeTest, DuplicateAttribute) {
-    Override("a", ty.f32(), Expr(1.23_f),
-             Vector{
-                 Id(Source{{12, 34}}, 0_a),
-                 Id(Source{{56, 78}}, 1_a),
-             });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate id attribute
-12:34 note: first attribute declared here)");
-}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    OverrideAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            R"(1:2 error: @align is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: @binding is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            R"(1:2 error: @builtin is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: @diagnostic is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: @group is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kIndex},
+            R"(1:2 error: @index is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: @interpolate is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: @invariant is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            R"(1:2 error: @location is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: @must_use is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            R"(1:2 error: @offset is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            R"(1:2 error: @size is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: @stage is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: @stride is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: @workgroup_size is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: @binding is not valid for 'override' declaration)",
+        },
+        TestParams{
+            {AttributeKind::kId, AttributeKind::kId},
+            R"(3:4 error: duplicate id attribute
+1:2 note: first attribute declared here)",
+        }));
 
 using SwitchStatementAttributeTest = TestWithParams;
 TEST_P(SwitchStatementAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(Switch(Expr(0_a), Vector{DefaultCase()},
-                          createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(Switch(Expr(0_a), Vector{DefaultCase()}, CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for switch statements");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          SwitchStatementAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("switch statements")));
 
 using SwitchBodyAttributeTest = TestWithParams;
 TEST_P(SwitchBodyAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(Switch(Expr(0_a), Vector{DefaultCase()}, tint::Empty,
-                          createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(Switch(Expr(0_a), Vector{DefaultCase()}, tint::Empty, CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for switch body");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          SwitchBodyAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("switch body")));
 
 using IfStatementAttributeTest = TestWithParams;
 TEST_P(IfStatementAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(If(Expr(true), Block(), ElseStmt(),
-                      createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(If(Expr(true), Block(), ElseStmt(), CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for if statements");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          IfStatementAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("if statements")));
 
 using ForStatementAttributeTest = TestWithParams;
 TEST_P(ForStatementAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(For(nullptr, Expr(false), nullptr, Block(),
-                       createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(For(nullptr, Expr(false), nullptr, Block(), CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for for statements");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          ForStatementAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("for statements")));
 
 using LoopStatementAttributeTest = TestWithParams;
 TEST_P(LoopStatementAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(
-        Loop(Block(Return()), Block(), createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(Loop(Block(Return()), Block(), CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for loop statements");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          LoopStatementAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("loop statements")));
 
 using WhileStatementAttributeTest = TestWithParams;
 TEST_P(WhileStatementAttributeTest, IsValid) {
-    auto& params = GetParam();
-    EnableExtensionIfNecessary(params.kind);
+    EnableRequiredExtensions();
 
-    WrapInFunction(
-        While(Expr(false), Block(), createAttributes(Source{{12, 34}}, *this, params.kind)));
+    WrapInFunction(While(Expr(false), Block(), CreateAttributes()));
 
-    if (params.should_pass) {
-        EXPECT_TRUE(r()->Resolve()) << r()->error();
-    } else {
-        EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: " + name(params.kind) + " is not valid for while statements");
-    }
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          WhileStatementAttributeTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("while statements")));
 
-namespace BlockStatementTests {
-class BlockStatementTest : public TestWithParams {
-  protected:
-    void Check() {
-        if (GetParam().should_pass) {
-            EXPECT_TRUE(r()->Resolve()) << r()->error();
-        } else {
-            EXPECT_FALSE(r()->Resolve());
-            EXPECT_EQ(r()->error(),
-                      "error: " + name(GetParam().kind) + " is not valid for block statements");
-        }
-    }
-
-  public:
-    BlockStatementTest() { EnableExtensionIfNecessary(GetParam().kind); }
-};
+using BlockStatementTest = TestWithParams;
 TEST_P(BlockStatementTest, CompoundStatement) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             Block(Vector{Return()}, createAttributes({}, *this, GetParam().kind)),
+             Block(Vector{Return()}, CreateAttributes()),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, FunctionBody) {
-    Func("foo", tint::Empty, ty.void_(),
-         Block(Vector{Return()}, createAttributes({}, *this, GetParam().kind)));
-    Check();
+    Func("foo", tint::Empty, ty.void_(), Block(Vector{Return()}, CreateAttributes()));
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, IfStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             If(Expr(true), Block(Vector{Return()}, createAttributes({}, *this, GetParam().kind))),
+             If(Expr(true), Block(Vector{Return()}, CreateAttributes())),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, ElseStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
              If(Expr(true), Block(Vector{Return()}),
-                Else(Block(Vector{Return()}, createAttributes({}, *this, GetParam().kind)))),
+                Else(Block(Vector{Return()}, CreateAttributes()))),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, ForStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             For(nullptr, Expr(true), nullptr,
-                 Block(Vector{Break()}, createAttributes({}, *this, GetParam().kind))),
+             For(nullptr, Expr(true), nullptr, Block(Vector{Break()}, CreateAttributes())),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, LoopStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             Loop(Block(Vector{Break()}, createAttributes({}, *this, GetParam().kind))),
+             Loop(Block(Vector{Break()}, CreateAttributes())),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, WhileStatementBody) {
-    Func(
-        "foo", tint::Empty, ty.void_(),
-        Vector{
-            While(Expr(true), Block(Vector{Break()}, createAttributes({}, *this, GetParam().kind))),
-        });
-    Check();
+    Func("foo", tint::Empty, ty.void_(),
+         Vector{
+             While(Expr(true), Block(Vector{Break()}, CreateAttributes())),
+         });
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, CaseStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             Switch(1_a,
-                    Case(CaseSelector(1_a),
-                         Block(Vector{Break()}, createAttributes({}, *this, GetParam().kind))),
+             Switch(1_a, Case(CaseSelector(1_a), Block(Vector{Break()}, CreateAttributes())),
                     DefaultCase(Block({}))),
          });
-    Check();
+
+    CHECK();
 }
 TEST_P(BlockStatementTest, DefaultStatementBody) {
     Func("foo", tint::Empty, ty.void_(),
          Vector{
-             Switch(
-                 1_a, Case(CaseSelector(1_a), Block()),
-                 DefaultCase(Block(Vector{Break()}, createAttributes({}, *this, GetParam().kind)))),
+             Switch(1_a, Case(CaseSelector(1_a), Block()),
+                    DefaultCase(Block(Vector{Break()}, CreateAttributes()))),
          });
-    Check();
+
+    CHECK();
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          BlockStatementTest,
-                         testing::Values(TestParams{AttributeKind::kAlign, false},
-                                         TestParams{AttributeKind::kBinding, false},
-                                         TestParams{AttributeKind::kBuiltin, false},
-                                         TestParams{AttributeKind::kDiagnostic, true},
-                                         TestParams{AttributeKind::kGroup, false},
-                                         TestParams{AttributeKind::kId, false},
-                                         TestParams{AttributeKind::kIndex, false},
-                                         TestParams{AttributeKind::kInterpolate, false},
-                                         TestParams{AttributeKind::kInvariant, false},
-                                         TestParams{AttributeKind::kLocation, false},
-                                         TestParams{AttributeKind::kMustUse, false},
-                                         TestParams{AttributeKind::kOffset, false},
-                                         TestParams{AttributeKind::kSize, false},
-                                         TestParams{AttributeKind::kStage, false},
-                                         TestParams{AttributeKind::kStride, false},
-                                         TestParams{AttributeKind::kWorkgroup, false},
-                                         TestParams{AttributeKind::kBindingAndGroup, false}));
-
-}  // namespace BlockStatementTests
+                         testing::ValuesIn(OnlyDiagnosticValidFor("block statements")));
 
 }  // namespace
 }  // namespace AttributeTests
@@ -1509,9 +2109,9 @@
     } else {
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(r()->error(),
-                  "12:34 error: arrays decorated with the stride attribute must "
-                  "have a stride that is at least the size of the element type, "
-                  "and be a multiple of the element type's alignment value");
+                  "12:34 error: arrays decorated with the stride attribute must have a stride that "
+                  "is at least the size of the element type, and be a multiple of the element "
+                  "type's alignment value");
     }
 }
 
@@ -1720,96 +2320,10 @@
 }  // namespace
 }  // namespace ResourceTests
 
-namespace InvariantAttributeTests {
-namespace {
-using InvariantAttributeTests = ResolverTest;
-TEST_F(InvariantAttributeTests, InvariantWithPosition) {
-    auto* param = Param("p", ty.vec4<f32>(),
-                        Vector{
-                            Invariant(Source{{12, 34}}),
-                            Builtin(Source{{56, 78}}, core::BuiltinValue::kPosition),
-                        });
-    Func("main", Vector{param}, ty.vec4<f32>(),
-         Vector{
-             Return(Call<vec4<f32>>()),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kFragment),
-         },
-         Vector{
-             Location(0_a),
-         });
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(InvariantAttributeTests, InvariantWithoutPosition) {
-    auto* param = Param("p", ty.vec4<f32>(),
-                        Vector{
-                            Invariant(Source{{12, 34}}),
-                            Location(0_a),
-                        });
-    Func("main", Vector{param}, ty.vec4<f32>(),
-         Vector{
-             Return(Call<vec4<f32>>()),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kFragment),
-         },
-         Vector{
-             Location(0_a),
-         });
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: invariant attribute must only be applied to a "
-              "position builtin");
-}
-}  // namespace
-}  // namespace InvariantAttributeTests
-
-namespace MustUseAttributeTests {
-namespace {
-
-using MustUseAttributeTests = ResolverTest;
-TEST_F(MustUseAttributeTests, MustUse) {
-    Func("main", tint::Empty, ty.vec4<f32>(),
-         Vector{
-             Return(Call<vec4<f32>>()),
-         },
-         Vector{
-             MustUse(Source{{12, 34}}),
-         });
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-
-}  // namespace
-}  // namespace MustUseAttributeTests
-
 namespace WorkgroupAttributeTests {
 namespace {
 
 using WorkgroupAttribute = ResolverTest;
-TEST_F(WorkgroupAttribute, ComputeShaderPass) {
-    Func("main", tint::Empty, ty.void_(), tint::Empty,
-         Vector{
-             Stage(ast::PipelineStage::kCompute),
-             create<ast::WorkgroupAttribute>(Source{{12, 34}}, Expr(1_i)),
-         });
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(WorkgroupAttribute, Missing) {
-    Func(Source{{12, 34}}, "main", tint::Empty, ty.void_(), tint::Empty,
-         Vector{
-             Stage(ast::PipelineStage::kCompute),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: a compute shader must include 'workgroup_size' in its "
-              "attributes");
-}
-
 TEST_F(WorkgroupAttribute, NotAnEntryPoint) {
     Func("main", tint::Empty, ty.void_(), tint::Empty,
          Vector{
@@ -1817,7 +2331,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @workgroup_size is only valid for compute stages");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @workgroup_size is only valid for compute stages)");
 }
 
 TEST_F(WorkgroupAttribute, NotAComputeShader) {
@@ -1828,7 +2342,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @workgroup_size is only valid for compute stages");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @workgroup_size is only valid for compute stages)");
 }
 
 TEST_F(WorkgroupAttribute, DuplicateAttribute) {
@@ -1864,7 +2378,6 @@
 using InterpolateParameterTest = TestWithParams;
 TEST_P(InterpolateParameterTest, All) {
     auto& params = GetParam();
-
     Func("main",
          Vector{
              Param("a", ty.f32(),
@@ -1882,15 +2395,14 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: flat interpolation attribute must not have a "
-                  "sampling parameter");
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: flat interpolation attribute must not have a sampling parameter)");
     }
 }
 
 TEST_P(InterpolateParameterTest, IntegerScalar) {
     auto& params = GetParam();
-
     Func("main",
          Vector{
              Param("a", ty.i32(),
@@ -1906,22 +2418,21 @@
 
     if (params.type != core::InterpolationType::kFlat) {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: interpolation type must be 'flat' for integral "
-                  "user-defined IO types");
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: interpolation type must be 'flat' for integral user-defined IO types)");
     } else if (params.should_pass) {
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: flat interpolation attribute must not have a "
-                  "sampling parameter");
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: flat interpolation attribute must not have a sampling parameter)");
     }
 }
 
 TEST_P(InterpolateParameterTest, IntegerVector) {
     auto& params = GetParam();
-
     Func("main",
          Vector{
              Param("a", ty.vec4<u32>(),
@@ -1937,16 +2448,16 @@
 
     if (params.type != core::InterpolationType::kFlat) {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: interpolation type must be 'flat' for integral "
-                  "user-defined IO types");
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: interpolation type must be 'flat' for integral user-defined IO types)");
     } else if (params.should_pass) {
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "12:34 error: flat interpolation attribute must not have a "
-                  "sampling parameter");
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: flat interpolation attribute must not have a sampling parameter)");
     }
 }
 
@@ -2003,55 +2514,6 @@
 note: while analyzing entry point 'main')");
 }
 
-TEST_F(InterpolateTest, MissingLocationAttribute_Parameter) {
-    Func("main",
-         Vector{
-             Param("a", ty.vec4<f32>(),
-                   Vector{
-                       Builtin(core::BuiltinValue::kPosition),
-                       Interpolate(Source{{12, 34}}, core::InterpolationType::kFlat),
-                   }),
-         },
-         ty.void_(), tint::Empty,
-         Vector{
-             Stage(ast::PipelineStage::kFragment),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(12:34 error: interpolate attribute must only be used with @location)");
-}
-
-TEST_F(InterpolateTest, MissingLocationAttribute_ReturnType) {
-    Func("main", tint::Empty, ty.vec4<f32>(),
-         Vector{
-             Return(Call<vec4<f32>>()),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kVertex),
-         },
-         Vector{
-             Builtin(core::BuiltinValue::kPosition),
-             Interpolate(Source{{12, 34}}, core::InterpolationType::kFlat),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(12:34 error: interpolate attribute must only be used with @location)");
-}
-
-TEST_F(InterpolateTest, MissingLocationAttribute_Struct) {
-    Structure("S",
-              Vector{
-                  Member("a", ty.f32(),
-                         Vector{Interpolate(Source{{12, 34}}, core::InterpolationType::kFlat)}),
-              });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(12:34 error: interpolate attribute must only be used with @location)");
-}
-
 using GroupAndBindingTest = ResolverTest;
 
 TEST_F(GroupAndBindingTest, Const_I32) {
@@ -2287,21 +2749,6 @@
 }  // namespace
 }  // namespace InterpolateTests
 
-namespace MustUseTests {
-namespace {
-
-using MustUseAttributeTest = ResolverTest;
-TEST_F(MustUseAttributeTest, UsedOnFnWithNoReturnValue) {
-    Func("fn_must_use", tint::Empty, ty.void_(), tint::Empty, Vector{MustUse(Source{{12, 34}})});
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(12:34 error: @must_use can only be applied to functions that return a value)");
-}
-
-}  // namespace
-}  // namespace MustUseTests
-
 namespace InternalAttributeDeps {
 namespace {
 
diff --git a/src/tint/lang/wgsl/resolver/builtins_validation_test.cc b/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
index 1da3470..616161d 100644
--- a/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
@@ -147,7 +147,7 @@
     } else {
         StringStream err;
         err << "12:34 error: @builtin(" << params.builtin << ")";
-        err << " cannot be used in input of " << params.stage << " pipeline stage";
+        err << " cannot be used for " << params.stage << " shader input";
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(r()->error(), err.str());
     }
@@ -179,9 +179,8 @@
              Location(0_a),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        "12:34 error: @builtin(frag_depth) cannot be used in input of fragment pipeline stage");
+    EXPECT_EQ(r()->error(),
+              "12:34 error: @builtin(frag_depth) cannot be used for fragment shader input");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragDepthIsInputStruct_Fail) {
@@ -215,9 +214,8 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: @builtin(frag_depth) cannot be used in input of "
-              "fragment pipeline stage\n"
-              "note: while analyzing entry point 'fragShader'");
+              R"(12:34 error: @builtin(frag_depth) cannot be used for fragment shader input
+note: while analyzing entry point 'fragShader')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, StructBuiltinInsideEntryPoint_Ignored) {
diff --git a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index cb4489f..9ea0b05 100644
--- a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
@@ -47,9 +47,9 @@
               });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: use of '@index' attribute requires enabling extension "
-              "'chromium_internal_dual_source_blending'");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of @index requires enabling extension 'chromium_internal_dual_source_blending')");
 }
 
 class DualSourceBlendingExtensionTests : public ResolverTest {
@@ -107,7 +107,7 @@
     Structure("Output", Vector{
                             Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
                             Member(Source{{12, 34}}, "b", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
+                                   Vector{Location(Source{{12, 34}}, 0_a), Index(0_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
@@ -115,14 +115,30 @@
 }
 
 // Using the index attribute without a location attribute should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithMissingLocationAttribute) {
+TEST_F(DualSourceBlendingExtensionTests, IndexWithMissingLocationAttribute_Struct) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
                                    Vector{Index(Source{{12, 34}}, 1_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: index attribute must only be used with @location");
+    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+}
+
+// Using the index attribute without a location attribute should fail.
+TEST_F(DualSourceBlendingExtensionTests, IndexWithMissingLocationAttribute_ReturnValue) {
+    Func("F", Empty, ty.vec4<f32>(),
+         Vector{
+             Return(Call<vec4<f32>>()),
+         },
+         Vector{Stage(ast::PipelineStage::kFragment)},
+         Vector{
+             Index(Source{{12, 34}}, 1_a),
+             Builtin(core::BuiltinValue::kPointSize),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
 }
 
 // Using an index attribute on a struct member should pass.
@@ -148,14 +164,30 @@
 }
 
 // Using the index attribute with a non-zero location should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithNonZeroLocation) {
+TEST_F(DualSourceBlendingExtensionTests, IndexWithNonZeroLocation_Struct) {
     Structure("Output",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(1_a), Index(Source{{12, 34}}, 0_a)}),
               });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: index attribute must only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+}
+
+// Using the index attribute with a non-zero location should fail.
+TEST_F(DualSourceBlendingExtensionTests, IndexWithNonZeroLocation_ReturnValue) {
+    Func("F", Empty, ty.vec4<f32>(),
+         Vector{
+             Return(Call<vec4<f32>>()),
+         },
+         Vector{Stage(ast::PipelineStage::kFragment)},
+         Vector{
+             Location(1_a),
+             Index(Source{{12, 34}}, 1_a),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
 }
 
 class DualSourceBlendingExtensionTestWithParams : public ResolverTestWithParam<int> {
@@ -166,20 +198,40 @@
 };
 
 // Rendering to multiple render targets while using dual source blending should fail.
-TEST_P(DualSourceBlendingExtensionTestWithParams, MultipleRenderTargetsNotAllowed) {
-    Structure("Output",
+TEST_P(DualSourceBlendingExtensionTestWithParams,
+       MultipleRenderTargetsNotAllowed_IndexThenNonZeroLocation) {
+    Structure("S",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
-                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(1_a)}),
-                  Member("c", ty.vec4<f32>(), Vector{Location(Source{{12, 34}}, AInt(GetParam()))}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{1, 2}}, 1_a)}),
+                  Member("c", ty.vec4<f32>(), Vector{Location(Source{{3, 4}}, AInt(GetParam()))}),
               });
+    Func("F", Empty, ty("S"), Vector{Return(Call("S"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    StringStream err;
-    err << "12:34 error: Multiple render targets are not allowed when using dual source blending. "
-           "The output @location("
-        << GetParam() << ") is not allowed as a render target.";
-    EXPECT_EQ(r()->error(), err.str());
+    EXPECT_EQ(r()->error(),
+              R"(1:2 error: pipeline cannot use both non-zero @index and non-zero @location
+3:4 note: non-zero @location declared here
+note: while analyzing entry point 'F')");
+}
+
+TEST_P(DualSourceBlendingExtensionTestWithParams,
+       MultipleRenderTargetsNotAllowed_NonZeroLocationThenIndex) {
+    Structure("S",
+              Vector{
+                  Member("a", ty.vec4<f32>(), Vector{Location(Source{{1, 2}}, AInt(GetParam()))}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
+                  Member("c", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{3, 4}}, 1_a)}),
+              });
+    Func(Source{{5, 6}}, "F", Empty, ty("S"), Vector{Return(Call("S"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(3:4 error: pipeline cannot use both non-zero @index and non-zero @location
+1:2 note: non-zero @location declared here
+5:6 note: while analyzing entry point 'F')");
 }
 
 INSTANTIATE_TEST_SUITE_P(DualSourceBlendingExtensionTests,
diff --git a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
index 034e3b0..444f9e5 100644
--- a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
@@ -1072,7 +1072,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location is not valid for compute shader output)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @location cannot be used by compute shaders)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocation_Output) {
@@ -1087,7 +1087,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location is not valid for compute shader inputs)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @location cannot be used by compute shaders)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocationStructMember_Output) {
@@ -1107,7 +1107,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: @location is not valid for compute shader output\n"
+              "12:34 error: @location cannot be used by compute shaders\n"
               "56:78 note: while analyzing entry point 'main'");
 }
 
@@ -1126,7 +1126,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: @location is not valid for compute shader inputs\n"
+              "12:34 error: @location cannot be used by compute shaders\n"
               "56:78 note: while analyzing entry point 'main'");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/override_test.cc b/src/tint/lang/wgsl/resolver/override_test.cc
index 6bd7223..3077e79 100644
--- a/src/tint/lang/wgsl/resolver/override_test.cc
+++ b/src/tint/lang/wgsl/resolver/override_test.cc
@@ -46,7 +46,7 @@
         ASSERT_NE(sem, nullptr);
         EXPECT_EQ(sem->Declaration(), var);
         EXPECT_TRUE(sem->Declaration()->Is<ast::Override>());
-        EXPECT_EQ(sem->OverrideId().value, id);
+        EXPECT_EQ(sem->Attributes().override_id->value, id);
         EXPECT_FALSE(sem->ConstantValue());
     }
 };
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index c3d2f53..d5a0e93 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -389,7 +389,7 @@
                 }
 
                 auto o = OverrideId{static_cast<decltype(OverrideId::value)>(value)};
-                sem->SetOverrideId(o);
+                sem->Attributes().override_id = o;
 
                 // Track the constant IDs that are specified in the shader.
                 override_ids_.Add(o, sem);
@@ -626,7 +626,7 @@
                     if (!value) {
                         return kErrored;
                     }
-                    global->SetLocation(value.Get());
+                    global->Attributes().location = value.Get();
                     return kSuccess;
                 },
                 [&](const ast::IndexAttribute* attr) {
@@ -637,7 +637,7 @@
                     if (!value) {
                         return kErrored;
                     }
-                    global->SetIndex(value.Get());
+                    global->Attributes().index = value.Get();
                     return kSuccess;
                 },
                 [&](const ast::BuiltinAttribute* attr) {
@@ -675,7 +675,7 @@
         }
 
         if (group && binding) {
-            global->SetBindingPoint(BindingPoint{group.value(), binding.value()});
+            global->Attributes().binding_point = BindingPoint{group.value(), binding.value()};
         }
 
     } else {
@@ -720,7 +720,7 @@
                     if (TINT_UNLIKELY(!value)) {
                         return false;
                     }
-                    sem->SetLocation(value.Get());
+                    sem->Attributes().location = value.Get();
                     return true;
                 },
                 [&](const ast::BuiltinAttribute* attr) -> bool { return BuiltinAttribute(attr); },
@@ -766,7 +766,7 @@
             }
         }
         if (group && binding) {
-            sem->SetBindingPoint(BindingPoint{group.value(), binding.value()});
+            sem->Attributes().binding_point = BindingPoint{group.value(), binding.value()};
         }
     } else {
         for (auto* attribute : param->attributes) {
@@ -862,8 +862,8 @@
         auto* sem = sem_.Get(override);
 
         OverrideId id;
-        if (ast::HasAttribute<ast::IdAttribute>(override->attributes)) {
-            id = sem->OverrideId();
+        if (auto sem_id = sem->Attributes().override_id) {
+            id = *sem_id;
         } else {
             // No ID was specified, so allocate the next available ID.
             while (!ids_exhausted && override_ids_.Contains(next_id)) {
@@ -879,7 +879,7 @@
             increment_next_id();
         }
 
-        const_cast<sem::GlobalVariable*>(sem)->SetOverrideId(id);
+        const_cast<sem::GlobalVariable*>(sem)->Attributes().override_id = id;
     }
     return true;
 }
diff --git a/src/tint/lang/wgsl/resolver/resolver_test.cc b/src/tint/lang/wgsl/resolver/resolver_test.cc
index 9b3c171..f78bdc3 100644
--- a/src/tint/lang/wgsl/resolver/resolver_test.cc
+++ b/src/tint/lang/wgsl/resolver/resolver_test.cc
@@ -896,9 +896,9 @@
     auto* func_sem = Sem().Get(func);
     ASSERT_NE(func_sem, nullptr);
     EXPECT_EQ(func_sem->Parameters().Length(), 3u);
-    EXPECT_EQ(3u, func_sem->Parameters()[0]->Location());
-    EXPECT_FALSE(func_sem->Parameters()[1]->Location().has_value());
-    EXPECT_EQ(1u, func_sem->Parameters()[2]->Location());
+    EXPECT_EQ(3u, func_sem->Parameters()[0]->Attributes().location);
+    EXPECT_FALSE(func_sem->Parameters()[1]->Attributes().location.has_value());
+    EXPECT_EQ(1u, func_sem->Parameters()[2]->Attributes().location);
 }
 
 TEST_F(ResolverTest, Function_GlobalVariable_Location) {
@@ -910,7 +910,7 @@
 
     auto* sem = Sem().Get<sem::GlobalVariable>(var);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(3u, sem->Location());
+    EXPECT_EQ(3u, sem->Attributes().location);
 }
 
 TEST_F(ResolverTest, Function_RegisterInputOutputVariables) {
@@ -1931,8 +1931,10 @@
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-    EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s1)->BindingPoint(), (BindingPoint{1u, 2u}));
-    EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s2)->BindingPoint(), (BindingPoint{3u, 4u}));
+    EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s1)->Attributes().binding_point,
+              (BindingPoint{1u, 2u}));
+    EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s2)->Attributes().binding_point,
+              (BindingPoint{3u, 4u}));
 }
 
 TEST_F(ResolverTest, Function_EntryPoints_StageAttribute) {
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index c05eca2..67ba42d 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -137,10 +137,7 @@
 
 // Helper to stringify a pipeline IO attribute.
 std::string AttrToStr(const ast::Attribute* attr) {
-    return Switch(
-        attr,  //
-        [&](const ast::BuiltinAttribute*) { return "@builtin"; },
-        [&](const ast::LocationAttribute*) { return "@location"; });
+    return "@" + attr->Name();
 }
 
 template <typename CALLBACK>
@@ -770,17 +767,14 @@
         return false;
     }
 
-    for (auto* attr : decl->attributes) {
-        if (attr->Is<ast::IdAttribute>()) {
-            auto id = v->OverrideId();
-            if (auto var = override_ids.Find(id); var && *var != v) {
-                AddError("@id values must be unique", attr->source);
-                AddNote(
-                    "a override with an ID of " + std::to_string(id.value) +
+    if (auto id = v->Attributes().override_id) {
+        if (auto var = override_ids.Find(*id); var && *var != v) {
+            auto* attr = ast::GetAttribute<ast::IdAttribute>(v->Declaration()->attributes);
+            AddError("@id values must be unique", attr->source);
+            AddNote("a override with an ID of " + std::to_string(id->value) +
                         " was previously declared here:",
                     ast::GetAttribute<ast::IdAttribute>((*var)->Declaration()->attributes)->source);
-                return false;
-            }
+            return false;
         }
     }
 
@@ -990,8 +984,8 @@
 
     if (is_stage_mismatch) {
         StringStream err;
-        err << "@builtin(" << builtin << ") cannot be used in "
-            << (is_input ? "input of " : "output of ") << stage_name.str() << " pipeline stage";
+        err << "@builtin(" << builtin << ") cannot be used for " << stage_name.str() << " shader "
+            << (is_input ? "input" : "output");
         AddError(err.str(), attr->source);
         return false;
     }
@@ -1000,7 +994,13 @@
 }
 
 bool Validator::InterpolateAttribute(const ast::InterpolateAttribute* attr,
-                                     const core::type::Type* storage_ty) const {
+                                     const core::type::Type* storage_ty,
+                                     const ast::PipelineStage stage) const {
+    if (stage == ast::PipelineStage::kCompute) {
+        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
+        return false;
+    }
+
     auto* type = storage_ty->UnwrapRef();
 
     auto i_type = sem_.AsInterpolationType(sem_.Get(attr->type));
@@ -1022,6 +1022,15 @@
     return true;
 }
 
+bool Validator::InvariantAttribute(const ast::InvariantAttribute* attr,
+                                   const ast::PipelineStage stage) const {
+    if (stage == ast::PipelineStage::kCompute) {
+        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
+        return false;
+    }
+    return true;
+}
+
 bool Validator::Function(const sem::Function* func, ast::PipelineStage stage) const {
     auto* decl = func->Declaration();
 
@@ -1104,7 +1113,9 @@
     // TODO(jrprice): This state could be stored in sem::Function instead, and then passed to
     // sem::Function since it would be useful there too.
     Hashset<core::BuiltinValue, 4> builtins;
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locationsAndIndexes;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
+    const ast::LocationAttribute* first_nonzero_location = nullptr;
+    const ast::IndexAttribute* first_nonzero_index = nullptr;
     enum class ParamOrRetType {
         kParameter,
         kReturnType,
@@ -1125,73 +1136,67 @@
         const ast::InterpolateAttribute* interpolate_attribute = nullptr;
         const ast::InvariantAttribute* invariant_attribute = nullptr;
         for (auto* attr : attrs) {
-            auto is_invalid_compute_shader_attribute = false;
+            bool ok = Switch(
+                attr,  //
+                [&](const ast::BuiltinAttribute* builtin_attr) {
+                    auto builtin = sem_.Get(builtin_attr)->Value();
 
-            if (auto* builtin_attr = attr->As<ast::BuiltinAttribute>()) {
-                auto builtin = sem_.Get(builtin_attr)->Value();
+                    if (pipeline_io_attribute) {
+                        AddError("multiple entry point IO attributes", attr->source);
+                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
+                                pipeline_io_attribute->source);
+                        return false;
+                    }
+                    pipeline_io_attribute = attr;
 
-                if (pipeline_io_attribute) {
-                    AddError("multiple entry point IO attributes", attr->source);
-                    AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
-                            pipeline_io_attribute->source);
-                    return false;
-                }
-                pipeline_io_attribute = attr;
+                    if (builtins.Contains(builtin)) {
+                        StringStream err;
+                        err << "@builtin(" << builtin << ") appears multiple times as pipeline "
+                            << (param_or_ret == ParamOrRetType::kParameter ? "input" : "output");
+                        AddError(err.str(), decl->source);
+                        return false;
+                    }
 
-                if (builtins.Contains(builtin)) {
-                    StringStream err;
-                    err << "@builtin(" << builtin << ") appears multiple times as pipeline "
-                        << (param_or_ret == ParamOrRetType::kParameter ? "input" : "output");
-                    AddError(err.str(), decl->source);
-                    return false;
-                }
+                    if (!BuiltinAttribute(
+                            builtin_attr, ty, stage,
+                            /* is_input */ param_or_ret == ParamOrRetType::kParameter)) {
+                        return false;
+                    }
+                    builtins.Add(builtin);
+                    return true;
+                },
+                [&](const ast::LocationAttribute* loc_attr) {
+                    location_attribute = loc_attr;
+                    if (pipeline_io_attribute) {
+                        AddError("multiple entry point IO attributes", attr->source);
+                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
+                                pipeline_io_attribute->source);
+                        return false;
+                    }
+                    pipeline_io_attribute = attr;
 
-                if (!BuiltinAttribute(builtin_attr, ty, stage,
-                                      /* is_input */ param_or_ret == ParamOrRetType::kParameter)) {
-                    return false;
-                }
-                builtins.Add(builtin);
-            } else if (auto* loc_attr = attr->As<ast::LocationAttribute>()) {
-                location_attribute = loc_attr;
-                if (pipeline_io_attribute) {
-                    AddError("multiple entry point IO attributes", attr->source);
-                    AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
-                            pipeline_io_attribute->source);
-                    return false;
-                }
-                pipeline_io_attribute = attr;
+                    if (TINT_UNLIKELY(!location.has_value())) {
+                        TINT_ICE() << "@location has no value";
+                        return false;
+                    }
 
-                bool is_input = param_or_ret == ParamOrRetType::kParameter;
+                    return LocationAttribute(loc_attr, ty, stage, source);
+                },
+                [&](const ast::IndexAttribute* index_attr) {
+                    index_attribute = index_attr;
+                    return IndexAttribute(index_attr, stage);
+                },
+                [&](const ast::InterpolateAttribute* interpolate) {
+                    interpolate_attribute = interpolate;
+                    return InterpolateAttribute(interpolate, ty, stage);
+                },
+                [&](const ast::InvariantAttribute* invariant) {
+                    invariant_attribute = invariant;
+                    return InvariantAttribute(invariant, stage);
+                },
+                [&](Default) { return true; });
 
-                if (TINT_UNLIKELY(!location.has_value())) {
-                    TINT_ICE() << "Location has no value";
-                    return false;
-                }
-
-                if (!LocationAttribute(loc_attr, ty, stage, source, is_input)) {
-                    return false;
-                }
-            } else if (auto* index_attr = attr->As<ast::IndexAttribute>()) {
-                index_attribute = index_attr;
-                return IndexAttribute(index_attr, stage);
-            } else if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
-                if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
-                    is_invalid_compute_shader_attribute = true;
-                } else if (!InterpolateAttribute(interpolate, ty)) {
-                    return false;
-                }
-                interpolate_attribute = interpolate;
-            } else if (auto* invariant = attr->As<ast::InvariantAttribute>()) {
-                if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
-                    is_invalid_compute_shader_attribute = true;
-                }
-                invariant_attribute = invariant;
-            }
-            if (is_invalid_compute_shader_attribute) {
-                std::string input_or_output =
-                    param_or_ret == ParamOrRetType::kParameter ? "inputs" : "output";
-                AddError("@" + attr->Name() + " is not valid for compute shader " + input_or_output,
-                         attr->source);
+            if (!ok) {
                 return false;
             }
         }
@@ -1234,37 +1239,38 @@
             }
 
             if (index_attribute) {
-                if (Is<ast::LocationAttribute>(pipeline_io_attribute)) {
-                    AddError("index attribute must only be used with @location",
-                             index_attribute->source);
-                    return false;
-                }
-
                 // Because HLSL specifies dual source blending targets with SV_Target0 and 1, we
-                // should restrict targets with index attributes to location 0 for easy translation
+                // should restrict targets with @index to location 0 for easy translation
                 // in the backend writers.
-                if (location.value() != 0) {
-                    AddError("index attribute must only be used with @location(0)",
-                             index_attribute->source);
+                if (location.value_or(1) != 0) {
+                    AddError("@index can only be used with @location(0)", index_attribute->source);
                     return false;
                 }
             }
 
             if (location_attribute) {
-                uint32_t idx = 0xffffffff;
-                if (index_attribute) {
-                    idx = index.value();
+                if (!first_nonzero_location && location > 0u) {
+                    first_nonzero_location = location_attribute;
+                }
+                if (!first_nonzero_index && index > 0u) {
+                    first_nonzero_index = index_attribute;
+                }
+                if (first_nonzero_location && first_nonzero_index) {
+                    AddError("pipeline cannot use both non-zero @index and non-zero @location",
+                             first_nonzero_index->source);
+                    AddNote("non-zero @location declared here", first_nonzero_location->source);
+                    return false;
                 }
 
-                std::pair<uint32_t, uint32_t> locationAndIndex(location.value(), idx);
-                if (!locationsAndIndexes.Add(locationAndIndex)) {
+                std::pair<uint32_t, uint32_t> location_and_index(location.value(),
+                                                                 index.value_or(0));
+                if (!locations_and_indices.Add(location_and_index)) {
                     StringStream err;
-                    if (!index_attribute) {
-                        err << "@location(" << location.value() << ") appears multiple times";
-                    } else {
-                        err << "@location(" << location.value() << ") @index(" << index.value()
-                            << ") appears multiple times";
+                    err << "@location(" << location.value() << ") ";
+                    if (index_attribute) {
+                        err << "@index(" << index.value() << ") ";
                     }
+                    err << "appears multiple times";
                     AddError(err.str(), location_attribute->source);
                     return false;
                 }
@@ -1273,7 +1279,7 @@
             if (interpolate_attribute) {
                 if (!pipeline_io_attribute ||
                     !pipeline_io_attribute->Is<ast::LocationAttribute>()) {
-                    AddError("interpolate attribute must only be used with @location",
+                    AddError("@interpolate can only be used with @location",
                              interpolate_attribute->source);
                     return false;
                 }
@@ -1288,7 +1294,7 @@
                     }
                 }
                 if (!has_position) {
-                    AddError("invariant attribute must only be applied to a position builtin",
+                    AddError("@invariant must be applied to a position builtin",
                              invariant_attribute->source);
                     return false;
                 }
@@ -1327,9 +1333,10 @@
 
     for (auto* param : func->Parameters()) {
         auto* param_decl = param->Declaration();
+        auto& attrs = param->Attributes();
         if (!validate_entry_point_attributes(param_decl->attributes, param->Type(),
                                              param_decl->source, ParamOrRetType::kParameter,
-                                             param->Location(), std::nullopt)) {
+                                             attrs.location, attrs.index)) {
             return false;
         }
     }
@@ -1337,7 +1344,7 @@
     // Clear IO sets after parameter validation. Builtin and location attributes in return types
     // should be validated independently from those used in parameters.
     builtins.Clear();
-    locationsAndIndexes.Clear();
+    locations_and_indices.Clear();
 
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!validate_entry_point_attributes(decl->return_type_attributes, func->ReturnType(),
@@ -1383,7 +1390,7 @@
         if (!var_decl) {
             continue;
         }
-        auto bp = global->BindingPoint();
+        auto bp = global->Attributes().binding_point;
         if (!bp) {
             continue;
         }
@@ -2146,8 +2153,7 @@
         return false;
     }
 
-    auto has_index = false;
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locationsAndIndexes;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
     for (auto* member : str->Members()) {
         if (auto* r = member->Type()->As<sem::Array>()) {
             if (r->Count()->Is<core::type::RuntimeArrayCount>()) {
@@ -2179,20 +2185,16 @@
                 attr,  //
                 [&](const ast::InvariantAttribute* invariant) {
                     invariant_attribute = invariant;
-                    return true;
+                    return InvariantAttribute(invariant, stage);
                 },
                 [&](const ast::LocationAttribute* location) {
                     location_attribute = location;
                     TINT_ASSERT(member->Attributes().location.has_value());
-                    if (!LocationAttribute(location, member->Type(), stage,
-                                           member->Declaration()->source)) {
-                        return false;
-                    }
-                    return true;
+                    return LocationAttribute(location, member->Type(), stage,
+                                             member->Declaration()->source);
                 },
                 [&](const ast::IndexAttribute* index) {
                     index_attribute = index;
-                    has_index = true;
                     return IndexAttribute(index, stage);
                 },
                 [&](const ast::BuiltinAttribute* builtin_attr) {
@@ -2208,16 +2210,13 @@
                 },
                 [&](const ast::InterpolateAttribute* interpolate) {
                     interpolate_attribute = interpolate;
-                    if (!InterpolateAttribute(interpolate, member->Type())) {
-                        return false;
-                    }
-                    return true;
+                    return InterpolateAttribute(interpolate, member->Type(), stage);
                 },
                 [&](const ast::StructMemberSizeAttribute*) {
                     if (!member->Type()->HasCreationFixedFootprint()) {
                         AddError(
-                            "@size can only be applied to members where the member's type size "
-                            "can be fully determined at shader creation time",
+                            "@size can only be applied to members where the member's type size can "
+                            "be fully determined at shader creation time",
                             attr->source);
                         return false;
                     }
@@ -2230,62 +2229,40 @@
         }
 
         if (invariant_attribute && !has_position) {
-            AddError("invariant attribute must only be applied to a position builtin",
+            AddError("@invariant must be applied to a position builtin",
                      invariant_attribute->source);
             return false;
         }
 
         if (index_attribute) {
-            if (!location_attribute) {
-                AddError("index attribute must only be used with @location",
-                         index_attribute->source);
-                return false;
-            }
-
             // Because HLSL specifies dual source blending targets with SV_Target0 and 1, we should
             // restrict targets with index attributes to location 0 for easy translation in the
             // backend writers.
-            if (member->Attributes().location.value() != 0) {
-                AddError("index attribute must only be used with @location(0)",
-                         index_attribute->source);
+            if (member->Attributes().location.value_or(1) != 0) {
+                AddError("@index can only be used with @location(0)", index_attribute->source);
                 return false;
             }
         }
 
         if (interpolate_attribute && !location_attribute) {
-            AddError("interpolate attribute must only be used with @location",
-                     interpolate_attribute->source);
+            AddError("@interpolate can only be used with @location", interpolate_attribute->source);
             return false;
         }
 
         // Ensure all locations and index pairs are unique
         if (location_attribute) {
-            uint32_t index = 0xffffffff;
-            if (index_attribute) {
-                index = member->Attributes().index.value();
-            }
             uint32_t location = member->Attributes().location.value();
-            if (has_index && location != 0) {
-                StringStream err;
-                err << "Multiple render targets are not allowed when using dual source blending. "
-                       "The output @location("
-                    << location << ") is not allowed as a render target.";
-                AddError(err.str(), location_attribute->source);
-                return false;
-            }
+            uint32_t index = member->Attributes().index.value_or(0);
 
-            std::pair<uint32_t, uint32_t> locationAndIndex(location, index);
-            if (!locationsAndIndexes.Add(locationAndIndex)) {
+            std::pair<uint32_t, uint32_t> location_and_index(location, index);
+            if (!locations_and_indices.Add(location_and_index)) {
                 StringStream err;
-                if (!index_attribute) {
-                    err << "@location(" << location << ") appears multiple times";
-                    AddError(err.str(), location_attribute->source);
-                } else {
-                    err << "@location(" << location << ") @index(" << index
-                        << ") appears multiple times";
-                    AddError(err.str(), index_attribute->source);
+                err << "@location(" << location << ") ";
+                if (index_attribute) {
+                    err << "@index(" << index << ") ";
                 }
-
+                err << "appears multiple times";
+                AddError(err.str(), location_attribute->source);
                 return false;
             }
         }
@@ -2294,15 +2271,12 @@
     return true;
 }
 
-bool Validator::LocationAttribute(const ast::LocationAttribute* loc_attr,
+bool Validator::LocationAttribute(const ast::LocationAttribute* attr,
                                   const core::type::Type* type,
                                   ast::PipelineStage stage,
-                                  const Source& source,
-                                  const bool is_input) const {
-    std::string inputs_or_output = is_input ? "inputs" : "output";
+                                  const Source& source) const {
     if (stage == ast::PipelineStage::kCompute) {
-        AddError("@" + loc_attr->Name() + " is not valid for compute shader " + inputs_or_output,
-                 loc_attr->source);
+        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
         return false;
     }
 
@@ -2312,32 +2286,28 @@
         AddNote(
             "@location must only be applied to declarations of numeric scalar or numeric vector "
             "type",
-            loc_attr->source);
+            attr->source);
         return false;
     }
 
     return true;
 }
 
-bool Validator::IndexAttribute(const ast::IndexAttribute* index_attr,
-                               ast::PipelineStage stage) const {
+bool Validator::IndexAttribute(const ast::IndexAttribute* attr,
+                               ast::PipelineStage stage,
+                               const std::optional<bool> is_input) const {
     if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumInternalDualSourceBlending)) {
         AddError(
-            "use of '@index' attribute requires enabling extension "
-            "'chromium_internal_dual_source_blending'",
-            index_attr->source);
+            "use of @index requires enabling extension 'chromium_internal_dual_source_blending'",
+            attr->source);
         return false;
     }
 
-    if (stage == ast::PipelineStage::kCompute) {
-        AddError("@" + index_attr->Name() + " is not valid for compute shader output",
-                 index_attr->source);
-        return false;
-    }
-
-    if (stage == ast::PipelineStage::kVertex) {
-        AddError("@" + index_attr->Name() + " is not valid for vertex shader output",
-                 index_attr->source);
+    bool is_stage_non_fragment =
+        stage != ast::PipelineStage::kNone && stage != ast::PipelineStage::kFragment;
+    bool is_output = is_input.value_or(false);
+    if (is_stage_non_fragment || is_output) {
+        AddError(AttrToStr(attr) + " can only be used for fragment shader output", attr->source);
         return false;
     }
 
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index 20735de..03466ee 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -315,11 +315,20 @@
     bool IncrementDecrementStatement(const ast::IncrementDecrementStatement* stmt) const;
 
     /// Validates an interpolate attribute
-    /// @param attr the interpolation attribute to validate
+    /// @param attr the attribute to validate
     /// @param storage_type the storage type of the attached variable
-    /// @returns true on succes, false otherwise
+    /// @param stage the current pipeline stage
+    /// @returns true on success, false otherwise
     bool InterpolateAttribute(const ast::InterpolateAttribute* attr,
-                              const core::type::Type* storage_type) const;
+                              const core::type::Type* storage_type,
+                              const ast::PipelineStage stage) const;
+
+    /// Validates an invariant attribute
+    /// @param attr the attribute to validate
+    /// @param stage the current pipeline stage
+    /// @returns true on success, false otherwise
+    bool InvariantAttribute(const ast::InvariantAttribute* attr,
+                            const ast::PipelineStage stage) const;
 
     /// Validates a builtin call
     /// @param call the builtin call to validate
@@ -332,23 +341,25 @@
     bool LocalVariable(const sem::Variable* v) const;
 
     /// Validates a location attribute
-    /// @param loc_attr the location attribute to validate
+    /// @param attr the attribute to validate
     /// @param type the variable type
     /// @param stage the current pipeline stage
-    /// @param source the source of the attribute
-    /// @param is_input true if this is an input variable
+    /// @param source the source of declaration using the attribute
     /// @returns true on success, false otherwise.
-    bool LocationAttribute(const ast::LocationAttribute* loc_attr,
+    bool LocationAttribute(const ast::LocationAttribute* attr,
                            const core::type::Type* type,
-                           ast::PipelineStage stage,
-                           const Source& source,
-                           const bool is_input = false) const;
+                           const ast::PipelineStage stage,
+                           const Source& source) const;
 
     /// Validates a index attribute
     /// @param index_attr the index attribute to validate
     /// @param stage the current pipeline stage
+    /// @param is_input true if is an input variable, false if output variable, std::nullopt is
+    /// unknown.
     /// @returns true on success, false otherwise.
-    bool IndexAttribute(const ast::IndexAttribute* index_attr, ast::PipelineStage stage) const;
+    bool IndexAttribute(const ast::IndexAttribute* index_attr,
+                        ast::PipelineStage stage,
+                        const std::optional<bool> is_input = std::nullopt) const;
 
     /// Validates a loop statement
     /// @param stmt the loop statement
diff --git a/src/tint/lang/wgsl/sem/function.cc b/src/tint/lang/wgsl/sem/function.cc
index 4d1d3c9..9a56376 100644
--- a/src/tint/lang/wgsl/sem/function.cc
+++ b/src/tint/lang/wgsl/sem/function.cc
@@ -81,7 +81,7 @@
             continue;
         }
 
-        if (auto bp = global->BindingPoint()) {
+        if (auto bp = global->Attributes().binding_point) {
             ret.push_back({global, *bp});
         }
     }
@@ -96,7 +96,7 @@
             continue;
         }
 
-        if (auto bp = global->BindingPoint()) {
+        if (auto bp = global->Attributes().binding_point) {
             ret.push_back({global, *bp});
         }
     }
@@ -140,7 +140,7 @@
     for (auto* global : TransitivelyReferencedGlobals()) {
         auto* unwrapped_type = global->Type()->UnwrapRef();
         if (unwrapped_type->TypeInfo().Is(type)) {
-            if (auto bp = global->BindingPoint()) {
+            if (auto bp = global->Attributes().binding_point) {
                 ret.push_back({global, *bp});
             }
         }
@@ -168,7 +168,7 @@
             continue;
         }
 
-        if (auto bp = global->BindingPoint()) {
+        if (auto bp = global->Attributes().binding_point) {
             ret.push_back({global, *bp});
         }
     }
@@ -193,7 +193,7 @@
             continue;
         }
 
-        if (auto bp = global->BindingPoint()) {
+        if (auto bp = global->Attributes().binding_point) {
             ret.push_back({global, *bp});
         }
     }
diff --git a/src/tint/lang/wgsl/sem/variable.h b/src/tint/lang/wgsl/sem/variable.h
index bca5d1b..3ee24f3 100644
--- a/src/tint/lang/wgsl/sem/variable.h
+++ b/src/tint/lang/wgsl/sem/variable.h
@@ -151,6 +151,22 @@
     const CastableBase* shadows_ = nullptr;
 };
 
+/// Attributes that can be applied to global variables
+struct GlobalVariableAttributes {
+    /// the pipeline constant ID associated with the variable
+    std::optional<tint::OverrideId> override_id;
+    /// the resource binding point for the variable, if set.
+    std::optional<tint::BindingPoint> binding_point;
+    /// The `location` attribute value for the variable, if set
+    /// @note a GlobalVariable generally doesn't have a `location` in WGSL, as it isn't allowed by
+    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
+    std::optional<uint32_t> location;
+    /// The `index` attribute value for the variable, if set
+    /// @note a GlobalVariable generally doesn't have a `index` in WGSL, as it isn't allowed by
+    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
+    std::optional<uint32_t> index;
+};
+
 /// GlobalVariable is a module-scope variable
 class GlobalVariable final : public Castable<GlobalVariable, Variable> {
   public:
@@ -161,34 +177,6 @@
     /// Destructor
     ~GlobalVariable() override;
 
-    /// @param binding_point the resource binding point for the parameter
-    void SetBindingPoint(std::optional<tint::BindingPoint> binding_point) {
-        binding_point_ = binding_point;
-    }
-
-    /// @returns the resource binding point for the variable
-    std::optional<tint::BindingPoint> BindingPoint() const { return binding_point_; }
-
-    /// @param id the constant identifier to assign to this variable
-    void SetOverrideId(OverrideId id) { override_id_ = id; }
-
-    /// @returns the pipeline constant ID associated with the variable
-    tint::OverrideId OverrideId() const { return override_id_; }
-
-    /// @param location the location value for the parameter, if set
-    /// @note a GlobalVariable generally doesn't have a `location` in WGSL, as it isn't allowed by
-    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
-    void SetLocation(std::optional<uint32_t> location) { location_ = location; }
-
-    /// @returns the location value for the parameter, if set
-    std::optional<uint32_t> Location() const { return location_; }
-
-    /// @param index the index value for the parameter, if set
-    void SetIndex(std::optional<uint32_t> index) { index_ = index; }
-
-    /// @returns the index value for the parameter, if set
-    std::optional<uint32_t> Index() const { return index_; }
-
     /// Records that this variable (transitively) references the given override variable.
     /// @param var the module-scope override variable
     void AddTransitivelyReferencedOverride(const GlobalVariable* var);
@@ -198,12 +186,30 @@
         return transitively_referenced_overrides_;
     }
 
+    /// @return the mutable attributes for the variable
+    GlobalVariableAttributes& Attributes() { return attributes_; }
+
+    /// @return the immutable attributes for the variable
+    const GlobalVariableAttributes& Attributes() const { return attributes_; }
+
   private:
     std::optional<tint::BindingPoint> binding_point_;
     tint::OverrideId override_id_;
-    std::optional<uint32_t> location_;
-    std::optional<uint32_t> index_;
     UniqueVector<const GlobalVariable*, 4> transitively_referenced_overrides_;
+    GlobalVariableAttributes attributes_;
+};
+
+/// Attributes that can be applied to parameters
+struct ParameterAttributes {
+    /// the resource binding point for the variable, if set.
+    /// @note a Parameter generally doesn't have a `group` or `binding` attribute in WGSL, as it
+    /// isn't allowed by the spec. The binding point maybe attached by transforms such as
+    /// CanonicalizeEntryPointIO.
+    std::optional<tint::BindingPoint> binding_point;
+    /// The `location` attribute value for the variable, if set
+    std::optional<uint32_t> location;
+    /// The `index` attribute value for the variable, if set
+    std::optional<uint32_t> index;
 };
 
 /// Parameter is a function parameter
@@ -227,9 +233,6 @@
         return static_cast<const ast::Parameter*>(Variable::Declaration());
     }
 
-    /// @param index the index value for the parameter, if set
-    void SetIndex(uint32_t index) { index_ = index; }
-
     /// @return the index of the parameter in the function
     uint32_t Index() const { return index_; }
 
@@ -252,27 +255,18 @@
     /// @returns the Type, Function or Variable that this local variable shadows
     const CastableBase* Shadows() const { return shadows_; }
 
-    /// @param binding_point the resource binding point for the parameter
-    void SetBindingPoint(std::optional<tint::BindingPoint> binding_point) {
-        binding_point_ = binding_point;
-    }
+    /// @return the mutable attributes for the parameter
+    ParameterAttributes& Attributes() { return attributes_; }
 
-    /// @returns the resource binding point for the parameter
-    std::optional<tint::BindingPoint> BindingPoint() const { return binding_point_; }
-
-    /// @param location the location value for the parameter, if set
-    void SetLocation(std::optional<uint32_t> location) { location_ = location; }
-
-    /// @returns the location value for the parameter, if set
-    std::optional<uint32_t> Location() const { return location_; }
+    /// @return the immutable attributes for the parameter
+    const ParameterAttributes& Attributes() const { return attributes_; }
 
   private:
-    uint32_t index_ = 0;
+    const uint32_t index_ = 0;
     core::ParameterUsage usage_ = core::ParameterUsage::kNone;
     CallTarget const* owner_ = nullptr;
     const CastableBase* shadows_ = nullptr;
-    std::optional<tint::BindingPoint> binding_point_;
-    std::optional<uint32_t> location_;
+    ParameterAttributes attributes_;
 };
 
 /// VariableUser holds the semantic information for an identifier expression
diff --git a/src/tint/lang/wgsl/wgsl.def b/src/tint/lang/wgsl/wgsl.def
index a66c2ba..8c545c9 100644
--- a/src/tint/lang/wgsl/wgsl.def
+++ b/src/tint/lang/wgsl/wgsl.def
@@ -87,7 +87,11 @@
   // A Chromium-specific extension that enables dual source blending.
   chromium_internal_dual_source_blending
   // A Chromium-specific extension that enables pixel local storage.
+  // Cannot be used with chromium_experimental_framebuffer_fetch
   chromium_experimental_pixel_local
+  // A Chromium-specific extension that enables framebuffer fetching.
+  // Cannot be used with chromium_experimental_pixel_local
+  chromium_experimental_framebuffer_fetch
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#language-extensions-sec