From 4d65fc91bb284269dd723fadb8ecead1eb28696c Mon Sep 17 00:00:00 2001 From: James Price Date: Thu, 17 Nov 2022 17:27:27 +0000 Subject: [PATCH] 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 Reviewed-by: Ben Clayton Commit-Queue: James Price --- docs/tint/origin-trial-changes.md | 2 + src/tint/BUILD.gn | 1 + src/tint/CMakeLists.txt | 1 + src/tint/ast/disable_validation_attribute.cc | 2 + src/tint/ast/disable_validation_attribute.h | 3 + src/tint/resolver/alias_analysis_test.cc | 897 ++++++++++++++++++ src/tint/resolver/resolver.cc | 180 ++++ src/tint/resolver/resolver.h | 28 + .../module_scope_var_to_entry_point_param.cc | 10 + ...ule_scope_var_to_entry_point_param_test.cc | 4 + 10 files changed, 1128 insertions(+) create mode 100644 src/tint/resolver/alias_analysis_test.cc diff --git a/docs/tint/origin-trial-changes.md b/docs/tint/origin-trial-changes.md index 8aaaeb32c9..a55e39f961 100644 --- a/docs/tint/origin-trial-changes.md +++ b/docs/tint/origin-trial-changes.md @@ -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 diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn index 36fd66b062..2ced868b84 100644 --- a/src/tint/BUILD.gn +++ b/src/tint/BUILD.gn @@ -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", diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt index 978986ee3b..f26c138d4d 100644 --- a/src/tint/CMakeLists.txt +++ b/src/tint/CMakeLists.txt @@ -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 diff --git a/src/tint/ast/disable_validation_attribute.cc b/src/tint/ast/disable_validation_attribute.cc index 2ae8c16571..465a0ec7fe 100644 --- a/src/tint/ast/disable_validation_attribute.cc +++ b/src/tint/ast/disable_validation_attribute.cc @@ -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 ""; } diff --git a/src/tint/ast/disable_validation_attribute.h b/src/tint/ast/disable_validation_attribute.h index d4c7455461..f52b10717d 100644 --- a/src/tint/ast/disable_validation_attribute.h +++ b/src/tint/ast/disable_validation_attribute.h @@ -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 diff --git a/src/tint/resolver/alias_analysis_test.cc b/src/tint/resolver/alias_analysis_test.cc new file mode 100644 index 0000000000..38710b6678 --- /dev/null +++ b/src/tint/resolver/alias_analysis_test.cc @@ -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, p2 : ptr) { +// +// } +// 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 { + protected: + void SetUp() override { + utils::Vector 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&& body, const char* err = nullptr) { + auto addrspace = GetParam().address_space; + Func("target", + utils::Vector{ + Param("p1", ty.pointer(addrspace)), + Param("p2", ty.pointer(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, p2 : ptr) { + // _ = *p1; + // *p2 = 42; + // } + // fn f1(p1 : ptr, p2 : ptr) { + // f2(p1, p2); + // } + // + // f1(p1, p2); + Func("f2", + utils::Vector{ + Param("p1", ty.pointer(GetParam().address_space)), + Param("p2", ty.pointer(GetParam().address_space)), + }, + ty.void_(), + utils::Vector{ + Assign(Phony(), Deref("p1")), + Assign(Deref("p2"), 42_a), + }); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(GetParam().address_space)), + Param("p2", ty.pointer(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) { + // _ = *p1; + // } + // fn f2(p2 : ptr) { + // *p2 = 42; + // } + // + // f1(p1); + // f2(p2); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(GetParam().address_space)), + }, + ty.void_(), + utils::Vector{ + Assign(Phony(), Deref("p1")), + }); + Func("f2", + utils::Vector{ + Param("p2", ty.pointer(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& 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 global_1 : i32; +// var global_2 : i32; +// fn target(p1 : ptr) { +// +// } +// fn caller() { +// target(aliased ? &global_1 : &global_2); +// } +class OnePointerOneModuleScope : public ResolverTestWithParam { + 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&& body, const char* err = nullptr) { + Func("target", + utils::Vector{ + Param("p1", ty.pointer(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) { + // *p1 = 42; + // } + // fn f1(p1 : ptr) { + // _ = *p1; + // f2(&global_1); + // } + // + // f1(p1); + Func("f2", + utils::Vector{ + Param("p1", ty.pointer(ast::AddressSpace::kPrivate)), + }, + ty.void_(), + utils::Vector{ + Assign(Deref("p1"), 42_a), + }); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(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) { + // _ = *p1; + // global_1 = 42; + // } + // fn f1(p1 : ptr) { + // f2(p1); + // } + // + // f1(p1); + Func("f2", + utils::Vector{ + Param("p1", ty.pointer(ast::AddressSpace::kPrivate)), + }, + ty.void_(), + utils::Vector{ + Assign(Phony(), Deref("p1")), + Assign(Expr(Source{{56, 78}}, "global_1"), 42_a), + }); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(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) { + // _ = *p1; + // } + // fn f1(p1 : ptr) { + // *p1 = 42; + // f2(&global_1); + // } + // + // f1(p1); + Func("f2", + utils::Vector{ + Param("p1", ty.pointer(ast::AddressSpace::kPrivate)), + }, + ty.void_(), + utils::Vector{ + Assign(Phony(), Deref("p1")), + }); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(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) { + // *p1 = 42; + // _ = global_1; + // } + // fn f1(p1 : ptr) { + // f2(p1); + // } + // + // f1(p1); + Func("f2", + utils::Vector{ + Param("p1", ty.pointer(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(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) { + // _ = *p1; + // } + // fn f2() { + // global_1 = 42; + // } + // + // f1(p1); + // f2(); + Func("f1", + utils::Vector{ + Param("p1", ty.pointer(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& 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, p2 : ptr) { +// *p1 = 42; +// +// } +// fn caller() { +// var v1 : i32; +// var v2 : i32; +// target(&v1, aliased ? &v1 : &v2); +// } +class Use : public ResolverTestWithParam { + 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(ast::AddressSpace::kFunction)), + Param("p2", ty.pointer(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 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(*p2); + Run(Assign(Phony(), Bitcast(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(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 data : array; + // _ = data[*p2]; + GlobalVar("data", ast::AddressSpace::kPrivate, ty.array()); + 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) -> i32 { return *p; } + // foo(p2); + Func("foo", + utils::Vector{ + Param("p", ty.pointer(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& 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, p2 : ptr) { +// *p1 = true; +// +// } +// fn caller() { +// var v1 : bool; +// var v2 : bool; +// target(&v1, aliased ? &v1 : &v2); +// } +class UseBool : public ResolverTestWithParam { + 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(ast::AddressSpace::kFunction)), + Param("p2", ty.pointer(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& 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, p2 : ptr) { + // 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, p2 : ptr) { + // _ = (*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, p2 : ptr) { + // _ = *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) { + // _ = *p; + // *p = 42; + // } + // fn f2() { + // var v : i32; + // f1(&v); + // } + Func("f1", + utils::Vector{ + Param("p", ty.pointer(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) { + // *p = 42; + // } + // fn f3(p : ptr) { + // *p = 42; + // } + // fn f1() { + // var v : i32; + // f2(&v); + // f3(&v); + // } + Func("f2", + utils::Vector{ + Param("p", ty.pointer(ast::AddressSpace::kFunction)), + }, + ty.void_(), + utils::Vector{ + Assign(Deref("p"), 42_a), + }); + Func("f3", + utils::Vector{ + Param("p", ty.pointer(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 diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc index 3dc054ce88..8c95457f20 100644 --- a/src/tint/resolver/resolver.cc +++ b/src/tint/resolver/resolver.cc @@ -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(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( @@ -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()) { + 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(); + 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 arg_reads; + std::unordered_map arg_writes; + for (size_t i = 0; i < args.Length(); i++) { + auto* arg = args[i]; + if (!arg->Type()->Is()) { + 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(); } @@ -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); }); } diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h index 38e624cc24..46527ff86e 100644 --- a/src/tint/resolver/resolver.h +++ b/src/tint/resolver/resolver.h @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -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 module_scope_writes; + /// The set of module-scope variables that are read from, and where that read occurs. + std::unordered_map module_scope_reads; + /// The set of function parameters that are written to. + std::unordered_set parameter_writes; + /// The set of function parameters that are read from. + std::unordered_set parameter_reads; + }; + ProgramBuilder* const builder_; diag::List& diagnostics_; ConstEval const_eval_; @@ -435,6 +462,7 @@ class Resolver { utils::Hashmap atomic_composite_info_; utils::Bitset<0> marked_; ExprEvalStageConstraint expr_eval_stage_constraint_; + std::unordered_map alias_analysis_infos_; utils::Hashmap override_ids_; utils::Hashmap array_inits_; utils::Hashmap struct_inits_; diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.cc b/src/tint/transform/module_scope_var_to_entry_point_param.cc index 599591ab1b..8a69bd327e 100644 --- a/src/tint/transform/module_scope_var_to_entry_point_param.cc +++ b/src/tint/transform/module_scope_var_to_entry_point_param.cc @@ -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. diff --git a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc index 8b9ecbad28..821542b990 100644 --- a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc +++ b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc @@ -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, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr) { *(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, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr) { 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, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr) { 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, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr) { *(tint_symbol_1) = a; *(tint_symbol_2) = b;