validation: clean up function attributes validation and unit-tests

- clean up function decorations unit tests
- clean up interpolate and invariant validation and unittest
- add separate unit-tests for each shader stage input and output
- add [[builtin(position)]] tests
- add validation and test for:
structures with 'location' decorated members cannot be used as compute shaders input

Bug: tint:1007
Change-Id: I12e97e163b3a77bc76ce21faba241683eec5d917
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/58942
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/resolver/builtins_validation_test.cc b/src/resolver/builtins_validation_test.cc
index 4af121d..38d04e0 100644
--- a/src/resolver/builtins_validation_test.cc
+++ b/src/resolver/builtins_validation_test.cc
@@ -48,6 +48,16 @@
   return Params{DataType<T>::AST, builtin, stage, is_valid};
 }
 static constexpr Params cases[] = {
+    ParamsFor<vec4<f32>>(ast::Builtin::kPosition,
+                         ast::PipelineStage::kVertex,
+                         false),
+    ParamsFor<vec4<f32>>(ast::Builtin::kPosition,
+                         ast::PipelineStage::kFragment,
+                         true),
+    ParamsFor<vec4<f32>>(ast::Builtin::kPosition,
+                         ast::PipelineStage::kCompute,
+                         false),
+
     ParamsFor<u32>(ast::Builtin::kVertexIndex,
                    ast::PipelineStage::kVertex,
                    true),
@@ -183,7 +193,7 @@
 TEST_F(ResolverBuiltinsValidationTest, FragDepthIsInput_Fail) {
   // [[stage(fragment)]]
   // fn fs_main(
-  //   [[builtin(kFragDepth)]] fd: f32,
+  //   [[builtin(frag_depth)]] fd: f32,
   // ) -> [[location(0)]] f32 { return 1.0; }
   auto* fd = Param(
       "fd", ty.f32(),
@@ -197,9 +207,9 @@
             "fragment pipeline stage");
 }
 
-TEST_F(ResolverBuiltinsValidationTest, FragDepthIsInputStruct_Fail) {
-  // Struct MyInputs {
-  //   [[builtin(front_facing)]] ff: bool;
+TEST_F(ResolverBuiltinsValidationTest, FragDepthIsInputStruct_Ignored) {
+  // struct MyInputs {
+  //   [[builtin(frag_depth)]] ff: f32;
   // };
   // [[stage(fragment)]]
   // fn fragShader(arg: MyInputs) -> [[location(0)]] f32 { return 1.0; }
@@ -211,11 +221,7 @@
 
   Func("fragShader", {Param("arg", ty.Of(s))}, ty.f32(), {Return(1.0f)},
        {Stage(ast::PipelineStage::kFragment)}, {Location(0)});
-  EXPECT_FALSE(r()->Resolve());
-  EXPECT_EQ(
-      r()->error(),
-      "12:34 error: builtin(frag_depth) cannot be used in input of fragment "
-      "pipeline stage\nnote: while analysing entry point fragShader");
+  EXPECT_TRUE(r()->Resolve());
 }
 }  // namespace StageTest
 
diff --git a/src/resolver/decoration_validation_test.cc b/src/resolver/decoration_validation_test.cc
index 48b6c8c..03639c7 100644
--- a/src/resolver/decoration_validation_test.cc
+++ b/src/resolver/decoration_validation_test.cc
@@ -13,11 +13,6 @@
 // limitations under the License.
 
 #include "src/ast/disable_validation_decoration.h"
-#include "src/ast/override_decoration.h"
-#include "src/ast/return_statement.h"
-#include "src/ast/stage_decoration.h"
-#include "src/ast/struct_block_decoration.h"
-#include "src/ast/workgroup_decoration.h"
 #include "src/resolver/resolver.h"
 #include "src/resolver/resolver_test_helper.h"
 
@@ -55,7 +50,6 @@
 
 namespace DecorationTests {
 namespace {
-
 enum class DecorationKind {
   kAlign,
   kBinding,
@@ -75,7 +69,7 @@
   kBindingAndGroup,
 };
 
-bool IsBindingDecoration(DecorationKind kind) {
+static bool IsBindingDecoration(DecorationKind kind) {
   switch (kind) {
     case DecorationKind::kBinding:
     case DecorationKind::kGroup:
@@ -106,8 +100,7 @@
       return {builder.create<ast::GroupDecoration>(source, 1u)};
     case DecorationKind::kInterpolate:
       return {builder.Interpolate(source, ast::InterpolationType::kLinear,
-                                  ast::InterpolationSampling::kCenter),
-              builder.Location(0)};
+                                  ast::InterpolationSampling::kCenter)};
     case DecorationKind::kInvariant:
       return {builder.Invariant(source)};
     case DecorationKind::kLocation:
@@ -117,7 +110,7 @@
     case DecorationKind::kOffset:
       return {builder.create<ast::StructMemberOffsetDecoration>(source, 4u)};
     case DecorationKind::kSize:
-      return {builder.create<ast::StructMemberSizeDecoration>(source, 4u)};
+      return {builder.create<ast::StructMemberSizeDecoration>(source, 16u)};
     case DecorationKind::kStage:
       return {builder.Stage(source, ast::PipelineStage::kCompute)};
     case DecorationKind::kStride:
@@ -134,6 +127,7 @@
   return {};
 }
 
+namespace FunctionInputAndOutputTests {
 using FunctionParameterDecorationTest = TestWithParams;
 TEST_P(FunctionParameterDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -171,114 +165,6 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
-using EntryPointParameterDecorationTest = TestWithParams;
-TEST_P(EntryPointParameterDecorationTest, IsValid) {
-  auto& params = GetParam();
-
-  Func("main",
-       ast::VariableList{Param("a", ty.vec4<f32>(),
-                               createDecorations({}, *this, params.kind))},
-       ty.void_(), {},
-       ast::DecorationList{Stage(ast::PipelineStage::kFragment)});
-
-  if (params.should_pass) {
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-  } else {
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "error: decoration is not valid for function parameters");
-  }
-}
-INSTANTIATE_TEST_SUITE_P(
-    ResolverDecorationValidationTest,
-    EntryPointParameterDecorationTest,
-    testing::Values(TestParams{DecorationKind::kAlign, false},
-                    TestParams{DecorationKind::kBinding, false},
-                    TestParams{DecorationKind::kBuiltin, true},
-                    TestParams{DecorationKind::kGroup, false},
-                    TestParams{DecorationKind::kInterpolate, true},
-                    // kInvariant tested separately (requires position builtin)
-                    TestParams{DecorationKind::kLocation, true},
-                    TestParams{DecorationKind::kOverride, false},
-                    TestParams{DecorationKind::kOffset, false},
-                    TestParams{DecorationKind::kSize, false},
-                    TestParams{DecorationKind::kStage, false},
-                    TestParams{DecorationKind::kStride, false},
-                    TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false},
-                    TestParams{DecorationKind::kBindingAndGroup, false}));
-
-TEST_F(EntryPointParameterDecorationTest, DuplicateDecoration) {
-  Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1.f)},
-       {Stage(ast::PipelineStage::kFragment)},
-       {
-           Location(Source{{12, 34}}, 2),
-           Location(Source{{56, 78}}, 3),
-       });
-
-  EXPECT_FALSE(r()->Resolve());
-  EXPECT_EQ(r()->error(),
-            R"(56:78 error: duplicate location decoration
-12:34 note: first decoration declared here)");
-}
-
-TEST_F(EntryPointParameterDecorationTest, DuplicateInternalDecoration) {
-  auto* s =
-      Param("s", ty.sampler(ast::SamplerKind::kSampler),
-            ast::DecorationList{
-                create<ast::BindingDecoration>(0),
-                create<ast::GroupDecoration>(0),
-                ASTNodes().Create<ast::DisableValidationDecoration>(
-                    ID(), ast::DisabledValidation::kBindingPointCollision),
-                ASTNodes().Create<ast::DisableValidationDecoration>(
-                    ID(), ast::DisabledValidation::kEntryPointParameter),
-            });
-  Func("f", {s}, ty.void_(), {}, {Stage(ast::PipelineStage::kFragment)});
-
-  EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(EntryPointParameterDecorationTest, ComputeShaderLocation) {
-  auto* input = Param("input", ty.vec4<f32>(),
-                      ast::DecorationList{Location(Source{{12, 34}}, 1)});
-  Func("main", {input}, ty.void_(), {},
-       {Stage(ast::PipelineStage::kCompute),
-        create<ast::WorkgroupDecoration>(Source{{12, 34}}, Expr(1))});
-
-  EXPECT_FALSE(r()->Resolve());
-  EXPECT_EQ(r()->error(),
-            "12:34 error: decoration is not valid for compute shader function "
-            "parameters");
-}
-
-TEST_F(EntryPointParameterDecorationTest, InvariantWithPosition) {
-  auto* param = Param("p", ty.vec4<f32>(),
-                      {Invariant(Source{{12, 34}}),
-                       Builtin(Source{{56, 78}}, ast::Builtin::kPosition)});
-  Func("main", ast::VariableList{param}, ty.vec4<f32>(),
-       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
-       ast::DecorationList{Stage(ast::PipelineStage::kFragment)},
-       ast::DecorationList{
-           Location(0),
-       });
-  EXPECT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(EntryPointParameterDecorationTest, InvariantWithoutPosition) {
-  auto* param =
-      Param("p", ty.vec4<f32>(), {Invariant(Source{{12, 34}}), Location(0)});
-  Func("main", ast::VariableList{param}, ty.vec4<f32>(),
-       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
-       ast::DecorationList{Stage(ast::PipelineStage::kFragment)},
-       ast::DecorationList{
-           Location(0),
-       });
-  EXPECT_FALSE(r()->Resolve());
-  EXPECT_EQ(r()->error(),
-            "12:34 error: invariant attribute must only be applied to a "
-            "position builtin");
-}
-
 using FunctionReturnTypeDecorationTest = TestWithParams;
 TEST_P(FunctionReturnTypeDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -313,41 +199,46 @@
                     TestParams{DecorationKind::kStructBlock, false},
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
+}  // namespace FunctionInputAndOutputTests
 
-using EntryPointReturnTypeDecorationTest = TestWithParams;
-TEST_P(EntryPointReturnTypeDecorationTest, IsValid) {
+namespace EntryPointInputAndOutputTests {
+using ComputeShaderParameterDecorationTest = TestWithParams;
+TEST_P(ComputeShaderParameterDecorationTest, IsValid) {
   auto& params = GetParam();
-
-  Func("main", ast::VariableList{}, ty.vec4<f32>(),
-       {Return(Construct(ty.vec4<f32>(), 1.f))},
-       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)},
-       createDecorations({}, *this, params.kind));
+  auto* p = Param("a", ty.vec4<f32>(),
+                  createDecorations(Source{{12, 34}}, *this, params.kind));
+  Func("main", ast::VariableList{p}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
 
   if (params.should_pass) {
     EXPECT_TRUE(r()->Resolve()) << r()->error();
   } else {
     EXPECT_FALSE(r()->Resolve());
-    if (params.kind == DecorationKind::kLocation ||
-        params.kind == DecorationKind::kInterpolate) {
+    if (params.kind == DecorationKind::kBuiltin) {
       EXPECT_EQ(r()->error(),
-                "error: decoration is not valid for compute shader entry point "
-                "return types");
+                "12:34 error: builtin(position) cannot be used in input of "
+                "compute pipeline stage");
+    } else if (params.kind == DecorationKind::kInterpolate ||
+               params.kind == DecorationKind::kLocation ||
+               params.kind == DecorationKind::kInvariant) {
+      EXPECT_EQ(
+          r()->error(),
+          "12:34 error: decoration is not valid for compute shader inputs");
     } else {
       EXPECT_EQ(r()->error(),
-                "error: decoration is not valid for entry point return types");
+                "12:34 error: decoration is not valid for function parameters");
     }
   }
 }
-
 INSTANTIATE_TEST_SUITE_P(
     ResolverDecorationValidationTest,
-    EntryPointReturnTypeDecorationTest,
+    ComputeShaderParameterDecorationTest,
     testing::Values(TestParams{DecorationKind::kAlign, false},
                     TestParams{DecorationKind::kBinding, false},
-                    TestParams{DecorationKind::kBuiltin, true},
+                    TestParams{DecorationKind::kBuiltin, false},
                     TestParams{DecorationKind::kGroup, false},
                     TestParams{DecorationKind::kInterpolate, false},
-                    // kInvariant tested separately (requires position builtin)
+                    TestParams{DecorationKind::kInvariant, false},
                     TestParams{DecorationKind::kLocation, false},
                     TestParams{DecorationKind::kOverride, false},
                     TestParams{DecorationKind::kOffset, false},
@@ -358,6 +249,271 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
+using FragmentShaderParameterDecorationTest = TestWithParams;
+TEST_P(FragmentShaderParameterDecorationTest, IsValid) {
+  auto& params = GetParam();
+  auto decos = createDecorations(Source{{12, 34}}, *this, params.kind);
+  if (params.kind != DecorationKind::kBuiltin &&
+      params.kind != DecorationKind::kLocation) {
+    decos.push_back(Builtin(Source{{34, 56}}, ast::Builtin::kPosition));
+  }
+  auto* p = Param("a", ty.vec4<f32>(), decos);
+  Func("frag_main", {p}, ty.void_(), {},
+       {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: decoration is not valid for function parameters");
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    FragmentShaderParameterDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, true},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, true},
+                    TestParams{DecorationKind::kInvariant, true},
+                    TestParams{DecorationKind::kLocation, true},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, false},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
+using VertexShaderParameterDecorationTest = TestWithParams;
+TEST_P(VertexShaderParameterDecorationTest, IsValid) {
+  auto& params = GetParam();
+  auto decos = createDecorations(Source{{12, 34}}, *this, params.kind);
+  if (params.kind != DecorationKind::kLocation) {
+    decos.push_back(Location(Source{{34, 56}}, 2));
+  }
+  auto* p = Param("a", ty.vec4<f32>(), decos);
+  Func("vertex_main", ast::VariableList{p}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kVertex)},
+       {Builtin(ast::Builtin::kPosition)});
+
+  if (params.should_pass) {
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+  } else {
+    EXPECT_FALSE(r()->Resolve());
+    if (params.kind == DecorationKind::kBuiltin) {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: builtin(position) cannot be used in input of "
+                "vertex pipeline stage");
+    } else if (params.kind == DecorationKind::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: decoration is not valid for function parameters");
+    }
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    VertexShaderParameterDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, false},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, true},
+                    TestParams{DecorationKind::kInvariant, false},
+                    TestParams{DecorationKind::kLocation, true},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, false},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
+using ComputeShaderReturnTypeDecorationTest = TestWithParams;
+TEST_P(ComputeShaderReturnTypeDecorationTest, IsValid) {
+  auto& params = GetParam();
+  Func("main", ast::VariableList{}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>(), 1.f))},
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)},
+       createDecorations(Source{{12, 34}}, *this, params.kind));
+
+  if (params.should_pass) {
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+  } else {
+    EXPECT_FALSE(r()->Resolve());
+    if (params.kind == DecorationKind::kBuiltin) {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: builtin(position) cannot be used in output of "
+                "compute pipeline stage");
+    } else if (params.kind == DecorationKind::kInterpolate ||
+               params.kind == DecorationKind::kLocation ||
+               params.kind == DecorationKind::kInvariant) {
+      EXPECT_EQ(
+          r()->error(),
+          "12:34 error: decoration is not valid for compute shader output");
+    } else {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: decoration is not valid for entry point return "
+                "types");
+    }
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    ComputeShaderReturnTypeDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, false},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, false},
+                    TestParams{DecorationKind::kInvariant, false},
+                    TestParams{DecorationKind::kLocation, false},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, false},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
+using FragmentShaderReturnTypeDecorationTest = TestWithParams;
+TEST_P(FragmentShaderReturnTypeDecorationTest, IsValid) {
+  auto& params = GetParam();
+  auto decos = createDecorations(Source{{12, 34}}, *this, params.kind);
+  decos.push_back(Location(Source{{34, 56}}, 2));
+  Func("frag_main", {}, ty.vec4<f32>(), {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kFragment)}, decos);
+
+  if (params.should_pass) {
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+  } else {
+    EXPECT_FALSE(r()->Resolve());
+    if (params.kind == DecorationKind::kBuiltin) {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: builtin(position) cannot be used in output of "
+                "fragment pipeline stage");
+    } else if (params.kind == DecorationKind::kInvariant) {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: invariant attribute must only be applied to a "
+                "position builtin");
+    } else if (params.kind == DecorationKind::kLocation) {
+      EXPECT_EQ(r()->error(),
+                "34:56 error: duplicate location decoration\n"
+                "12:34 note: first decoration declared here");
+    } else {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: decoration is not valid for entry point return "
+                "types");
+    }
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    FragmentShaderReturnTypeDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, false},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, true},
+                    TestParams{DecorationKind::kInvariant, false},
+                    TestParams{DecorationKind::kLocation, false},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, false},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
+using VertexShaderReturnTypeDecorationTest = TestWithParams;
+TEST_P(VertexShaderReturnTypeDecorationTest, IsValid) {
+  auto& params = GetParam();
+  auto decos = createDecorations(Source{{12, 34}}, *this, params.kind);
+  // a vertex shader must include the 'position' builtin in its return type
+  if (params.kind != DecorationKind::kBuiltin) {
+    decos.push_back(Builtin(Source{{34, 56}}, ast::Builtin::kPosition));
+  }
+  Func("vertex_main", ast::VariableList{}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kVertex)}, decos);
+
+  if (params.should_pass) {
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+  } else {
+    EXPECT_FALSE(r()->Resolve());
+    if (params.kind == DecorationKind::kLocation) {
+      EXPECT_EQ(r()->error(),
+                "34:56 error: multiple entry point IO attributes\n"
+                "12:34 note: previously consumed location(1)");
+    } else {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: decoration is not valid for entry point return "
+                "types");
+    }
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    VertexShaderReturnTypeDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, true},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, true},
+                    TestParams{DecorationKind::kInvariant, true},
+                    TestParams{DecorationKind::kLocation, false},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, false},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
+using EntryPointParameterDecorationTest = TestWithParams;
+TEST_F(EntryPointParameterDecorationTest, DuplicateDecoration) {
+  Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1.f)},
+       {Stage(ast::PipelineStage::kFragment)},
+       {
+           Location(Source{{12, 34}}, 2),
+           Location(Source{{56, 78}}, 3),
+       });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate location decoration
+12:34 note: first decoration declared here)");
+}
+
+TEST_F(EntryPointParameterDecorationTest, DuplicateInternalDecoration) {
+  auto* s =
+      Param("s", ty.sampler(ast::SamplerKind::kSampler),
+            ast::DecorationList{
+                create<ast::BindingDecoration>(0),
+                create<ast::GroupDecoration>(0),
+                ASTNodes().Create<ast::DisableValidationDecoration>(
+                    ID(), ast::DisabledValidation::kBindingPointCollision),
+                ASTNodes().Create<ast::DisableValidationDecoration>(
+                    ID(), ast::DisabledValidation::kEntryPointParameter),
+            });
+  Func("f", {s}, ty.void_(), {}, {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+using EntryPointReturnTypeDecorationTest = ResolverTest;
 TEST_F(EntryPointReturnTypeDecorationTest, DuplicateDecoration) {
   Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1.f)},
        ast::DecorationList{Stage(ast::PipelineStage::kFragment)},
@@ -372,73 +528,20 @@
 12:34 note: first decoration declared here)");
 }
 
-TEST_F(EntryPointReturnTypeDecorationTest, InvariantWithPosition) {
-  Func("main", ast::VariableList{}, ty.vec4<f32>(),
-       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
-       ast::DecorationList{Stage(ast::PipelineStage::kVertex)},
+TEST_F(EntryPointReturnTypeDecorationTest, DuplicateInternalDecoration) {
+  Func("f", {}, ty.i32(), {Return(1)}, {Stage(ast::PipelineStage::kFragment)},
        ast::DecorationList{
-           Invariant(Source{{12, 34}}),
-           Builtin(Source{{56, 78}}, ast::Builtin::kPosition),
+           ASTNodes().Create<ast::DisableValidationDecoration>(
+               ID(), ast::DisabledValidation::kBindingPointCollision),
+           ASTNodes().Create<ast::DisableValidationDecoration>(
+               ID(), ast::DisabledValidation::kEntryPointParameter),
        });
+
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
+}  // namespace EntryPointInputAndOutputTests
 
-TEST_F(EntryPointReturnTypeDecorationTest, InvariantWithoutPosition) {
-  Func("main", ast::VariableList{}, ty.vec4<f32>(),
-       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
-       ast::DecorationList{Stage(ast::PipelineStage::kVertex)},
-       ast::DecorationList{
-           Invariant(Source{{12, 34}}),
-           Location(Source{{56, 78}}, 0),
-       });
-
-  EXPECT_FALSE(r()->Resolve());
-  EXPECT_EQ(r()->error(),
-            "12:34 error: invariant attribute must only be applied to a "
-            "position builtin");
-}
-
-using ArrayDecorationTest = TestWithParams;
-TEST_P(ArrayDecorationTest, IsValid) {
-  auto& params = GetParam();
-
-  auto* arr = ty.array(ty.f32(), 0,
-                       createDecorations(Source{{12, 34}}, *this, params.kind));
-  Structure("mystruct",
-            {
-                Member("a", arr),
-            },
-            {create<ast::StructBlockDecoration>()});
-
-  WrapInFunction();
-
-  if (params.should_pass) {
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-  } else {
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: decoration is not valid for array types");
-  }
-}
-INSTANTIATE_TEST_SUITE_P(
-    ResolverDecorationValidationTest,
-    ArrayDecorationTest,
-    testing::Values(TestParams{DecorationKind::kAlign, false},
-                    TestParams{DecorationKind::kBinding, false},
-                    TestParams{DecorationKind::kBuiltin, false},
-                    TestParams{DecorationKind::kGroup, false},
-                    TestParams{DecorationKind::kInterpolate, false},
-                    TestParams{DecorationKind::kInvariant, false},
-                    TestParams{DecorationKind::kLocation, false},
-                    TestParams{DecorationKind::kOverride, false},
-                    TestParams{DecorationKind::kOffset, false},
-                    TestParams{DecorationKind::kSize, false},
-                    TestParams{DecorationKind::kStage, false},
-                    TestParams{DecorationKind::kStride, true},
-                    TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false},
-                    TestParams{DecorationKind::kBindingAndGroup, false}));
-
+namespace StructAndStructMemberTests {
 using StructDecorationTest = TestWithParams;
 TEST_P(StructDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -484,19 +587,15 @@
                 create<ast::StructBlockDecoration>(Source{{12, 34}}),
                 create<ast::StructBlockDecoration>(Source{{56, 78}}),
             });
-
   WrapInFunction();
-
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
             R"(56:78 error: duplicate block decoration
 12:34 note: first decoration declared here)");
 }
-
 using StructMemberDecorationTest = TestWithParams;
 TEST_P(StructMemberDecorationTest, IsValid) {
   auto& params = GetParam();
-
   ast::StructMemberList members;
   if (params.kind == DecorationKind::kBuiltin) {
     members.push_back(
@@ -507,11 +606,8 @@
         {Member("a", ty.f32(),
                 createDecorations(Source{{12, 34}}, *this, params.kind))});
   }
-
   Structure("mystruct", members);
-
   WrapInFunction();
-
   if (params.should_pass) {
     EXPECT_TRUE(r()->Resolve()) << r()->error();
   } else {
@@ -538,7 +634,6 @@
                     TestParams{DecorationKind::kStructBlock, false},
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
-
 TEST_F(StructMemberDecorationTest, DuplicateDecoration) {
   Structure("mystruct", {
                             Member("a", ty.i32(),
@@ -549,15 +644,12 @@
                                            Source{{56, 78}}, 8u),
                                    }),
                         });
-
   WrapInFunction();
-
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
             R"(56:78 error: duplicate align decoration
 12:34 note: first decoration declared here)");
 }
-
 TEST_F(StructMemberDecorationTest, InvariantDecorationWithPosition) {
   Structure("mystruct", {
                             Member("a", ty.vec4<f32>(),
@@ -566,11 +658,9 @@
                                        Builtin(ast::Builtin::kPosition),
                                    }),
                         });
-
   WrapInFunction();
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
-
 TEST_F(StructMemberDecorationTest, InvariantDecorationWithoutPosition) {
   Structure("mystruct", {
                             Member("a", ty.vec4<f32>(),
@@ -578,7 +668,6 @@
                                        Invariant(Source{{12, 34}}),
                                    }),
                         });
-
   WrapInFunction();
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
@@ -586,6 +675,49 @@
             "position builtin");
 }
 
+}  // namespace StructAndStructMemberTests
+
+using ArrayDecorationTest = TestWithParams;
+TEST_P(ArrayDecorationTest, IsValid) {
+  auto& params = GetParam();
+
+  auto* arr = ty.array(ty.f32(), 0,
+                       createDecorations(Source{{12, 34}}, *this, params.kind));
+  Structure("mystruct",
+            {
+                Member("a", arr),
+            },
+            {create<ast::StructBlockDecoration>()});
+
+  WrapInFunction();
+
+  if (params.should_pass) {
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+  } else {
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "12:34 error: decoration is not valid for array types");
+  }
+}
+INSTANTIATE_TEST_SUITE_P(
+    ResolverDecorationValidationTest,
+    ArrayDecorationTest,
+    testing::Values(TestParams{DecorationKind::kAlign, false},
+                    TestParams{DecorationKind::kBinding, false},
+                    TestParams{DecorationKind::kBuiltin, false},
+                    TestParams{DecorationKind::kGroup, false},
+                    TestParams{DecorationKind::kInterpolate, false},
+                    TestParams{DecorationKind::kInvariant, false},
+                    TestParams{DecorationKind::kLocation, false},
+                    TestParams{DecorationKind::kOverride, false},
+                    TestParams{DecorationKind::kOffset, false},
+                    TestParams{DecorationKind::kSize, false},
+                    TestParams{DecorationKind::kStage, false},
+                    TestParams{DecorationKind::kStride, true},
+                    TestParams{DecorationKind::kStructBlock, false},
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
+
 using VariableDecorationTest = TestWithParams;
 TEST_P(VariableDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -697,41 +829,6 @@
 12:34 note: first decoration declared here)");
 }
 
-using FunctionDecorationTest = TestWithParams;
-TEST_P(FunctionDecorationTest, IsValid) {
-  auto& params = GetParam();
-
-  ast::DecorationList decos =
-      createDecorations(Source{{12, 34}}, *this, params.kind);
-  Func("foo", ast::VariableList{}, ty.void_(), ast::StatementList{}, decos);
-
-  if (params.should_pass) {
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-  } else {
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: decoration is not valid for functions");
-  }
-}
-INSTANTIATE_TEST_SUITE_P(
-    ResolverDecorationValidationTest,
-    FunctionDecorationTest,
-    testing::Values(TestParams{DecorationKind::kAlign, false},
-                    TestParams{DecorationKind::kBinding, false},
-                    TestParams{DecorationKind::kBuiltin, false},
-                    TestParams{DecorationKind::kGroup, false},
-                    TestParams{DecorationKind::kInterpolate, false},
-                    TestParams{DecorationKind::kInvariant, false},
-                    TestParams{DecorationKind::kLocation, false},
-                    TestParams{DecorationKind::kOverride, false},
-                    TestParams{DecorationKind::kOffset, false},
-                    TestParams{DecorationKind::kSize, false},
-                    // Skip kStage as we do not apply it in this test
-                    TestParams{DecorationKind::kStride, false},
-                    TestParams{DecorationKind::kStructBlock, false},
-                    // Skip kWorkgroup as this is a different error
-                    TestParams{DecorationKind::kBindingAndGroup, false}));
-
 }  // namespace
 }  // namespace DecorationTests
 
@@ -1046,6 +1143,108 @@
 }  // namespace
 }  // namespace ResourceTests
 
+namespace LocationDecorationTests {
+namespace {
+using LocationDecorationTests = ResolverTest;
+TEST_F(LocationDecorationTests, ComputeShaderLocation_Input) {
+  Func("main", {}, ty.i32(), {Return(Expr(1))},
+       {Stage(ast::PipelineStage::kCompute),
+        create<ast::WorkgroupDecoration>(Source{{12, 34}}, Expr(1))},
+       ast::DecorationList{Location(Source{{12, 34}}, 1)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: decoration is not valid for compute shader output");
+}
+
+TEST_F(LocationDecorationTests, ComputeShaderLocation_Output) {
+  auto* input = Param("input", ty.i32(),
+                      ast::DecorationList{Location(Source{{12, 34}}, 0u)});
+  Func("main", {input}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kCompute),
+        create<ast::WorkgroupDecoration>(Source{{12, 34}}, Expr(1))});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: decoration is not valid for compute shader inputs");
+}
+
+TEST_F(LocationDecorationTests, ComputeShaderLocationStructMember_Output) {
+  auto* m = Member("m", ty.i32(),
+                   ast::DecorationList{Location(Source{{12, 34}}, 0u)});
+  auto* s = Structure("S", {m});
+  Func(Source{{56, 78}}, "main", {}, ty.Of(s),
+       ast::StatementList{Return(Expr(Construct(ty.Of(s))))},
+       {Stage(ast::PipelineStage::kCompute),
+        create<ast::WorkgroupDecoration>(Source{{12, 34}}, Expr(1))});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: decoration is not valid for compute shader output\n"
+            "56:78 note: while analysing entry point 'main'");
+}
+
+TEST_F(LocationDecorationTests, ComputeShaderLocationStructMember_Input) {
+  auto* m = Member("m", ty.i32(),
+                   ast::DecorationList{Location(Source{{12, 34}}, 0u)});
+  auto* s = Structure("S", {m});
+  auto* input = Param("input", ty.Of(s));
+  Func(Source{{56, 78}}, "main", {input}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kCompute),
+        create<ast::WorkgroupDecoration>(Source{{12, 34}}, Expr(1))});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: decoration is not valid for compute shader inputs\n"
+            "56:78 note: while analysing entry point 'main'");
+}
+
+TEST_F(LocationDecorationTests, BadType) {
+  auto* p = Param(Source{{12, 34}}, "a", ty.mat2x2<f32>(), {Location(0)});
+  Func("frag_main", {p}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: User defined entry point IO types must be a numeric "
+            "scalar, a numeric vector, or a structure");
+}
+}  // namespace
+}  // namespace LocationDecorationTests
+
+namespace InvariantDecorationTests {
+namespace {
+using InvariantDecorationTests = ResolverTest;
+TEST_F(InvariantDecorationTests, InvariantWithPosition) {
+  auto* param = Param("p", ty.vec4<f32>(),
+                      {Invariant(Source{{12, 34}}),
+                       Builtin(Source{{56, 78}}, ast::Builtin::kPosition)});
+  Func("main", ast::VariableList{param}, ty.vec4<f32>(),
+       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
+       ast::DecorationList{Stage(ast::PipelineStage::kFragment)},
+       ast::DecorationList{
+           Location(0),
+       });
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(InvariantDecorationTests, InvariantWithoutPosition) {
+  auto* param =
+      Param("p", ty.vec4<f32>(), {Invariant(Source{{12, 34}}), Location(0)});
+  Func("main", ast::VariableList{param}, ty.vec4<f32>(),
+       ast::StatementList{Return(Construct(ty.vec4<f32>()))},
+       ast::DecorationList{Stage(ast::PipelineStage::kFragment)},
+       ast::DecorationList{
+           Location(0),
+       });
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: invariant attribute must only be applied to a "
+            "position builtin");
+}
+}  // namespace
+}  // namespace InvariantDecorationTests
+
 namespace WorkgroupDecorationTests {
 namespace {
 
diff --git a/src/resolver/entry_point_validation_test.cc b/src/resolver/entry_point_validation_test.cc
index bd0201f..27c4dd6 100644
--- a/src/resolver/entry_point_validation_test.cc
+++ b/src/resolver/entry_point_validation_test.cc
@@ -160,7 +160,7 @@
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
 13:43 note: previously consumed location(0)
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest,
@@ -183,7 +183,7 @@
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
             R"(14:52 error: missing entry point IO attribute
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_NestedStruct) {
@@ -209,7 +209,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(14:52 error: entry point IO types cannot contain nested structures
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_RuntimeArray) {
@@ -254,7 +254,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(12:34 error: builtin(frag_depth) attribute appears multiple times as pipeline output
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_DuplicateLocation) {
@@ -276,7 +276,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(12:34 error: location(1) attribute appears multiple times as pipeline output
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Location) {
@@ -369,7 +369,7 @@
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
 13:43 note: previously consumed location(0)
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest,
@@ -389,7 +389,7 @@
 
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(), R"(14:52 error: missing entry point IO attribute
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_NestedStruct) {
@@ -413,7 +413,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(14:52 error: entry point IO types cannot contain nested structures
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_RuntimeArray) {
@@ -477,7 +477,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(12:34 error: builtin(sample_index) attribute appears multiple times as pipeline input
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_DuplicateLocation) {
@@ -515,7 +515,7 @@
   EXPECT_EQ(
       r()->error(),
       R"(12:34 error: location(1) attribute appears multiple times as pipeline input
-12:34 note: while analysing entry point main)");
+12:34 note: while analysing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, VertexShaderMustReturnPosition) {
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index f7bb4e4..2656568 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -1138,18 +1138,9 @@
           "decoration is not valid for non-entry point function parameters",
           deco->source());
       return false;
-    } else if (auto* interpolate = deco->As<ast::InterpolateDecoration>()) {
-      if (!ValidateInterpolateDecoration(interpolate, info->type)) {
-        return false;
-      }
-    } else if (deco->Is<ast::LocationDecoration>()) {
-      if (func->pipeline_stage() == ast::PipelineStage::kCompute) {
-        AddError(
-            "decoration is not valid for compute shader function parameters",
-            deco->source());
-        return false;
-      }
     } else if (!deco->IsAnyOf<ast::BuiltinDecoration, ast::InvariantDecoration,
+                              ast::LocationDecoration,
+                              ast::InterpolateDecoration,
                               ast::InternalDecoration>() &&
                (IsValidationEnabled(
                     info->declaration->decorations(),
@@ -1198,7 +1189,8 @@
 
 bool Resolver::ValidateBuiltinDecoration(const ast::BuiltinDecoration* deco,
                                          const sem::Type* storage_type,
-                                         const bool is_input) {
+                                         const bool is_input,
+                                         const bool is_struct_member) {
   auto* type = storage_type->UnwrapRef();
   const auto stage = current_function_
                          ? current_function_->declaration->pipeline_stage()
@@ -1206,15 +1198,13 @@
   std::stringstream stage_name;
   stage_name << stage;
   bool is_stage_mismatch = false;
+  bool is_output = !is_input;
   switch (deco->value()) {
     case ast::Builtin::kPosition:
       if (stage != ast::PipelineStage::kNone &&
-          !(stage == ast::PipelineStage::kFragment && is_input) &&
-          !(stage == ast::PipelineStage::kVertex && !is_input)) {
-        AddError(deco_to_str(deco) + " cannot be used in " +
-                     (is_input ? "input of " : "output of ") +
-                     stage_name.str() + " pipeline stage",
-                 deco->source());
+          !((is_input && stage == ast::PipelineStage::kFragment) ||
+            (is_output && stage == ast::PipelineStage::kVertex))) {
+        is_stage_mismatch = true;
       }
       if (!(type->is_float_vector() && type->As<sem::Vector>()->Width() == 4)) {
         AddError("store type of " + deco_to_str(deco) + " must be 'vec4<f32>'",
@@ -1311,12 +1301,16 @@
       break;
   }
 
-  if (is_stage_mismatch) {
-    AddError(deco_to_str(deco) + " cannot be used in " +
-                 (is_input ? "input of " : "output of ") + stage_name.str() +
-                 " pipeline stage",
-             deco->source());
-    return false;
+  // ignore builtin attribute on struct members to facillate data movement
+  // between stages
+  if (!is_struct_member) {
+    if (is_stage_mismatch) {
+      AddError(deco_to_str(deco) + " cannot be used in " +
+                   (is_input ? "input of " : "output of ") + stage_name.str() +
+                   " pipeline stage",
+               deco->source());
+      return false;
+    }
   }
 
   return true;
@@ -1409,28 +1403,14 @@
             deco->source());
         return false;
       }
-
-      if (auto* interpolate = deco->As<ast::InterpolateDecoration>()) {
-        if (!ValidateInterpolateDecoration(interpolate, info->return_type)) {
-          return false;
-        }
-      } else if (deco->Is<ast::LocationDecoration>()) {
-        if (func->pipeline_stage() == ast::PipelineStage::kCompute) {
-          AddError(
-              "decoration is not valid for compute shader entry point return "
-              "types",
-              deco->source());
-          return false;
-        }
-      } else if (!deco->IsAnyOf<ast::BuiltinDecoration, ast::InternalDecoration,
-                                ast::InvariantDecoration>() &&
-                 (IsValidationEnabled(
-                      info->declaration->decorations(),
-                      ast::DisabledValidation::kEntryPointParameter) &&
-                  IsValidationEnabled(
-                      info->declaration->decorations(),
-                      ast::DisabledValidation::
-                          kIgnoreConstructibleFunctionParameter))) {
+      if (!deco->IsAnyOf<ast::BuiltinDecoration, ast::InternalDecoration,
+                         ast::LocationDecoration, ast::InterpolateDecoration,
+                         ast::InvariantDecoration>() &&
+          (IsValidationEnabled(info->declaration->decorations(),
+                               ast::DisabledValidation::kEntryPointParameter) &&
+           IsValidationEnabled(info->declaration->decorations(),
+                               ast::DisabledValidation::
+                                   kIgnoreConstructibleFunctionParameter))) {
         AddError("decoration is not valid for entry point return types",
                  deco->source());
         return false;
@@ -1473,6 +1453,7 @@
     ast::Decoration* pipeline_io_attribute = nullptr;
     ast::InvariantDecoration* invariant_attribute = nullptr;
     for (auto* deco : decos) {
+      auto is_invalid_compute_shader_decoration = false;
       if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
         if (pipeline_io_attribute) {
           AddError("multiple entry point IO attributes", deco->source());
@@ -1490,14 +1471,14 @@
                    func->source());
           return false;
         }
-        builtins.emplace(builtin->value());
 
         if (!ValidateBuiltinDecoration(
                 builtin, ty,
-                /* is_input */ param_or_ret == ParamOrRetType::kParameter)) {
+                /* is_input */ param_or_ret == ParamOrRetType::kParameter,
+                /* is_struct_member */ is_struct_member)) {
           return false;
         }
-
+        builtins.emplace(builtin->value());
       } else if (auto* location = deco->As<ast::LocationDecoration>()) {
         if (pipeline_io_attribute) {
           AddError("multiple entry point IO attributes", deco->source());
@@ -1515,10 +1496,31 @@
                    func->source());
           return false;
         }
+
+        if (func->pipeline_stage() == ast::PipelineStage::kCompute) {
+          is_invalid_compute_shader_decoration = true;
+        }
         locations.emplace(location->value());
+      } else if (auto* interpolate = deco->As<ast::InterpolateDecoration>()) {
+        if (func->pipeline_stage() == ast::PipelineStage::kCompute) {
+          is_invalid_compute_shader_decoration = true;
+        } else if (!ValidateInterpolateDecoration(interpolate, ty)) {
+          return false;
+        }
       } else if (auto* invariant = deco->As<ast::InvariantDecoration>()) {
+        if (func->pipeline_stage() == ast::PipelineStage::kCompute) {
+          is_invalid_compute_shader_decoration = true;
+        }
         invariant_attribute = invariant;
       }
+      if (is_invalid_compute_shader_decoration) {
+        std::string input_or_output =
+            param_or_ret == ParamOrRetType::kParameter ? "inputs" : "output";
+        AddError(
+            "decoration is not valid for compute shader " + input_or_output,
+            deco->source());
+        return false;
+      }
     }
 
     // Check that we saw a pipeline IO attribute iff we need one.
@@ -1587,8 +1589,8 @@
         if (member->Type()->Is<sem::Struct>()) {
           AddError("entry point IO types cannot contain nested structures",
                    member->Declaration()->source());
-          AddNote("while analysing entry point " +
-                      builder_->Symbols().NameFor(func->symbol()),
+          AddNote("while analysing entry point '" +
+                      builder_->Symbols().NameFor(func->symbol()) + "'",
                   func->source());
           return false;
         }
@@ -1597,8 +1599,8 @@
           if (arr->IsRuntimeSized()) {
             AddError("entry point IO types cannot contain runtime sized arrays",
                      member->Declaration()->source());
-            AddNote("while analysing entry point " +
-                        builder_->Symbols().NameFor(func->symbol()),
+            AddNote("while analysing entry point '" +
+                        builder_->Symbols().NameFor(func->symbol()) + "'",
                     func->source());
             return false;
           }
@@ -1607,8 +1609,8 @@
         if (!validate_entry_point_decorations_inner(
                 member->Declaration()->decorations(), member->Type(),
                 member->Declaration()->source(), param_or_ret, true)) {
-          AddNote("while analysing entry point " +
-                      builder_->Symbols().NameFor(func->symbol()),
+          AddNote("while analysing entry point '" +
+                      builder_->Symbols().NameFor(func->symbol()) + "'",
                   func->source());
           return false;
         }
@@ -3938,7 +3940,9 @@
         invariant_attribute = invariant;
       }
       if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
-        if (!ValidateBuiltinDecoration(builtin, member->Type())) {
+        if (!ValidateBuiltinDecoration(builtin, member->Type(),
+                                       /* is_input */ false,
+                                       /* is_struct_member */ true)) {
           return false;
         }
         if (builtin->value() == ast::Builtin::kPosition) {
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index 600ef22..446bac6 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -281,7 +281,8 @@
   bool ValidateAssignment(const ast::AssignmentStatement* a);
   bool ValidateBuiltinDecoration(const ast::BuiltinDecoration* deco,
                                  const sem::Type* storage_type,
-                                 const bool is_input = true);
+                                 const bool is_input,
+                                 const bool is_struct_member);
   bool ValidateCallStatement(ast::CallStatement* stmt);
   bool ValidateEntryPoint(const ast::Function* func, const FunctionInfo* info);
   bool ValidateFunction(const ast::Function* func, const FunctionInfo* info);