resolver: Validate compound assignment statements

Reuse the logic for resolving binary operator result types that was
implemented for binary expressions. This validates that the LHS and
RHS are compatible for the target operator. We then try to match the
resolved result type against the LHS store type.

Bug: tint:1325

Change-Id: If80a883079bb71fa6c4eb5545654279fefffacb4
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/74362
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
This commit is contained in:
James Price 2022-03-31 22:30:10 +00:00
parent 4924186bbd
commit c022ff5b49
8 changed files with 400 additions and 11 deletions

View File

@ -732,6 +732,7 @@ if(TINT_BUILD_TESTS)
resolver/builtin_validation_test.cc resolver/builtin_validation_test.cc
resolver/call_test.cc resolver/call_test.cc
resolver/call_validation_test.cc resolver/call_validation_test.cc
resolver/compound_assignment_validation_test.cc
resolver/compound_statement_test.cc resolver/compound_statement_test.cc
resolver/control_block_validation_test.cc resolver/control_block_validation_test.cc
resolver/attribute_validation_test.cc resolver/attribute_validation_test.cc

View File

@ -32,6 +32,7 @@
#include "src/tint/ast/call_expression.h" #include "src/tint/ast/call_expression.h"
#include "src/tint/ast/call_statement.h" #include "src/tint/ast/call_statement.h"
#include "src/tint/ast/case_statement.h" #include "src/tint/ast/case_statement.h"
#include "src/tint/ast/compound_assignment_statement.h"
#include "src/tint/ast/continue_statement.h" #include "src/tint/ast/continue_statement.h"
#include "src/tint/ast/depth_multisampled_texture.h" #include "src/tint/ast/depth_multisampled_texture.h"
#include "src/tint/ast/depth_texture.h" #include "src/tint/ast/depth_texture.h"
@ -2265,6 +2266,40 @@ class ProgramBuilder {
Expr(std::forward<RhsExpressionInit>(rhs))); Expr(std::forward<RhsExpressionInit>(rhs)));
} }
/// Creates a ast::CompoundAssignmentStatement with input lhs and rhs
/// expressions, and a binary operator.
/// @param source the source information
/// @param lhs the left hand side expression initializer
/// @param rhs the right hand side expression initializer
/// @param op the binary operator
/// @returns the compound assignment statement pointer
template <typename LhsExpressionInit, typename RhsExpressionInit>
const ast::CompoundAssignmentStatement* CompoundAssign(
const Source& source,
LhsExpressionInit&& lhs,
RhsExpressionInit&& rhs,
ast::BinaryOp op) {
return create<ast::CompoundAssignmentStatement>(
source, Expr(std::forward<LhsExpressionInit>(lhs)),
Expr(std::forward<RhsExpressionInit>(rhs)), op);
}
/// Creates a ast::CompoundAssignmentStatement with input lhs and rhs
/// expressions, and a binary operator.
/// @param lhs the left hand side expression initializer
/// @param rhs the right hand side expression initializer
/// @param op the binary operator
/// @returns the compound assignment statement pointer
template <typename LhsExpressionInit, typename RhsExpressionInit>
const ast::CompoundAssignmentStatement* CompoundAssign(
LhsExpressionInit&& lhs,
RhsExpressionInit&& rhs,
ast::BinaryOp op) {
return create<ast::CompoundAssignmentStatement>(
Expr(std::forward<LhsExpressionInit>(lhs)),
Expr(std::forward<RhsExpressionInit>(rhs)), op);
}
/// Creates a ast::LoopStatement with input body and optional continuing /// Creates a ast::LoopStatement with input body and optional continuing
/// @param source the source information /// @param source the source information
/// @param body the loop body /// @param body the loop body

View File

@ -0,0 +1,300 @@
// 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/tint/resolver/resolver.h"
#include "gmock/gmock.h"
#include "src/tint/resolver/resolver_test_helper.h"
#include "src/tint/sem/storage_texture_type.h"
namespace tint {
namespace resolver {
namespace {
using ResolverCompoundAssignmentValidationTest = ResolverTest;
TEST_F(ResolverCompoundAssignmentValidationTest, CompatibleTypes) {
// var a : i32 = 2;
// a += 2
auto* var = Var("a", ty.i32(), ast::StorageClass::kNone, Expr(2));
WrapInFunction(var,
CompoundAssign(Source{{12, 34}}, "a", 2, ast::BinaryOp::kAdd));
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest, CompatibleTypesThroughAlias) {
// alias myint = i32;
// var a : myint = 2;
// a += 2
auto* myint = Alias("myint", ty.i32());
auto* var = Var("a", ty.Of(myint), ast::StorageClass::kNone, Expr(2));
WrapInFunction(var,
CompoundAssign(Source{{12, 34}}, "a", 2, ast::BinaryOp::kAdd));
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest,
CompatibleTypesAssignThroughPointer) {
// var a : i32;
// let b : ptr<function,i32> = &a;
// *b += 2;
const auto func = ast::StorageClass::kFunction;
auto* var_a = Var("a", ty.i32(), func, Expr(2));
auto* var_b = Const("b", ty.pointer<int>(func), AddressOf(Expr("a")));
WrapInFunction(
var_a, var_b,
CompoundAssign(Source{{12, 34}}, Deref("b"), 2, ast::BinaryOp::kAdd));
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest, IncompatibleTypes) {
// {
// var a : i32 = 2;
// a += 2.3;
// }
auto* var = Var("a", ty.i32(), ast::StorageClass::kNone, Expr(2));
auto* assign =
CompoundAssign(Source{{12, 34}}, "a", 2.3f, ast::BinaryOp::kAdd);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"12:34 error: compound assignment operand types are invalid: i32 "
"add f32");
}
TEST_F(ResolverCompoundAssignmentValidationTest, IncompatibleOp) {
// {
// var a : f32 = 1.0;
// a |= 2.0;
// }
auto* var = Var("a", ty.f32(), ast::StorageClass::kNone, Expr(1.f));
auto* assign =
CompoundAssign(Source{{12, 34}}, "a", 2.0f, ast::BinaryOp::kOr);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(
r()->error(),
"12:34 error: compound assignment operand types are invalid: f32 or f32");
}
TEST_F(ResolverCompoundAssignmentValidationTest, VectorScalar_Pass) {
// {
// var a : vec4<f32>;
// a += 1.0;
// }
auto* var = Var("a", ty.vec4<f32>(), ast::StorageClass::kNone);
auto* assign =
CompoundAssign(Source{{12, 34}}, "a", 1.f, ast::BinaryOp::kAdd);
WrapInFunction(var, assign);
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest, ScalarVector_Fail) {
// {
// var a : f32;
// a += vec4<f32>();
// }
auto* var = Var("a", ty.f32(), ast::StorageClass::kNone);
auto* assign =
CompoundAssign(Source{{12, 34}}, "a", vec4<f32>(), ast::BinaryOp::kAdd);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(), "12:34 error: cannot assign 'vec4<f32>' to 'f32'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, MatrixScalar_Pass) {
// {
// var a : mat4x4<f32>;
// a *= 2.0;
// }
auto* var = Var("a", ty.mat4x4<f32>(), ast::StorageClass::kNone);
auto* assign =
CompoundAssign(Source{{12, 34}}, "a", 2.f, ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest, ScalarMatrix_Fail) {
// {
// var a : f32;
// a *= mat4x4();
// }
auto* var = Var("a", ty.f32(), ast::StorageClass::kNone);
auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x4<f32>(),
ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(), "12:34 error: cannot assign 'mat4x4<f32>' to 'f32'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, VectorMatrix_Pass) {
// {
// var a : vec4<f32>;
// a *= mat4x4();
// }
auto* var = Var("a", ty.vec4<f32>(), ast::StorageClass::kNone);
auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x4<f32>(),
ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverCompoundAssignmentValidationTest, VectorMatrix_ColumnMismatch) {
// {
// var a : vec4<f32>;
// a *= mat4x2();
// }
auto* var = Var("a", ty.vec4<f32>(), ast::StorageClass::kNone);
auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x2<f32>(),
ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"12:34 error: compound assignment operand types are invalid: "
"vec4<f32> multiply mat4x2<f32>");
}
TEST_F(ResolverCompoundAssignmentValidationTest, VectorMatrix_ResultMismatch) {
// {
// var a : vec4<f32>;
// a *= mat2x4();
// }
auto* var = Var("a", ty.vec4<f32>(), ast::StorageClass::kNone);
auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat2x4<f32>(),
ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"12:34 error: cannot assign 'vec2<f32>' to 'vec4<f32>'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, MatrixVector_Fail) {
// {
// var a : mat4x4<f32>;
// a *= vec4();
// }
auto* var = Var("a", ty.mat4x4<f32>(), ast::StorageClass::kNone);
auto* assign = CompoundAssign(Source{{12, 34}}, "a", vec4<f32>(),
ast::BinaryOp::kMultiply);
WrapInFunction(var, assign);
ASSERT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"12:34 error: cannot assign 'vec4<f32>' to 'mat4x4<f32>'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, Phony) {
// {
// _ += 1;
// }
WrapInFunction(
CompoundAssign(Source{{56, 78}}, Phony(), 1, ast::BinaryOp::kAdd));
EXPECT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"56:78 error: compound assignment operand types are invalid: void "
"add i32");
}
TEST_F(ResolverCompoundAssignmentValidationTest, ReadOnlyBuffer) {
// @group(0) @binding(0) var<storage,read> a : i32;
// {
// a += 1;
// }
Global(Source{{12, 34}}, "a", ty.i32(), ast::StorageClass::kStorage,
ast::Access::kRead, GroupAndBinding(0, 0));
WrapInFunction(CompoundAssign(Source{{56, 78}}, "a", 1, ast::BinaryOp::kAdd));
EXPECT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"56:78 error: cannot store into a read-only type 'ref<storage, "
"i32, read>'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, LhsConstant) {
// let a = 1;
// a += 1;
auto* a = Const(Source{{12, 34}}, "a", nullptr, Expr(1));
WrapInFunction(
a, CompoundAssign(Expr(Source{{56, 78}}, "a"), 1, ast::BinaryOp::kAdd));
EXPECT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(), R"(56:78 error: cannot assign to const
12:34 note: 'a' is declared here:)");
}
TEST_F(ResolverCompoundAssignmentValidationTest, LhsLiteral) {
// 1 += 1;
WrapInFunction(
CompoundAssign(Expr(Source{{56, 78}}, 1), 1, ast::BinaryOp::kAdd));
EXPECT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(), "56:78 error: cannot assign to value of type 'i32'");
}
TEST_F(ResolverCompoundAssignmentValidationTest, LhsAtomic) {
// var<workgroup> a : atomic<i32>;
// a += a;
Global(Source{{12, 34}}, "a", ty.atomic(ty.i32()),
ast::StorageClass::kWorkgroup);
WrapInFunction(
CompoundAssign(Source{{56, 78}}, "a", "a", ast::BinaryOp::kAdd));
EXPECT_FALSE(r()->Resolve());
EXPECT_EQ(r()->error(),
"56:78 error: compound assignment operand types are invalid: "
"atomic<i32> add atomic<i32>");
}
} // namespace
} // namespace resolver
} // namespace tint

View File

@ -222,6 +222,10 @@ class DependencyScanner {
[&](const ast::CallStatement* r) { // [&](const ast::CallStatement* r) { //
TraverseExpression(r->expr); TraverseExpression(r->expr);
}, },
[&](const ast::CompoundAssignmentStatement* a) {
TraverseExpression(a->lhs);
TraverseExpression(a->rhs);
},
[&](const ast::ForLoopStatement* l) { [&](const ast::ForLoopStatement* l) {
scope_stack_.Push(); scope_stack_.Push();
TINT_DEFER(scope_stack_.Pop()); TINT_DEFER(scope_stack_.Pop());

View File

@ -860,6 +860,9 @@ sem::Statement* Resolver::Statement(const ast::Statement* stmt) {
[&](const ast::AssignmentStatement* a) { return AssignmentStatement(a); }, [&](const ast::AssignmentStatement* a) { return AssignmentStatement(a); },
[&](const ast::BreakStatement* b) { return BreakStatement(b); }, [&](const ast::BreakStatement* b) { return BreakStatement(b); },
[&](const ast::CallStatement* c) { return CallStatement(c); }, [&](const ast::CallStatement* c) { return CallStatement(c); },
[&](const ast::CompoundAssignmentStatement* c) {
return CompoundAssignmentStatement(c);
},
[&](const ast::ContinueStatement* c) { return ContinueStatement(c); }, [&](const ast::ContinueStatement* c) { return ContinueStatement(c); },
[&](const ast::DiscardStatement* d) { return DiscardStatement(d); }, [&](const ast::DiscardStatement* d) { return DiscardStatement(d); },
[&](const ast::FallthroughStatement* f) { [&](const ast::FallthroughStatement* f) {
@ -2584,7 +2587,7 @@ sem::Statement* Resolver::AssignmentStatement(
behaviors.Add(lhs->Behaviors()); behaviors.Add(lhs->Behaviors());
} }
return ValidateAssignment(stmt); return ValidateAssignment(stmt, TypeOf(stmt->rhs));
}); });
} }
@ -2610,6 +2613,37 @@ sem::Statement* Resolver::CallStatement(const ast::CallStatement* stmt) {
}); });
} }
sem::Statement* Resolver::CompoundAssignmentStatement(
const ast::CompoundAssignmentStatement* stmt) {
auto* sem = builder_->create<sem::Statement>(
stmt, current_compound_statement_, current_function_);
return StatementScope(stmt, sem, [&] {
auto* lhs = Expression(stmt->lhs);
if (!lhs) {
return false;
}
auto* rhs = Expression(stmt->rhs);
if (!rhs) {
return false;
}
sem->Behaviors() = rhs->Behaviors() + lhs->Behaviors();
auto* lhs_ty = lhs->Type()->UnwrapRef();
auto* rhs_ty = rhs->Type()->UnwrapRef();
auto* ty = BinaryOpType(lhs_ty, rhs_ty, stmt->op);
if (!ty) {
AddError("compound assignment operand types are invalid: " +
TypeNameOf(lhs_ty) + " " + FriendlyName(stmt->op) + " " +
TypeNameOf(rhs_ty),
stmt->source);
return false;
}
return ValidateAssignment(stmt, ty);
});
}
sem::Statement* Resolver::ContinueStatement( sem::Statement* Resolver::ContinueStatement(
const ast::ContinueStatement* stmt) { const ast::ContinueStatement* stmt) {
auto* sem = builder_->create<sem::Statement>( auto* sem = builder_->create<sem::Statement>(

View File

@ -214,6 +214,8 @@ class Resolver {
sem::Statement* BreakStatement(const ast::BreakStatement*); sem::Statement* BreakStatement(const ast::BreakStatement*);
sem::Statement* CallStatement(const ast::CallStatement*); sem::Statement* CallStatement(const ast::CallStatement*);
sem::CaseStatement* CaseStatement(const ast::CaseStatement*); sem::CaseStatement* CaseStatement(const ast::CaseStatement*);
sem::Statement* CompoundAssignmentStatement(
const ast::CompoundAssignmentStatement*);
sem::Statement* ContinueStatement(const ast::ContinueStatement*); sem::Statement* ContinueStatement(const ast::ContinueStatement*);
sem::Statement* DiscardStatement(const ast::DiscardStatement*); sem::Statement* DiscardStatement(const ast::DiscardStatement*);
sem::ElseStatement* ElseStatement(const ast::ElseStatement*); sem::ElseStatement* ElseStatement(const ast::ElseStatement*);
@ -245,7 +247,7 @@ class Resolver {
const Source& source); const Source& source);
bool ValidateAtomic(const ast::Atomic* a, const sem::Atomic* s); bool ValidateAtomic(const ast::Atomic* a, const sem::Atomic* s);
bool ValidateAtomicVariable(const sem::Variable* var); bool ValidateAtomicVariable(const sem::Variable* var);
bool ValidateAssignment(const ast::AssignmentStatement* a); bool ValidateAssignment(const ast::Statement* a, const sem::Type* rhs_ty);
bool ValidateBitcast(const ast::BitcastExpression* cast, const sem::Type* to); bool ValidateBitcast(const ast::BitcastExpression* cast, const sem::Type* to);
bool ValidateBreakStatement(const sem::Statement* stmt); bool ValidateBreakStatement(const sem::Statement* stmt);
bool ValidateBuiltinAttribute(const ast::BuiltinAttribute* attr, bool ValidateBuiltinAttribute(const ast::BuiltinAttribute* attr,

View File

@ -2264,10 +2264,22 @@ bool Resolver::ValidateSwitch(const ast::SwitchStatement* s) {
return true; return true;
} }
bool Resolver::ValidateAssignment(const ast::AssignmentStatement* a) { bool Resolver::ValidateAssignment(const ast::Statement* a,
auto const* rhs_ty = TypeOf(a->rhs); const sem::Type* rhs_ty) {
const ast::Expression* lhs;
const ast::Expression* rhs;
if (auto* assign = a->As<ast::AssignmentStatement>()) {
lhs = assign->lhs;
rhs = assign->rhs;
} else if (auto* compound = a->As<ast::CompoundAssignmentStatement>()) {
lhs = compound->lhs;
rhs = compound->rhs;
} else {
TINT_ICE(Resolver, diagnostics_) << "invalid assignment statement";
return false;
}
if (a->lhs->Is<ast::PhonyExpression>()) { if (lhs->Is<ast::PhonyExpression>()) {
// https://www.w3.org/TR/WGSL/#phony-assignment-section // https://www.w3.org/TR/WGSL/#phony-assignment-section
auto* ty = rhs_ty->UnwrapRef(); auto* ty = rhs_ty->UnwrapRef();
if (!ty->IsConstructible() && if (!ty->IsConstructible() &&
@ -2276,26 +2288,26 @@ bool Resolver::ValidateAssignment(const ast::AssignmentStatement* a) {
"cannot assign '" + TypeNameOf(rhs_ty) + "cannot assign '" + TypeNameOf(rhs_ty) +
"' to '_'. '_' can only be assigned a constructible, pointer, " "' to '_'. '_' can only be assigned a constructible, pointer, "
"texture or sampler type", "texture or sampler type",
a->rhs->source); rhs->source);
return false; return false;
} }
return true; // RHS can be anything. return true; // RHS can be anything.
} }
// https://gpuweb.github.io/gpuweb/wgsl/#assignment-statement // https://gpuweb.github.io/gpuweb/wgsl/#assignment-statement
auto const* lhs_ty = TypeOf(a->lhs); auto const* lhs_ty = TypeOf(lhs);
if (auto* var = ResolvedSymbol<sem::Variable>(a->lhs)) { if (auto* var = ResolvedSymbol<sem::Variable>(lhs)) {
auto* decl = var->Declaration(); auto* decl = var->Declaration();
if (var->Is<sem::Parameter>()) { if (var->Is<sem::Parameter>()) {
AddError("cannot assign to function parameter", a->lhs->source); AddError("cannot assign to function parameter", lhs->source);
AddNote("'" + builder_->Symbols().NameFor(decl->symbol) + AddNote("'" + builder_->Symbols().NameFor(decl->symbol) +
"' is declared here:", "' is declared here:",
decl->source); decl->source);
return false; return false;
} }
if (decl->is_const) { if (decl->is_const) {
AddError("cannot assign to const", a->lhs->source); AddError("cannot assign to const", lhs->source);
AddNote("'" + builder_->Symbols().NameFor(decl->symbol) + AddNote("'" + builder_->Symbols().NameFor(decl->symbol) +
"' is declared here:", "' is declared here:",
decl->source); decl->source);
@ -2307,7 +2319,7 @@ bool Resolver::ValidateAssignment(const ast::AssignmentStatement* a) {
if (!lhs_ref) { if (!lhs_ref) {
// LHS is not a reference, so it has no storage. // LHS is not a reference, so it has no storage.
AddError("cannot assign to value of type '" + TypeNameOf(lhs_ty) + "'", AddError("cannot assign to value of type '" + TypeNameOf(lhs_ty) + "'",
a->lhs->source); lhs->source);
return false; return false;
} }

View File

@ -245,6 +245,7 @@ tint_unittests_source_set("tint_unittests_resolver_src") {
"../../src/tint/resolver/builtins_validation_test.cc", "../../src/tint/resolver/builtins_validation_test.cc",
"../../src/tint/resolver/call_test.cc", "../../src/tint/resolver/call_test.cc",
"../../src/tint/resolver/call_validation_test.cc", "../../src/tint/resolver/call_validation_test.cc",
"../../src/tint/resolver/compound_assignment_validation_test.cc",
"../../src/tint/resolver/compound_statement_test.cc", "../../src/tint/resolver/compound_statement_test.cc",
"../../src/tint/resolver/control_block_validation_test.cc", "../../src/tint/resolver/control_block_validation_test.cc",
"../../src/tint/resolver/dependency_graph_test.cc", "../../src/tint/resolver/dependency_graph_test.cc",