// Copyright 2021 The Tint Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "src/tint/ast/disable_validation_attribute.h" #include "src/tint/resolver/resolver.h" #include "src/tint/resolver/resolver_test_helper.h" #include "src/tint/transform/add_spirv_block_attribute.h" #include "gmock/gmock.h" using namespace tint::number_suffixes; // NOLINT namespace tint::resolver { // Helpers and typedefs template using DataType = builder::DataType; template using vec2 = builder::vec2; template using vec3 = builder::vec3; template using vec4 = builder::vec4; template using mat2x2 = builder::mat2x2; template using mat3x3 = builder::mat3x3; template using mat4x4 = builder::mat4x4; template using alias = builder::alias; template using alias1 = builder::alias1; template using alias2 = builder::alias2; template using alias3 = builder::alias3; namespace AttributeTests { namespace { enum class AttributeKind { kAlign, kBinding, kBuiltin, kGroup, kId, kInterpolate, kInvariant, kLocation, kOffset, kSize, kStage, kStride, kWorkgroup, kBindingAndGroup, }; static bool IsBindingAttribute(AttributeKind kind) { switch (kind) { case AttributeKind::kBinding: case AttributeKind::kGroup: case AttributeKind::kBindingAndGroup: return true; default: return false; } } struct TestParams { AttributeKind kind; bool should_pass; }; struct TestWithParams : ResolverTestWithParam {}; static ast::AttributeList createAttributes(const Source& source, ProgramBuilder& builder, AttributeKind kind) { switch (kind) { case AttributeKind::kAlign: return {builder.create(source, 4u)}; case AttributeKind::kBinding: return {builder.create(source, 1u)}; case AttributeKind::kBuiltin: return {builder.Builtin(source, ast::Builtin::kPosition)}; case AttributeKind::kGroup: return {builder.create(source, 1u)}; case AttributeKind::kId: return {builder.create(source, 0u)}; case AttributeKind::kInterpolate: return {builder.Interpolate(source, ast::InterpolationType::kLinear, ast::InterpolationSampling::kCenter)}; case AttributeKind::kInvariant: return {builder.Invariant(source)}; case AttributeKind::kLocation: return {builder.Location(source, 1)}; case AttributeKind::kOffset: return {builder.create(source, 4u)}; case AttributeKind::kSize: return {builder.create(source, 16u)}; case AttributeKind::kStage: return {builder.Stage(source, ast::PipelineStage::kCompute)}; case AttributeKind::kStride: return {builder.create(source, 4u)}; case AttributeKind::kWorkgroup: return {builder.create(source, builder.Expr(1_i))}; case AttributeKind::kBindingAndGroup: return {builder.create(source, 1_u), builder.create(source, 1_u)}; } return {}; } namespace FunctionInputAndOutputTests { using FunctionParameterAttributeTest = TestWithParams; TEST_P(FunctionParameterAttributeTest, IsValid) { auto& params = GetParam(); Func("main", ast::VariableList{Param("a", ty.vec4(), createAttributes({}, *this, params.kind))}, ty.void_(), {}); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "error: attribute is not valid for non-entry point function " "parameters"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, FunctionParameterAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using FunctionReturnTypeAttributeTest = TestWithParams; TEST_P(FunctionReturnTypeAttributeTest, IsValid) { auto& params = GetParam(); Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1_f)}, {}, createAttributes({}, *this, params.kind)); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "error: attribute is not valid for non-entry point function " "return types"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, FunctionReturnTypeAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); } // namespace FunctionInputAndOutputTests namespace EntryPointInputAndOutputTests { using ComputeShaderParameterAttributeTest = TestWithParams; TEST_P(ComputeShaderParameterAttributeTest, IsValid) { auto& params = GetParam(); auto* p = Param("a", ty.vec4(), createAttributes(Source{{12, 34}}, *this, params.kind)); Func("main", ast::VariableList{p}, ty.void_(), {}, {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_i)}); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (params.kind == AttributeKind::kBuiltin) { EXPECT_EQ(r()->error(), "12:34 error: builtin(position) cannot be used in input of " "compute pipeline stage"); } else if (params.kind == AttributeKind::kInterpolate || params.kind == AttributeKind::kLocation || params.kind == AttributeKind::kInvariant) { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for compute shader inputs"); } else { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, ComputeShaderParameterAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using FragmentShaderParameterAttributeTest = TestWithParams; TEST_P(FragmentShaderParameterAttributeTest, IsValid) { auto& params = GetParam(); auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind); if (params.kind != AttributeKind::kBuiltin && params.kind != AttributeKind::kLocation) { attrs.push_back(Builtin(Source{{34, 56}}, ast::Builtin::kPosition)); } auto* p = Param("a", ty.vec4(), attrs); Func("frag_main", {p}, ty.void_(), {}, {Stage(ast::PipelineStage::kFragment)}); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, FragmentShaderParameterAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, true}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, // kInterpolate tested separately (requires @location) TestParams{AttributeKind::kInvariant, true}, TestParams{AttributeKind::kLocation, true}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using VertexShaderParameterAttributeTest = TestWithParams; TEST_P(VertexShaderParameterAttributeTest, IsValid) { auto& params = GetParam(); auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind); if (params.kind != AttributeKind::kLocation) { attrs.push_back(Location(Source{{34, 56}}, 2)); } auto* p = Param("a", ty.vec4(), attrs); Func("vertex_main", ast::VariableList{p}, ty.vec4(), {Return(Construct(ty.vec4()))}, {Stage(ast::PipelineStage::kVertex)}, {Builtin(ast::Builtin::kPosition)}); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (params.kind == AttributeKind::kBuiltin) { EXPECT_EQ(r()->error(), "12:34 error: builtin(position) cannot be used in input of " "vertex pipeline stage"); } else if (params.kind == AttributeKind::kInvariant) { EXPECT_EQ(r()->error(), "12:34 error: invariant attribute must only be applied to a " "position builtin"); } else { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, VertexShaderParameterAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, true}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, true}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using ComputeShaderReturnTypeAttributeTest = TestWithParams; TEST_P(ComputeShaderReturnTypeAttributeTest, IsValid) { auto& params = GetParam(); Func("main", ast::VariableList{}, ty.vec4(), {Return(Construct(ty.vec4(), 1_f))}, {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_i)}, createAttributes(Source{{12, 34}}, *this, params.kind)); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (params.kind == AttributeKind::kBuiltin) { EXPECT_EQ(r()->error(), "12:34 error: builtin(position) cannot be used in output of " "compute pipeline stage"); } else if (params.kind == AttributeKind::kInterpolate || params.kind == AttributeKind::kLocation || params.kind == AttributeKind::kInvariant) { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for compute shader output"); } else { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for entry point return " "types"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, ComputeShaderReturnTypeAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using FragmentShaderReturnTypeAttributeTest = TestWithParams; TEST_P(FragmentShaderReturnTypeAttributeTest, IsValid) { auto& params = GetParam(); auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind); attrs.push_back(Location(Source{{34, 56}}, 2)); Func("frag_main", {}, ty.vec4(), {Return(Construct(ty.vec4()))}, {Stage(ast::PipelineStage::kFragment)}, attrs); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (params.kind == AttributeKind::kBuiltin) { EXPECT_EQ(r()->error(), "12:34 error: builtin(position) cannot be used in output of " "fragment pipeline stage"); } else if (params.kind == AttributeKind::kInvariant) { EXPECT_EQ(r()->error(), "12:34 error: invariant attribute must only be applied to a " "position builtin"); } else if (params.kind == AttributeKind::kLocation) { EXPECT_EQ(r()->error(), "34:56 error: duplicate location attribute\n" "12:34 note: first attribute declared here"); } else { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for entry point return " "types"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, FragmentShaderReturnTypeAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, true}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using VertexShaderReturnTypeAttributeTest = TestWithParams; TEST_P(VertexShaderReturnTypeAttributeTest, IsValid) { auto& params = GetParam(); auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind); // a vertex shader must include the 'position' builtin in its return type if (params.kind != AttributeKind::kBuiltin) { attrs.push_back(Builtin(Source{{34, 56}}, ast::Builtin::kPosition)); } Func("vertex_main", ast::VariableList{}, ty.vec4(), {Return(Construct(ty.vec4()))}, {Stage(ast::PipelineStage::kVertex)}, attrs); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (params.kind == AttributeKind::kLocation) { EXPECT_EQ(r()->error(), "34:56 error: multiple entry point IO attributes\n" "12:34 note: previously consumed location(1)"); } else { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for entry point return " "types"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, VertexShaderReturnTypeAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, true}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, // kInterpolate tested separately (requires @location) TestParams{AttributeKind::kInvariant, true}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using EntryPointParameterAttributeTest = TestWithParams; TEST_F(EntryPointParameterAttributeTest, DuplicateAttribute) { Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1_f)}, {Stage(ast::PipelineStage::kFragment)}, { Location(Source{{12, 34}}, 2), Location(Source{{56, 78}}, 3), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate location attribute 12:34 note: first attribute declared here)"); } TEST_F(EntryPointParameterAttributeTest, DuplicateInternalAttribute) { auto* s = Param("s", ty.sampler(ast::SamplerKind::kSampler), ast::AttributeList{ create(0), create(0), Disable(ast::DisabledValidation::kBindingPointCollision), Disable(ast::DisabledValidation::kEntryPointParameter), }); Func("f", {s}, ty.void_(), {}, {Stage(ast::PipelineStage::kFragment)}); EXPECT_TRUE(r()->Resolve()) << r()->error(); } using EntryPointReturnTypeAttributeTest = ResolverTest; TEST_F(EntryPointReturnTypeAttributeTest, DuplicateAttribute) { Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1_f)}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}, ast::AttributeList{ Location(Source{{12, 34}}, 2), Location(Source{{56, 78}}, 3), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate location attribute 12:34 note: first attribute declared here)"); } TEST_F(EntryPointReturnTypeAttributeTest, DuplicateInternalAttribute) { Func("f", {}, ty.i32(), {Return(1_i)}, {Stage(ast::PipelineStage::kFragment)}, ast::AttributeList{ Disable(ast::DisabledValidation::kBindingPointCollision), Disable(ast::DisabledValidation::kEntryPointParameter), }); EXPECT_TRUE(r()->Resolve()) << r()->error(); } } // namespace EntryPointInputAndOutputTests namespace StructAndStructMemberTests { using StructAttributeTest = TestWithParams; using SpirvBlockAttribute = transform::AddSpirvBlockAttribute::SpirvBlockAttribute; TEST_P(StructAttributeTest, IsValid) { auto& params = GetParam(); auto* str = create(Sym("mystruct"), ast::StructMemberList{Member("a", ty.f32())}, createAttributes(Source{{12, 34}}, *this, params.kind)); AST().AddGlobalDeclaration(str); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for struct declarations"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, StructAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using StructMemberAttributeTest = TestWithParams; TEST_P(StructMemberAttributeTest, IsValid) { auto& params = GetParam(); ast::StructMemberList members; if (params.kind == AttributeKind::kBuiltin) { members.push_back( {Member("a", ty.vec4(), createAttributes(Source{{12, 34}}, *this, params.kind))}); } else { members.push_back( {Member("a", ty.f32(), createAttributes(Source{{12, 34}}, *this, params.kind))}); } Structure("mystruct", members); WrapInFunction(); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for structure members"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, StructMemberAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, true}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, true}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, // kInterpolate tested separately (requires @location) // kInvariant tested separately (requires position builtin) TestParams{AttributeKind::kLocation, true}, TestParams{AttributeKind::kOffset, true}, TestParams{AttributeKind::kSize, true}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); TEST_F(StructMemberAttributeTest, DuplicateAttribute) { Structure("mystruct", { Member("a", ty.i32(), { create(Source{{12, 34}}, 4u), create(Source{{56, 78}}, 8u), }), }); WrapInFunction(); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate align attribute 12:34 note: first attribute declared here)"); } TEST_F(StructMemberAttributeTest, InvariantAttributeWithPosition) { Structure("mystruct", { Member("a", ty.vec4(), { Invariant(), Builtin(ast::Builtin::kPosition), }), }); WrapInFunction(); EXPECT_TRUE(r()->Resolve()) << r()->error(); } TEST_F(StructMemberAttributeTest, InvariantAttributeWithoutPosition) { Structure("mystruct", { Member("a", ty.vec4(), { Invariant(Source{{12, 34}}), }), }); WrapInFunction(); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: invariant attribute must only be applied to a " "position builtin"); } } // namespace StructAndStructMemberTests using ArrayAttributeTest = TestWithParams; TEST_P(ArrayAttributeTest, IsValid) { auto& params = GetParam(); auto* arr = ty.array(ty.f32(), nullptr, createAttributes(Source{{12, 34}}, *this, params.kind)); Structure("mystruct", { Member("a", arr), }); WrapInFunction(); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for array types"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, ArrayAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, true}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); using VariableAttributeTest = TestWithParams; TEST_P(VariableAttributeTest, IsValid) { auto& params = GetParam(); if (IsBindingAttribute(params.kind)) { Global("a", ty.sampler(ast::SamplerKind::kSampler), ast::StorageClass::kNone, nullptr, createAttributes(Source{{12, 34}}, *this, params.kind)); } else { Global("a", ty.f32(), ast::StorageClass::kPrivate, nullptr, createAttributes(Source{{12, 34}}, *this, params.kind)); } WrapInFunction(); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); if (!IsBindingAttribute(params.kind)) { EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for variables"); } } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, VariableAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, false}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, true})); TEST_F(VariableAttributeTest, DuplicateAttribute) { Global("a", ty.sampler(ast::SamplerKind::kSampler), ast::AttributeList{ create(Source{{12, 34}}, 2), create(2), create(Source{{56, 78}}, 3), }); WrapInFunction(); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate binding attribute 12:34 note: first attribute declared here)"); } TEST_F(VariableAttributeTest, LocalVariable) { auto* v = Var("a", ty.f32(), ast::AttributeList{ create(Source{{12, 34}}, 2), }); WrapInFunction(v); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attributes are not valid on local variables"); } using ConstantAttributeTest = TestWithParams; TEST_P(ConstantAttributeTest, IsValid) { auto& params = GetParam(); GlobalConst("a", ty.f32(), Expr(1.23_f), createAttributes(Source{{12, 34}}, *this, params.kind)); WrapInFunction(); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for constants"); } } INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, ConstantAttributeTest, testing::Values(TestParams{AttributeKind::kAlign, false}, TestParams{AttributeKind::kBinding, false}, TestParams{AttributeKind::kBuiltin, false}, TestParams{AttributeKind::kGroup, false}, TestParams{AttributeKind::kId, true}, TestParams{AttributeKind::kInterpolate, false}, TestParams{AttributeKind::kInvariant, false}, TestParams{AttributeKind::kLocation, false}, TestParams{AttributeKind::kOffset, false}, TestParams{AttributeKind::kSize, false}, TestParams{AttributeKind::kStage, false}, TestParams{AttributeKind::kStride, false}, TestParams{AttributeKind::kWorkgroup, false}, TestParams{AttributeKind::kBindingAndGroup, false})); TEST_F(ConstantAttributeTest, DuplicateAttribute) { GlobalConst("a", ty.f32(), Expr(1.23_f), ast::AttributeList{ create(Source{{12, 34}}, 0), create(Source{{56, 78}}, 1), }); WrapInFunction(); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate id attribute 12:34 note: first attribute declared here)"); } } // namespace } // namespace AttributeTests namespace ArrayStrideTests { namespace { struct Params { builder::ast_type_func_ptr create_el_type; uint32_t stride; bool should_pass; }; template constexpr Params ParamsFor(uint32_t stride, bool should_pass) { return Params{DataType::AST, stride, should_pass}; } struct TestWithParams : ResolverTestWithParam {}; using ArrayStrideTest = TestWithParams; TEST_P(ArrayStrideTest, All) { auto& params = GetParam(); auto* el_ty = params.create_el_type(*this); std::stringstream ss; ss << "el_ty: " << FriendlyName(el_ty) << ", stride: " << params.stride << ", should_pass: " << params.should_pass; SCOPED_TRACE(ss.str()); auto* arr = ty.array(Source{{12, 34}}, el_ty, 4_u, params.stride); Global("myarray", arr, ast::StorageClass::kPrivate); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: arrays decorated with the stride attribute must " "have a stride that is at least the size of the element type, " "and be a multiple of the element type's alignment value."); } } struct SizeAndAlignment { uint32_t size; uint32_t align; }; constexpr SizeAndAlignment default_u32 = {4, 4}; constexpr SizeAndAlignment default_i32 = {4, 4}; constexpr SizeAndAlignment default_f32 = {4, 4}; constexpr SizeAndAlignment default_vec2 = {8, 8}; constexpr SizeAndAlignment default_vec3 = {12, 16}; constexpr SizeAndAlignment default_vec4 = {16, 16}; constexpr SizeAndAlignment default_mat2x2 = {16, 8}; constexpr SizeAndAlignment default_mat3x3 = {48, 16}; constexpr SizeAndAlignment default_mat4x4 = {64, 16}; INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest, ArrayStrideTest, testing::Values( // Succeed because stride >= element size (while being multiple of // element alignment) ParamsFor(default_u32.size, true), ParamsFor(default_i32.size, true), ParamsFor(default_f32.size, true), ParamsFor>(default_vec2.size, true), // vec3's default size is not a multiple of its alignment // ParamsFor, default_vec3.size, true}, ParamsFor>(default_vec4.size, true), ParamsFor>(default_mat2x2.size, true), ParamsFor>(default_mat3x3.size, true), ParamsFor>(default_mat4x4.size, true), // Fail because stride is < element size ParamsFor(default_u32.size - 1, false), ParamsFor(default_i32.size - 1, false), ParamsFor(default_f32.size - 1, false), ParamsFor>(default_vec2.size - 1, false), ParamsFor>(default_vec3.size - 1, false), ParamsFor>(default_vec4.size - 1, false), ParamsFor>(default_mat2x2.size - 1, false), ParamsFor>(default_mat3x3.size - 1, false), ParamsFor>(default_mat4x4.size - 1, false), // Succeed because stride equals multiple of element alignment ParamsFor(default_u32.align * 7, true), ParamsFor(default_i32.align * 7, true), ParamsFor(default_f32.align * 7, true), ParamsFor>(default_vec2.align * 7, true), ParamsFor>(default_vec3.align * 7, true), ParamsFor>(default_vec4.align * 7, true), ParamsFor>(default_mat2x2.align * 7, true), ParamsFor>(default_mat3x3.align * 7, true), ParamsFor>(default_mat4x4.align * 7, true), // Fail because stride is not multiple of element alignment ParamsFor((default_u32.align - 1) * 7, false), ParamsFor((default_i32.align - 1) * 7, false), ParamsFor((default_f32.align - 1) * 7, false), ParamsFor>((default_vec2.align - 1) * 7, false), ParamsFor>((default_vec3.align - 1) * 7, false), ParamsFor>((default_vec4.align - 1) * 7, false), ParamsFor>((default_mat2x2.align - 1) * 7, false), ParamsFor>((default_mat3x3.align - 1) * 7, false), ParamsFor>((default_mat4x4.align - 1) * 7, false))); TEST_F(ArrayStrideTest, DuplicateAttribute) { auto* arr = ty.array(Source{{12, 34}}, ty.i32(), 4_u, { create(Source{{12, 34}}, 4_i), create(Source{{56, 78}}, 4_i), }); Global("myarray", arr, ast::StorageClass::kPrivate); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate stride attribute 12:34 note: first attribute declared here)"); } } // namespace } // namespace ArrayStrideTests namespace ResourceTests { namespace { using ResourceAttributeTest = ResolverTest; TEST_F(ResourceAttributeTest, UniformBufferMissingBinding) { auto* s = Structure("S", {Member("x", ty.i32())}); Global(Source{{12, 34}}, "G", ty.Of(s), ast::StorageClass::kUniform); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, StorageBufferMissingBinding) { auto* s = Structure("S", {Member("x", ty.i32())}); Global(Source{{12, 34}}, "G", ty.Of(s), ast::StorageClass::kStorage, ast::Access::kRead); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, TextureMissingBinding) { Global(Source{{12, 34}}, "G", ty.depth_texture(ast::TextureDimension::k2d), ast::StorageClass::kNone); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, SamplerMissingBinding) { Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler), ast::StorageClass::kNone); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, BindingPairMissingBinding) { Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler), ast::StorageClass::kNone, ast::AttributeList{ create(1), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, BindingPairMissingGroup) { Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler), ast::StorageClass::kNone, ast::AttributeList{ create(1), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: resource variables require @group and @binding attributes)"); } TEST_F(ResourceAttributeTest, BindingPointUsedTwiceByEntryPoint) { Global(Source{{12, 34}}, "A", ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()), ast::StorageClass::kNone, ast::AttributeList{ create(1), create(2), }); Global(Source{{56, 78}}, "B", ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()), ast::StorageClass::kNone, ast::AttributeList{ create(1), create(2), }); Func("F", {}, ty.void_(), { Decl(Var("a", ty.vec4(), ast::StorageClass::kNone, Call("textureLoad", "A", vec2(1_i, 2_i), 0_i))), Decl(Var("b", ty.vec4(), ast::StorageClass::kNone, Call("textureLoad", "B", vec2(1_i, 2_i), 0_i))), }, {Stage(ast::PipelineStage::kFragment)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ( r()->error(), R"(56:78 error: entry point 'F' references multiple variables that use the same resource binding @group(2), @binding(1) 12:34 note: first resource binding usage declared here)"); } TEST_F(ResourceAttributeTest, BindingPointUsedTwiceByDifferentEntryPoints) { Global(Source{{12, 34}}, "A", ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()), ast::StorageClass::kNone, ast::AttributeList{ create(1), create(2), }); Global(Source{{56, 78}}, "B", ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()), ast::StorageClass::kNone, ast::AttributeList{ create(1), create(2), }); Func("F_A", {}, ty.void_(), { Decl(Var("a", ty.vec4(), ast::StorageClass::kNone, Call("textureLoad", "A", vec2(1_i, 2_i), 0_i))), }, {Stage(ast::PipelineStage::kFragment)}); Func("F_B", {}, ty.void_(), { Decl(Var("b", ty.vec4(), ast::StorageClass::kNone, Call("textureLoad", "B", vec2(1_i, 2_i), 0_i))), }, {Stage(ast::PipelineStage::kFragment)}); EXPECT_TRUE(r()->Resolve()) << r()->error(); } TEST_F(ResourceAttributeTest, BindingPointOnNonResource) { Global(Source{{12, 34}}, "G", ty.f32(), ast::StorageClass::kPrivate, ast::AttributeList{ create(1), create(2), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: non-resource variables must not have @group or @binding attributes)"); } } // namespace } // namespace ResourceTests namespace InvariantAttributeTests { namespace { using InvariantAttributeTests = ResolverTest; TEST_F(InvariantAttributeTests, InvariantWithPosition) { auto* param = Param("p", ty.vec4(), {Invariant(Source{{12, 34}}), Builtin(Source{{56, 78}}, ast::Builtin::kPosition)}); Func("main", ast::VariableList{param}, ty.vec4(), ast::StatementList{Return(Construct(ty.vec4()))}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}, ast::AttributeList{ Location(0), }); EXPECT_TRUE(r()->Resolve()) << r()->error(); } TEST_F(InvariantAttributeTests, InvariantWithoutPosition) { auto* param = Param("p", ty.vec4(), {Invariant(Source{{12, 34}}), Location(0)}); Func("main", ast::VariableList{param}, ty.vec4(), ast::StatementList{Return(Construct(ty.vec4()))}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}, ast::AttributeList{ Location(0), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: invariant attribute must only be applied to a " "position builtin"); } } // namespace } // namespace InvariantAttributeTests namespace WorkgroupAttributeTests { namespace { using WorkgroupAttribute = ResolverTest; TEST_F(WorkgroupAttribute, ComputeShaderPass) { Func("main", {}, ty.void_(), {}, {Stage(ast::PipelineStage::kCompute), create(Source{{12, 34}}, Expr(1_i))}); EXPECT_TRUE(r()->Resolve()) << r()->error(); } TEST_F(WorkgroupAttribute, Missing) { Func(Source{{12, 34}}, "main", {}, ty.void_(), {}, {Stage(ast::PipelineStage::kCompute)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: a compute shader must include 'workgroup_size' in its " "attributes"); } TEST_F(WorkgroupAttribute, NotAnEntryPoint) { Func("main", {}, ty.void_(), {}, {create(Source{{12, 34}}, Expr(1_i))}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: the workgroup_size attribute is only valid for " "compute stages"); } TEST_F(WorkgroupAttribute, NotAComputeShader) { Func("main", {}, ty.void_(), {}, {Stage(ast::PipelineStage::kFragment), create(Source{{12, 34}}, Expr(1_i))}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: the workgroup_size attribute is only valid for " "compute stages"); } TEST_F(WorkgroupAttribute, DuplicateAttribute) { Func(Source{{12, 34}}, "main", {}, ty.void_(), {}, { Stage(ast::PipelineStage::kCompute), WorkgroupSize(Source{{12, 34}}, 1_i, nullptr, nullptr), WorkgroupSize(Source{{56, 78}}, 2_i, nullptr, nullptr), }); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(56:78 error: duplicate workgroup_size attribute 12:34 note: first attribute declared here)"); } } // namespace } // namespace WorkgroupAttributeTests namespace InterpolateTests { namespace { using InterpolateTest = ResolverTest; struct Params { ast::InterpolationType type; ast::InterpolationSampling sampling; bool should_pass; }; struct TestWithParams : ResolverTestWithParam {}; using InterpolateParameterTest = TestWithParams; TEST_P(InterpolateParameterTest, All) { auto& params = GetParam(); Func("main", ast::VariableList{ Param("a", ty.f32(), {Location(0), Interpolate(Source{{12, 34}}, params.type, params.sampling)})}, ty.void_(), {}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}); if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: flat interpolation attribute must not have a " "sampling parameter"); } } TEST_P(InterpolateParameterTest, IntegerScalar) { auto& params = GetParam(); Func("main", ast::VariableList{ Param("a", ty.i32(), {Location(0), Interpolate(Source{{12, 34}}, params.type, params.sampling)})}, ty.void_(), {}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}); if (params.type != ast::InterpolationType::kFlat) { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: interpolation type must be 'flat' for integral " "user-defined IO types"); } else if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: flat interpolation attribute must not have a " "sampling parameter"); } } TEST_P(InterpolateParameterTest, IntegerVector) { auto& params = GetParam(); Func("main", ast::VariableList{ Param("a", ty.vec4(), {Location(0), Interpolate(Source{{12, 34}}, params.type, params.sampling)})}, ty.void_(), {}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}); if (params.type != ast::InterpolationType::kFlat) { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: interpolation type must be 'flat' for integral " "user-defined IO types"); } else if (params.should_pass) { EXPECT_TRUE(r()->Resolve()) << r()->error(); } else { EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), "12:34 error: flat interpolation attribute must not have a " "sampling parameter"); } } INSTANTIATE_TEST_SUITE_P( ResolverAttributeValidationTest, InterpolateParameterTest, testing::Values( Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kNone, true}, Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kCenter, true}, Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kCentroid, true}, Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kSample, true}, Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kNone, true}, Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kCenter, true}, Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kCentroid, true}, Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kSample, true}, // flat interpolation must not have a sampling type Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone, true}, Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kCenter, false}, Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kCentroid, false}, Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kSample, false})); TEST_F(InterpolateTest, FragmentInput_Integer_MissingFlatInterpolation) { Func("main", ast::VariableList{Param(Source{{12, 34}}, "a", ty.i32(), {Location(0)})}, ty.void_(), {}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ( r()->error(), R"(12:34 error: integral user-defined fragment inputs must have a flat interpolation attribute)"); } TEST_F(InterpolateTest, VertexOutput_Integer_MissingFlatInterpolation) { auto* s = Structure("S", { Member("pos", ty.vec4(), {Builtin(ast::Builtin::kPosition)}), Member(Source{{12, 34}}, "u", ty.u32(), {Location(0)}), }); Func("main", {}, ty.Of(s), {Return(Construct(ty.Of(s)))}, ast::AttributeList{Stage(ast::PipelineStage::kVertex)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ( r()->error(), R"(12:34 error: integral user-defined vertex outputs must have a flat interpolation attribute note: while analysing entry point 'main')"); } TEST_F(InterpolateTest, MissingLocationAttribute_Parameter) { Func("main", ast::VariableList{Param("a", ty.vec4(), {Builtin(ast::Builtin::kPosition), Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone)})}, ty.void_(), {}, ast::AttributeList{Stage(ast::PipelineStage::kFragment)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: interpolate attribute must only be used with @location)"); } TEST_F(InterpolateTest, MissingLocationAttribute_ReturnType) { Func("main", {}, ty.vec4(), {Return(Construct(ty.vec4()))}, ast::AttributeList{Stage(ast::PipelineStage::kVertex)}, {Builtin(ast::Builtin::kPosition), Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone)}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: interpolate attribute must only be used with @location)"); } TEST_F(InterpolateTest, MissingLocationAttribute_Struct) { Structure("S", {Member("a", ty.f32(), {Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone)})}); EXPECT_FALSE(r()->Resolve()); EXPECT_EQ(r()->error(), R"(12:34 error: interpolate attribute must only be used with @location)"); } } // namespace } // namespace InterpolateTests } // namespace tint::resolver