|  | // 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/const_eval_test.h" | 
|  |  | 
|  | using namespace tint::number_suffixes;  // NOLINT | 
|  |  | 
|  | namespace tint::resolver { | 
|  | namespace { | 
|  |  | 
|  | TEST_F(ResolverConstEvalTest, StructMemberAccess) { | 
|  | Structure("Inner", utils::Vector{ | 
|  | Member("i1", ty.i32()), | 
|  | Member("i2", ty.u32()), | 
|  | Member("i3", ty.f32()), | 
|  | Member("i4", ty.bool_()), | 
|  | }); | 
|  |  | 
|  | Structure("Outer", utils::Vector{ | 
|  | Member("o1", ty.type_name("Inner")), | 
|  | Member("o2", ty.type_name("Inner")), | 
|  | }); | 
|  | auto* outer_expr = Construct(ty.type_name("Outer"),  // | 
|  | Construct(ty.type_name("Inner"), 1_i, 2_u, 3_f, true), | 
|  | Construct(ty.type_name("Inner"))); | 
|  | auto* o1_expr = MemberAccessor(outer_expr, "o1"); | 
|  | auto* i2_expr = MemberAccessor(o1_expr, "i2"); | 
|  | WrapInFunction(i2_expr); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* outer = Sem().Get(outer_expr); | 
|  | ASSERT_NE(outer, nullptr); | 
|  | auto* str = outer->Type()->As<sem::Struct>(); | 
|  | ASSERT_NE(str, nullptr); | 
|  | EXPECT_EQ(str->Members().Length(), 2u); | 
|  | ASSERT_NE(outer->ConstantValue(), nullptr); | 
|  | EXPECT_TYPE(outer->ConstantValue()->Type(), outer->Type()); | 
|  | EXPECT_FALSE(outer->ConstantValue()->AllEqual()); | 
|  | EXPECT_TRUE(outer->ConstantValue()->AnyZero()); | 
|  | EXPECT_FALSE(outer->ConstantValue()->AllZero()); | 
|  |  | 
|  | auto* o1 = Sem().Get(o1_expr); | 
|  | ASSERT_NE(o1->ConstantValue(), nullptr); | 
|  | EXPECT_FALSE(o1->ConstantValue()->AllEqual()); | 
|  | EXPECT_FALSE(o1->ConstantValue()->AnyZero()); | 
|  | EXPECT_FALSE(o1->ConstantValue()->AllZero()); | 
|  | EXPECT_TRUE(o1->ConstantValue()->Type()->Is<sem::Struct>()); | 
|  | EXPECT_EQ(o1->ConstantValue()->Index(0)->ValueAs<i32>(), 1_i); | 
|  | EXPECT_EQ(o1->ConstantValue()->Index(1)->ValueAs<u32>(), 2_u); | 
|  | EXPECT_EQ(o1->ConstantValue()->Index(2)->ValueAs<f32>(), 3_f); | 
|  | EXPECT_EQ(o1->ConstantValue()->Index(2)->ValueAs<bool>(), true); | 
|  |  | 
|  | auto* i2 = Sem().Get(i2_expr); | 
|  | ASSERT_NE(i2->ConstantValue(), nullptr); | 
|  | EXPECT_TRUE(i2->ConstantValue()->AllEqual()); | 
|  | EXPECT_FALSE(i2->ConstantValue()->AnyZero()); | 
|  | EXPECT_FALSE(i2->ConstantValue()->AllZero()); | 
|  | EXPECT_TRUE(i2->ConstantValue()->Type()->Is<type::U32>()); | 
|  | EXPECT_EQ(i2->ConstantValue()->ValueAs<u32>(), 2_u); | 
|  | } | 
|  |  | 
|  | TEST_F(ResolverConstEvalTest, Matrix_AFloat_Construct_From_AInt_Vectors) { | 
|  | auto* c = Const("a", Construct(ty.mat(nullptr, 2, 2),  // | 
|  | Construct(ty.vec(nullptr, 2), Expr(1_a), Expr(2_a)), | 
|  | Construct(ty.vec(nullptr, 2), Expr(3_a), Expr(4_a)))); | 
|  | WrapInFunction(c); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* sem = Sem().Get(c); | 
|  | ASSERT_NE(sem, nullptr); | 
|  | EXPECT_TRUE(sem->Type()->Is<type::Matrix>()); | 
|  | auto* cv = sem->ConstantValue(); | 
|  | EXPECT_TYPE(cv->Type(), sem->Type()); | 
|  | EXPECT_TRUE(cv->Index(0)->Type()->Is<type::Vector>()); | 
|  | EXPECT_TRUE(cv->Index(0)->Index(0)->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_FALSE(cv->AllEqual()); | 
|  | EXPECT_FALSE(cv->AnyZero()); | 
|  | EXPECT_FALSE(cv->AllZero()); | 
|  | auto* c0 = cv->Index(0); | 
|  | auto* c1 = cv->Index(1); | 
|  | EXPECT_EQ(c0->Index(0)->ValueAs<AFloat>(), 1.0); | 
|  | EXPECT_EQ(c0->Index(1)->ValueAs<AFloat>(), 2.0); | 
|  | EXPECT_EQ(c1->Index(0)->ValueAs<AFloat>(), 3.0); | 
|  | EXPECT_EQ(c1->Index(1)->ValueAs<AFloat>(), 4.0); | 
|  | } | 
|  |  | 
|  | TEST_F(ResolverConstEvalTest, MatrixMemberAccess_AFloat) { | 
|  | auto* c = | 
|  | Const("a", Construct(ty.mat(nullptr, 2, 3),  // | 
|  | Construct(ty.vec(nullptr, 3), Expr(1.0_a), Expr(2.0_a), Expr(3.0_a)), | 
|  | Construct(ty.vec(nullptr, 3), Expr(4.0_a), Expr(5.0_a), Expr(6.0_a)))); | 
|  |  | 
|  | auto* col_0 = Const("col_0", IndexAccessor("a", Expr(0_i))); | 
|  | auto* col_1 = Const("col_1", IndexAccessor("a", Expr(1_i))); | 
|  | auto* e00 = Const("e00", IndexAccessor("col_0", Expr(0_i))); | 
|  | auto* e01 = Const("e01", IndexAccessor("col_0", Expr(1_i))); | 
|  | auto* e02 = Const("e02", IndexAccessor("col_0", Expr(2_i))); | 
|  | auto* e10 = Const("e10", IndexAccessor("col_1", Expr(0_i))); | 
|  | auto* e11 = Const("e11", IndexAccessor("col_1", Expr(1_i))); | 
|  | auto* e12 = Const("e12", IndexAccessor("col_1", Expr(2_i))); | 
|  |  | 
|  | (void)col_0; | 
|  | (void)col_1; | 
|  |  | 
|  | WrapInFunction(c, col_0, col_1, e00, e01, e02, e10, e11, e12); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* sem = Sem().Get(c); | 
|  | ASSERT_NE(sem, nullptr); | 
|  | EXPECT_TRUE(sem->Type()->Is<type::Matrix>()); | 
|  | auto* cv = sem->ConstantValue(); | 
|  | EXPECT_TYPE(cv->Type(), sem->Type()); | 
|  | EXPECT_TRUE(cv->Index(0)->Type()->Is<type::Vector>()); | 
|  | EXPECT_TRUE(cv->Index(0)->Index(0)->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_FALSE(cv->AllEqual()); | 
|  | EXPECT_FALSE(cv->AnyZero()); | 
|  | EXPECT_FALSE(cv->AllZero()); | 
|  |  | 
|  | auto* sem_col0 = Sem().Get(col_0); | 
|  | ASSERT_NE(sem_col0, nullptr); | 
|  | EXPECT_TRUE(sem_col0->Type()->Is<type::Vector>()); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(0)->ValueAs<AFloat>(), 1.0); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(1)->ValueAs<AFloat>(), 2.0); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(2)->ValueAs<AFloat>(), 3.0); | 
|  |  | 
|  | auto* sem_col1 = Sem().Get(col_1); | 
|  | ASSERT_NE(sem_col1, nullptr); | 
|  | EXPECT_TRUE(sem_col1->Type()->Is<type::Vector>()); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(0)->ValueAs<AFloat>(), 4.0); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(1)->ValueAs<AFloat>(), 5.0); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(2)->ValueAs<AFloat>(), 6.0); | 
|  |  | 
|  | auto* sem_e00 = Sem().Get(e00); | 
|  | ASSERT_NE(sem_e00, nullptr); | 
|  | EXPECT_TRUE(sem_e00->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e00->ConstantValue()->ValueAs<AFloat>(), 1.0); | 
|  |  | 
|  | auto* sem_e01 = Sem().Get(e01); | 
|  | ASSERT_NE(sem_e01, nullptr); | 
|  | EXPECT_TRUE(sem_e01->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e01->ConstantValue()->ValueAs<AFloat>(), 2.0); | 
|  |  | 
|  | auto* sem_e02 = Sem().Get(e02); | 
|  | ASSERT_NE(sem_e02, nullptr); | 
|  | EXPECT_TRUE(sem_e02->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e02->ConstantValue()->ValueAs<AFloat>(), 3.0); | 
|  |  | 
|  | auto* sem_e10 = Sem().Get(e10); | 
|  | ASSERT_NE(sem_e10, nullptr); | 
|  | EXPECT_TRUE(sem_e10->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e10->ConstantValue()->ValueAs<AFloat>(), 4.0); | 
|  |  | 
|  | auto* sem_e11 = Sem().Get(e11); | 
|  | ASSERT_NE(sem_e11, nullptr); | 
|  | EXPECT_TRUE(sem_e11->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e11->ConstantValue()->ValueAs<AFloat>(), 5.0); | 
|  |  | 
|  | auto* sem_e12 = Sem().Get(e12); | 
|  | ASSERT_NE(sem_e12, nullptr); | 
|  | EXPECT_TRUE(sem_e12->Type()->Is<type::AbstractFloat>()); | 
|  | EXPECT_EQ(sem_e12->ConstantValue()->ValueAs<AFloat>(), 6.0); | 
|  | } | 
|  |  | 
|  | TEST_F(ResolverConstEvalTest, MatrixMemberAccess_f32) { | 
|  | auto* c = | 
|  | Const("a", Construct(ty.mat(nullptr, 2, 3),  // | 
|  | Construct(ty.vec(nullptr, 3), Expr(1.0_f), Expr(2.0_f), Expr(3.0_f)), | 
|  | Construct(ty.vec(nullptr, 3), Expr(4.0_f), Expr(5.0_f), Expr(6.0_f)))); | 
|  |  | 
|  | auto* col_0 = Const("col_0", IndexAccessor("a", Expr(0_i))); | 
|  | auto* col_1 = Const("col_1", IndexAccessor("a", Expr(1_i))); | 
|  | auto* e00 = Const("e00", IndexAccessor("col_0", Expr(0_i))); | 
|  | auto* e01 = Const("e01", IndexAccessor("col_0", Expr(1_i))); | 
|  | auto* e02 = Const("e02", IndexAccessor("col_0", Expr(2_i))); | 
|  | auto* e10 = Const("e10", IndexAccessor("col_1", Expr(0_i))); | 
|  | auto* e11 = Const("e11", IndexAccessor("col_1", Expr(1_i))); | 
|  | auto* e12 = Const("e12", IndexAccessor("col_1", Expr(2_i))); | 
|  |  | 
|  | (void)col_0; | 
|  | (void)col_1; | 
|  |  | 
|  | WrapInFunction(c, col_0, col_1, e00, e01, e02, e10, e11, e12); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* sem = Sem().Get(c); | 
|  | ASSERT_NE(sem, nullptr); | 
|  | EXPECT_TRUE(sem->Type()->Is<type::Matrix>()); | 
|  | auto* cv = sem->ConstantValue(); | 
|  | EXPECT_TYPE(cv->Type(), sem->Type()); | 
|  | EXPECT_TRUE(cv->Index(0)->Type()->Is<type::Vector>()); | 
|  | EXPECT_TRUE(cv->Index(0)->Index(0)->Type()->Is<type::F32>()); | 
|  | EXPECT_FALSE(cv->AllEqual()); | 
|  | EXPECT_FALSE(cv->AnyZero()); | 
|  | EXPECT_FALSE(cv->AllZero()); | 
|  |  | 
|  | auto* sem_col0 = Sem().Get(col_0); | 
|  | ASSERT_NE(sem_col0, nullptr); | 
|  | EXPECT_TRUE(sem_col0->Type()->Is<type::Vector>()); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(0)->ValueAs<f32>(), 1.0f); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(1)->ValueAs<f32>(), 2.0f); | 
|  | EXPECT_EQ(sem_col0->ConstantValue()->Index(2)->ValueAs<f32>(), 3.0f); | 
|  |  | 
|  | auto* sem_col1 = Sem().Get(col_1); | 
|  | ASSERT_NE(sem_col1, nullptr); | 
|  | EXPECT_TRUE(sem_col1->Type()->Is<type::Vector>()); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(0)->ValueAs<f32>(), 4.0f); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(1)->ValueAs<f32>(), 5.0f); | 
|  | EXPECT_EQ(sem_col1->ConstantValue()->Index(2)->ValueAs<f32>(), 6.0f); | 
|  |  | 
|  | auto* sem_e00 = Sem().Get(e00); | 
|  | ASSERT_NE(sem_e00, nullptr); | 
|  | EXPECT_TRUE(sem_e00->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e00->ConstantValue()->ValueAs<f32>(), 1.0f); | 
|  |  | 
|  | auto* sem_e01 = Sem().Get(e01); | 
|  | ASSERT_NE(sem_e01, nullptr); | 
|  | EXPECT_TRUE(sem_e01->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e01->ConstantValue()->ValueAs<f32>(), 2.0f); | 
|  |  | 
|  | auto* sem_e02 = Sem().Get(e02); | 
|  | ASSERT_NE(sem_e02, nullptr); | 
|  | EXPECT_TRUE(sem_e02->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e02->ConstantValue()->ValueAs<f32>(), 3.0f); | 
|  |  | 
|  | auto* sem_e10 = Sem().Get(e10); | 
|  | ASSERT_NE(sem_e10, nullptr); | 
|  | EXPECT_TRUE(sem_e10->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e10->ConstantValue()->ValueAs<f32>(), 4.0f); | 
|  |  | 
|  | auto* sem_e11 = Sem().Get(e11); | 
|  | ASSERT_NE(sem_e11, nullptr); | 
|  | EXPECT_TRUE(sem_e11->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e11->ConstantValue()->ValueAs<f32>(), 5.0f); | 
|  |  | 
|  | auto* sem_e12 = Sem().Get(e12); | 
|  | ASSERT_NE(sem_e12, nullptr); | 
|  | EXPECT_TRUE(sem_e12->Type()->Is<type::F32>()); | 
|  | EXPECT_EQ(sem_e12->ConstantValue()->ValueAs<f32>(), 6.0f); | 
|  | } | 
|  |  | 
|  | namespace ArrayAccess { | 
|  | struct Case { | 
|  | Value input; | 
|  | }; | 
|  | static Case C(Value input) { | 
|  | return Case{std::move(input)}; | 
|  | } | 
|  | static std::ostream& operator<<(std::ostream& o, const Case& c) { | 
|  | return o << "input: " << c.input; | 
|  | } | 
|  |  | 
|  | using ResolverConstEvalArrayAccessTest = ResolverTestWithParam<Case>; | 
|  | TEST_P(ResolverConstEvalArrayAccessTest, Test) { | 
|  | Enable(ast::Extension::kF16); | 
|  |  | 
|  | auto& param = GetParam(); | 
|  | auto* expr = param.input.Expr(*this); | 
|  | auto* a = Const("a", expr); | 
|  |  | 
|  | utils::Vector<const ast::IndexAccessorExpression*, 4> index_accessors; | 
|  | for (size_t i = 0; i < param.input.args.Length(); ++i) { | 
|  | auto* index = IndexAccessor("a", Expr(i32(i))); | 
|  | index_accessors.Push(index); | 
|  | } | 
|  |  | 
|  | utils::Vector<const ast::Statement*, 5> stmts; | 
|  | stmts.Push(WrapInStatement(a)); | 
|  | for (auto* ia : index_accessors) { | 
|  | stmts.Push(WrapInStatement(ia)); | 
|  | } | 
|  | WrapInFunction(std::move(stmts)); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* sem = Sem().Get(expr); | 
|  | ASSERT_NE(sem, nullptr); | 
|  | auto* arr = sem->Type()->As<type::Array>(); | 
|  | ASSERT_NE(arr, nullptr); | 
|  |  | 
|  | EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type()); | 
|  | for (size_t i = 0; i < index_accessors.Length(); ++i) { | 
|  | auto* ia_sem = Sem().Get(index_accessors[i]); | 
|  | ASSERT_NE(ia_sem, nullptr); | 
|  | ASSERT_NE(ia_sem->ConstantValue(), nullptr); | 
|  | EXPECT_EQ(ia_sem->ConstantValue()->ValueAs<AInt>(), i); | 
|  | } | 
|  | } | 
|  | template <typename T> | 
|  | std::vector<Case> ArrayAccessCases() { | 
|  | if constexpr (std::is_same_v<T, bool>) { | 
|  | return { | 
|  | C(Array(false, true)), | 
|  | }; | 
|  | } else { | 
|  | return { | 
|  | C(Array(T(0))),                          // | 
|  | C(Array(T(0), T(1))),                    // | 
|  | C(Array(T(0), T(1), T(2))),              // | 
|  | C(Array(T(0), T(1), T(2), T(3))),        // | 
|  | C(Array(T(0), T(1), T(2), T(3), T(4))),  // | 
|  | }; | 
|  | } | 
|  | } | 
|  | INSTANTIATE_TEST_SUITE_P(  // | 
|  | ArrayAccess, | 
|  | ResolverConstEvalArrayAccessTest, | 
|  | testing::ValuesIn(Concat(ArrayAccessCases<AInt>(),    // | 
|  | ArrayAccessCases<AFloat>(),  // | 
|  | ArrayAccessCases<i32>(),     // | 
|  | ArrayAccessCases<u32>(),     // | 
|  | ArrayAccessCases<f32>(),     // | 
|  | ArrayAccessCases<f16>(),     // | 
|  | ArrayAccessCases<bool>()))); | 
|  | }  // namespace ArrayAccess | 
|  |  | 
|  | namespace VectorAccess { | 
|  | struct Case { | 
|  | Value input; | 
|  | }; | 
|  | static Case C(Value input) { | 
|  | return Case{std::move(input)}; | 
|  | } | 
|  | static std::ostream& operator<<(std::ostream& o, const Case& c) { | 
|  | return o << "input: " << c.input; | 
|  | } | 
|  |  | 
|  | using ResolverConstEvalVectorAccessTest = ResolverTestWithParam<Case>; | 
|  | TEST_P(ResolverConstEvalVectorAccessTest, Test) { | 
|  | Enable(ast::Extension::kF16); | 
|  |  | 
|  | auto& param = GetParam(); | 
|  | auto* expr = param.input.Expr(*this); | 
|  | auto* a = Const("a", expr); | 
|  |  | 
|  | utils::Vector<const ast::IndexAccessorExpression*, 4> index_accessors; | 
|  | for (size_t i = 0; i < param.input.args.Length(); ++i) { | 
|  | auto* index = IndexAccessor("a", Expr(i32(i))); | 
|  | index_accessors.Push(index); | 
|  | } | 
|  |  | 
|  | utils::Vector<const ast::Statement*, 5> stmts; | 
|  | stmts.Push(WrapInStatement(a)); | 
|  | for (auto* ia : index_accessors) { | 
|  | stmts.Push(WrapInStatement(ia)); | 
|  | } | 
|  | WrapInFunction(std::move(stmts)); | 
|  |  | 
|  | EXPECT_TRUE(r()->Resolve()) << r()->error(); | 
|  |  | 
|  | auto* sem = Sem().Get(expr); | 
|  | ASSERT_NE(sem, nullptr); | 
|  | auto* vec = sem->Type()->As<type::Vector>(); | 
|  | ASSERT_NE(vec, nullptr); | 
|  |  | 
|  | EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type()); | 
|  | for (size_t i = 0; i < index_accessors.Length(); ++i) { | 
|  | auto* ia_sem = Sem().Get(index_accessors[i]); | 
|  | ASSERT_NE(ia_sem, nullptr); | 
|  | ASSERT_NE(ia_sem->ConstantValue(), nullptr); | 
|  | EXPECT_EQ(ia_sem->ConstantValue()->ValueAs<AInt>(), i); | 
|  | } | 
|  | } | 
|  | template <typename T> | 
|  | std::vector<Case> VectorAccessCases() { | 
|  | if constexpr (std::is_same_v<T, bool>) { | 
|  | return { | 
|  | C(Vec(false, true)), | 
|  | }; | 
|  | } else { | 
|  | return { | 
|  | C(Vec(T(0), T(1))),              // | 
|  | C(Vec(T(0), T(1), T(2))),        // | 
|  | C(Vec(T(0), T(1), T(2), T(3))),  // | 
|  | }; | 
|  | } | 
|  | } | 
|  | INSTANTIATE_TEST_SUITE_P(  // | 
|  | VectorAccess, | 
|  | ResolverConstEvalVectorAccessTest, | 
|  | testing::ValuesIn(Concat(VectorAccessCases<AInt>(),    // | 
|  | VectorAccessCases<AFloat>(),  // | 
|  | VectorAccessCases<i32>(),     // | 
|  | VectorAccessCases<u32>(),     // | 
|  | VectorAccessCases<f32>(),     // | 
|  | VectorAccessCases<f16>(),     // | 
|  | VectorAccessCases<bool>()))); | 
|  | }  // namespace VectorAccess | 
|  |  | 
|  | }  // namespace | 
|  | }  // namespace tint::resolver |