[validator] Remove requirement to have an entry point

The SPIR-V and HLSL sanitizing transforms add an empty one if
necessary.

Fixed: tint:679
Change-Id: Ic98ff3109d7381b1fbc2de68d95d57e15c7a67c0
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/46700
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/transform/hlsl.cc b/src/transform/hlsl.cc
index 0a08e24..918d6e3 100644
--- a/src/transform/hlsl.cc
+++ b/src/transform/hlsl.cc
@@ -16,6 +16,7 @@
 
 #include <utility>
 
+#include "src/ast/stage_decoration.h"
 #include "src/ast/variable_decl_statement.h"
 #include "src/program_builder.h"
 #include "src/semantic/expression.h"
@@ -32,6 +33,7 @@
   ProgramBuilder out;
   CloneContext ctx(&out, in);
   PromoteArrayInitializerToConstVar(ctx);
+  AddEmptyEntryPoint(ctx);
   ctx.Clone();
   return Output{Program(std::move(out))};
 }
@@ -105,5 +107,16 @@
   }
 }
 
+void Hlsl::AddEmptyEntryPoint(CloneContext& ctx) const {
+  for (auto* func : ctx.src->AST().Functions()) {
+    if (func->IsEntryPoint()) {
+      return;
+    }
+  }
+  ctx.dst->Func(
+      "_tint_unused_entry_point", {}, ctx.dst->ty.void_(), {},
+      {ctx.dst->create<ast::StageDecoration>(ast::PipelineStage::kVertex)});
+}
+
 }  // namespace transform
 }  // namespace tint
diff --git a/src/transform/hlsl.h b/src/transform/hlsl.h
index 53ad35f..df903a7 100644
--- a/src/transform/hlsl.h
+++ b/src/transform/hlsl.h
@@ -44,6 +44,8 @@
   /// the array usage statement.
   /// See crbug.com/tint/406 for more details
   void PromoteArrayInitializerToConstVar(CloneContext& ctx) const;
+  /// Add an empty shader entry point if none exist in the module.
+  void AddEmptyEntryPoint(CloneContext& ctx) const;
 };
 
 }  // namespace transform
diff --git a/src/transform/hlsl_test.cc b/src/transform/hlsl_test.cc
index bc6b264..4ce2259 100644
--- a/src/transform/hlsl_test.cc
+++ b/src/transform/hlsl_test.cc
@@ -143,6 +143,20 @@
   EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(HlslTest, AddEmptyEntryPoint) {
+  auto* src = R"()";
+
+  auto* expect = R"(
+[[stage(vertex)]]
+fn _tint_unused_entry_point() -> void {
+}
+)";
+
+  auto got = Run<Hlsl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace transform
 }  // namespace tint
diff --git a/src/transform/spirv.cc b/src/transform/spirv.cc
index d29cfa4..bbbff3f 100644
--- a/src/transform/spirv.cc
+++ b/src/transform/spirv.cc
@@ -19,6 +19,7 @@
 
 #include "src/ast/call_statement.h"
 #include "src/ast/return_statement.h"
+#include "src/ast/stage_decoration.h"
 #include "src/program_builder.h"
 #include "src/semantic/function.h"
 #include "src/semantic/statement.h"
@@ -42,6 +43,7 @@
   ProgramBuilder out2;
   CloneContext ctx2(&out2, &tmp);
   HandleSampleMaskBuiltins(ctx2);
+  AddEmptyEntryPoint(ctx2);
   ctx2.Clone();
 
   return Output{Program(std::move(out2))};
@@ -234,6 +236,17 @@
   }
 }
 
+void Spirv::AddEmptyEntryPoint(CloneContext& ctx) const {
+  for (auto* func : ctx.src->AST().Functions()) {
+    if (func->IsEntryPoint()) {
+      return;
+    }
+  }
+  ctx.dst->Func(
+      "_tint_unused_entry_point", {}, ctx.dst->ty.void_(), {},
+      {ctx.dst->create<ast::StageDecoration>(ast::PipelineStage::kCompute)});
+}
+
 Symbol Spirv::HoistToInputVariables(
     CloneContext& ctx,
     const ast::Function* func,
diff --git a/src/transform/spirv.h b/src/transform/spirv.h
index 40bc54e..7ac53e1 100644
--- a/src/transform/spirv.h
+++ b/src/transform/spirv.h
@@ -47,6 +47,8 @@
   void HandleEntryPointIOTypes(CloneContext& ctx) const;
   /// Change type of sample mask builtin variables to single element arrays.
   void HandleSampleMaskBuiltins(CloneContext& ctx) const;
+  /// Add an empty shader entry point if none exist in the module.
+  void AddEmptyEntryPoint(CloneContext& ctx) const;
 
   /// Recursively create module-scope input variables for `ty` and add
   /// function-scope variables for structs to `func`.
diff --git a/src/transform/spirv_test.cc b/src/transform/spirv_test.cc
index 9df9ed2..bb21aea 100644
--- a/src/transform/spirv_test.cc
+++ b/src/transform/spirv_test.cc
@@ -833,6 +833,20 @@
   EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(SpirvTest, AddEmptyEntryPoint) {
+  auto* src = R"()";
+
+  auto* expect = R"(
+[[stage(compute)]]
+fn _tint_unused_entry_point() -> void {
+}
+)";
+
+  auto got = Run<Spirv>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
 // Test that different transforms within the sanitizer interact correctly.
 TEST_F(SpirvTest, MultipleTransforms) {
   auto* src = R"(
diff --git a/src/validator/validator_function_test.cc b/src/validator/validator_function_test.cc
index d74e103..0d3d549 100644
--- a/src/validator/validator_function_test.cc
+++ b/src/validator/validator_function_test.cc
@@ -78,25 +78,7 @@
       "12:34 v-0020: only one stage decoration permitted per entry point");
 }
 
-TEST_F(ValidateFunctionTest, OnePipelineStageFunctionMustBePresent_Pass) {
-  // [[stage(vertex)]]
-  // fn vtx_func() -> void { return; }
-
-  Func("vtx_func", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
-           create<ast::ReturnStatement>(),
-       },
-       ast::DecorationList{
-           create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-       });
-
-  ValidatorImpl& v = Build();
-
-  EXPECT_TRUE(v.Validate()) << v.error();
-}
-
-TEST_F(ValidateFunctionTest, OnePipelineStageFunctionMustBePresent_Fail) {
-  // fn vtx_func() -> void { return; }
+TEST_F(ValidateFunctionTest, NoPipelineEntryPoints) {
   Func("vtx_func", ast::VariableList{}, ty.void_(),
        ast::StatementList{
            create<ast::ReturnStatement>(),
@@ -105,10 +87,7 @@
 
   ValidatorImpl& v = Build();
 
-  EXPECT_FALSE(v.Validate());
-  EXPECT_EQ(v.error(),
-            "v-0003: At least one of vertex, fragment or compute shader must "
-            "be present");
+  EXPECT_TRUE(v.Validate()) << v.error();
 }
 
 TEST_F(ValidateFunctionTest, FunctionVarInitWithParam) {
diff --git a/src/validator/validator_impl.cc b/src/validator/validator_impl.cc
index 6b10921..acc10fa 100644
--- a/src/validator/validator_impl.cc
+++ b/src/validator/validator_impl.cc
@@ -138,10 +138,8 @@
 }
 
 bool ValidatorImpl::ValidateEntryPoint(const ast::FunctionList& funcs) {
-  auto shader_is_present = false;
   for (auto* func : funcs) {
     if (func->IsEntryPoint()) {
-      shader_is_present = true;
       auto stage_deco_count = 0;
       for (auto* deco : func->decorations()) {
         if (deco->Is<ast::StageDecoration>()) {
@@ -158,12 +156,6 @@
       }
     }
   }
-  if (!shader_is_present) {
-    add_error(Source{}, "v-0003",
-              "At least one of vertex, fragment or compute shader must "
-              "be present");
-    return false;
-  }
   return true;
 }
 
diff --git a/src/validator/validator_test.cc b/src/validator/validator_test.cc
index c79eff1..4a69b34 100644
--- a/src/validator/validator_test.cc
+++ b/src/validator/validator_test.cc
@@ -65,7 +65,7 @@
 
   ValidatorImpl& v = Build();
 
-  EXPECT_FALSE(v.Validate()) << v.error();
+  EXPECT_TRUE(v.Validate()) << v.error();
 }
 
 TEST_F(ValidatorTest, GlobalVariableUnique_Pass) {