[resolver] Add entry point IO validation

Checks the following things:
- Non-struct entry point parameters must have pipeline IO attributes
- Non-struct entry point return type must have a pipeline IO attribute
- Structs used as entry point parameters and return values much have
  pipeline IO attributes on every member
- Structs used as entry point parameters and return values cannot have
  runtime array or nested struct members
- Multiple pipeline IO attributes on a parameter, return type, or
  struct member is not allowed
- Any given builtin and location attribute can only appear once for
  the return type, and across all parameters

Removed tests for nested structs from the SPIR-V transform/backend.

Fixed a couple of other tests with missing pipeline IO attributes.

Fixed: tint:512
Change-Id: I4c48fe24099333c8c7fcd45934c09baa6830883c
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/46701
Auto-Submit: James Price <jrprice@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 12fca4e..7d9003e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -476,6 +476,7 @@
     resolver/builtins_validation_test.cc
     resolver/control_block_validation_test.cc
     resolver/decoration_validation_test.cc
+    resolver/entry_point_validation_test.cc
     resolver/function_validation_test.cc
     resolver/host_shareable_validation_test.cc
     resolver/intrinsic_test.cc
diff --git a/src/program_builder.h b/src/program_builder.h
index 613fb62..3e1dd17 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -32,6 +32,7 @@
 #include "src/ast/return_statement.h"
 #include "src/ast/scalar_constructor_expression.h"
 #include "src/ast/sint_literal.h"
+#include "src/ast/stage_decoration.h"
 #include "src/ast/stride_decoration.h"
 #include "src/ast/struct_member_align_decoration.h"
 #include "src/ast/struct_member_offset_decoration.h"
@@ -1246,6 +1247,51 @@
     return Case(ast::CaseSelectorList{}, body);
   }
 
+  /// Creates an ast::BuiltinDecoration
+  /// @param source the source information
+  /// @param builtin the builtin value
+  /// @returns the builtin decoration pointer
+  ast::BuiltinDecoration* Builtin(Source source, ast::Builtin builtin) {
+    return create<ast::BuiltinDecoration>(source, builtin);
+  }
+
+  /// Creates an ast::BuiltinDecoration
+  /// @param builtin the builtin value
+  /// @returns the builtin decoration pointer
+  ast::BuiltinDecoration* Builtin(ast::Builtin builtin) {
+    return create<ast::BuiltinDecoration>(source_, builtin);
+  }
+
+  /// Creates an ast::LocationDecoration
+  /// @param source the source information
+  /// @param location the location value
+  /// @returns the location decoration pointer
+  ast::LocationDecoration* Location(Source source, uint32_t location) {
+    return create<ast::LocationDecoration>(source, location);
+  }
+
+  /// Creates an ast::LocationDecoration
+  /// @param location the location value
+  /// @returns the location decoration pointer
+  ast::LocationDecoration* Location(uint32_t location) {
+    return create<ast::LocationDecoration>(source_, location);
+  }
+
+  /// Creates an ast::StageDecoration
+  /// @param source the source information
+  /// @param stage the pipeline stage
+  /// @returns the stage decoration pointer
+  ast::StageDecoration* Stage(Source source, ast::PipelineStage stage) {
+    return create<ast::StageDecoration>(source, stage);
+  }
+
+  /// Creates an ast::StageDecoration
+  /// @param stage the pipeline stage
+  /// @returns the stage decoration pointer
+  ast::StageDecoration* Stage(ast::PipelineStage stage) {
+    return create<ast::StageDecoration>(source_, stage);
+  }
+
   /// Sets the current builder source to `src`
   /// @param src the Source used for future create() calls
   void SetSource(const Source& src) {
diff --git a/src/resolver/entry_point_validation_test.cc b/src/resolver/entry_point_validation_test.cc
new file mode 100644
index 0000000..067186b
--- /dev/null
+++ b/src/resolver/entry_point_validation_test.cc
@@ -0,0 +1,504 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/ast/builtin_decoration.h"
+#include "src/ast/location_decoration.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/stage_decoration.h"
+#include "src/ast/struct_block_decoration.h"
+#include "src/resolver/resolver.h"
+#include "src/resolver/resolver_test_helper.h"
+
+#include "gmock/gmock.h"
+
+namespace tint {
+namespace {
+
+class ResolverEntryPointValidationTest : public resolver::TestHelper,
+                                         public testing::Test {};
+
+TEST_F(ResolverEntryPointValidationTest, ReturnTypeAttribute_Location) {
+  // [[stage(vertex)]]
+  // fn main() -> [[location(0)]] f32 { return 1.0; }
+  Func(Source{{12, 34}}, "main", {}, ty.f32(), {Return(Expr(1.0f))},
+       {Stage(ast::PipelineStage::kVertex)}, {Location(0)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnTypeAttribute_Builtin) {
+  // [[stage(vertex)]]
+  // fn main() -> [[builtin(position)]] vec4<f32> { return vec4<f32>(); }
+  Func(Source{{12, 34}}, "main", {}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kVertex)},
+       {Builtin(ast::Builtin::kPosition)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnTypeAttribute_Missing) {
+  // [[stage(vertex)]]
+  // fn main() -> f32 {
+  //   return 1.0;
+  // }
+  Func(Source{{12, 34}}, "main", {}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kVertex)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: missing entry point IO attribute on return type");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnTypeAttribute_Multiple) {
+  // [[stage(vertex)]]
+  // fn main() -> [[location(0)]] [[builtin(position)]] vec4<f32> {
+  //   return vec4<f32>();
+  // }
+  Func(Source{{12, 34}}, "main", {}, ty.vec4<f32>(),
+       {Return(Construct(ty.vec4<f32>()))},
+       {Stage(ast::PipelineStage::kVertex)},
+       {Location(Source{{13, 43}}, 0),
+        Builtin(Source{{14, 52}}, ast::Builtin::kPosition)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
+13:43 note: previously consumed location(0))");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnTypeAttribute_Struct) {
+  // struct Output {
+  // };
+  // [[stage(vertex)]]
+  // fn main() -> [[location(0)]] Output {
+  //   return Output();
+  // }
+  auto* output = Structure("Output", {});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kVertex)}, {Location(Source{{13, 43}}, 0)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "13:43 error: entry point IO attributes must not be used on structure "
+      "return types");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_Valid) {
+  // struct Output {
+  //   [[location(0)]] a : f32;
+  //   [[builtin(frag_depth)]] b : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure(
+      "Output", {Member("a", ty.f32(), {Location(0)}),
+                 Member("b", ty.f32(), {Builtin(ast::Builtin::kFragDepth)})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest,
+       ReturnType_Struct_MemberMultipleAttributes) {
+  // struct Output {
+  //   [[location(0)]] [[builtin(frag_depth)]] a : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure(
+      "Output",
+      {Member("a", ty.f32(),
+              {Location(Source{{13, 43}}, 0),
+               Builtin(Source{{14, 52}}, ast::Builtin::kFragDepth)})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest,
+       ReturnType_Struct_MemberMissingAttribute) {
+  // struct Output {
+  //   [[location(0)]] a : f32;
+  //   b : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure(
+      "Output", {Member(Source{{13, 43}}, "a", ty.f32(), {Location(0)}),
+                 Member(Source{{14, 52}}, "b", ty.f32(), {})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_NestedStruct) {
+  // struct Inner {
+  //   [[location(0)]] b : f32;
+  // };
+  // struct Output {
+  //   [[location(0)]] a : Inner;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* inner = Structure(
+      "Inner", {Member(Source{{13, 43}}, "a", ty.f32(), {Location(0)})});
+  auto* output = Structure(
+      "Output", {Member(Source{{14, 52}}, "a", inner, {Location(0)})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(14:52 error: entry point IO types cannot contain nested structures
+12:34 note: while analysing entry point main)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_RuntimeArray) {
+  // [[block]]
+  // struct Output {
+  //   [[location(0)]] a : array<f32>;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure(
+      "Output",
+      {Member(Source{{13, 43}}, "a", ty.array<float>(), {Location(0)})},
+      {create<ast::StructBlockDecoration>()});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(13:43 error: entry point IO types cannot contain runtime sized arrays
+12:34 note: while analysing entry point main)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_DuplicateBuiltins) {
+  // struct Output {
+  //   [[builtin(frag_depth)]] a : f32;
+  //   [[builtin(frag_depth)]] b : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure(
+      "Output", {Member("a", ty.f32(), {Builtin(ast::Builtin::kFragDepth)}),
+                 Member("b", ty.f32(), {Builtin(ast::Builtin::kFragDepth)})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_DuplicateLocation) {
+  // struct Output {
+  //   [[location(1)]] a : f32;
+  //   [[location(1)]] b : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main() -> Output {
+  //   return Output();
+  // }
+  auto* output = Structure("Output", {Member("a", ty.f32(), {Location(1)}),
+                                      Member("b", ty.f32(), {Location(1)})});
+  Func(Source{{12, 34}}, "main", {}, output, {Return(Construct(output))},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Location) {
+  // [[stage(fragment)]]
+  // fn main([[location(0)]] param : f32) {}
+  auto* param = Const("param", ty.f32(), nullptr, {Location(0)});
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Builtin) {
+  // [[stage(fragment)]]
+  // fn main([[builtin(frag_depth)]] param : f32) {}
+  auto* param = Const("param", ty.vec4<f32>(), nullptr,
+                      {Builtin(ast::Builtin::kFragDepth)});
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Missing) {
+  // [[stage(fragment)]]
+  // fn main(param : f32) {}
+  auto* param = Const(Source{{13, 43}}, "param", ty.vec4<f32>(), nullptr);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "13:43 error: missing entry point IO attribute on parameter");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Multiple) {
+  // [[stage(fragment)]]
+  // fn main([[location(0)]] [[builtin(vertex_index)]] param : u32) {}
+  auto* param = Const("param", ty.u32(), nullptr,
+                      {Location(Source{{13, 43}}, 0),
+                       Builtin(Source{{14, 52}}, ast::Builtin::kVertexIndex)});
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
+13:43 note: previously consumed location(0))");
+}
+
+TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Struct) {
+  // struct Input {
+  // };
+  // [[stage(fragment)]]
+  // fn main([[location(0)]] param : Input) {}
+  auto* input = Structure("Input", {});
+  auto* param = Const("param", input, nullptr, {Location(Source{{13, 43}}, 0)});
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "13:43 error: entry point IO attributes must not be used on structure "
+      "parameters");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_Valid) {
+  // struct Input {
+  //   [[location(0)]] a : f32;
+  //   [[builtin(sample_index)]] b : u32;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param : Input) {}
+  auto* input = Structure(
+      "Input", {Member("a", ty.f32(), {Location(0)}),
+                Member("b", ty.u32(), {Builtin(ast::Builtin::kSampleIndex)})});
+  auto* param = Const("param", input);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverEntryPointValidationTest,
+       Parameter_Struct_MemberMultipleAttributes) {
+  // struct Input {
+  //   [[location(0)]] [[builtin(sample_index)]] a : u32;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param : Input) {}
+  auto* input = Structure(
+      "Input",
+      {Member("a", ty.f32(),
+              {Location(Source{{13, 43}}, 0),
+               Builtin(Source{{14, 52}}, ast::Builtin::kSampleIndex)})});
+  auto* param = Const("param", input);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest,
+       Parameter_Struct_MemberMissingAttribute) {
+  // struct Input {
+  //   [[location(0)]] a : f32;
+  //   b : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param : Input) {}
+  auto* input = Structure(
+      "Input", {Member(Source{{13, 43}}, "a", ty.f32(), {Location(0)}),
+                Member(Source{{14, 52}}, "b", ty.f32(), {})});
+  auto* param = Const("param", input);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_NestedStruct) {
+  // struct Inner {
+  //   [[location(0)]] b : f32;
+  // };
+  // struct Input {
+  //   [[location(0)]] a : Inner;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param : Input) {}
+  auto* inner = Structure(
+      "Inner", {Member(Source{{13, 43}}, "a", ty.f32(), {Location(0)})});
+  auto* input =
+      Structure("Input", {Member(Source{{14, 52}}, "a", inner, {Location(0)})});
+  auto* param = Const("param", input);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(14:52 error: entry point IO types cannot contain nested structures
+12:34 note: while analysing entry point main)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_RuntimeArray) {
+  // [[block]]
+  // struct Input {
+  //   [[location(0)]] a : array<f32>;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param : Input) {}
+  auto* input = Structure(
+      "Input",
+      {Member(Source{{13, 43}}, "a", ty.array<float>(), {Location(0)})},
+      {create<ast::StructBlockDecoration>()});
+  auto* param = Const("param", input);
+  Func(Source{{12, 34}}, "main", {param}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(13:43 error: entry point IO types cannot contain runtime sized arrays
+12:34 note: while analysing entry point main)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_DuplicateBuiltins) {
+  // [[stage(fragment)]]
+  // fn main([[builtin(sample_index)]] param_a : u32,
+  //         [[builtin(sample_index)]] param_b : u32) {}
+  auto* param_a = Const("param_a", ty.u32(), nullptr,
+                        {Builtin(ast::Builtin::kSampleIndex)});
+  auto* param_b = Const("param_b", ty.u32(), nullptr,
+                        {Builtin(ast::Builtin::kSampleIndex)});
+  Func(Source{{12, 34}}, "main", {param_a, param_b}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: builtin(sample_index) attribute appears multiple times as "
+      "pipeline input");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_DuplicateBuiltins) {
+  // struct InputA {
+  //   [[builtin(sample_index)]] a : u32;
+  // };
+  // struct InputB {
+  //   [[builtin(sample_index)]] a : u32;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param_a : InputA, param_b : InputB) {}
+  auto* input_a = Structure(
+      "InputA", {Member("a", ty.u32(), {Builtin(ast::Builtin::kSampleIndex)})});
+  auto* input_b = Structure(
+      "InputB", {Member("a", ty.u32(), {Builtin(ast::Builtin::kSampleIndex)})});
+  auto* param_a = Const("param_a", input_a);
+  auto* param_b = Const("param_b", input_b);
+  Func(Source{{12, 34}}, "main", {param_a, param_b}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  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)");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_DuplicateLocation) {
+  // [[stage(fragment)]]
+  // fn main([[location(1)]] param_a : f32,
+  //         [[location(1)]] param_b : f32) {}
+  auto* param_a = Const("param_a", ty.u32(), nullptr, {Location(1)});
+  auto* param_b = Const("param_b", ty.u32(), nullptr, {Location(1)});
+  Func(Source{{12, 34}}, "main", {param_a, param_b}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: location(1) attribute appears multiple times as "
+            "pipeline input");
+}
+
+TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_DuplicateLocation) {
+  // struct InputA {
+  //   [[location(1)]] a : f32;
+  // };
+  // struct InputB {
+  //   [[location(1)]] a : f32;
+  // };
+  // [[stage(fragment)]]
+  // fn main(param_a : InputA, param_b : InputB) {}
+  auto* input_a = Structure("InputA", {Member("a", ty.f32(), {Location(1)})});
+  auto* input_b = Structure("InputB", {Member("a", ty.f32(), {Location(1)})});
+  auto* param_a = Const("param_a", input_a);
+  auto* param_b = Const("param_b", input_b);
+  Func(Source{{12, 34}}, "main", {param_a, param_b}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  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)");
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 79173e8..5c54e48 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -268,8 +268,7 @@
     }
 
     for (auto* deco : func->return_type_decorations()) {
-      if (!(deco->Is<ast::BuiltinDecoration>() ||
-            deco->Is<ast::LocationDecoration>())) {
+      if (!deco->IsAnyOf<ast::BuiltinDecoration, ast::LocationDecoration>()) {
         diagnostics_.add_error(
             "decoration is not valid for function return types",
             deco->source());
@@ -278,6 +277,186 @@
     }
   }
 
+  if (func->IsEntryPoint()) {
+    if (!ValidateEntryPoint(func)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool Resolver::ValidateEntryPoint(const ast::Function* func) {
+  // Use a lambda to validate the entry point decorations for a type.
+  // Persistent state is used to track which builtins and locations have already
+  // been seen, in order to catch conflicts.
+  // TODO(jrprice): This state could be stored in FunctionInfo instead, and then
+  // passed to semantic::Function since it would be useful there too.
+  std::unordered_set<ast::Builtin> builtins;
+  std::unordered_set<uint32_t> locations;
+  enum class ParamOrRetType {
+    kParameter,
+    kReturnType,
+  };
+  // Helper to stringify a pipeline IO decoration.
+  auto deco_to_str = [](const ast::Decoration* deco) {
+    std::stringstream str;
+    if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
+      str << "builtin(" << builtin->value() << ")";
+    } else if (auto* location = deco->As<ast::LocationDecoration>()) {
+      str << "location(" << location->value() << ")";
+    }
+    return str.str();
+  };
+  // Inner lambda that is applied to a type and all of its members.
+  auto validate_entry_point_decorations_inner =
+      [&](const ast::DecorationList& decos, type::Type* ty, Source source,
+          ParamOrRetType param_or_ret, bool is_struct_member) {
+        // Scan decorations for pipeline IO attributes.
+        // Check for overlap with attributes that have been seen previously.
+        ast::Decoration* pipeline_io_attribute = nullptr;
+        for (auto* deco : decos) {
+          if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
+            if (pipeline_io_attribute) {
+              diagnostics_.add_error("multiple entry point IO attributes",
+                                     deco->source());
+              diagnostics_.add_note(
+                  "previously consumed " + deco_to_str(pipeline_io_attribute),
+                  pipeline_io_attribute->source());
+              return false;
+            }
+            pipeline_io_attribute = deco;
+
+            if (builtins.count(builtin->value())) {
+              diagnostics_.add_error(
+                  deco_to_str(builtin) +
+                      " attribute appears multiple times as pipeline " +
+                      (param_or_ret == ParamOrRetType::kParameter ? "input"
+                                                                  : "output"),
+                  func->source());
+              return false;
+            }
+            builtins.emplace(builtin->value());
+
+          } else if (auto* location = deco->As<ast::LocationDecoration>()) {
+            if (pipeline_io_attribute) {
+              diagnostics_.add_error("multiple entry point IO attributes",
+                                     deco->source());
+              diagnostics_.add_note(
+                  "previously consumed " + deco_to_str(pipeline_io_attribute),
+                  pipeline_io_attribute->source());
+              return false;
+            }
+            pipeline_io_attribute = deco;
+
+            if (locations.count(location->value())) {
+              diagnostics_.add_error(
+                  deco_to_str(location) +
+                      " attribute appears multiple times as pipeline " +
+                      (param_or_ret == ParamOrRetType::kParameter ? "input"
+                                                                  : "output"),
+                  func->source());
+              return false;
+            }
+            locations.emplace(location->value());
+          }
+        }
+
+        // Check that we saw a pipeline IO attribute iff we need one.
+        if (ty->UnwrapAliasIfNeeded()->Is<type::Struct>()) {
+          if (pipeline_io_attribute) {
+            diagnostics_.add_error(
+                "entry point IO attributes must not be used on structure " +
+                    std::string(param_or_ret == ParamOrRetType::kParameter
+                                    ? "parameters"
+                                    : "return types"),
+                pipeline_io_attribute->source());
+            return false;
+          }
+        } else {
+          if (!pipeline_io_attribute) {
+            std::string err = "missing entry point IO attribute";
+            if (!is_struct_member) {
+              err += (param_or_ret == ParamOrRetType::kParameter
+                          ? " on parameter"
+                          : " on return type");
+            }
+            diagnostics_.add_error(err, source);
+            return false;
+          }
+        }
+
+        return true;
+      };
+
+  // Outer lambda for validating the entry point decorations for a type.
+  auto validate_entry_point_decorations = [&](const ast::DecorationList& decos,
+                                              type::Type* ty, Source source,
+                                              ParamOrRetType param_or_ret) {
+    // Validate the decorations for the type.
+    if (!validate_entry_point_decorations_inner(decos, ty, source, param_or_ret,
+                                                false)) {
+      return false;
+    }
+
+    if (auto* struct_ty = ty->UnwrapAliasIfNeeded()->As<type::Struct>()) {
+      // Validate the decorations for each struct members, and also check for
+      // invalid member types.
+      for (auto* member : struct_ty->impl()->members()) {
+        auto* member_ty = member->type()->UnwrapAliasIfNeeded();
+        if (member_ty->Is<type::Struct>()) {
+          diagnostics_.add_error(
+              "entry point IO types cannot contain nested structures",
+              member->source());
+          diagnostics_.add_note("while analysing entry point " +
+                                    builder_->Symbols().NameFor(func->symbol()),
+                                func->source());
+          return false;
+        } else if (auto* arr = member_ty->As<type::Array>()) {
+          if (arr->IsRuntimeArray()) {
+            diagnostics_.add_error(
+                "entry point IO types cannot contain runtime sized arrays",
+                member->source());
+            diagnostics_.add_note(
+                "while analysing entry point " +
+                    builder_->Symbols().NameFor(func->symbol()),
+                func->source());
+            return false;
+          }
+        }
+
+        if (!validate_entry_point_decorations_inner(member->decorations(),
+                                                    member_ty, member->source(),
+                                                    param_or_ret, true)) {
+          diagnostics_.add_note("while analysing entry point " +
+                                    builder_->Symbols().NameFor(func->symbol()),
+                                func->source());
+          return false;
+        }
+      }
+    }
+
+    return true;
+  };
+
+  for (auto* param : func->params()) {
+    if (!validate_entry_point_decorations(
+            param->decorations(), param->declared_type(), param->source(),
+            ParamOrRetType::kParameter)) {
+      return false;
+    }
+  }
+
+  if (!func->return_type()->Is<type::Void>()) {
+    builtins.clear();
+    locations.clear();
+    if (!validate_entry_point_decorations(func->return_type_decorations(),
+                                          func->return_type(), func->source(),
+                                          ParamOrRetType::kReturnType)) {
+      return false;
+    }
+  }
+
   return true;
 }
 
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index 0c0700b..d74e826 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -234,6 +234,7 @@
   bool ValidateVariable(const ast::Variable* param);
   bool ValidateParameter(const ast::Variable* param);
   bool ValidateFunction(const ast::Function* func);
+  bool ValidateEntryPoint(const ast::Function* func);
   bool ValidateStructure(const type::Struct* st);
   bool ValidateReturn(const ast::ReturnStatement* ret);
   bool ValidateSwitch(const ast::SwitchStatement* s);
diff --git a/src/transform/renamer_test.cc b/src/transform/renamer_test.cc
index cfd3290..add9c84 100644
--- a/src/transform/renamer_test.cc
+++ b/src/transform/renamer_test.cc
@@ -83,7 +83,7 @@
 TEST_F(RenamerTest, PreserveSwizzles) {
   auto* src = R"(
 [[stage(vertex)]]
-fn entry() -> vec4<f32> {
+fn entry() -> [[builtin(position)]] vec4<f32> {
   var v : vec4<f32>;
   var rgba : f32;
   var xyzw : f32;
@@ -93,7 +93,7 @@
 
   auto* expect = R"(
 [[stage(vertex)]]
-fn _tint_1() -> vec4<f32> {
+fn _tint_1() -> [[builtin(position)]] vec4<f32> {
   var _tint_2 : vec4<f32>;
   var _tint_3 : f32;
   var _tint_4 : f32;
@@ -120,7 +120,7 @@
 TEST_F(RenamerTest, PreserveIntrinsics) {
   auto* src = R"(
 [[stage(vertex)]]
-fn entry() -> vec4<f32> {
+fn entry() -> [[builtin(position)]] vec4<f32> {
   var blah : vec4<f32>;
   return abs(blah);
 }
@@ -128,7 +128,7 @@
 
   auto* expect = R"(
 [[stage(vertex)]]
-fn _tint_1() -> vec4<f32> {
+fn _tint_1() -> [[builtin(position)]] vec4<f32> {
   var _tint_2 : vec4<f32>;
   return abs(_tint_2);
 }
diff --git a/src/transform/spirv_test.cc b/src/transform/spirv_test.cc
index bb21aea..7e44dee 100644
--- a/src/transform/spirv_test.cc
+++ b/src/transform/spirv_test.cc
@@ -224,86 +224,10 @@
   EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvTest, HandleEntryPointIOTypes_StructParameters_Nested) {
-  auto* src = R"(
-struct Builtins {
-  [[builtin(frag_coord)]] coord : vec4<f32>;
-};
-
-struct Locations {
-  [[location(2)]] l2 : f32;
-  [[location(3)]] l3 : f32;
-};
-
-struct Other {
-  l : Locations;
-};
-
-struct FragmentInput {
-  b : Builtins;
-  o : Other;
-  [[location(1)]] value : f32;
-};
-
-[[stage(fragment)]]
-fn frag_main(inputs : FragmentInput) -> void {
-  var col : f32 = inputs.b.coord.x * inputs.value;
-  var l : f32 = inputs.o.l.l2 + inputs.o.l.l3;
-}
-)";
-
-  auto* expect = R"(
-struct Builtins {
-  coord : vec4<f32>;
-};
-
-struct Locations {
-  l2 : f32;
-  l3 : f32;
-};
-
-struct Other {
-  l : Locations;
-};
-
-struct FragmentInput {
-  b : Builtins;
-  o : Other;
-  value : f32;
-};
-
-[[builtin(frag_coord)]] var<in> tint_symbol_12 : vec4<f32>;
-
-[[location(2)]] var<in> tint_symbol_14 : f32;
-
-[[location(3)]] var<in> tint_symbol_15 : f32;
-
-[[location(1)]] var<in> tint_symbol_18 : f32;
-
-[[stage(fragment)]]
-fn frag_main() -> void {
-  const tint_symbol_13 : Builtins = Builtins(tint_symbol_12);
-  const tint_symbol_16 : Locations = Locations(tint_symbol_14, tint_symbol_15);
-  const tint_symbol_17 : Other = Other(tint_symbol_16);
-  const tint_symbol_19 : FragmentInput = FragmentInput(tint_symbol_13, tint_symbol_17, tint_symbol_18);
-  var col : f32 = (tint_symbol_19.b.coord.x * tint_symbol_19.value);
-  var l : f32 = (tint_symbol_19.o.l.l2 + tint_symbol_19.o.l.l3);
-}
-)";
-
-  auto got = Run<Spirv>(src);
-
-  EXPECT_EQ(expect, str(got));
-}
-
 TEST_F(SpirvTest, HandleEntryPointIOTypes_StructParameters_EmptyBody) {
   auto* src = R"(
-struct Locations {
-  [[location(1)]] value : f32;
-};
-
 struct FragmentInput {
-  locations : Locations;
+  [[location(1)]] value : f32;
 };
 
 [[stage(fragment)]]
@@ -312,15 +236,11 @@
 )";
 
   auto* expect = R"(
-struct Locations {
+struct FragmentInput {
   value : f32;
 };
 
-struct FragmentInput {
-  locations : Locations;
-};
-
-[[location(1)]] var<in> tint_symbol_5 : f32;
+[[location(1)]] var<in> tint_symbol_3 : f32;
 
 [[stage(fragment)]]
 fn frag_main() -> void {
@@ -381,97 +301,6 @@
   EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvTest, HandleEntryPointIOTypes_ReturnStruct_Nested) {
-  auto* src = R"(
-struct Builtins {
-  [[builtin(position)]] pos : vec4<f32>;
-};
-
-struct Locations {
-  [[location(2)]] l2 : f32;
-  [[location(3)]] l3 : f32;
-};
-
-struct Other {
-  l : Locations;
-};
-
-struct VertexOutput {
-  b : Builtins;
-  o : Other;
-  [[location(1)]] value : f32;
-};
-
-[[stage(vertex)]]
-fn vert_main() -> VertexOutput {
-  if (false) {
-    return VertexOutput();
-  }
-  var output : VertexOutput = VertexOutput();
-  output.b.pos = vec4<f32>(1.0, 2.0, 3.0, 0.0);
-  output.o.l.l2 = 4.0;
-  output.o.l.l3 = 5.0;
-  output.value = 6.0;
-  return output;
-}
-)";
-
-  auto* expect = R"(
-struct Builtins {
-  pos : vec4<f32>;
-};
-
-struct Locations {
-  l2 : f32;
-  l3 : f32;
-};
-
-struct Other {
-  l : Locations;
-};
-
-struct VertexOutput {
-  b : Builtins;
-  o : Other;
-  value : f32;
-};
-
-[[builtin(position)]] var<out> tint_symbol_13 : vec4<f32>;
-
-[[location(2)]] var<out> tint_symbol_14 : f32;
-
-[[location(3)]] var<out> tint_symbol_15 : f32;
-
-[[location(1)]] var<out> tint_symbol_16 : f32;
-
-fn tint_symbol_17(tint_symbol_12 : VertexOutput) -> void {
-  tint_symbol_13 = tint_symbol_12.b.pos;
-  tint_symbol_14 = tint_symbol_12.o.l.l2;
-  tint_symbol_15 = tint_symbol_12.o.l.l3;
-  tint_symbol_16 = tint_symbol_12.value;
-}
-
-[[stage(vertex)]]
-fn vert_main() -> void {
-  if (false) {
-    tint_symbol_17(VertexOutput());
-    return;
-  }
-  var output : VertexOutput = VertexOutput();
-  output.b.pos = vec4<f32>(1.0, 2.0, 3.0, 0.0);
-  output.o.l.l2 = 4.0;
-  output.o.l.l3 = 5.0;
-  output.value = 6.0;
-  tint_symbol_17(output);
-  return;
-}
-)";
-
-  auto got = Run<Spirv>(src);
-
-  EXPECT_EQ(expect, str(got));
-}
-
 TEST_F(SpirvTest, HandleEntryPointIOTypes_SharedStruct_SameShader) {
   auto* src = R"(
 struct Interface {
@@ -558,150 +387,6 @@
   EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvTest, HandleEntryPointIOTypes_SharedSubStruct) {
-  auto* src = R"(
-struct Interface {
-  [[location(1)]] value : f32;
-};
-
-struct VertexOutput {
-  [[builtin(position)]] pos : vec4<f32>;
-  interface : Interface;
-};
-
-struct FragmentInput {
-  [[builtin(sample_index)]] index : u32;
-  interface : Interface;
-};
-
-[[stage(vertex)]]
-fn vert_main() -> VertexOutput {
-  return VertexOutput(vec4<f32>(), Interface(42.0));
-}
-
-[[stage(fragment)]]
-fn frag_main(inputs : FragmentInput) -> void {
-  var x : f32 = inputs.interface.value;
-}
-)";
-
-  auto* expect = R"(
-struct Interface {
-  value : f32;
-};
-
-struct VertexOutput {
-  pos : vec4<f32>;
-  interface : Interface;
-};
-
-struct FragmentInput {
-  index : u32;
-  interface : Interface;
-};
-
-[[builtin(position)]] var<out> tint_symbol_9 : vec4<f32>;
-
-[[location(1)]] var<out> tint_symbol_10 : f32;
-
-fn tint_symbol_11(tint_symbol_8 : VertexOutput) -> void {
-  tint_symbol_9 = tint_symbol_8.pos;
-  tint_symbol_10 = tint_symbol_8.interface.value;
-}
-
-[[stage(vertex)]]
-fn vert_main() -> void {
-  tint_symbol_11(VertexOutput(vec4<f32>(), Interface(42.0)));
-  return;
-}
-
-[[builtin(sample_index)]] var<in> tint_symbol_13 : u32;
-
-[[location(1)]] var<in> tint_symbol_14 : f32;
-
-[[stage(fragment)]]
-fn frag_main() -> void {
-  const tint_symbol_15 : Interface = Interface(tint_symbol_14);
-  const tint_symbol_16 : FragmentInput = FragmentInput(tint_symbol_13, tint_symbol_15);
-  var x : f32 = tint_symbol_16.interface.value;
-}
-)";
-
-  auto got = Run<Spirv>(src);
-
-  EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(SpirvTest, HandleEntryPointIOTypes_NestedStruct_TypeAlias) {
-  auto* src = R"(
-type myf32 = f32;
-
-struct Location {
-  [[location(2)]] l2 : myf32;
-};
-
-type MyLocation = Location;
-
-struct VertexIO {
-  l : MyLocation;
-  [[location(1)]] value : myf32;
-};
-
-type MyVertexInput = VertexIO;
-
-type MyVertexOutput = VertexIO;
-
-[[stage(vertex)]]
-fn vert_main(inputs : MyVertexInput) -> MyVertexOutput {
-  return inputs;
-}
-)";
-
-  auto* expect = R"(
-type myf32 = f32;
-
-struct Location {
-  l2 : myf32;
-};
-
-type MyLocation = Location;
-
-struct VertexIO {
-  l : MyLocation;
-  value : myf32;
-};
-
-type MyVertexInput = VertexIO;
-
-type MyVertexOutput = VertexIO;
-
-[[location(2)]] var<in> tint_symbol_8 : myf32;
-
-[[location(1)]] var<in> tint_symbol_10 : myf32;
-
-[[location(2)]] var<out> tint_symbol_14 : myf32;
-
-[[location(1)]] var<out> tint_symbol_15 : myf32;
-
-fn tint_symbol_17(tint_symbol_13 : MyVertexOutput) -> void {
-  tint_symbol_14 = tint_symbol_13.l.l2;
-  tint_symbol_15 = tint_symbol_13.value;
-}
-
-[[stage(vertex)]]
-fn vert_main() -> void {
-  const tint_symbol_9 : MyLocation = MyLocation(tint_symbol_8);
-  const tint_symbol_11 : MyVertexInput = MyVertexInput(tint_symbol_9, tint_symbol_10);
-  tint_symbol_17(tint_symbol_11);
-  return;
-}
-)";
-
-  auto got = Run<Spirv>(src);
-
-  EXPECT_EQ(expect, str(got));
-}
-
 TEST_F(SpirvTest, HandleEntryPointIOTypes_StructLayoutDecorations) {
   auto* src = R"(
 [[block]]
diff --git a/src/validator/validator_function_test.cc b/src/validator/validator_function_test.cc
index 0d3d549..89ba7e4 100644
--- a/src/validator/validator_function_test.cc
+++ b/src/validator/validator_function_test.cc
@@ -99,9 +99,7 @@
   auto* baz = Var("baz", ty.f32(), ast::StorageClass::kFunction, Expr("bar"));
 
   Func("foo", ast::VariableList{bar}, ty.void_(), ast::StatementList{Decl(baz)},
-       ast::DecorationList{
-           create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-       });
+       ast::DecorationList{});
 
   ValidatorImpl& v = Build();
 
@@ -117,9 +115,7 @@
   auto* baz = Const("baz", ty.f32(), Expr("bar"));
 
   Func("foo", ast::VariableList{bar}, ty.void_(), ast::StatementList{Decl(baz)},
-       ast::DecorationList{
-           create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-       });
+       ast::DecorationList{});
 
   ValidatorImpl& v = Build();
 
diff --git a/src/writer/spirv/builder_entry_point_test.cc b/src/writer/spirv/builder_entry_point_test.cc
index f722d81..6c4b3b2 100644
--- a/src/writer/spirv/builder_entry_point_test.cc
+++ b/src/writer/spirv/builder_entry_point_test.cc
@@ -178,74 +178,37 @@
   Validate(b);
 }
 
-TEST_F(BuilderTest, EntryPoint_SharedSubStruct) {
+TEST_F(BuilderTest, EntryPoint_SharedStruct) {
   // struct Interface {
   //   [[location(1)]] value : f32;
   // };
   //
-  // struct VertexOutput {
-  //   [[builtin(position)]] pos : vec4<f32>;
-  //   interface : Interface;
-  // };
-  //
-  // struct FragmentInput {
-  //   [[location(0)]] mul : f32;
-  //   interface : Interface;
-  // };
-  //
   // [[stage(vertex)]]
-  // fn vert_main() -> VertexOutput {
-  //   return VertexOutput(vec4<f32>(), Interface(42.0));
+  // fn vert_main() -> Interface {
+  //   return Interface(42.0);
   // }
   //
   // [[stage(fragment)]]
-  // fn frag_main(inputs : FragmentInput) -> [[builtin(frag_depth)]] f32 {
-  //   return inputs.mul * inputs.interface.value;
+  // fn frag_main(inputs : Interface) -> [[builtin(frag_depth)]] f32 {
+  //   return inputs.value;
   // }
 
-  auto* interface =
-      Structure("Interface",
-                ast::StructMemberList{Member(
-                    "value", ty.f32(),
-                    ast::DecorationList{create<ast::LocationDecoration>(1u)})});
-  auto* vertex_output = Structure(
-      "VertexOutput",
-      ast::StructMemberList{
-          Member("pos", ty.vec4<f32>(),
-                 ast::DecorationList{
-                     create<ast::BuiltinDecoration>(ast::Builtin::kPosition)}),
-          Member("interface", interface)});
-  auto* fragment_input = Structure(
-      "FragmentInput",
-      ast::StructMemberList{
-          Member("mul", ty.f32(),
-                 ast::DecorationList{create<ast::LocationDecoration>(0u)}),
-          Member("interface", interface)});
+  auto* interface = Structure(
+      "Interface",
+      {Member("value", ty.f32(),
+              ast::DecorationList{create<ast::LocationDecoration>(1u)})});
 
-  auto* vert_retval = Construct(vertex_output, Construct(ty.vec4<f32>()),
-                                Construct(interface, 42.f));
-  Func("vert_main", ast::VariableList{}, vertex_output,
-       ast::StatementList{
-           create<ast::ReturnStatement>(vert_retval),
-       },
-       ast::DecorationList{
-           create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-       });
+  auto* vert_retval = Construct(interface, 42.f);
+  Func("vert_main", ast::VariableList{}, interface,
+       {create<ast::ReturnStatement>(vert_retval)},
+       {create<ast::StageDecoration>(ast::PipelineStage::kVertex)});
 
-  auto* frag_retval =
-      Mul(MemberAccessor(Expr("inputs"), "mul"),
-          MemberAccessor(MemberAccessor(Expr("inputs"), "interface"), "value"));
   auto* frag_inputs =
-      Var("inputs", fragment_input, ast::StorageClass::kFunction, nullptr);
+      Var("inputs", interface, ast::StorageClass::kFunction, nullptr);
   Func("frag_main", ast::VariableList{frag_inputs}, ty.f32(),
-       ast::StatementList{
-           create<ast::ReturnStatement>(frag_retval),
-       },
-       ast::DecorationList{
-           create<ast::StageDecoration>(ast::PipelineStage::kFragment),
-       },
-       ast::DecorationList{
-           create<ast::BuiltinDecoration>(ast::Builtin::kFragDepth)});
+       {create<ast::ReturnStatement>(MemberAccessor(Expr("inputs"), "value"))},
+       {create<ast::StageDecoration>(ast::PipelineStage::kFragment)},
+       {create<ast::BuiltinDecoration>(ast::Builtin::kFragDepth)});
 
   spirv::Builder& b = SanitizeAndBuild();
 
@@ -253,93 +216,63 @@
 
   EXPECT_EQ(DumpBuilder(b), R"(OpCapability Shader
 OpMemoryModel Logical GLSL450
-OpEntryPoint Vertex %24 "vert_main" %1 %6
-OpEntryPoint Fragment %34 "frag_main" %11 %9 %12
-OpExecutionMode %34 OriginUpperLeft
-OpExecutionMode %34 DepthReplacing
-OpName %1 "tint_symbol_9"
-OpName %6 "tint_symbol_10"
-OpName %9 "tint_symbol_13"
-OpName %11 "tint_symbol_14"
-OpName %12 "tint_symbol_18"
-OpName %15 "VertexOutput"
-OpMemberName %15 0 "pos"
-OpMemberName %15 1 "interface"
-OpName %16 "Interface"
-OpMemberName %16 0 "value"
-OpName %17 "tint_symbol_11"
-OpName %18 "tint_symbol_8"
-OpName %24 "vert_main"
-OpName %31 "tint_symbol_19"
-OpName %32 "tint_symbol_17"
-OpName %34 "frag_main"
-OpName %38 "FragmentInput"
-OpMemberName %38 0 "mul"
-OpMemberName %38 1 "interface"
-OpDecorate %1 BuiltIn Position
-OpDecorate %6 Location 1
-OpDecorate %9 Location 0
-OpDecorate %11 Location 1
-OpDecorate %12 BuiltIn FragDepth
-OpMemberDecorate %15 0 Offset 0
-OpMemberDecorate %15 1 Offset 16
-OpMemberDecorate %16 0 Offset 0
-OpMemberDecorate %38 0 Offset 0
-OpMemberDecorate %38 1 Offset 4
-%4 = OpTypeFloat 32
-%3 = OpTypeVector %4 4
+OpEntryPoint Vertex %16 "vert_main" %1
+OpEntryPoint Fragment %25 "frag_main" %5 %7
+OpExecutionMode %25 OriginUpperLeft
+OpExecutionMode %25 DepthReplacing
+OpName %1 "tint_symbol_4"
+OpName %5 "tint_symbol_7"
+OpName %7 "tint_symbol_10"
+OpName %10 "Interface"
+OpMemberName %10 0 "value"
+OpName %11 "tint_symbol_5"
+OpName %12 "tint_symbol_3"
+OpName %16 "vert_main"
+OpName %22 "tint_symbol_11"
+OpName %23 "tint_symbol_9"
+OpName %25 "frag_main"
+OpDecorate %1 Location 1
+OpDecorate %5 Location 1
+OpDecorate %7 BuiltIn FragDepth
+OpMemberDecorate %10 0 Offset 0
+%3 = OpTypeFloat 32
 %2 = OpTypePointer Output %3
-%5 = OpConstantNull %3
-%1 = OpVariable %2 Output %5
-%7 = OpTypePointer Output %4
-%8 = OpConstantNull %4
-%6 = OpVariable %7 Output %8
-%10 = OpTypePointer Input %4
-%9 = OpVariable %10 Input
-%11 = OpVariable %10 Input
-%12 = OpVariable %7 Output %8
-%14 = OpTypeVoid
-%16 = OpTypeStruct %4
-%15 = OpTypeStruct %3 %16
-%13 = OpTypeFunction %14 %15
-%23 = OpTypeFunction %14
-%27 = OpConstant %4 42
-%28 = OpConstantComposite %16 %27
-%29 = OpConstantComposite %15 %5 %28
-%30 = OpTypeFunction %14 %4
-%38 = OpTypeStruct %4 %16
-%17 = OpFunction %14 None %13
-%18 = OpFunctionParameter %15
-%19 = OpLabel
-%20 = OpCompositeExtract %3 %18 0
-OpStore %1 %20
-%21 = OpCompositeExtract %16 %18 1
-%22 = OpCompositeExtract %4 %21 0
-OpStore %6 %22
+%4 = OpConstantNull %3
+%1 = OpVariable %2 Output %4
+%6 = OpTypePointer Input %3
+%5 = OpVariable %6 Input
+%7 = OpVariable %2 Output %4
+%9 = OpTypeVoid
+%10 = OpTypeStruct %3
+%8 = OpTypeFunction %9 %10
+%15 = OpTypeFunction %9
+%19 = OpConstant %3 42
+%20 = OpConstantComposite %10 %19
+%21 = OpTypeFunction %9 %3
+%11 = OpFunction %9 None %8
+%12 = OpFunctionParameter %10
+%13 = OpLabel
+%14 = OpCompositeExtract %3 %12 0
+OpStore %1 %14
 OpReturn
 OpFunctionEnd
-%24 = OpFunction %14 None %23
-%25 = OpLabel
-%26 = OpFunctionCall %14 %17 %29
+%16 = OpFunction %9 None %15
+%17 = OpLabel
+%18 = OpFunctionCall %9 %11 %20
 OpReturn
 OpFunctionEnd
-%31 = OpFunction %14 None %30
-%32 = OpFunctionParameter %4
-%33 = OpLabel
-OpStore %12 %32
+%22 = OpFunction %9 None %21
+%23 = OpFunctionParameter %3
+%24 = OpLabel
+OpStore %7 %23
 OpReturn
 OpFunctionEnd
-%34 = OpFunction %14 None %23
-%35 = OpLabel
-%36 = OpLoad %4 %11
-%37 = OpCompositeConstruct %16 %36
-%39 = OpLoad %4 %9
-%40 = OpCompositeConstruct %38 %39 %37
-%42 = OpCompositeExtract %4 %40 0
-%43 = OpCompositeExtract %16 %40 1
-%44 = OpCompositeExtract %4 %43 0
-%45 = OpFMul %4 %42 %44
-%41 = OpFunctionCall %14 %31 %45
+%25 = OpFunction %9 None %15
+%26 = OpLabel
+%27 = OpLoad %3 %5
+%28 = OpCompositeConstruct %10 %27
+%30 = OpCompositeExtract %3 %28 0
+%29 = OpFunctionCall %9 %22 %30
 OpReturn
 OpFunctionEnd
 )");
diff --git a/test/BUILD.gn b/test/BUILD.gn
index c981ebf..87c8a8f 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -172,6 +172,7 @@
     "../src/resolver/builtins_validation_test.cc",
     "../src/resolver/control_block_validation_test.cc",
     "../src/resolver/decoration_validation_test.cc",
+    "../src/resolver/entry_point_validation_test.cc",
     "../src/resolver/function_validation_test.cc",
     "../src/resolver/host_shareable_validation_test.cc",
     "../src/resolver/intrinsic_test.cc",