blob: e7bd78fb1368921b4a5994360602d30d64ecd2bf [file] [log] [blame]
// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "src/tint/lang/wgsl/resolver/resolver.h"
#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
#include "src/tint/utils/text/string_stream.h"
#include "gmock/gmock.h"
namespace tint::resolver {
namespace {
using namespace tint::core::fluent_types; // NOLINT
using namespace tint::core::number_suffixes; // NOLINT
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 {
const core::AddressSpace address_space; // The address space for the pointers.
const bool aliased; // Whether the pointers alias or not.
};
class TwoPointers : public ResolverTestWithParam<TwoPointerConfig> {
protected:
void SetUp() override {
Vector<const ast::Statement*, 4> body;
if (GetParam().address_space == core::AddressSpace::kFunction) {
body.Push(Decl(Var("v1", ty.i32())));
body.Push(Decl(Var("v2", ty.i32())));
} else {
GlobalVar("v1", core::AddressSpace::kPrivate, ty.i32());
GlobalVar("v2", core::AddressSpace::kPrivate, ty.i32());
}
body.Push(CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, GetParam().aliased ? "v1" : "v2"))));
Func("caller", tint::Empty, ty.void_(), body);
}
void Run(Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
auto addrspace = GetParam().address_space;
Func("target",
Vector{
Param("p1", ty.ptr<i32>(addrspace)),
Param("p2", ty.ptr<i32>(addrspace)),
},
ty.void_(), std::move(body));
if (GetParam().aliased && err) {
EXPECT_FALSE(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 error: 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 error: 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 error: 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",
Vector{
Param("p1", ty.ptr<i32>(GetParam().address_space)),
Param("p2", ty.ptr<i32>(GetParam().address_space)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
Assign(Deref("p2"), 42_a),
});
Func("f1",
Vector{
Param("p1", ty.ptr<i32>(GetParam().address_space)),
Param("p2", ty.ptr<i32>(GetParam().address_space)),
},
ty.void_(),
Vector{
CallStmt(Call("f2", "p1", "p2")),
});
Run(
{
CallStmt(Call("f1", "p1", "p2")),
},
R"(56:78 error: 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",
Vector{
Param("p1", ty.ptr<i32>(GetParam().address_space)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
});
Func("f2",
Vector{
Param("p2", ty.ptr<i32>(GetParam().address_space)),
},
ty.void_(),
Vector{
Assign(Deref("p2"), 42_a),
});
Run(
{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2", "p2")),
},
R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
TwoPointers,
::testing::Values(TwoPointerConfig{core::AddressSpace::kFunction, false},
TwoPointerConfig{core::AddressSpace::kFunction, true},
TwoPointerConfig{core::AddressSpace::kPrivate, false},
TwoPointerConfig{core::AddressSpace::kPrivate, true}),
[](const ::testing::TestParamInfo<TwoPointers::ParamType>& p) {
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", core::AddressSpace::kPrivate, ty.i32());
GlobalVar("global_2", core::AddressSpace::kPrivate, ty.i32());
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("target",
AddressOf(Source{{12, 34}}, GetParam() ? "global_1" : "global_2"))),
});
}
void Run(Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
Func("target",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(), std::move(body));
if (GetParam() && err) {
EXPECT_FALSE(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 error: 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 error: 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 error: 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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Deref("p1"), 42_a),
});
Func("f1",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 error: 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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
});
Func("f1",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
CallStmt(Call("f2", "p1")),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 error: 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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
});
Func("f1",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Deref("p1"), 42_a),
CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 error: 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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Deref("p1"), 42_a),
Assign(Phony(), Expr(Source{{56, 78}}, "global_1")),
});
Func("f1",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
CallStmt(Call("f2", "p1")),
});
Run(
{
CallStmt(Call("f1", "p1")),
},
R"(12:34 error: 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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p1")),
});
Func("f2", tint::Empty, ty.void_(),
Vector{
Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
});
Run(
{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2")),
},
R"(12:34 error: 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", tint::Empty, ty.void_(),
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",
Vector{
Param("p1", ty.ptr<i32>(core::AddressSpace::kFunction)),
Param("p2", ty.ptr<i32>(core::AddressSpace::kFunction)),
},
ty.void_(),
Vector{
Assign(Deref("p1"), 42_a),
stmt,
});
if (GetParam() && err) {
EXPECT_FALSE(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 error: 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 error: 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, core::BinaryOp::kAdd),
R"(56:78 error: 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", core::AddressSpace::kPrivate, ty.i32());
Run(CompoundAssign("global", Deref("p2"), core::BinaryOp::kAdd),
R"(56:78 error: 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 error: 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 error: 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 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_FunctionCallArg) {
// abs(*p2);
Run(Assign(Phony(), Call("abs", Deref("p2"))), R"(56:78 error: 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 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(Use, Read_Convert) {
// _ = f32(*p2);
Run(Assign(Phony(), Call<f32>(Deref("p2"))),
R"(56:78 error: 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", core::AddressSpace::kPrivate, ty.array<f32, 4>());
Run(Assign(Phony(), IndexAccessor("data", Deref("p2"))),
R"(56:78 error: 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 error: 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", core::AddressSpace::kFunction, Deref("p2"))),
R"(56:78 error: 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",
Vector{
Param("p", ty.ptr<i32>(core::AddressSpace::kFunction)),
},
ty.i32(),
Vector{
Return(Deref("p")),
});
Run(Assign(Phony(), Call("foo", "p2")), R"(56:78 error: 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"), Vector{DefaultCase(Block())}),
R"(56:78 error: 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", tint::Empty, ty.void_(),
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",
Vector{
Param("p1", ty.ptr<bool>(core::AddressSpace::kFunction)),
Param("p2", ty.ptr<bool>(core::AddressSpace::kFunction)),
},
ty.void_(),
Vector{
Assign(Deref("p1"), true),
stmt,
});
if (GetParam() && err) {
EXPECT_FALSE(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 error: 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 error: 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 error: 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 error: 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", Vector{Member("a", ty.i32())});
Func("f2",
Vector{
Param("p1", ty.ptr<function>(ty("S"))),
Param("p2", ty.ptr<function>(ty("S"))),
},
ty.void_(),
Vector{
Decl(Let("newp", AddressOf(MemberAccessor(Deref("p2"), "a")))),
Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
});
Func("f1", tint::Empty, ty.void_(),
Vector{
Decl(Var("v", ty("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", Vector{Member("a", ty.i32())});
Func("f2",
Vector{
Param("p1", ty.ptr<function>(ty("S"))),
Param("p2", ty.ptr<function>(ty("S"))),
},
ty.void_(),
Vector{
Assign(Phony(), MemberAccessor(Deref("p2"), "a")),
Assign(Deref("p1"), Call("S")),
});
Func("f1", tint::Empty, ty.void_(),
Vector{
Decl(Var("v", ty("S"))),
CallStmt(
Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
});
EXPECT_FALSE(r()->Resolve()) << r()->error();
EXPECT_EQ(r()->error(), R"(56:76 error: 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", Vector{Member("a", ty.i32())});
Func("f2",
Vector{
Param("p1", ty.ptr<function>(ty("S"))),
Param("p2", ty.ptr<function>(ty("S"))),
},
ty.void_(),
Vector{
Assign(Phony(), Deref("p2")),
Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
});
Func("f1", tint::Empty, ty.void_(),
Vector{
Decl(Var("v", ty("S"))),
CallStmt(
Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
});
EXPECT_FALSE(r()->Resolve()) << r()->error();
EXPECT_EQ(r()->error(), R"(56:76 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_F(ResolverAliasAnalysisTest, Read_MultiComponentSwizzle) {
// fn f2(p1 : ptr<function, vec4<f32>, p2 : ptr<function, vec4<f32>) {
// _ = (*p2).zy;
// *p1 = vec4<f32>();
// }
// fn f1() {
// var v : vec4<f32>;
// f2(&v, &v);
// }
Structure("S", Vector{Member("a", ty.i32())});
Func("f2",
Vector{
Param("p1", ty.ptr<function, vec4<f32>>()),
Param("p2", ty.ptr<function, vec4<f32>>()),
},
ty.void_(),
Vector{
Assign(Phony(), MemberAccessor(Deref("p2"), "zy")),
Assign(Deref("p1"), Call<vec4<f32>>()),
});
Func("f1", tint::Empty, ty.void_(),
Vector{
Decl(Var("v", ty.vec4<f32>())),
CallStmt(
Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
});
EXPECT_FALSE(r()->Resolve()) << r()->error();
EXPECT_EQ(r()->error(), R"(56:76 error: 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",
Vector{
Param("p", ty.ptr<i32>(core::AddressSpace::kFunction)),
},
ty.void_(),
Vector{
Decl(Var("v", ty.i32())),
Assign(Phony(), Deref("p")),
Assign(Deref("p"), 42_a),
});
Func("f2", tint::Empty, ty.void_(),
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", tint::Empty, ty.void_(),
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",
Vector{
Param("p", ty.ptr<i32>(core::AddressSpace::kFunction)),
},
ty.void_(),
Vector{
Assign(Deref("p"), 42_a),
});
Func("f3",
Vector{
Param("p", ty.ptr<i32>(core::AddressSpace::kFunction)),
},
ty.void_(),
Vector{
Assign(Deref("p"), 42_a),
});
Func("f1", tint::Empty, ty.void_(),
Vector{
Decl(Var("v", ty.i32())),
CallStmt(Call("f2", AddressOf("v"))),
CallStmt(Call("f3", AddressOf("v"))),
});
EXPECT_TRUE(r()->Resolve()) << r()->error();
}
////////////////////////////////////////////////////////////////////////////////
// Atomics
////////////////////////////////////////////////////////////////////////////////
class AtomicPointers
: public ResolverTestWithParam<
std::tuple<wgsl::BuiltinFn, wgsl::BuiltinFn, core::AddressSpace, bool /* aliased */>> {
protected:
static constexpr std::string_view kPass = "<PASS>";
ast::Type Ptr() {
auto address_space = std::get<2>(GetParam());
if (address_space == storage) {
return ty.ptr<storage, atomic<i32>, read_write>();
} else {
return ty.ptr<atomic<i32>>(address_space);
}
}
void SetUp() override {
auto address_space = std::get<2>(GetParam());
if (address_space == storage) {
GlobalVar("v1", address_space, read_write, ty.Of<atomic<i32>>(), //
Binding(0_a), Group(0_a));
GlobalVar("v2", address_space, read_write, ty.Of<atomic<i32>>(), //
Binding(1_a), Group(0_a));
} else {
GlobalVar("v1", address_space, ty.Of<atomic<i32>>());
GlobalVar("v2", address_space, ty.Of<atomic<i32>>());
}
}
bool IsWrite(wgsl::BuiltinFn fn) const {
switch (fn) {
case wgsl::BuiltinFn::kAtomicStore:
case wgsl::BuiltinFn::kAtomicAdd:
case wgsl::BuiltinFn::kAtomicSub:
case wgsl::BuiltinFn::kAtomicMax:
case wgsl::BuiltinFn::kAtomicMin:
case wgsl::BuiltinFn::kAtomicAnd:
case wgsl::BuiltinFn::kAtomicOr:
case wgsl::BuiltinFn::kAtomicXor:
case wgsl::BuiltinFn::kAtomicExchange:
case wgsl::BuiltinFn::kAtomicCompareExchangeWeak:
return true;
default:
return false;
}
}
bool ShouldPass() const {
auto [builtin_a, builtin_b, space, aliased] = GetParam();
bool fail = aliased && (IsWrite(builtin_a) || IsWrite(builtin_b));
return !fail;
}
const ast::Statement* CallBuiltin(wgsl::BuiltinFn fn, std::string_view ptr) {
switch (fn) {
case wgsl::BuiltinFn::kAtomicLoad:
return CallStmt(Call(fn, ptr));
case wgsl::BuiltinFn::kAtomicStore:
case wgsl::BuiltinFn::kAtomicAdd:
case wgsl::BuiltinFn::kAtomicSub:
case wgsl::BuiltinFn::kAtomicMax:
case wgsl::BuiltinFn::kAtomicMin:
case wgsl::BuiltinFn::kAtomicAnd:
case wgsl::BuiltinFn::kAtomicOr:
case wgsl::BuiltinFn::kAtomicXor:
case wgsl::BuiltinFn::kAtomicExchange:
return CallStmt(Call(fn, ptr, 42_a));
case wgsl::BuiltinFn::kAtomicCompareExchangeWeak:
return CallStmt(Call(fn, ptr, 10_a, 42_a));
default:
TINT_UNIMPLEMENTED() << fn;
return nullptr;
}
}
std::string Run() {
if (r()->Resolve()) {
return std::string(kPass);
}
return r()->error();
}
};
TEST_P(AtomicPointers, CallDirect) {
// var<ADDRESS_SPACE> v1 : atomic<i32>;
// var<ADDRESS_SPACE> v2 : atomic<i32>;
//
// fn caller() {
// callee(&v1, aliased ? &v1 : &v2);
// }
//
// fn callee(p1 : PTR, p2 : PTR) {
// <builtin-a>(p1);
// <builtin-b>(p2);
// }
auto [builtin_a, builtin_b, space, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("callee", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("callee", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
Vector{
CallBuiltin(builtin_a, "p1"),
CallBuiltin(builtin_b, "p2"),
});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(AtomicPointers, CallThroughChain) {
// var<ADDRESS_SPACE> v1 : atomic<i32>;
// var<ADDRESS_SPACE> v2 : atomic<i32>;
//
// fn caller() {
// callee(&v1, aliased ? &v1 : &v2);
// }
//
// fn f2(p1 : PTR, p2 : PTR) {
// f1(p1, p2);
// }
//
// fn f1(p1 : PTR, p2 : PTR) {
// callee(p1, p2);
// }
//
// fn callee(p1 : PTR, p2 : PTR) {
// <builtin-a>(p1);
// <builtin-b>(p2);
// }
auto [builtin_a, builtin_b, space, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("callee", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("f2", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
Vector{
CallStmt(Call("f1", "p1", "p2")),
});
Func("f1", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
Vector{
CallStmt(Call("callee", "p1", "p2")),
});
Func("callee", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
Vector{
CallBuiltin(builtin_a, "p1"),
CallBuiltin(builtin_b, "p2"),
});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(AtomicPointers, ReadWriteAcrossDifferentFunctions) {
// var<ADDRESS_SPACE> v1 : atomic<i32>;
// var<ADDRESS_SPACE> v2 : atomic<i32>;
//
// fn caller() {
// f(&v1, aliased ? &v1 : &v2);
// }
//
// fn f(p1 : PTR, p2 : PTR) {
// f1(p1);
// f2(p2);
// }
//
// fn f1(p : PTR) {
// <builtin-a>(p);
// }
//
// fn f2(p : PTR) {
// <builtin-b>(p);
// }
auto [builtin_a, builtin_b, space, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("f", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("f", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
Vector{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2", "p2")),
});
Func("f1", Vector{Param("p", Ptr())}, ty.void_(),
Vector{
CallBuiltin(builtin_a, "p"),
});
Func("f2", Vector{Param("p", Ptr())}, ty.void_(),
Vector{
CallBuiltin(builtin_b, "p"),
});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
std::array kAtomicFns{
wgsl::BuiltinFn::kAtomicLoad,
wgsl::BuiltinFn::kAtomicStore,
wgsl::BuiltinFn::kAtomicAdd,
wgsl::BuiltinFn::kAtomicSub,
wgsl::BuiltinFn::kAtomicMax,
wgsl::BuiltinFn::kAtomicMin,
wgsl::BuiltinFn::kAtomicAnd,
wgsl::BuiltinFn::kAtomicOr,
wgsl::BuiltinFn::kAtomicXor,
wgsl::BuiltinFn::kAtomicExchange,
wgsl::BuiltinFn::kAtomicCompareExchangeWeak,
};
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
AtomicPointers,
::testing::Combine(::testing::ValuesIn(kAtomicFns),
::testing::ValuesIn(kAtomicFns),
::testing::Values(core::AddressSpace::kWorkgroup,
core::AddressSpace::kStorage),
::testing::Values(true, false)));
////////////////////////////////////////////////////////////////////////////////
// WorkgroupUniformLoad
////////////////////////////////////////////////////////////////////////////////
enum class WorkgroupUniformLoadAction {
kRead,
kWrite,
kWorkgroupUniformLoad,
};
std::array kWorkgroupUniformLoadActions{
WorkgroupUniformLoadAction::kRead,
WorkgroupUniformLoadAction::kWrite,
WorkgroupUniformLoadAction::kWorkgroupUniformLoad,
};
class WorkgroupUniformLoad
: public ResolverTestWithParam<
std::tuple<WorkgroupUniformLoadAction, WorkgroupUniformLoadAction, bool>> {
protected:
static constexpr std::string_view kPass = "<PASS>";
void SetUp() override {
GlobalVar("v1", workgroup, ty.i32());
GlobalVar("v2", workgroup, ty.i32());
}
const ast::Statement* Do(WorkgroupUniformLoadAction action, std::string_view ptr) {
switch (action) {
case WorkgroupUniformLoadAction::kRead:
return Assign(Phony(), Deref(ptr));
case WorkgroupUniformLoadAction::kWrite:
return Assign(Deref(ptr), 42_a);
case WorkgroupUniformLoadAction::kWorkgroupUniformLoad:
return Assign(Phony(), Call(wgsl::BuiltinFn::kWorkgroupUniformLoad, ptr));
}
return nullptr;
}
bool IsWrite(WorkgroupUniformLoadAction action) const {
return action == WorkgroupUniformLoadAction::kWrite;
}
bool ShouldPass() const {
auto [action_a, action_b, aliased] = GetParam();
bool fail = aliased && (IsWrite(action_a) || IsWrite(action_b));
return !fail;
}
std::string Run() {
if (r()->Resolve()) {
return std::string(kPass);
}
return r()->error();
}
};
TEST_P(WorkgroupUniformLoad, CallDirect) {
// var<workgroup> v1 : i32;
// var<workgroup> v2 : i32;
//
// fn caller() {
// callee(&v1, aliased ? &v1 : &v2);
// }
//
// fn callee(p1 : PTR, p2 : PTR) {
// <action-a>(p1);
// <action-b>(p2);
// }
auto [action_a, action_b, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("callee", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("callee",
Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
ty.void_(),
Vector{
Do(action_a, "p1"),
Do(action_b, "p2"),
});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(WorkgroupUniformLoad, CallThroughChain) {
// var<workgroup> v1 : i32;
// var<workgroup> v2 : i32;
//
// fn caller() {
// callee(&v1, aliased ? &v1 : &v2);
// }
//
// fn f2(p1 : PTR, p2 : PTR) {
// f1(p1, p2);
// }
//
// fn f1(p1 : PTR, p2 : PTR) {
// callee(p1, p2);
// }
//
// fn callee(p1 : PTR, p2 : PTR) {
// <action-a>(p1);
// <action-b>(p2);
// }
auto [action_a, action_b, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("callee", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("f2", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
ty.void_(),
Vector{
CallStmt(Call("f1", "p1", "p2")),
});
Func("f1", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
ty.void_(),
Vector{
CallStmt(Call("callee", "p1", "p2")),
});
Func("callee",
Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
ty.void_(),
Vector{
Do(action_a, "p1"),
Do(action_b, "p2"),
});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
TEST_P(WorkgroupUniformLoad, ReadWriteAcrossDifferentFunctions) {
// var<workgroup> v1 : i32;
// var<workgroup> v2 : i32;
//
// fn caller() {
// f(&v1, aliased ? &v1 : &v2);
// }
//
// fn f(p1 : PTR, p2 : PTR) {
// f1(p1);
// f2(p2);
// }
//
// fn f1(p : PTR) {
// <action-a>(p);
// }
//
// fn f2(p : PTR) {
// <action-b>(p);
// }
auto [action_a, action_b, aliased] = GetParam();
Func("caller", tint::Empty, ty.void_(),
Vector{
CallStmt(Call("f", //
AddressOf(Source{{12, 34}}, "v1"),
AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
});
Func("f", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
ty.void_(),
Vector{
CallStmt(Call("f1", "p1")),
CallStmt(Call("f2", "p2")),
});
Func("f1", Vector{Param("p", ty.ptr<workgroup, i32>())}, ty.void_(), Vector{Do(action_a, "p")});
Func("f2", Vector{Param("p", ty.ptr<workgroup, i32>())}, ty.void_(), Vector{Do(action_b, "p")});
EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
12:34 note: aliases with another argument passed here)");
}
INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
WorkgroupUniformLoad,
::testing::Combine(::testing::ValuesIn(kWorkgroupUniformLoadActions),
::testing::ValuesIn(kWorkgroupUniformLoadActions),
::testing::Values(true, false)));
} // namespace
} // namespace tint::resolver