resolver: Validate calls to void callables

Calls to functions and intrinsics that do not return a value must only be used by a call statement.

Fixed: chromium:1241460
Change-Id: I0f940c942b55a5212367dbf9e261083beb4560ec
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/62440
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/ast/intrinsic_texture_helper_test.cc b/src/ast/intrinsic_texture_helper_test.cc
index 4330526..1d298a5 100644
--- a/src/ast/intrinsic_texture_helper_test.cc
+++ b/src/ast/intrinsic_texture_helper_test.cc
@@ -2237,6 +2237,18 @@
   };
 }
 
+bool ReturnsVoid(ValidTextureOverload texture_overload) {
+  switch (texture_overload) {
+    case ValidTextureOverload::kStoreWO1dRgba32float:
+    case ValidTextureOverload::kStoreWO2dRgba32float:
+    case ValidTextureOverload::kStoreWO2dArrayRgba32float:
+    case ValidTextureOverload::kStoreWO3dRgba32float:
+      return true;
+    default:
+      return false;
+  }
+}
+
 }  // namespace test
 }  // namespace intrinsic
 }  // namespace ast
diff --git a/src/ast/intrinsic_texture_helper_test.h b/src/ast/intrinsic_texture_helper_test.h
index 92c6986..4460908 100644
--- a/src/ast/intrinsic_texture_helper_test.h
+++ b/src/ast/intrinsic_texture_helper_test.h
@@ -185,6 +185,10 @@
   kStoreWO3dRgba32float,             // Not permutated for all texel formats
 };
 
+/// @param texture_overload the ValidTextureOverload
+/// @returns true if the ValidTextureOverload intrinsic returns no value.
+bool ReturnsVoid(ValidTextureOverload texture_overload);
+
 /// Describes a texture intrinsic overload
 struct TextureOverloadCase {
   /// Constructor for textureSample...() functions
diff --git a/src/resolver/call_test.cc b/src/resolver/call_test.cc
index 3c96d10..585099a 100644
--- a/src/resolver/call_test.cc
+++ b/src/resolver/call_test.cc
@@ -90,7 +90,7 @@
     args.push_back(p.create_value(*this, 0));
   }
 
-  Func("foo", std::move(params), ty.void_(), {Return()});
+  Func("foo", std::move(params), ty.f32(), {Return(1.23f)});
   auto* call = Call("foo", std::move(args));
   WrapInFunction(call);
 
diff --git a/src/resolver/function_validation_test.cc b/src/resolver/function_validation_test.cc
index d0c7556..5aa7bac 100644
--- a/src/resolver/function_validation_test.cc
+++ b/src/resolver/function_validation_test.cc
@@ -217,6 +217,20 @@
 }
 
 TEST_F(ResolverFunctionValidationTest,
+       FunctionTypeMustMatchReturnStatementType_void_fail) {
+  // fn v { return; }
+  // fn func { return v(); }
+  Func("v", {}, ty.void_(), {Return()});
+  Func("func", {}, ty.void_(),
+       {
+           Return(Call(Source{Source::Location{12, 34}}, "v")),
+       });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(), "12:34 error: function 'v' does not return a value");
+}
+
+TEST_F(ResolverFunctionValidationTest,
        FunctionTypeMustMatchReturnStatementTypeMissing_fail) {
   // fn func -> f32 { return; }
   Func("func", ast::VariableList{}, ty.f32(),
diff --git a/src/resolver/intrinsic_test.cc b/src/resolver/intrinsic_test.cc
index e899bab..9270e0a 100644
--- a/src/resolver/intrinsic_test.cc
+++ b/src/resolver/intrinsic_test.cc
@@ -557,7 +557,7 @@
   auto param = GetParam();
 
   auto* call = Call(param.name);
-  WrapInFunction(call);
+  WrapInFunction(create<ast::CallStatement>(call));
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
   ASSERT_NE(TypeOf(call), nullptr);
@@ -568,7 +568,7 @@
   auto param = GetParam();
 
   auto* call = Call(param.name, vec4<f32>(1.f, 2.f, 3.f, 4.f), 1.0f);
-  WrapInFunction(call);
+  WrapInFunction(create<ast::CallStatement>(call));
 
   EXPECT_FALSE(r()->Resolve());
 
@@ -2082,8 +2082,10 @@
   param.buildSamplerVariable(this);
 
   auto* call = Call(param.function, param.args(this));
-  Func("func", {}, ty.void_(), {Ignore(call)},
-       {create<ast::StageDecoration>(ast::PipelineStage::kFragment)});
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
+  Func("func", {}, ty.void_(), {stmt}, {Stage(ast::PipelineStage::kFragment)});
 
   ASSERT_TRUE(r()->Resolve()) << r()->error();
 
diff --git a/src/resolver/intrinsic_validation_test.cc b/src/resolver/intrinsic_validation_test.cc
index 6313ccc..77b7e94 100644
--- a/src/resolver/intrinsic_validation_test.cc
+++ b/src/resolver/intrinsic_validation_test.cc
@@ -21,6 +21,20 @@
 
 using ResolverIntrinsicValidationTest = ResolverTest;
 
+TEST_F(ResolverIntrinsicValidationTest,
+       FunctionTypeMustMatchReturnStatementType_void_fail) {
+  // fn func { return workgroupBarrier(); }
+  Func("func", {}, ty.void_(),
+       {
+           Return(Call(Source{Source::Location{12, 34}}, "workgroupBarrier")),
+       });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: intrinsic 'workgroupBarrier' does not return a value");
+}
+
 TEST_F(ResolverIntrinsicValidationTest, InvalidPipelineStageDirect) {
   // [[stage(compute), workgroup_size(1)]] fn func { return dpdx(1.0); }
 
@@ -42,16 +56,16 @@
 
   auto* dpdx = create<ast::CallExpression>(Source{{3, 4}}, Expr("dpdx"),
                                            ast::ExpressionList{Expr(1.0f)});
-  Func(Source{{1, 2}}, "f0", ast::VariableList{}, ty.void_(), {Ignore(dpdx)});
+  Func(Source{{1, 2}}, "f0", {}, ty.void_(), {Ignore(dpdx)});
 
-  Func(Source{{3, 4}}, "f1", ast::VariableList{}, ty.void_(),
-       {Ignore(Call("f0"))});
+  Func(Source{{3, 4}}, "f1", {}, ty.void_(),
+       {create<ast::CallStatement>(Call("f0"))});
 
-  Func(Source{{5, 6}}, "f2", ast::VariableList{}, ty.void_(),
-       {Ignore(Call("f1"))});
+  Func(Source{{5, 6}}, "f2", {}, ty.void_(),
+       {create<ast::CallStatement>(Call("f1"))});
 
-  Func(Source{{7, 8}}, "main", ast::VariableList{}, ty.void_(),
-       {Ignore(Call("f2"))},
+  Func(Source{{7, 8}}, "main", {}, ty.void_(),
+       {create<ast::CallStatement>(Call("f2"))},
        {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
 
   EXPECT_FALSE(r()->Resolve());
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 3eb9629..8f3206b 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -1463,12 +1463,9 @@
   };
 
   // Inner lambda that is applied to a type and all of its members.
-  auto validate_entry_point_decorations_inner = [&](const ast::DecorationList&
-                                                        decos,
-                                                    sem::Type* ty,
-                                                    Source source,
-                                                    ParamOrRetType param_or_ret,
-                                                    bool is_struct_member) {
+  auto validate_entry_point_decorations_inner =
+      [&](const ast::DecorationList& decos, sem::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;
@@ -1478,14 +1475,16 @@
           if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
             if (pipeline_io_attribute) {
               AddError("multiple entry point IO attributes", deco->source());
-          AddNote("previously consumed " + deco_to_str(pipeline_io_attribute),
+              AddNote(
+                  "previously consumed " + deco_to_str(pipeline_io_attribute),
                   pipeline_io_attribute->source());
               return false;
             }
             pipeline_io_attribute = deco;
 
             if (builtins.count(builtin->value())) {
-          AddError(deco_to_str(builtin) +
+              AddError(
+                  deco_to_str(builtin) +
                       " attribute appears multiple times as pipeline " +
                       (param_or_ret == ParamOrRetType::kParameter ? "input"
                                                                   : "output"),
@@ -1503,7 +1502,8 @@
           } else if (auto* location = deco->As<ast::LocationDecoration>()) {
             if (pipeline_io_attribute) {
               AddError("multiple entry point IO attributes", deco->source());
-          AddNote("previously consumed " + deco_to_str(pipeline_io_attribute),
+              AddNote(
+                  "previously consumed " + deco_to_str(pipeline_io_attribute),
                   pipeline_io_attribute->source());
               return false;
             }
@@ -1514,7 +1514,8 @@
                                             is_input)) {
               return false;
             }
-      } else if (auto* interpolate = deco->As<ast::InterpolateDecoration>()) {
+          } 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)) {
@@ -1528,7 +1529,8 @@
           }
           if (is_invalid_compute_shader_decoration) {
             std::string input_or_output =
-            param_or_ret == ParamOrRetType::kParameter ? "inputs" : "output";
+                param_or_ret == ParamOrRetType::kParameter ? "inputs"
+                                                           : "output";
             AddError(
                 "decoration is not valid for compute shader " + input_or_output,
                 deco->source());
@@ -1536,13 +1538,13 @@
           }
         }
 
-    if (IsValidationEnabled(decos,
-                            ast::DisabledValidation::kEntryPointParameter)) {
+        if (IsValidationEnabled(
+                decos, ast::DisabledValidation::kEntryPointParameter)) {
           if (!ty->Is<sem::Struct>() && !pipeline_io_attribute) {
             std::string err = "missing entry point IO attribute";
             if (!is_struct_member) {
-          err +=
-              (param_or_ret == ParamOrRetType::kParameter ? " on parameter"
+              err += (param_or_ret == ParamOrRetType::kParameter
+                          ? " on parameter"
                           : " on return type");
             }
             AddError(err, source);
@@ -2338,28 +2340,42 @@
     }
   }
 
+  return ValidateCall(call);
+}
+
+bool Resolver::ValidateCall(ast::CallExpression* call) {
+  if (TypeOf(call)->Is<sem::Void>()) {
+    bool is_call_statement = false;
+    if (current_statement_) {
+      if (auto* call_stmt =
+              As<ast::CallStatement>(current_statement_->Declaration())) {
+        if (call_stmt->expr() == call) {
+          is_call_statement = true;
+        }
+      }
+    }
+    if (!is_call_statement) {
+      // https://gpuweb.github.io/gpuweb/wgsl/#function-call-expr
+      // If the called function does not return a value, a function call
+      // statement should be used instead.
+      auto* ident = call->func();
+      auto name = builder_->Symbols().NameFor(ident->symbol());
+      // A function call is made to either a user declared function or an
+      // intrinsic. function_calls_ only maps CallExpression to user declared
+      // functions
+      bool is_function = function_calls_.count(call) != 0;
+      AddError((is_function ? "function" : "intrinsic") + std::string(" '") +
+                   name + "' does not return a value",
+               call->source());
+      return false;
+    }
+  }
+
   return true;
 }
 
 bool Resolver::ValidateCallStatement(ast::CallStatement* stmt) {
-  const sem::Type* return_type = nullptr;
-  // A function call is made to either a user declared function or an intrinsic.
-  // function_calls_ only maps CallExpression to user declared functions
-  auto it = function_calls_.find(stmt->expr());
-  if (it != function_calls_.end()) {
-    return_type = it->second.function->return_type;
-  } else {
-    // Must be an intrinsic call
-    auto* target = builder_->Sem().Get(stmt->expr())->Target();
-    if (auto* intrinsic = target->As<sem::Intrinsic>()) {
-      return_type = intrinsic->ReturnType();
-    } else {
-      TINT_ICE(Resolver, diagnostics_)
-          << "call target was not an intrinsic, but a "
-          << intrinsic->TypeInfo().name;
-    }
-  }
-
+  const sem::Type* return_type = TypeOf(stmt->expr());
   if (!return_type->Is<sem::Void>()) {
     // https://gpuweb.github.io/gpuweb/wgsl/#function-call-statement
     // A function call statement executes a function call where the called
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index 37de090..90f305c 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -276,6 +276,7 @@
                                  const sem::Type* storage_type,
                                  const bool is_input,
                                  const bool is_struct_member);
+  bool ValidateCall(ast::CallExpression* call);
   bool ValidateCallStatement(ast::CallStatement* stmt);
   bool ValidateEntryPoint(const ast::Function* func, const FunctionInfo* info);
   bool ValidateFunction(const ast::Function* func, const FunctionInfo* info);
diff --git a/src/resolver/validation_test.cc b/src/resolver/validation_test.cc
index 58abd73..07396a7 100644
--- a/src/resolver/validation_test.cc
+++ b/src/resolver/validation_test.cc
@@ -94,11 +94,11 @@
   Global("dst", ty.vec4<f32>(), ast::StorageClass::kPrivate);
   auto* stmt = Assign(Expr("dst"), Expr(Source{{3, 4}}, "wg"));
 
-  Func(Source{{5, 6}}, "f2", ast::VariableList{}, ty.void_(), {stmt});
-  Func(Source{{7, 8}}, "f1", ast::VariableList{}, ty.void_(),
-       {Ignore(Call("f2"))});
-  Func(Source{{9, 10}}, "f0", ast::VariableList{}, ty.void_(),
-       {Ignore(Call("f1"))},
+  Func(Source{{5, 6}}, "f2", {}, ty.void_(), {stmt});
+  Func(Source{{7, 8}}, "f1", {}, ty.void_(),
+       {create<ast::CallStatement>(Call("f2"))});
+  Func(Source{{9, 10}}, "f0", {}, ty.void_(),
+       {create<ast::CallStatement>(Call("f1"))},
        ast::DecorationList{Stage(ast::PipelineStage::kFragment)});
 
   EXPECT_FALSE(r()->Resolve());
diff --git a/src/writer/hlsl/generator_impl_call_test.cc b/src/writer/hlsl/generator_impl_call_test.cc
index a0391e5..7d6e51c 100644
--- a/src/writer/hlsl/generator_impl_call_test.cc
+++ b/src/writer/hlsl/generator_impl_call_test.cc
@@ -23,8 +23,7 @@
 using HlslGeneratorImplTest_Call = TestHelper;
 
 TEST_F(HlslGeneratorImplTest_Call, EmitExpression_Call_WithoutParams) {
-  Func("my_func", ast::VariableList{}, ty.void_(), ast::StatementList{},
-       ast::DecorationList{});
+  Func("my_func", {}, ty.f32(), {Return(1.23f)});
 
   auto* call = Call("my_func");
   WrapInFunction(call);
@@ -42,7 +41,7 @@
            Param(Sym(), ty.f32()),
            Param(Sym(), ty.f32()),
        },
-       ty.void_(), ast::StatementList{}, ast::DecorationList{});
+       ty.f32(), {Return(1.23f)});
   Global("param1", ty.f32(), ast::StorageClass::kPrivate);
   Global("param2", ty.f32(), ast::StorageClass::kPrivate);
 
diff --git a/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc b/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
index 1572cbf..279209e 100644
--- a/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
+++ b/src/writer/hlsl/generator_impl_intrinsic_texture_test.cc
@@ -361,14 +361,11 @@
   param.buildSamplerVariable(this);
 
   auto* call = Call(param.function, param.args(this));
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
 
-  Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
-           Ignore(call),
-       },
-       ast::DecorationList{
-           Stage(ast::PipelineStage::kFragment),
-       });
+  Func("main", {}, ty.void_(), {stmt}, {Stage(ast::PipelineStage::kFragment)});
 
   GeneratorImpl& gen = SanitizeAndBuild();
 
diff --git a/src/writer/msl/generator_impl_call_test.cc b/src/writer/msl/generator_impl_call_test.cc
index 3f2b2cb..3b6379c 100644
--- a/src/writer/msl/generator_impl_call_test.cc
+++ b/src/writer/msl/generator_impl_call_test.cc
@@ -23,8 +23,7 @@
 using MslGeneratorImplTest = TestHelper;
 
 TEST_F(MslGeneratorImplTest, EmitExpression_Call_WithoutParams) {
-  Func("my_func", ast::VariableList{}, ty.void_(), ast::StatementList{},
-       ast::DecorationList{});
+  Func("my_func", {}, ty.f32(), {Return(1.23f)});
 
   auto* call = Call("my_func");
   WrapInFunction(call);
@@ -42,7 +41,7 @@
            Param(Sym(), ty.f32()),
            Param(Sym(), ty.f32()),
        },
-       ty.void_(), ast::StatementList{}, ast::DecorationList{});
+       ty.f32(), {Return(1.23f)});
   Global("param1", ty.f32(), ast::StorageClass::kPrivate);
   Global("param2", ty.f32(), ast::StorageClass::kPrivate);
 
diff --git a/src/writer/msl/generator_impl_intrinsic_test.cc b/src/writer/msl/generator_impl_intrinsic_test.cc
index b2b718b..eba80cc 100644
--- a/src/writer/msl/generator_impl_intrinsic_test.cc
+++ b/src/writer/msl/generator_impl_intrinsic_test.cc
@@ -299,7 +299,7 @@
 
 TEST_F(MslGeneratorImplTest, StorageBarrier) {
   auto* call = Call("storageBarrier");
-  WrapInFunction(call);
+  WrapInFunction(create<ast::CallStatement>(call));
 
   GeneratorImpl& gen = Build();
 
@@ -310,7 +310,7 @@
 
 TEST_F(MslGeneratorImplTest, WorkgroupBarrier) {
   auto* call = Call("workgroupBarrier");
-  WrapInFunction(call);
+  WrapInFunction(create<ast::CallStatement>(call));
 
   GeneratorImpl& gen = Build();
 
diff --git a/src/writer/msl/generator_impl_intrinsic_texture_test.cc b/src/writer/msl/generator_impl_intrinsic_texture_test.cc
index c1f0787..f8ebd4d 100644
--- a/src/writer/msl/generator_impl_intrinsic_texture_test.cc
+++ b/src/writer/msl/generator_impl_intrinsic_texture_test.cc
@@ -273,14 +273,11 @@
 
   auto* call =
       create<ast::CallExpression>(Expr(param.function), param.args(this));
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
 
-  Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
-           Ignore(call),
-       },
-       ast::DecorationList{
-           Stage(ast::PipelineStage::kFragment),
-       });
+  Func("main", {}, ty.void_(), {stmt}, {Stage(ast::PipelineStage::kFragment)});
 
   GeneratorImpl& gen = Build();
 
diff --git a/src/writer/spirv/builder_intrinsic_texture_test.cc b/src/writer/spirv/builder_intrinsic_texture_test.cc
index 2f19989..78146dc 100644
--- a/src/writer/spirv/builder_intrinsic_texture_test.cc
+++ b/src/writer/spirv/builder_intrinsic_texture_test.cc
@@ -3691,9 +3691,10 @@
   auto* sampler = param.buildSamplerVariable(this);
 
   auto* call = Call(param.function, param.args(this));
-
-  Func("func", {}, ty.void_(), {Ignore(call)},
-       {create<ast::StageDecoration>(ast::PipelineStage::kFragment)});
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
+  Func("func", {}, ty.void_(), {stmt}, {Stage(ast::PipelineStage::kFragment)});
 
   spirv::Builder& b = Build();
 
@@ -3719,13 +3720,10 @@
 
   auto* call = Call(param.function, param.args(this));
 
-  Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
-           Ignore(call),
-       },
-       ast::DecorationList{
-           Stage(ast::PipelineStage::kFragment),
-       });
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
+  Func("main", {}, ty.void_(), {stmt}, {Stage(ast::PipelineStage::kFragment)});
 
   spirv::Builder& b = Build();
 
@@ -3744,7 +3742,10 @@
   auto* sampler = param.buildSamplerVariable(this);
 
   auto* call = Call(param.function, param.args(this));
-  Func("func", {}, ty.void_(), {Ignore(call)},
+  auto* stmt = ast::intrinsic::test::ReturnsVoid(param.overload)
+                   ? create<ast::CallStatement>(call)
+                   : Ignore(call);
+  Func("func", {}, ty.void_(), {stmt},
        {create<ast::StageDecoration>(ast::PipelineStage::kFragment)});
 
   spirv::Builder& b = Build();
diff --git a/src/writer/wgsl/generator_impl_call_test.cc b/src/writer/wgsl/generator_impl_call_test.cc
index 42c360d..514f4a6 100644
--- a/src/writer/wgsl/generator_impl_call_test.cc
+++ b/src/writer/wgsl/generator_impl_call_test.cc
@@ -23,8 +23,7 @@
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, EmitExpression_Call_WithoutParams) {
-  Func("my_func", ast::VariableList{}, ty.void_(), ast::StatementList{},
-       ast::DecorationList{});
+  Func("my_func", {}, ty.f32(), {Return(1.23f)});
 
   auto* call = Call("my_func");
   WrapInFunction(call);
@@ -42,7 +41,7 @@
            Param(Sym(), ty.f32()),
            Param(Sym(), ty.f32()),
        },
-       ty.void_(), ast::StatementList{}, ast::DecorationList{});
+       ty.f32(), {Return(1.23f)});
   Global("param1", ty.f32(), ast::StorageClass::kPrivate);
   Global("param2", ty.f32(), ast::StorageClass::kPrivate);