tint: Implement pointer alias analysis

Track reads and writes to pointer parameters for each function in the
Resolver, as well as accesses to module-scope variables. At function
call sites, check the root identifiers of each pointer argument to
determine if problematic aliasing occurs.

The MSL backend passes pointers to sub-objects to functions when
handling workgroup storage variables, which triggers the alias
analysis. Add a validation override for this scenario.

Bug: tint:1675

Change-Id: I81a40d1309df65521cc5ad39764d6a09a260f51e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/110167
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: James Price <jrprice@google.com>
This commit is contained in:
James Price 2022-11-17 17:27:27 +00:00 committed by Dawn LUCI CQ
parent d6f9a8a253
commit 4d65fc91bb
10 changed files with 1128 additions and 0 deletions

View File

@ -5,6 +5,8 @@
### Deprecated Features
* The `sig` member of the return type of `frexp()` has been renamed to `fract`. [tint:1757](crbug.com/tint/1757)
* Calling a function with multiple pointer arguments that alias each other is now a warning, and
will become an error in a future release. [tint:1675](crbug.com/tint/1675)
## Changes for M109

View File

@ -1116,6 +1116,7 @@ if (tint_build_unittests) {
sources = [
"resolver/address_space_layout_validation_test.cc",
"resolver/address_space_validation_test.cc",
"resolver/alias_analysis_test.cc",
"resolver/array_accessor_test.cc",
"resolver/assignment_validation_test.cc",
"resolver/atomics_test.cc",

View File

@ -840,6 +840,7 @@ if(TINT_BUILD_TESTS)
program_builder_test.cc
program_test.cc
reflection_test.cc
resolver/alias_analysis_test.cc
resolver/array_accessor_test.cc
resolver/assignment_validation_test.cc
resolver/atomics_test.cc

View File

@ -43,6 +43,8 @@ std::string DisableValidationAttribute::InternalName() const {
return "disable_validation__ignore_stride";
case DisabledValidation::kIgnoreInvalidPointerArgument:
return "disable_validation__ignore_invalid_pointer_argument";
case DisabledValidation::kIgnorePointerAliasing:
return "disable_validation__ignore_pointer_aliasing";
}
return "<invalid>";
}

View File

@ -42,6 +42,9 @@ enum class DisabledValidation {
/// When applied to a pointer function parameter, the validator will not require a function call
/// argument passed for that parameter to have a certain form.
kIgnoreInvalidPointerArgument,
/// When applied to a function declaration, the validator will not complain if multiple
/// pointer arguments alias when that function is called.
kIgnorePointerAliasing,
};
/// An internal attribute used to tell the validator to ignore specific

View File

@ -0,0 +1,897 @@
// Copyright 2022 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 "src/tint/resolver/resolver_test_helper.h"
#include "gmock/gmock.h"
using namespace tint::number_suffixes; // NOLINT
namespace tint::resolver {
namespace {
struct ResolverAliasAnalysisTest : public resolver::TestHelper, public testing::Test {};
// Base test harness for tests that pass two pointers to a function.
//
// fn target(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
// <test statements>
// }
// fn caller() {
// var v1 : i32;
// var v2 : i32;
// target(&v1, aliased ? &v1 : &v2);
// }
struct TwoPointerConfig {
ast::AddressSpace address_space; // The address space for the pointers.
bool aliased; // Whether the pointers alias or not.
};
class TwoPointers : public ResolverTestWithParam<TwoPointerConfig> {
protected:
void SetUp() override {
utils::Vector<const ast::Statement*, 4> body;
if (GetParam().address_space == ast::AddressSpace::kFunction) {
body.Push(Decl(Var("v1", ty.i32())));
body.Push(Decl(Var("v2", ty.i32())));
} else {
GlobalVar("v1", ast::AddressSpace::kPrivate, ty.i32());
GlobalVar("v2", ast::AddressSpace::kPrivate, ty.i32());
}
body.Push(CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, GetParam().aliased ? "v1" : "v2"))));
Func("caller", utils::Empty, ty.void_(), body);
}
void Run(utils::Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
auto addrspace = GetParam().address_space;
Func("target",
utils::Vector{
Param("p1", ty.pointer<i32>(addrspace)),
Param("p2", ty.pointer<i32>(addrspace)),
},
ty.void_(), std::move(body));
if (GetParam().aliased && err) {
EXPECT_TRUE(r()->Resolve());
EXPECT_EQ(r()->error(), err);
} else {
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
}
};
TEST_P(TwoPointers, ReadRead) {
// _ = *p1;
// _ = *p2;
Run({
Assign(Phony(), Deref("p1")),
Assign(Phony(), Deref("p2")),
});
}
TEST_P(TwoPointers, ReadWrite) {
// _ = *p1;
// *p2 = 42;
Run(
{
Assign(Phony(), Deref("p1")),
Assign(Deref("p2"), 42_a),
},
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(TwoPointers, WriteRead) {
// *p1 = 42;
// _ = *p2;
Run(
{
Assign(Deref("p1"), 42_a),
Assign(Phony(), Deref("p2")),
},
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(TwoPointers, WriteWrite) {
// *p1 = 42;
// *p2 = 42;
Run(
{
Assign(Deref("p1"), 42_a),
Assign(Deref("p2"), 42_a),
},
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(TwoPointers, ReadWriteThroughChain) {
// fn f2(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
// _ = *p1;
// *p2 = 42;
// }
// fn f1(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
// f2(p1, p2);
// }
//
// f1(p1, p2);
Func("f2",
utils::Vector{
Param("p1", ty.pointer<i32>(GetParam().address_space)),
Param("p2", ty.pointer<i32>(GetParam().address_space)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
Assign(Deref("p2"), 42_a),
});
Func("f1",
utils::Vector{
Param("p1", ty.pointer<i32>(GetParam().address_space)),
Param("p2", ty.pointer<i32>(GetParam().address_space)),
},
ty.void_(),
utils::Vector{
CallStmt(Call("f2", "p1", "p2")),
});
Run(
{
CallStmt(Call("f1", "p1", "p2")),
},
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(TwoPointers, ReadWriteAcrossDifferentFunctions) {
// fn f1(p1 : ptr<function, i32>) {
// _ = *p1;
// }
// fn f2(p2 : ptr<function, i32>) {
// *p2 = 42;
// }
//
// f1(p1);
// f2(p2);
Func("f1",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(GetParam().address_space)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
});
Func("f2",
utils::Vector<const ast::Parameter*, 4>{
Param("p2", ty.pointer<i32>(GetParam().address_space)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p2"), 42_a),
});
Run(
{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2", "p2")),
},
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
TwoPointers,
::testing::Values(TwoPointerConfig{ast::AddressSpace::kFunction, false},
TwoPointerConfig{ast::AddressSpace::kFunction, true},
TwoPointerConfig{ast::AddressSpace::kPrivate, false},
TwoPointerConfig{ast::AddressSpace::kPrivate, true}),
[](const ::testing::TestParamInfo<TwoPointers::ParamType>& p) {
std::stringstream ss;
ss << (p.param.aliased ? "Aliased" : "Unaliased") << "_"
<< p.param.address_space;
return ss.str();
});
// Base test harness for tests that pass a pointer to a function that references a module-scope var.
//
// var<private> global_1 : i32;
// var<private> global_2 : i32;
// fn target(p1 : ptr<private, i32>) {
// <test statements>
// }
// fn caller() {
// target(aliased ? &global_1 : &global_2);
// }
class OnePointerOneModuleScope : public ResolverTestWithParam<bool> {
protected:
void SetUp() override {
GlobalVar("global_1", ast::AddressSpace::kPrivate, ty.i32());
GlobalVar("global_2", ast::AddressSpace::kPrivate, ty.i32());
Func("caller", utils::Empty, ty.void_(),
utils::Vector{
CallStmt(Call("target",
AddressOf(Source{{12, 34}}, GetParam() ? "global_1" : "global_2"))),
});
}
void Run(utils::Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
Func("target",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(), std::move(body));
if (GetParam() && err) {
EXPECT_TRUE(r()->Resolve());
EXPECT_EQ(r()->error(), err);
} else {
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
}
};
TEST_P(OnePointerOneModuleScope, ReadRead) {
// _ = *p1;
// _ = global_1;
Run({
Assign(Phony(), Deref("p1")),
Assign(Phony(), "global_1"),
});
}
TEST_P(OnePointerOneModuleScope, ReadWrite) {
// _ = *p1;
// global_1 = 42;
Run(
{
Assign(Phony(), Deref("p1")),
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable write in 'target')");
}
TEST_P(OnePointerOneModuleScope, WriteRead) {
// *p1 = 42;
// _ = global_1;
Run(
{
Assign(Deref("p1"), 42_a),
Assign(Phony(), Expr(Source{{56, 78}}, "global_1")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable read in 'target')");
}
TEST_P(OnePointerOneModuleScope, WriteWrite) {
// *p1 = 42;
// global_1 = 42;
Run(
{
Assign(Deref("p1"), 42_a),
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable write in 'target')");
}
TEST_P(OnePointerOneModuleScope, ReadWriteThroughChain_GlobalViaArg) {
// fn f2(p1 : ptr<private, i32>) {
// *p1 = 42;
// }
// fn f1(p1 : ptr<private, i32>) {
// _ = *p1;
// f2(&global_1);
// }
//
// f1(p1);
Func("f2",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p1"), 42_a),
});
Func("f1",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable write in 'f1')");
}
TEST_P(OnePointerOneModuleScope, ReadWriteThroughChain_Both) {
// fn f2(p1 : ptr<private, i32>) {
// _ = *p1;
// global_1 = 42;
// }
// fn f1(p1 : ptr<private, i32>) {
// f2(p1);
// }
//
// f1(p1);
Func("f2",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
});
Func("f1",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
CallStmt(Call("f2", "p1")),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable write in 'f2')");
}
TEST_P(OnePointerOneModuleScope, WriteReadThroughChain_GlobalViaArg) {
// fn f2(p1 : ptr<private, i32>) {
// _ = *p1;
// }
// fn f1(p1 : ptr<private, i32>) {
// *p1 = 42;
// f2(&global_1);
// }
//
// f1(p1);
Func("f2",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
});
Func("f1",
utils::Vector<const ast::Parameter*, 4>{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p1"), 42_a),
CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable read in 'f1')");
}
TEST_P(OnePointerOneModuleScope, WriteReadThroughChain_Both) {
// fn f2(p1 : ptr<private, i32>) {
// *p1 = 42;
// _ = global_1;
// }
// fn f1(p1 : ptr<private, i32>) {
// f2(p1);
// }
//
// f1(p1);
Func("f2",
utils::Vector{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p1"), 42_a),
Assign(Phony(), Expr(Source{{56, 78}}, "global_1")),
});
Func("f1",
utils::Vector{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
CallStmt(Call("f2", "p1")),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable read in 'f2')");
}
TEST_P(OnePointerOneModuleScope, ReadWriteAcrossDifferentFunctions) {
// fn f1(p1 : ptr<private, i32>) {
// _ = *p1;
// }
// fn f2() {
// global_1 = 42;
// }
//
// f1(p1);
// f2();
Func("f1",
utils::Vector{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p1")),
});
Func("f2", utils::Empty, ty.void_(),
utils::Vector{
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
});
Run(
{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2")),
},
R"(12:34 warning: invalid aliased pointer argument
56:78 note: aliases with module-scope variable write in 'f2')");
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
OnePointerOneModuleScope,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<bool>& p) {
return p.param ? "Aliased" : "Unaliased";
});
// Base test harness for tests that use a potentially aliased pointer in a variety of expressions.
//
// fn target(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
// *p1 = 42;
// <test statements>
// }
// fn caller() {
// var v1 : i32;
// var v2 : i32;
// target(&v1, aliased ? &v1 : &v2);
// }
class Use : public ResolverTestWithParam<bool> {
protected:
void SetUp() override {
Func("caller", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v1", ty.i32())),
Decl(Var("v2", ty.i32())),
CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, GetParam() ? "v1" : "v2"))),
});
}
void Run(const ast::Statement* stmt, const char* err = nullptr) {
Func("target",
utils::Vector{
Param("p1", ty.pointer<i32>(ast::AddressSpace::kFunction)),
Param("p2", ty.pointer<i32>(ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p1"), 42_a),
stmt,
});
if (GetParam() && err) {
EXPECT_TRUE(r()->Resolve());
EXPECT_EQ(r()->error(), err);
} else {
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
}
};
TEST_P(Use, NoAccess) {
// Expect no errors even when aliasing occurs.
Run(Assign(Phony(), 42_a));
}
TEST_P(Use, Write_Increment) {
// (*p2)++;
Run(Increment(Deref("p2")), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Write_Decrement) {
// (*p2)--;
Run(Decrement(Deref("p2")), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Write_CompoundAssignment_LHS) {
// *p2 += 42;
Run(CompoundAssign(Deref("p2"), 42_a, ast::BinaryOp::kAdd),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_CompoundAssignment_RHS) {
// var<private> global : i32;
// global += *p2;
GlobalVar("global", ast::AddressSpace::kPrivate, ty.i32());
Run(CompoundAssign("global", Deref("p2"), ast::BinaryOp::kAdd),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_BinaryOp_LHS) {
// _ = (*p2) + 1;
Run(Assign(Phony(), Add(Deref("p2"), 1_a)), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_BinaryOp_RHS) {
// _ = 1 + (*p2);
Run(Assign(Phony(), Add(1_a, Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_UnaryMinus) {
// _ = -(*p2);
Run(Assign(Phony(), Negation(Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_FunctionCallArg) {
// abs(*p2);
Run(CallStmt(Call("abs", Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_Bitcast) {
// _ = bitcast<f32>(*p2);
Run(Assign(Phony(), Bitcast<f32>(Deref("p2"))),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_Convert) {
// _ = f32(*p2);
Run(Assign(Phony(), Construct<f32>(Deref("p2"))),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_IndexAccessor) {
// var<private> data : array<f32, 4>;
// _ = data[*p2];
GlobalVar("data", ast::AddressSpace::kPrivate, ty.array<f32, 4>());
Run(Assign(Phony(), IndexAccessor("data", Deref("p2"))),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_LetInitializer) {
// let x = *p2;
Run(Decl(Let("x", Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_VarInitializer) {
// var x = *p2;
Run(Decl(Var("x", ast::AddressSpace::kFunction, Deref("p2"))),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_ReturnValue) {
// fn foo(p : ptr<function, i32>) -> i32 { return *p; }
// foo(p2);
Func("foo",
utils::Vector{
Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
},
ty.i32(),
utils::Vector{
Return(Deref("p")),
});
Run(Assign(Phony(), Call("foo", "p2")), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_Switch) {
// Switch (*p2) { default {} }
Run(Switch(Deref("p2"), utils::Vector{DefaultCase(Block())}),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, NoAccess_AddressOf_Deref) {
// Should not invoke the load-rule, and therefore expect no errors even when aliasing occurs.
// let newp = &(*p2);
Run(Decl(Let("newp", AddressOf(Deref("p2")))));
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
Use,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<bool>& p) {
return p.param ? "Aliased" : "Unaliased";
});
// Base test harness for tests that use a potentially aliased pointer in a variety of expressions.
// As above, but using the bool type to test expressions that invoke that load-rule for booleans.
//
// fn target(p1 : ptr<function, bool>, p2 : ptr<function, bool>) {
// *p1 = true;
// <test statements>
// }
// fn caller() {
// var v1 : bool;
// var v2 : bool;
// target(&v1, aliased ? &v1 : &v2);
// }
class UseBool : public ResolverTestWithParam<bool> {
protected:
void SetUp() override {
Func("caller", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v1", ty.bool_())),
Decl(Var("v2", ty.bool_())),
CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, GetParam() ? "v1" : "v2"))),
});
}
void Run(const ast::Statement* stmt, const char* err = nullptr) {
Func("target",
utils::Vector{
Param("p1", ty.pointer<bool>(ast::AddressSpace::kFunction)),
Param("p2", ty.pointer<bool>(ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p1"), true),
stmt,
});
if (GetParam() && err) {
EXPECT_TRUE(r()->Resolve());
EXPECT_EQ(r()->error(), err);
} else {
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
}
};
TEST_P(UseBool, Read_IfCond) {
// if (*p2) {}
Run(If(Deref("p2"), Block()), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(UseBool, Read_WhileCond) {
// while (*p2) {}
Run(While(Deref("p2"), Block()), R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(UseBool, Read_ForCond) {
// for (; *p2; ) {}
Run(For(nullptr, Deref("p2"), nullptr, Block()),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(UseBool, Read_BreakIf) {
// loop { continuing { break if (*p2); } }
Run(Loop(Block(), Block(BreakIf(Deref("p2")))),
R"(56:78 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
UseBool,
::testing::Values(false, true),
[](const ::testing::TestParamInfo<bool>& p) {
return p.param ? "Aliased" : "Unaliased";
});
TEST_F(ResolverAliasAnalysisTest, NoAccess_MemberAccessor) {
// Should not invoke the load-rule, and therefore expect no errors even when aliasing occurs.
//
// struct S { a : i32 }
// fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
// let newp = &((*p2).a);
// (*p1).a = 42;
// }
// fn f1() {
// var v : S;
// f2(&v, &v);
// }
Structure("S", utils::Vector{Member("a", ty.i32())});
Func("f2",
utils::Vector{
Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Decl(Let("newp", AddressOf(MemberAccessor(Deref("p2"), "a")))),
Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
});
Func("f1", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.type_name("S"))),
CallStmt(Call("f2", AddressOf("v"), AddressOf("v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverAliasAnalysisTest, Read_MemberAccessor) {
// struct S { a : i32 }
// fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
// _ = (*p2).a;
// *p1 = S();
// }
// fn f1() {
// var v : S;
// f2(&v, &v);
// }
Structure("S", utils::Vector{Member("a", ty.i32())});
Func("f2",
utils::Vector{
Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), MemberAccessor(Deref("p2"), "a")),
Assign(Deref("p1"), Construct(ty.type_name("S"))),
});
Func("f1", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.type_name("S"))),
CallStmt(
Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
EXPECT_EQ(r()->error(), R"(56:76 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_F(ResolverAliasAnalysisTest, Write_MemberAccessor) {
// struct S { a : i32 }
// fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
// _ = *p2;
// (*p1).a = 42;
// }
// fn f1() {
// var v : S;
// f2(&v, &v);
// }
Structure("S", utils::Vector{Member("a", ty.i32())});
Func("f2",
utils::Vector{
Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Phony(), Deref("p2")),
Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
});
Func("f1", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.type_name("S"))),
CallStmt(
Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
EXPECT_EQ(r()->error(), R"(56:76 warning: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_F(ResolverAliasAnalysisTest, SinglePointerReadWrite) {
// Test that we can both read and write from a single pointer parameter.
//
// fn f1(p : ptr<function, i32>) {
// _ = *p;
// *p = 42;
// }
// fn f2() {
// var v : i32;
// f1(&v);
// }
Func("f1",
utils::Vector{
Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Decl(Var("v", ty.i32())),
Assign(Phony(), Deref("p")),
Assign(Deref("p"), 42_a),
});
Func("f2", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.i32())),
CallStmt(Call("f1", AddressOf("v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverAliasAnalysisTest, AliasingInsideFunction) {
// Test that we can use two aliased pointers inside the same function they are created in.
//
// fn f1() {
// var v : i32;
// let p1 = &v;
// let p2 = &v;
// *p1 = 42;
// *p2 = 42;
// }
Func("f1", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.i32())),
Decl(Let("p1", AddressOf("v"))),
Decl(Let("p2", AddressOf("v"))),
Assign(Deref("p1"), 42_a),
Assign(Deref("p2"), 42_a),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
TEST_F(ResolverAliasAnalysisTest, NonOverlappingCalls) {
// Test that we pass the same pointer to multiple non-overlapping function calls.
//
// fn f2(p : ptr<function, i32>) {
// *p = 42;
// }
// fn f3(p : ptr<function, i32>) {
// *p = 42;
// }
// fn f1() {
// var v : i32;
// f2(&v);
// f3(&v);
// }
Func("f2",
utils::Vector{
Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p"), 42_a),
});
Func("f3",
utils::Vector{
Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
},
ty.void_(),
utils::Vector{
Assign(Deref("p"), 42_a),
});
Func("f1", utils::Empty, ty.void_(),
utils::Vector{
Decl(Var("v", ty.i32())),
CallStmt(Call("f2", AddressOf("v"))),
CallStmt(Call("f3", AddressOf("v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
} // namespace
} // namespace tint::resolver

View File

@ -372,6 +372,8 @@ sem::Variable* Resolver::Let(const ast::Let* v, bool is_global) {
return nullptr;
}
RegisterLoadIfNeeded(rhs);
// If the variable has no declared type, infer it from the RHS
if (!ty) {
ty = rhs->Type()->UnwrapRef(); // Implicit load of RHS
@ -578,6 +580,8 @@ sem::Variable* Resolver::Var(const ast::Var* var, bool is_global) {
if (!storage_ty) {
storage_ty = rhs->Type()->UnwrapRef(); // Implicit load of RHS
}
RegisterLoadIfNeeded(rhs);
}
if (!storage_ty) {
@ -1288,6 +1292,8 @@ sem::IfStatement* Resolver::IfStatement(const ast::IfStatement* stmt) {
sem->Behaviors() = cond->Behaviors();
sem->Behaviors().Remove(sem::Behavior::kNext);
RegisterLoadIfNeeded(cond);
Mark(stmt->body);
auto* body = builder_->create<sem::BlockStatement>(stmt->body, current_compound_statement_,
current_function_);
@ -1381,6 +1387,8 @@ sem::ForLoopStatement* Resolver::ForLoopStatement(const ast::ForLoopStatement* s
}
sem->SetCondition(cond);
behaviors.Add(cond->Behaviors());
RegisterLoadIfNeeded(cond);
}
if (auto* continuing = stmt->continuing) {
@ -1425,6 +1433,8 @@ sem::WhileStatement* Resolver::WhileStatement(const ast::WhileStatement* stmt) {
sem->SetCondition(cond);
behaviors.Add(cond->Behaviors());
RegisterLoadIfNeeded(cond);
Mark(stmt->body);
auto* body = builder_->create<sem::LoopBlockStatement>(
@ -1526,6 +1536,145 @@ sem::Expression* Resolver::Expression(const ast::Expression* root) {
return nullptr;
}
void Resolver::RegisterLoadIfNeeded(const sem::Expression* expr) {
if (!expr) {
return;
}
if (!expr->Type()->Is<sem::Reference>()) {
return;
}
if (!current_function_) {
// There is currently no situation where the Load Rule can be invoked outside of a function.
return;
}
auto& info = alias_analysis_infos_[current_function_];
Switch(
expr->RootIdentifier(),
[&](const sem::GlobalVariable* global) {
info.module_scope_reads.insert({global, expr});
},
[&](const sem::Parameter* param) { info.parameter_reads.insert(param); });
}
void Resolver::RegisterStore(const sem::Expression* expr) {
auto& info = alias_analysis_infos_[current_function_];
Switch(
expr->RootIdentifier(),
[&](const sem::GlobalVariable* global) {
info.module_scope_writes.insert({global, expr});
},
[&](const sem::Parameter* param) { info.parameter_writes.insert(param); });
}
bool Resolver::AliasAnalysis(const sem::Call* call) {
auto* target = call->Target()->As<sem::Function>();
if (!target) {
return true;
}
if (validator_.IsValidationDisabled(target->Declaration()->attributes,
ast::DisabledValidation::kIgnorePointerAliasing)) {
return true;
}
// Helper to generate an aliasing error diagnostic.
struct Alias {
const sem::Expression* expr; // the "other expression"
enum { Argument, ModuleScope } type; // the type of the "other" expression
std::string access; // the access performed for the "other" expression
};
auto make_error = [&](const sem::Expression* arg, Alias&& var) {
// TODO(crbug.com/tint/1675): Switch to error and return false after deprecation period.
AddWarning("invalid aliased pointer argument", arg->Declaration()->source);
switch (var.type) {
case Alias::Argument:
AddNote("aliases with another argument passed here",
var.expr->Declaration()->source);
break;
case Alias::ModuleScope: {
auto* func = var.expr->Stmt()->Function();
auto func_name = builder_->Symbols().NameFor(func->Declaration()->symbol);
AddNote(
"aliases with module-scope variable " + var.access + " in '" + func_name + "'",
var.expr->Declaration()->source);
break;
}
}
return true;
};
auto& args = call->Arguments();
auto& target_info = alias_analysis_infos_[target];
auto& caller_info = alias_analysis_infos_[current_function_];
// Track the set of root identifiers that are read and written by arguments passed in this call.
std::unordered_map<const sem::Variable*, const sem::Expression*> arg_reads;
std::unordered_map<const sem::Variable*, const sem::Expression*> arg_writes;
for (size_t i = 0; i < args.Length(); i++) {
auto* arg = args[i];
if (!arg->Type()->Is<sem::Pointer>()) {
continue;
}
auto* root = arg->RootIdentifier();
if (target_info.parameter_writes.count(target->Parameters()[i])) {
// Arguments that are written to can alias with any other argument or module-scope
// variable access.
if (arg_writes.count(root)) {
return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
}
if (arg_reads.count(root)) {
return make_error(arg, {arg_reads.at(root), Alias::Argument, "read"});
}
if (target_info.module_scope_reads.count(root)) {
return make_error(
arg, {target_info.module_scope_reads.at(root), Alias::ModuleScope, "read"});
}
if (target_info.module_scope_writes.count(root)) {
return make_error(
arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
}
arg_writes.insert({root, arg});
// Propagate the write access to the caller.
Switch(
root,
[&](const sem::GlobalVariable* global) {
caller_info.module_scope_writes.insert({global, arg});
},
[&](const sem::Parameter* param) { caller_info.parameter_writes.insert(param); });
} else if (target_info.parameter_reads.count(target->Parameters()[i])) {
// Arguments that are read from can alias with arguments or module-scope variables that
// are written to.
if (arg_writes.count(root)) {
return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
}
if (target_info.module_scope_writes.count(root)) {
return make_error(
arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
}
arg_reads.insert({root, arg});
// Propagate the read access to the caller.
Switch(
root,
[&](const sem::GlobalVariable* global) {
caller_info.module_scope_reads.insert({global, arg});
},
[&](const sem::Parameter* param) { caller_info.parameter_reads.insert(param); });
}
}
// Propagate module-scope variable uses to the caller.
for (auto read : target_info.module_scope_reads) {
caller_info.module_scope_reads.insert({read.first, read.second});
}
for (auto write : target_info.module_scope_writes) {
caller_info.module_scope_writes.insert({write.first, write.second});
}
return true;
}
const sem::Type* Resolver::ConcreteType(const sem::Type* ty,
const sem::Type* target_ty,
const Source& source) {
@ -1668,6 +1817,7 @@ sem::Expression* Resolver::IndexAccessor(const ast::IndexAccessorExpression* exp
// vec2(1, 2)[runtime-index]
obj = Materialize(obj);
}
RegisterLoadIfNeeded(idx);
if (!obj) {
return nullptr;
}
@ -1725,6 +1875,8 @@ sem::Expression* Resolver::Bitcast(const ast::BitcastExpression* expr) {
return nullptr;
}
RegisterLoadIfNeeded(inner);
const sem::Constant* val = nullptr;
if (auto r = const_eval_.Bitcast(ty, inner)) {
val = r.Get();
@ -1764,6 +1916,8 @@ sem::Call* Resolver::Call(const ast::CallExpression* expr) {
args.Push(arg);
args_stage = sem::EarliestStage(args_stage, arg->Stage());
arg_behaviors.Add(arg->Behaviors());
RegisterLoadIfNeeded(arg);
}
arg_behaviors.Remove(sem::Behavior::kNext);
@ -2205,6 +2359,10 @@ sem::Call* Resolver::FunctionCall(const ast::CallExpression* expr,
current_function_->AddTransitivelyReferencedGlobal(var);
}
if (!AliasAnalysis(call)) {
return nullptr;
}
// Note: Validation *must* be performed before calling this method.
CollectTextureSamplerPairs(target, call->Arguments());
}
@ -2520,6 +2678,9 @@ sem::Expression* Resolver::Binary(const ast::BinaryExpression* expr) {
}
}
RegisterLoadIfNeeded(lhs);
RegisterLoadIfNeeded(rhs);
const sem::Constant* value = nullptr;
if (stage == sem::EvaluationStage::kConstant) {
if (op.const_eval_fn) {
@ -2627,6 +2788,7 @@ sem::Expression* Resolver::UnaryOp(const ast::UnaryOpExpression* unary) {
stage = sem::EvaluationStage::kRuntime;
}
}
RegisterLoadIfNeeded(expr);
break;
}
}
@ -3076,6 +3238,8 @@ sem::Statement* Resolver::ReturnStatement(const ast::ReturnStatement* stmt) {
}
behaviors.Add(expr->Behaviors() - sem::Behavior::kNext);
value_ty = expr->Type()->UnwrapRef();
RegisterLoadIfNeeded(expr);
} else {
value_ty = builder_->create<sem::Void>();
}
@ -3099,6 +3263,8 @@ sem::SwitchStatement* Resolver::SwitchStatement(const ast::SwitchStatement* stmt
}
behaviors = cond->Behaviors() - sem::Behavior::kNext;
RegisterLoadIfNeeded(cond);
auto* cond_ty = cond->Type()->UnwrapRef();
// Determine the common type across all selectors and the switch expression
@ -3202,12 +3368,18 @@ sem::Statement* Resolver::AssignmentStatement(const ast::AssignmentStatement* st
}
}
RegisterLoadIfNeeded(rhs);
auto& behaviors = sem->Behaviors();
behaviors = rhs->Behaviors();
if (!is_phony_assignment) {
behaviors.Add(lhs->Behaviors());
}
if (!is_phony_assignment) {
RegisterStore(lhs);
}
return validator_.Assignment(stmt, sem_.TypeOf(stmt->rhs));
});
}
@ -3234,6 +3406,8 @@ sem::Statement* Resolver::BreakIfStatement(const ast::BreakIfStatement* stmt) {
sem->Behaviors() = cond->Behaviors();
sem->Behaviors().Add(sem::Behavior::kBreak);
RegisterLoadIfNeeded(cond);
return validator_.BreakIfStatement(sem, current_statement_);
});
}
@ -3265,6 +3439,9 @@ sem::Statement* Resolver::CompoundAssignmentStatement(
return false;
}
RegisterLoadIfNeeded(rhs);
RegisterStore(lhs);
sem->Behaviors() = rhs->Behaviors() + lhs->Behaviors();
auto* lhs_ty = lhs->Type()->UnwrapRef();
@ -3317,6 +3494,9 @@ sem::Statement* Resolver::IncrementDecrementStatement(
}
sem->Behaviors() = lhs->Behaviors();
RegisterLoadIfNeeded(lhs);
RegisterStore(lhs);
return validator_.IncrementDecrementStatement(stmt);
});
}

View File

@ -18,6 +18,8 @@
#include <memory>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@ -153,6 +155,18 @@ class Resolver {
sem::Expression* MemberAccessor(const ast::MemberAccessorExpression*);
sem::Expression* UnaryOp(const ast::UnaryOpExpression*);
/// Register a memory load from an expression, to track accesses to root identifiers in order to
/// perform alias analysis.
void RegisterLoadIfNeeded(const sem::Expression* expr);
/// Register a memory store to an expression, to track accesses to root identifiers in order to
/// perform alias analysis.
void RegisterStore(const sem::Expression* expr);
/// Perform pointer alias analysis for `call`.
/// @returns true is the call arguments are free from aliasing issues, false otherwise.
bool AliasAnalysis(const sem::Call* call);
/// If `expr` is not of an abstract-numeric type, then Materialize() will just return `expr`.
/// If `expr` is of an abstract-numeric type:
/// * Materialize will create and return a sem::Materialize node wrapping `expr`.
@ -423,6 +437,19 @@ class Resolver {
const char* constraint = nullptr;
};
/// AliasAnalysisInfo captures the memory accesses performed by a given function for the purpose
/// of determining if any two arguments alias at any callsite.
struct AliasAnalysisInfo {
/// The set of module-scope variables that are written to, and where that write occurs.
std::unordered_map<const sem::Variable*, const sem::Expression*> module_scope_writes;
/// The set of module-scope variables that are read from, and where that read occurs.
std::unordered_map<const sem::Variable*, const sem::Expression*> module_scope_reads;
/// The set of function parameters that are written to.
std::unordered_set<const sem::Variable*> parameter_writes;
/// The set of function parameters that are read from.
std::unordered_set<const sem::Variable*> parameter_reads;
};
ProgramBuilder* const builder_;
diag::List& diagnostics_;
ConstEval const_eval_;
@ -435,6 +462,7 @@ class Resolver {
utils::Hashmap<const sem::Type*, const Source*, 8> atomic_composite_info_;
utils::Bitset<0> marked_;
ExprEvalStageConstraint expr_eval_stage_constraint_;
std::unordered_map<const sem::Function*, AliasAnalysisInfo> alias_analysis_infos_;
utils::Hashmap<OverrideId, const sem::Variable*, 8> override_ids_;
utils::Hashmap<ArrayInitializerSig, sem::CallTarget*, 8> array_inits_;
utils::Hashmap<StructInitializerSig, sem::CallTarget*, 8> struct_inits_;

View File

@ -354,6 +354,7 @@ struct ModuleScopeVarToEntryPointParam::State {
for (auto* func_ast : functions_to_process) {
auto* func_sem = ctx.src->Sem().Get(func_ast);
bool is_entry_point = func_ast->IsEntryPoint();
bool needs_pointer_aliasing = false;
// Map module-scope variables onto their replacement.
struct NewVar {
@ -424,6 +425,9 @@ struct ModuleScopeVarToEntryPointParam::State {
is_wrapped);
} else {
ProcessVariableInUserFunction(func_ast, var, new_var_symbol, is_pointer);
if (var->AddressSpace() == ast::AddressSpace::kWorkgroup) {
needs_pointer_aliasing = true;
}
}
// Record the replacement symbol.
@ -434,6 +438,12 @@ struct ModuleScopeVarToEntryPointParam::State {
ReplaceUsesInFunction(func_ast, var, new_var_symbol, is_pointer, is_wrapped);
}
// Allow pointer aliasing if needed.
if (needs_pointer_aliasing) {
ctx.InsertBack(func_ast->attributes,
ctx.dst->Disable(ast::DisabledValidation::kIgnorePointerAliasing));
}
if (!workgroup_parameter_members.IsEmpty()) {
// Create the workgroup memory parameter.
// The parameter is a struct that contains members for each workgroup variable.

View File

@ -125,12 +125,14 @@ fn zoo(@internal(disable_validation__ignore_address_space) @internal(disable_val
*(tint_symbol) = (*(tint_symbol) * 2.0);
}
@internal(disable_validation__ignore_pointer_aliasing)
fn bar(a : f32, b : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_1 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr<workgroup, f32>) {
*(tint_symbol_1) = a;
*(tint_symbol_2) = b;
zoo(tint_symbol_1);
}
@internal(disable_validation__ignore_pointer_aliasing)
fn foo(a : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_3 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr<workgroup, f32>) {
let b : f32 = 2.0;
bar(a, b, tint_symbol_3, tint_symbol_4);
@ -188,6 +190,7 @@ fn main() {
foo(1.0, &(tint_symbol_5), &(tint_symbol_6));
}
@internal(disable_validation__ignore_pointer_aliasing)
fn foo(a : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_3 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr<workgroup, f32>) {
let b : f32 = 2.0;
bar(a, b, tint_symbol_3, tint_symbol_4);
@ -197,6 +200,7 @@ fn foo(a : f32, @internal(disable_validation__ignore_address_space) @internal(di
fn no_uses() {
}
@internal(disable_validation__ignore_pointer_aliasing)
fn bar(a : f32, b : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_1 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr<workgroup, f32>) {
*(tint_symbol_1) = a;
*(tint_symbol_2) = b;