| // Copyright 2026 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include "src/tint/lang/core/ir/structural_validator.h" |
| |
| #include <algorithm> |
| #include <string_view> |
| |
| #include "src/tint/lang/core/binary_op.h" |
| #include "src/tint/lang/core/intrinsic/table.h" |
| #include "src/tint/lang/core/ir/constant.h" |
| #include "src/tint/lang/core/ir/constexpr_if.h" |
| #include "src/tint/lang/core/ir/multi_in_block.h" |
| #include "src/tint/lang/core/ir/referenced_functions.h" |
| #include "src/tint/lang/core/ir/terminate_invocation.h" |
| #include "src/tint/lang/core/ir/unused.h" |
| #include "src/tint/lang/core/type/array.h" |
| #include "src/tint/lang/core/type/array_count.h" |
| #include "src/tint/lang/core/type/binding_array.h" |
| #include "src/tint/lang/core/type/bool.h" |
| #include "src/tint/lang/core/type/buffer.h" |
| #include "src/tint/lang/core/type/f16.h" |
| #include "src/tint/lang/core/type/f32.h" |
| #include "src/tint/lang/core/type/function.h" |
| #include "src/tint/lang/core/type/i32.h" |
| #include "src/tint/lang/core/type/i8.h" |
| #include "src/tint/lang/core/type/matrix.h" |
| #include "src/tint/lang/core/type/memory_view.h" |
| #include "src/tint/lang/core/type/pointer.h" |
| #include "src/tint/lang/core/type/reference.h" |
| #include "src/tint/lang/core/type/sampled_texture.h" |
| #include "src/tint/lang/core/type/storage_texture.h" |
| #include "src/tint/lang/core/type/subgroup_matrix.h" |
| #include "src/tint/lang/core/type/type.h" |
| #include "src/tint/lang/core/type/u16.h" |
| #include "src/tint/lang/core/type/u32.h" |
| #include "src/tint/lang/core/type/u64.h" |
| #include "src/tint/lang/core/type/u8.h" |
| #include "src/tint/lang/core/type/vector.h" |
| #include "src/tint/lang/core/type/void.h" |
| #include "src/tint/utils/containers/predicates.h" |
| #include "src/tint/utils/containers/reverse.h" |
| #include "src/tint/utils/containers/transform.h" |
| #include "src/tint/utils/ice/ice.h" |
| #include "src/tint/utils/macros/defer.h" |
| #include "src/tint/utils/math/math.h" |
| #include "src/tint/utils/result.h" |
| #include "src/tint/utils/rtti/switch.h" |
| #include "src/tint/utils/text/text_style.h" |
| |
| namespace tint::core::ir::validator { |
| namespace { |
| |
| /// @returns the parent block of @p block |
| const Block* ParentBlockOf(const Block* block) { |
| if (auto* parent = block->Parent()) { |
| return parent->Block(); |
| } |
| return nullptr; |
| } |
| |
| /// @returns true if @p block directly or transitively holds the instruction @p inst |
| bool TransitivelyHolds(const Block* block, const Instruction* inst) { |
| for (auto* b = inst->Block(); b; b = ParentBlockOf(b)) { |
| if (b == block) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| template <typename CTX, typename IMPL> |
| void WalkTypeAndMembers(CTX& ctx, |
| const core::type::Type* type, |
| const IOAttributes& attr, |
| IMPL&& impl); |
| /// Helper that walks the members of a struct, called from WalkTypeAndMembers and its helpers |
| /// @param ctx a context object to pass to the impl function |
| /// @param str the struct to walk the members of |
| /// @param impl an impl function to be run, see WalkTypeAndMembers for details |
| template <typename CTX, typename IMPL> |
| void WalkStructMembers(CTX& ctx, const core::type::Struct* str, IMPL&& impl) { |
| for (auto* member : str->Members()) { |
| WalkTypeAndMembers(ctx, member->Type(), member->Attributes(), impl); |
| } |
| } |
| |
| /// Helper that walks an array's element type, called from WalkTypeAndMembers and its helpers |
| /// @param ctx a context object to pass to the impl function |
| /// @param arr the array to walk the element type of |
| /// @param impl an impl function to be run, see WalkTypeAndMembers for details |
| template <typename CTX, typename IMPL> |
| void WalkArrayElements(CTX& ctx, const core::type::Array* arr, IMPL&& impl) { |
| WalkTypeAndMembers(ctx, arr->ElemType(), IOAttributes{}, impl); |
| } |
| |
| /// Helper for walking a type that maybe a struct, calling an impl function for the type and each of |
| /// its members. |
| /// @param ctx a context object to pass to the implementation function |
| /// @param type the type to walk |
| /// @param attr the attributes for @p type |
| /// @param impl a function with the signature `void(const core::type::Type*, const IOAttributes&, |
| /// CTX&)` that is called for each type. |
| template <typename CTX, typename IMPL> |
| void WalkTypeAndMembers(CTX& ctx, |
| const core::type::Type* type, |
| const IOAttributes& attr, |
| IMPL&& impl) { |
| impl(ctx, type, attr); |
| tint::Switch( |
| type, [&](const core::type::Struct* s) { WalkStructMembers(ctx, s, impl); }, |
| [&](const core::type::Array* a) { WalkArrayElements(ctx, a, impl); }); |
| } |
| |
| } // namespace |
| |
| Structural::Structural(const Module& ir, diag::List& diagnostics) |
| : ir_(ir), diag_(diagnostics), referenced_module_vars_(ir) {} |
| |
| Structural::~Structural() = default; |
| |
| Disassembler& Structural::Disassemble() { |
| if (!disassembler_) { |
| disassembler_.emplace(ir::Disassembler(ir_)); |
| } |
| return *disassembler_; |
| } |
| |
| void Structural::Validate() { |
| RunStructuralSoundnessChecks(); |
| |
| CheckForRecursion(); |
| CheckForOrphanedInstructions(); |
| CheckStageRestrictedInstructions(); |
| } |
| |
| void Structural::CheckForRecursion() { |
| if (diag_.ContainsErrors()) { |
| return; |
| } |
| |
| ReferencedFunctions<const Module> referenced_functions(ir_); |
| for (auto& func : ir_.functions) { |
| auto& refs = referenced_functions.TransitiveReferences(func); |
| if (refs.Contains(func)) { |
| // TODO(434684891): Consider improving this error with more information. |
| AddError(func) << "recursive function calls are not allowed"; |
| return; |
| } |
| } |
| } |
| |
| void Structural::CheckForOrphanedInstructions() { |
| if (diag_.ContainsErrors()) { |
| return; |
| } |
| |
| // Check for orphaned instructions. |
| for (auto* inst : ir_.Instructions()) { |
| if (!visited_instructions_.Contains(inst)) { |
| AddError(inst) << "orphaned instruction: " << inst->FriendlyName(); |
| } |
| } |
| } |
| |
| void Structural::CheckStageRestrictedInstructions() { |
| if (diag_.ContainsErrors()) { |
| return; |
| } |
| |
| // Check for instructions being used in stages that do not support them. |
| for (const auto& i : stage_restricted_instructions_) { |
| const auto& inst = i.key; |
| const auto& stages = i.value; |
| const auto* f = ContainingFunction(inst); |
| if (f == nullptr) { |
| continue; |
| } |
| |
| if (f->IsEntryPoint() && !stages.Contains(f->Stage())) { |
| AddError(inst) << "cannot be used in a " << f->Stage() << " shader"; |
| } else { |
| for (const Function* ep : ContainingEndPoints(f)) { |
| if (!stages.Contains(ep->Stage())) { |
| AddError(inst) << "cannot be used in a " << ep->Stage() << " shader"; |
| } |
| } |
| } |
| } |
| } |
| |
| void Structural::RunStructuralSoundnessChecks() { |
| scope_stack_.Push(); |
| TINT_DEFER({ |
| scope_stack_.Pop(); |
| TINT_ASSERT(scope_stack_.IsEmpty()); |
| TINT_ASSERT(tasks_.IsEmpty()); |
| TINT_ASSERT(control_stack_.IsEmpty()); |
| TINT_ASSERT(block_stack_.IsEmpty()); |
| }); |
| CheckRootBlock(ir_.root_block); |
| |
| for (auto& func : ir_.functions) { |
| if (!all_functions_.Add(func)) { |
| AddError(func) << "function " << NameOf(func) << " added to module multiple times"; |
| } |
| scope_stack_.Add(func); |
| } |
| |
| for (auto& func : ir_.functions) { |
| block_to_function_.Add(func->Block(), func); |
| CheckFunction(func); |
| } |
| } |
| |
| diag::Diagnostic& Structural::AddError(const Instruction* inst) { |
| auto src = Disassemble().InstructionSource(inst); |
| auto& diag = AddError(src) << inst->FriendlyName() << ": "; |
| |
| if (!block_stack_.IsEmpty()) { |
| AddNote(block_stack_.Back()) << "in block"; |
| |
| // Adding the note may trigger a resize and invalidate the error diagnostic reference, so we |
| // need to get a new reference to the error diagnostic here. |
| return *(diag_.end() - 2); |
| } |
| return diag; |
| } |
| |
| diag::Diagnostic& Structural::AddError(const Instruction* inst, size_t idx) { |
| auto src = |
| Disassemble().OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)}); |
| auto& diag = AddError(src) << inst->FriendlyName() << ": "; |
| |
| if (!block_stack_.IsEmpty()) { |
| AddNote(block_stack_.Back()) << "in block"; |
| |
| // Adding the note may trigger a resize and invalidate the error diagnostic reference, so we |
| // need to get a new reference to the error diagnostic here. |
| return *(diag_.end() - 2); |
| } |
| return diag; |
| } |
| |
| diag::Diagnostic& Structural::AddResultError(const Instruction* inst, size_t idx) { |
| auto src = |
| Disassemble().ResultSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)}); |
| auto& diag = AddError(src) << inst->FriendlyName() << ": "; |
| |
| if (!block_stack_.IsEmpty()) { |
| AddNote(block_stack_.Back()) << "in block"; |
| |
| // Adding the note may trigger a resize and invalidate the error diagnostic reference, so we |
| // need to get a new reference to the error diagnostic here. |
| return *(diag_.end() - 2); |
| } |
| return diag; |
| } |
| |
| diag::Diagnostic& Structural::AddError(const Block* blk) { |
| auto src = Disassemble().BlockSource(blk); |
| return AddError(src); |
| } |
| |
| diag::Diagnostic& Structural::AddError(const BlockParam* param) { |
| auto src = Disassemble().BlockParamSource(param); |
| return AddError(src); |
| } |
| |
| diag::Diagnostic& Structural::AddError(const Function* func) { |
| auto src = Disassemble().FunctionSource(func); |
| return AddError(src); |
| } |
| |
| diag::Diagnostic& Structural::AddError(const FunctionParam* param) { |
| auto src = Disassemble().FunctionParamSource(param); |
| return AddError(src); |
| } |
| |
| diag::Diagnostic& Structural::AddError(const CastableBase* base) { |
| diag::Diagnostic* diag = nullptr; |
| tint::Switch( |
| base, // |
| [&](const Block* block) { diag = &AddError(block); }, |
| [&](const BlockParam* param) { diag = &AddError(param); }, |
| [&](const Function* fn) { diag = &AddError(fn); }, |
| [&](const FunctionParam* param) { diag = &AddError(param); }, |
| [&](const Instruction* inst) { diag = &AddError(inst); }, |
| [&](const InstructionResult* res) { diag = &AddError(res); }); |
| TINT_ASSERT(diag); |
| return *diag; |
| } |
| |
| diag::Diagnostic& Structural::AddNote(const Instruction* inst) { |
| auto src = Disassemble().InstructionSource(inst); |
| return AddNote(src); |
| } |
| |
| diag::Diagnostic& Structural::AddNote(const Function* func) { |
| auto src = Disassemble().FunctionSource(func); |
| return AddNote(src); |
| } |
| |
| diag::Diagnostic& Structural::AddOperandNote(const Instruction* inst, size_t idx) { |
| auto src = |
| Disassemble().OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)}); |
| return AddNote(src); |
| } |
| |
| diag::Diagnostic& Structural::AddResultNote(const Instruction* inst, size_t idx) { |
| auto src = |
| Disassemble().ResultSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)}); |
| return AddNote(src); |
| } |
| |
| diag::Diagnostic& Structural::AddNote(const Block* blk) { |
| auto src = Disassemble().BlockSource(blk); |
| return AddNote(src); |
| } |
| |
| diag::Diagnostic& Structural::AddError(Source src) { |
| auto& diag = diag_.AddError(src); |
| diag.owned_file = Disassemble().File(); |
| return diag; |
| } |
| |
| diag::Diagnostic& Structural::AddNote(Source src) { |
| auto& diag = diag_.AddNote(src); |
| diag.owned_file = Disassemble().File(); |
| return diag; |
| } |
| |
| void Structural::AddDeclarationNote(const CastableBase* decl) { |
| tint::Switch( |
| decl, // |
| [&](const Block* block) { AddDeclarationNote(block); }, |
| [&](const BlockParam* param) { AddDeclarationNote(param); }, |
| [&](const Function* fn) { AddDeclarationNote(fn); }, |
| [&](const FunctionParam* param) { AddDeclarationNote(param); }, |
| [&](const Instruction* inst) { AddDeclarationNote(inst); }, |
| [&](const InstructionResult* res) { AddDeclarationNote(res); }); |
| } |
| |
| void Structural::AddDeclarationNote(const Block* block) { |
| auto src = Disassemble().BlockSource(block); |
| if (src.file) { |
| AddNote(src) << NameOf(block) << " declared here"; |
| } |
| } |
| |
| void Structural::AddDeclarationNote(const BlockParam* param) { |
| auto src = Disassemble().BlockParamSource(param); |
| if (src.file) { |
| AddNote(src) << NameOf(param) << " declared here"; |
| } |
| } |
| |
| void Structural::AddDeclarationNote(const Function* fn) { |
| AddNote(fn) << NameOf(fn) << " declared here"; |
| } |
| |
| void Structural::AddDeclarationNote(const FunctionParam* param) { |
| auto src = Disassemble().FunctionParamSource(param); |
| if (src.file) { |
| AddNote(src) << NameOf(param) << " declared here"; |
| } |
| } |
| |
| void Structural::AddDeclarationNote(const Instruction* inst) { |
| auto src = Disassemble().InstructionSource(inst); |
| if (src.file) { |
| AddNote(src) << NameOf(inst) << " declared here"; |
| } |
| } |
| |
| void Structural::AddDeclarationNote(const InstructionResult* res) { |
| if (auto* inst = res->Instruction()) { |
| auto results = inst->Results(); |
| for (size_t i = 0; i < results.Length(); i++) { |
| if (results[i] == res) { |
| AddResultNote(res->Instruction(), i) << NameOf(res) << " declared here"; |
| return; |
| } |
| } |
| } |
| } |
| |
| StyledText Structural::NameOf(const CastableBase* decl) { |
| return tint::Switch( |
| decl, // |
| [&](const core::type::Type* ty) { return NameOf(ty); }, |
| [&](const Value* value) { return NameOf(value); }, |
| [&](const Instruction* inst) { return NameOf(inst); }, |
| [&](const Block* block) { return NameOf(block); }, // |
| TINT_ICE_ON_NO_MATCH); |
| } |
| |
| StyledText Structural::NameOf(const core::type::Type* ty) { |
| auto name = ty ? ty->FriendlyName() : "undef"; |
| return StyledText{} << style::Type(name); |
| } |
| |
| StyledText Structural::NameOf(const Value* value) { |
| return Disassemble().NameOf(value); |
| } |
| |
| StyledText Structural::NameOf(const Instruction* inst) { |
| auto name = inst ? inst->FriendlyName() : "undef"; |
| return StyledText{} << style::Instruction(name); |
| } |
| |
| StyledText Structural::NameOf(const Block* block) { |
| auto parent_name = block->Parent() ? block->Parent()->FriendlyName() : "undef"; |
| return StyledText{} << style::Instruction(parent_name) << " block " |
| << Disassemble().NameOf(block); |
| } |
| |
| bool Structural::CheckResult(const Instruction* inst, size_t idx) { |
| auto* result = inst->Result(idx); |
| if (DAWN_UNLIKELY(result == nullptr)) { |
| AddResultError(inst, idx) << "result is undefined"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(result->Type() == nullptr)) { |
| AddResultError(inst, idx) << "result type is undefined"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(result->Instruction() == nullptr)) { |
| AddResultError(inst, idx) << "result instruction is undefined"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(result->Instruction() != inst)) { |
| AddResultError(inst, idx) |
| << "result instruction does not match instruction (possible double usage)"; |
| return false; |
| } |
| |
| if (!inst->Is<core::ir::Call>() && result->Type()->Is<core::type::Void>()) { |
| AddResultError(inst, idx) << "result type cannot be void"; |
| return false; |
| } |
| |
| if (inst->Is<core::ir::ControlInstruction>()) { |
| if (result->Type()->Is<core::type::Pointer>()) { |
| AddResultError(inst, idx) << "result type cannot be a pointer"; |
| return false; |
| } |
| if (!result->Type()->IsConstructible()) { |
| AddResultError(inst, idx) << "result type must be constructable"; |
| return false; |
| } |
| } |
| |
| if (result->Type()->Is<core::type::Void>() && ir_.NameOf(result)) { |
| AddResultError(inst, idx) << "void results must not have names"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Structural::CheckResults(const ir::Instruction* inst, std::optional<size_t> count = {}) { |
| if (count.has_value()) { |
| if (DAWN_UNLIKELY(inst->Results().Length() != count.value())) { |
| AddError(inst) << "expected exactly " << count.value() << " results, got " |
| << inst->Results().Length(); |
| return false; |
| } |
| } |
| |
| bool passed = true; |
| Hashset<const InstructionResult*, 4> seen_instruction_results; |
| for (size_t i = 0; i < inst->Results().Length(); i++) { |
| if (DAWN_UNLIKELY(!CheckResult(inst, i))) { |
| passed = false; |
| } |
| |
| if (!seen_instruction_results.Add(inst->Result(i))) { |
| AddResultError(inst, i) << "result was seen previously as a result"; |
| passed = false; |
| } |
| } |
| return passed; |
| } |
| |
| bool Structural::CheckOperand(const Instruction* inst, size_t idx) { |
| auto* operand = inst->Operand(idx); |
| |
| if (DAWN_UNLIKELY(operand == nullptr)) { |
| // var instructions are allowed to have a nullptr initializers. |
| // terminator instructions use nullptr operands to signal 'undef'. |
| if (inst->IsAnyOf<Terminator, Var>()) { |
| return true; |
| } |
| |
| AddError(inst, idx) << "operand is undefined"; |
| return false; |
| } |
| |
| // ir::Unused is a internal value used by some transforms to track unused entries, and is |
| // removed as part of generating an output shader. |
| if (DAWN_UNLIKELY(operand->Is<ir::Unused>())) { |
| return true; |
| } |
| |
| if (DAWN_UNLIKELY(operand->Type() == nullptr)) { |
| AddError(inst, idx) << "operand type is undefined"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(!operand->Alive())) { |
| AddError(inst, idx) << "operand is not alive"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(operand->Is<Constant>() && |
| operand->Type()->Is<core::type::SubgroupMatrix>())) { |
| AddError(inst, idx) << "subgroup_matrix values cannot be constant"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(!operand->HasUsage(inst, idx))) { |
| AddError(inst, idx) << "operand missing usage"; |
| return false; |
| } |
| |
| if (auto fn = operand->As<Function>(); fn && !all_functions_.Contains(fn)) { |
| AddError(inst, idx) << NameOf(operand) << " is not part of the module"; |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(!operand->Is<ir::Unused>() && !operand->Is<Constant>() && |
| !scope_stack_.Contains(operand))) { |
| AddError(inst, idx) << NameOf(operand) << " is not in scope"; |
| AddDeclarationNote(operand); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool Structural::CheckOperands(const ir::Instruction* inst, |
| size_t min_count, |
| std::optional<size_t> max_count) { |
| if (DAWN_UNLIKELY(inst->Operands().Length() < min_count)) { |
| if (max_count.has_value()) { |
| AddError(inst) << "expected between " << min_count << " and " << max_count.value() |
| << " operands, got " << inst->Operands().Length(); |
| } else { |
| AddError(inst) << "expected at least " << min_count << " operands, got " |
| << inst->Operands().Length(); |
| } |
| return false; |
| } |
| |
| if (DAWN_UNLIKELY(max_count.has_value() && inst->Operands().Length() > max_count.value())) { |
| AddError(inst) << "expected between " << min_count << " and " << max_count.value() |
| << " operands, got " << inst->Operands().Length(); |
| return false; |
| } |
| |
| bool passed = true; |
| for (size_t i = 0; i < inst->Operands().Length(); i++) { |
| if (DAWN_UNLIKELY(!CheckOperand(inst, i))) { |
| passed = false; |
| } |
| } |
| return passed; |
| } |
| |
| bool Structural::CheckOperands(const ir::Instruction* inst, std::optional<size_t> count = {}) { |
| if (count.has_value()) { |
| if (DAWN_UNLIKELY(inst->Operands().Length() != count.value())) { |
| AddError(inst) << "expected exactly " << count.value() << " operands, got " |
| << inst->Operands().Length(); |
| return false; |
| } |
| } |
| |
| bool passed = true; |
| for (size_t i = 0; i < inst->Operands().Length(); i++) { |
| if (DAWN_UNLIKELY(!CheckOperand(inst, i))) { |
| passed = false; |
| } |
| } |
| return passed; |
| } |
| |
| bool Structural::CheckResultsAndOperandRange(const ir::Instruction* inst, |
| size_t num_results, |
| size_t min_operands, |
| std::optional<size_t> max_operands = {}) { |
| // Intentionally avoiding short-circuiting here |
| bool results_passed = CheckResults(inst, num_results); |
| bool operands_passed = CheckOperands(inst, min_operands, max_operands); |
| return results_passed && operands_passed; |
| } |
| |
| bool Structural::CheckResultsAndOperands(const ir::Instruction* inst, |
| size_t num_results, |
| size_t num_operands) { |
| // Intentionally avoiding short-circuiting here |
| bool results_passed = CheckResults(inst, num_results); |
| bool operands_passed = CheckOperands(inst, num_operands); |
| return results_passed && operands_passed; |
| } |
| |
| void Structural::CheckType(const core::type::Type* root, std::function<diag::Diagnostic&()> diag) { |
| if (root == nullptr) { |
| return; |
| } |
| |
| if (!already_validated_types_.Add(root)) { |
| return; |
| } |
| |
| if (!ir_.properties.Contains(Property::kAllowNonCoreTypes) && !root->IsCore()) { |
| diag() << "non-core types not allowed in core IR"; |
| return; |
| } |
| |
| AddressSpace addrspace = AddressSpace::kUndefined; |
| if (auto* mv = root->As<core::type::MemoryView>()) { |
| addrspace = mv->AddressSpace(); |
| } |
| |
| Vector<const core::type::Type*, 8> stack{root}; |
| Hashset<const core::type::Type*, 8> seen{}; |
| while (!stack.IsEmpty()) { |
| auto ty = stack.Pop(); |
| if (!ty) { |
| continue; |
| } |
| if (ty->IsAbstract()) { |
| diag() << "abstracts are not permitted"; |
| return; |
| } |
| |
| bool chk = tint::Switch( |
| ty, // |
| [&](const core::type::Struct* str) { return CheckStruct(str, diag); }, |
| [&](const core::type::Reference* ref) { return CheckRef(ref, diag, root); }, |
| [&](const core::type::Pointer* ptr) { return CheckPtr(ptr, diag); }, |
| [&](const core::type::I8*) { return Check8BitInteger(diag); }, |
| [&](const core::type::U8*) { return Check8BitInteger(diag); }, |
| [&](const core::type::U16*) { return Check16BitInteger(diag); }, |
| [&](const core::type::U64*) { return Check64BitInteger(diag); }, |
| [&](const core::type::F16*) { return Check16BitFloat(diag); }, |
| [&](const core::type::Array* arr) { return CheckArray(arr, diag); }, |
| [&](const core::type::Vector* v) { return CheckVector(v, diag); }, |
| [&](const core::type::Matrix* m) { return CheckMatrix(m, diag); }, |
| [&](const core::type::Atomic* a) { return CheckAtomic(a, diag); }, |
| [&](const core::type::SampledTexture* s) { return CheckSampledTexture(s, diag); }, |
| [&](const core::type::MultisampledTexture* ms) { |
| return CheckMultisampledTexture(ms, diag); |
| }, |
| [&](const core::type::StorageTexture* s) { return CheckStorageTexture(s, diag); }, |
| [&](const core::type::InputAttachment* i) { return CheckInputAttachment(i, diag); }, |
| [&](const core::type::SubgroupMatrix* m) { |
| return CheckSubgroupMatrix(m, diag, addrspace); |
| }, |
| [&](const core::type::BindingArray* t) { |
| return CheckBindingArray(t, diag, addrspace); |
| }, |
| [&](const core::type::Buffer* buf) { return CheckBuffer(buf, diag); }, |
| [](Default) { return true; }); |
| if (!chk) { |
| return; |
| } |
| |
| if (auto* view = ty->As<core::type::MemoryView>()) { |
| if (seen.Add(view->StoreType())) { |
| stack.Push(view->StoreType()); |
| } |
| continue; |
| } |
| |
| // Visit the elements of a composite type. |
| auto type_count = ty->Elements(); |
| if (type_count.type) { |
| // Every element has the same type (e.g. array, vector, matrix, ...), so validate that |
| // type once if it has not been seen before. |
| if (seen.Add(type_count.type)) { |
| stack.Push(type_count.type); |
| } |
| continue; |
| } |
| |
| // Different elements have different types (e.g. a struct), so we need to validate each |
| // of them if they have not been seen before. |
| for (uint32_t i = 0; i < type_count.count; i++) { |
| if (auto* subtype = ty->Element(i)) { |
| if (seen.Add(subtype)) { |
| stack.Push(subtype); |
| } |
| } |
| } |
| } |
| } |
| |
| bool Structural::CheckBuffer(const core::type::Buffer* buf, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!ir_.properties.Contains(Property::kAllowBufferTypes)) { |
| diag() << "buffer types are not allowed in this context"; |
| return false; |
| } |
| if (auto count = buf->ConstantCount()) { |
| const bool allow_16_bits = ir_.properties.Contains(Property::kAllow16BitFloats) || |
| ir_.properties.Contains(Property::kAllow16BitIntegers); |
| const uint32_t divisor = allow_16_bits ? 2 : 4; |
| if (count.value() % divisor != 0) { |
| diag() << "buffer size must be evenly divisible by " << divisor; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool Structural::CheckBindingArray(const core::type::BindingArray* ba, |
| std::function<diag::Diagnostic&()>& diag, |
| core::AddressSpace addrspace) { |
| if (!ba->Count()->Is<core::type::ConstantArrayCount>()) { |
| diag() << "binding_array count must be a constant expression"; |
| return false; |
| } |
| |
| auto count = ba->Count()->As<core::type::ConstantArrayCount>()->value; |
| if (count == 0) { |
| diag() << "binding array requires a constant array size > 0"; |
| return false; |
| } |
| |
| if (!(addrspace == AddressSpace::kUndefined || addrspace == AddressSpace::kHandle) && |
| !ir_.properties.Contains(Property::kAllowMslEntryPointInterface)) { |
| diag() << "invalid address space for binding_array : " << addrspace; |
| return false; |
| } |
| |
| if (!ir_.properties.Contains(Property::kAllowNonCoreTypes)) { |
| if (!ba->ElemType()->Is<core::type::SampledTexture>()) { |
| diag() << "binding_array element type must be a sampled texture type"; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool Structural::CheckSubgroupMatrix(const core::type::SubgroupMatrix* m, |
| std::function<diag::Diagnostic&()>& diag, |
| core::AddressSpace addrspace) { |
| if (!m->Type() |
| ->IsAnyOf<core::type::F16, core::type::F32, core::type::I8, core::type::I32, |
| core::type::U8, core::type::U32>()) { |
| diag() << "invalid subgroup matrix component type: " << NameOf(m->Type()); |
| return false; |
| } |
| if (!(addrspace == AddressSpace::kUndefined || addrspace == AddressSpace::kFunction)) { |
| diag() << "invalid address space for subgroup matrix : " << addrspace; |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckInputAttachment(const core::type::InputAttachment* ia, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!ia->Type()->IsAnyOf<core::type::F32, core::type::I32, core::type::U32>()) { |
| diag() << "invalid input attachment component type: " << NameOf(ia->Type()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckStorageTexture(const core::type::StorageTexture* storage, |
| std::function<diag::Diagnostic&()>& diag) { |
| switch (storage->Dim()) { |
| case core::type::TextureDimension::kCube: |
| case core::type::TextureDimension::kCubeArray: |
| diag() << "dimension " << style::Literal(ToString(storage->Dim())) |
| << " for storage textures does not in WGSL yet"; |
| return false; |
| case core::type::TextureDimension::kNone: |
| diag() << "invalid texture dimension " << style::Literal(ToString(storage->Dim())); |
| return false; |
| default: |
| break; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckMultisampledTexture(const core::type::MultisampledTexture* ms, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!ms->Type()->IsAnyOf<core::type::F32, core::type::I32, core::type::U32>()) { |
| diag() << "invalid multisampled texture sample type: " << NameOf(ms->Type()); |
| return false; |
| } |
| |
| switch (ms->Dim()) { |
| case core::type::TextureDimension::k2d: |
| break; |
| default: |
| diag() << "invalid multisampled texture dimension: " |
| << style::Literal(ToString(ms->Dim())); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckSampledTexture(const core::type::SampledTexture* s, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!s->Type()->IsAnyOf<core::type::F32, core::type::I32, core::type::U32>()) { |
| diag() << "invalid sampled texture sample type: " << NameOf(s->Type()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckAtomic(const core::type::Atomic* atom, |
| std::function<diag::Diagnostic&()>& diag) { |
| // Prior to lowering we allow for atomic operations on vec2u to support the |
| // AtomicVec2UMinMax feature. |
| if (auto* vec = atom->Type()->As<core::type::Vector>()) { |
| if (vec->Width() == 2 && vec->Type()->Is<core::type::U32>()) { |
| return true; |
| } |
| } |
| |
| if (!atom->Type()->IsAnyOf<core::type::I32, core::type::U32, core::type::U64>()) { |
| diag() << "atomic subtype must be i32, u32 or u64 type is " << NameOf(atom->Type()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckMatrix(const core::type::Matrix* mat, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!mat->Type()->IsFloatScalar()) { |
| diag() << "matrix elements, " << NameOf(mat) << ", must be float scalars"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckVector(const core::type::Vector* vec, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!vec->Type()->IsScalar()) { |
| diag() << "vector elements, " << NameOf(vec) << ", must be scalars"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckArray(const core::type::Array* arr, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (!arr->ElemType()->HasCreationFixedFootprint()) { |
| diag() << "array elements, " << NameOf(arr) << ", must have creation-fixed footprint"; |
| return false; |
| } |
| if (auto* count = arr->Count()->As<core::type::ConstantArrayCount>()) { |
| if (count->value == 0) { |
| diag() << "array requires a constant array size > 0"; |
| return false; |
| } |
| return true; |
| } |
| |
| if (auto* val_count = arr->Count()->As<core::ir::type::ValueArrayCount>()) { |
| if (!val_count->value->Alive()) { |
| diag() << "ValueArrayCount value is not alive"; |
| return false; |
| } |
| if (!val_count->value->Type()->IsIntegerScalar()) { |
| diag() << "ValueArrayCount must be an integer scalar type"; |
| return false; |
| } |
| auto* inst_res = val_count->value->As<core::ir::InstructionResult>(); |
| if (!inst_res) { |
| diag() << "ValueArrayCount must be an instruction result"; |
| return false; |
| } |
| auto* inst = inst_res->Instruction(); |
| if (!inst || inst->Block() != ir_.root_block) { |
| diag() << "ValueArrayCount must be a module-scoped override expression"; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // 8-bit integer types are guarded by the Allow8BitIntegers property. |
| // They can be used as the component type of a subgroup matrix without the property. |
| bool Structural::Check8BitInteger(std::function<diag::Diagnostic&()>& diag) { |
| if (!ir_.properties.Contains(Property::kAllow8BitIntegers)) { |
| diag() << "8-bit integer types are not permitted"; |
| return false; |
| } |
| return true; |
| } |
| |
| // 16-bit integer types are guarded by the Allow16BitIntegers property. |
| bool Structural::Check16BitInteger(std::function<diag::Diagnostic&()>& diag) { |
| if (!ir_.properties.Contains(Property::kAllow16BitIntegers)) { |
| diag() << "16-bit integer types are not permitted"; |
| return false; |
| } |
| return true; |
| } |
| |
| // 64-bit integer types are guarded by the Allow64BitIntegers property. |
| bool Structural::Check64BitInteger(std::function<diag::Diagnostic&()>& diag) { |
| if (!ir_.properties.Contains(Property::kAllow64BitIntegers)) { |
| diag() << "64-bit integer types are not permitted"; |
| return false; |
| } |
| return true; |
| } |
| |
| // 16-bit float types are guarded by the Allow16BitFloats property. |
| bool Structural::Check16BitFloat(std::function<diag::Diagnostic&()>& diag) { |
| if (!ir_.properties.Contains(Property::kAllow16BitFloats)) { |
| diag() << "16-bit float types are not permitted"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckPtr(const core::type::Pointer* ptr, |
| std::function<diag::Diagnostic&()>& diag) { |
| if (ptr->StoreType()->Is<core::type::Void>()) { |
| diag() << "pointers to void are not permitted"; |
| return false; |
| } |
| |
| if (ptr->AddressSpace() == AddressSpace::kUniform || |
| ptr->AddressSpace() == AddressSpace::kHandle || |
| ptr->AddressSpace() == core::AddressSpace::kImmediate) { |
| if (ptr->Access() != core::Access::kRead) { |
| diag() << ToString(ptr->AddressSpace()) << " pointers must be read access"; |
| return false; |
| } |
| } |
| |
| if (ptr->AddressSpace() == AddressSpace::kWorkgroup || |
| ptr->AddressSpace() == AddressSpace::kFunction || |
| ptr->AddressSpace() == AddressSpace::kPrivate) { |
| if (ptr->Access() != core::Access::kReadWrite) { |
| diag() << ToString(ptr->AddressSpace()) << " pointers must be read_write access"; |
| return false; |
| } |
| } |
| |
| if (ptr->AddressSpace() == AddressSpace::kHandle) { |
| if (!ptr->StoreType()->IsHandle()) { |
| diag() << "the 'handle' address space can only be used for handle types"; |
| return false; |
| } |
| } else if (ptr->StoreType()->IsHandle()) { |
| diag() << "handle types can only be declared in the 'handle' address space"; |
| return false; |
| } |
| |
| if (ptr->StoreType()->Is<core::type::Pointer>()) { |
| diag() << "pointers to pointers are not allowed"; |
| return false; |
| } |
| |
| if (ptr->StoreType()->Is<core::type::Buffer>()) { |
| if (ptr->AddressSpace() != AddressSpace::kWorkgroup && |
| ptr->AddressSpace() != AddressSpace::kStorage && |
| ptr->AddressSpace() != AddressSpace::kUniform) { |
| diag() << "buffer types are not allowed in the '" << ToString(ptr->AddressSpace()) |
| << "' address space"; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool Structural::CheckRef(const core::type::Reference* ref, |
| std::function<diag::Diagnostic&()>& diag, |
| const core::type::Type* root) { |
| if (ref->StoreType()->Is<core::type::Void>()) { |
| diag() << "references to void are not permitted"; |
| return false; |
| } |
| |
| // Reference types are guarded by the AllowRefTypes property. |
| if (!ir_.properties.Contains(Property::kAllowRefTypes)) { |
| diag() << "reference types are not permitted here"; |
| return false; |
| } |
| // If they are allowed, reference types still cannot be nested. |
| if (ref != root) { |
| diag() << "nested reference types are not permitted"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool Structural::CheckStruct(const core::type::Struct* str, |
| std::function<diag::Diagnostic&()>& diag) { |
| uint32_t cur_offset = 0; |
| for (auto* member : str->Members()) { |
| if (member->Type()->Is<core::type::Void>()) { |
| diag() << "struct member " << member->Index() << " cannot have void type"; |
| return false; |
| } |
| if (member->Type()->Is<core::type::Buffer>()) { |
| diag() << "struct member " << member->Index() << " cannot have buffer type"; |
| return false; |
| } |
| |
| if (!CheckStructMemberAttributes(member, diag)) { |
| return false; |
| } |
| |
| if (!ir_.properties.Contains(Property::kAllowMslEntryPointInterface)) { |
| if (member->Type()->Is<core::type::Pointer>()) { |
| diag() << "struct member " << member->Index() << " cannot be a pointer type"; |
| return false; |
| } |
| |
| if (member->Type()->Is<core::type::Texture>()) { |
| diag() << "struct member " << member->Index() << " cannot be a texture type"; |
| return false; |
| } |
| |
| if (member->Type()->Is<core::type::Sampler>()) { |
| diag() << "struct member " << member->Index() << " cannot be a sampler type"; |
| return false; |
| } |
| } |
| |
| if (auto* arr = member->Type()->As<core::type::Array>(); |
| arr && arr->Count()->Is<core::type::RuntimeArrayCount>()) { |
| if (member != str->Members().Back()) { |
| diag() << "runtime-sized arrays can only be the last member of a " |
| "struct"; |
| return false; |
| } |
| } |
| |
| if (member->Align() == 0) { |
| diag() << "struct member must not have an alignment of 0"; |
| return false; |
| } |
| if (!tint::IsPowerOfTwo(member->Align())) { |
| diag() << "struct member alignment must be a power of 2"; |
| return false; |
| } |
| |
| if (member->Type()->Align() == 0) { |
| diag() << "struct member type must not have an alignment of 0"; |
| return false; |
| } |
| if (!tint::IsPowerOfTwo(member->Type()->Align())) { |
| diag() << "struct member type alignment must be a power of 2"; |
| return false; |
| } |
| if (ir_.properties.Contains(Property::kAllowStructMatrixDecorations)) { |
| if (member->RowMajor() || member->HasMatrixStride()) { |
| const core::type::Type* base_ty = member->Type(); |
| while (auto* arr = base_ty->As<core::type::Array>()) { |
| base_ty = arr->ElemType(); |
| } |
| if (!base_ty->Is<core::type::Matrix>()) { |
| if (member->RowMajor()) { |
| diag() << "RowMajor attribute can only be applied to a matrix or an array " |
| "of matrices"; |
| } else { |
| diag() << "MatrixStride attribute can only be applied to a matrix or an " |
| "array of matrices"; |
| } |
| return false; |
| } |
| } |
| } else { |
| if (member->RowMajor()) { |
| diag() << "Row major annotation not allowed on structures"; |
| return false; |
| } |
| if (member->HasMatrixStride()) { |
| diag() << "Matrix stride annotation not allowed on structures"; |
| return false; |
| } |
| } |
| |
| // TODO(448608979): Remove guard once updated to handle RowMajor correctly |
| if (!member->RowMajor()) { |
| if (member->Size() < member->Type()->Size()) { |
| diag() << "struct member " << member->Index() << " with size=" << member->Size() |
| << " must be at least as large as the type with size " |
| << member->Type()->Size(); |
| return false; |
| } |
| |
| if (member->Align() % member->Type()->Align() != 0) { |
| diag() << "struct member alignment (" << member->Align() |
| << ") must be divisible by type alignment (" << member->Type()->Align() |
| << ")"; |
| return false; |
| } |
| } |
| |
| cur_offset += (member->Offset() - cur_offset) + member->MinimumRequiredSize(); |
| } |
| if (str->Size() < cur_offset) { |
| diag() << "struct size (" << str->Size() << ") is smaller than the end of the last member (" |
| << cur_offset << ")"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void Structural::CheckRootBlock(const Block* blk) { |
| block_stack_.Push(blk); |
| TINT_DEFER(block_stack_.Pop()); |
| |
| Hashset<const core::ir::Value*, 8> pipeline_evaluatable{}; |
| |
| auto add_evaluatable = [&](const Instruction* inst, const bool is_creatable) { |
| if (auto* res = inst->Result(0); res != nullptr && is_creatable) { |
| pipeline_evaluatable.Add(res); |
| } |
| }; |
| |
| for (auto* inst : *blk) { |
| if (inst->Block() != blk) { |
| AddError(inst) << "instruction in root block does not have root block as parent"; |
| continue; |
| } |
| |
| auto is_pipeline_creatable = true; |
| for (auto* op : inst->Operands()) { |
| if (!op) { |
| continue; |
| } |
| if (op->Is<core::ir::Constant>()) { |
| continue; |
| } |
| if (pipeline_evaluatable.Contains(op)) { |
| continue; |
| } |
| is_pipeline_creatable = false; |
| break; |
| } |
| |
| if (!is_pipeline_creatable) { |
| AddError(inst) << "instruction is not evaluatable at pipeline creation time"; |
| } |
| |
| tint::Switch( |
| inst, // |
| [&](const core::ir::Override* o) { |
| if (ir_.properties.Contains(Property::kAllowOverrides)) { |
| CheckInstruction(o); |
| add_evaluatable(o, is_pipeline_creatable); |
| } else { |
| AddError(inst) << "root block: invalid instruction: " << inst->TypeInfo().name; |
| } |
| }, |
| [&](const core::ir::Var* var) { CheckInstruction(var); }, |
| [&](const core::ir::Let* let) { |
| if (ir_.properties.Contains(Property::kAllowModuleScopeLets)) { |
| CheckInstruction(let); |
| add_evaluatable(let, is_pipeline_creatable); |
| } else { |
| AddError(inst) << "root block: invalid instruction: " << inst->TypeInfo().name; |
| } |
| }, |
| [&](const core::ir::Construct* c) { |
| if (ir_.properties.Contains(Property::kAllowModuleScopeLets) || |
| ir_.properties.Contains(Property::kAllowOverrides)) { |
| CheckInstruction(c); |
| CheckOnlyUsedInRootBlock(inst); |
| add_evaluatable(c, is_pipeline_creatable); |
| } else { |
| AddError(inst) << "root block: invalid instruction: " << inst->TypeInfo().name; |
| } |
| }, |
| [&](Default) { |
| // Note, this validation around kAllowOverrides is looser than it could be. There |
| // are only certain expressions and builtins which can be used in an override, which |
| // currently isn't checked. |
| if (ir_.properties.Contains(Property::kAllowOverrides) && |
| inst->IsAnyOf<core::ir::Unary, core::ir::Binary, core::ir::BuiltinCall, |
| core::ir::Convert, core::ir::Swizzle, core::ir::Access, |
| core::ir::ConstExprIf>()) { |
| CheckInstruction(inst); |
| // If overrides are allowed we can have certain regular instructions in the root |
| // block, with the caveat that those instructions can _only_ be used in the root |
| // block. |
| CheckOnlyUsedInRootBlock(inst); |
| add_evaluatable(inst, is_pipeline_creatable); |
| } else { |
| AddError(inst) << "root block: invalid instruction: " << inst->TypeInfo().name; |
| } |
| }); |
| |
| // Process tasks queued by CheckInstruction (like AddResults) before moving to next |
| // instruction. |
| ProcessTasks(); |
| } |
| } |
| |
| void Structural::CheckOnlyUsedInRootBlock(const Instruction* inst) { |
| if (inst->Result(0)) { |
| for (auto& usage : inst->Result(0)->UsagesSorted()) { |
| if (usage.instruction->Block() != ir_.root_block) { |
| AddError(inst) << "root block: instruction used outside of root block " |
| << inst->TypeInfo().name; |
| } |
| } |
| } |
| |
| CheckInstruction(inst); |
| } |
| |
| void Structural::CheckFunction(const Function* func) { |
| // Scope holds the parameters and block |
| scope_stack_.Push(); |
| TINT_DEFER(scope_stack_.Pop()); |
| |
| // The recursion checks require this to be true as it will be asserted by the |
| // referenced_functions helper. |
| func->ForEachUseUnsorted([&](const Usage& use) { |
| if (use.instruction->As<UserCall>() || use.instruction->As<Return>()) { |
| return; |
| } |
| AddError(use.instruction, use.operand_index) << "function may not be used as a operand"; |
| }); |
| |
| if (!func->Type() || !func->Type()->Is<core::type::Function>()) { |
| AddError(func) << "functions must have type '<function>'"; |
| return; |
| } |
| |
| // Note: This is not a validator error because Function::SetBlock() asserts that the block is |
| // not null, and the disassembler will crash if this is not null. This should only be hit due |
| // to some sort of corruption, not a bad shader/programmer error. |
| TINT_ASSERT(func->Block()) << "root block for function is undefined"; |
| |
| if (func->Block()->Is<ir::MultiInBlock>()) { |
| AddError(func) << "root block for function cannot be a multi-in block"; |
| return; |
| } |
| |
| Hashset<const FunctionParam*, 4> param_set{}; |
| for (auto* param : func->Params()) { |
| if (!CheckFunctionParam(func, param, param_set)) { |
| return; |
| } |
| |
| scope_stack_.Add(param); |
| } |
| |
| // TODO(516717234): Move to functional |
| CheckType(func->ReturnType(), [&]() -> diag::Diagnostic& { return AddError(func); }); |
| |
| // TODO(516717234): Determine what below to move to function. |
| |
| ValidateIOAttributes(func); |
| CheckWorkgroupSize(func); |
| CheckSubgroupSize(func); |
| |
| CheckEntryPoint(func); |
| |
| QueueBlock(func->Block()); |
| ProcessTasks(); |
| } |
| |
| void Structural::CheckEntryPoint(const Function* func) { |
| if (!func->IsEntryPoint()) { |
| return; |
| } |
| |
| ValidateShaderIOAnnotations(func, func->ReturnType(), std::nullopt, func->ReturnAttributes(), |
| ShaderIOKind::kResultValue); |
| |
| WalkTypeAndMembers(func, func->ReturnType(), func->ReturnAttributes(), |
| [this](const Function* f, const core::type::Type* t, const IOAttributes&) { |
| CheckNotBool(f, t, "entry point returns can not be 'bool'"); |
| }); |
| |
| for (auto var : referenced_module_vars_.TransitiveReferences(func)) { |
| const auto* mv = var->Result()->Type()->As<core::type::MemoryView>(); |
| const auto* ty = var->Result()->Type()->UnwrapPtrOrRef(); |
| const auto attr = var->Attributes(); |
| if (!mv || !ty) { |
| continue; |
| } |
| |
| switch (mv->AddressSpace()) { |
| case AddressSpace::kIn: |
| case AddressSpace::kOut: |
| break; |
| default: |
| continue; |
| } |
| |
| if (func->IsFragment() && mv->AddressSpace() == AddressSpace::kIn) { |
| WalkTypeAndMembers(var, ty, attr, [this](const auto* v, const auto* t, const auto& a) { |
| CheckFrontFacingIfBool(v, a, t, |
| "input address space values referenced by fragment shaders " |
| "can only be 'bool' if decorated with " |
| "@builtin(front_facing)"); |
| }); |
| } else { |
| WalkTypeAndMembers(var, ty, attr, [this](const auto* v, const auto* t, const auto&) { |
| CheckNotBool(v, t, |
| "IO address space values referenced by shader entry points can " |
| "only be 'bool' if in the input space, used only by fragment " |
| "shaders and decorated with @builtin(front_facing)"); |
| }); |
| } |
| } |
| } |
| |
| bool Structural::CheckFunctionParam(const Function* func, |
| const FunctionParam* param, |
| Hashset<const FunctionParam*, 4>& param_set) { |
| if (!param->Alive()) { |
| AddError(param) << "destroyed parameter found in function parameter list"; |
| return false; |
| } |
| |
| if (!param_set.Add(param)) { |
| AddError(param) << "function parameter is not unique"; |
| return false; |
| } |
| |
| if (!param->Type()) { |
| AddError(param) << "function parameter has nullptr type"; |
| return false; |
| } |
| |
| if (!param->Function()) { |
| AddError(param) << "function parameter has nullptr parent function"; |
| return false; |
| } |
| |
| if (param->Function() != func) { |
| AddError(param) << "function parameter has incorrect parent function"; |
| AddNote(param->Function()) << "parent function declared here"; |
| return false; |
| } |
| |
| // TODO(516717234): Move to functional |
| CheckType(param->Type(), [&]() -> diag::Diagnostic& { return AddError(param); }); |
| |
| // TODO(516717234): Move to functional |
| if (func->IsFragment()) { |
| WalkTypeAndMembers(param, param->Type(), param->Attributes(), |
| [this](const auto* p, const auto* t, const auto& a) { |
| CheckFrontFacingIfBool( |
| p, a, t, |
| "fragment entry point params can only be a bool if " |
| "decorated with @builtin(front_facing)"); |
| }); |
| } else if (func->IsEntryPoint()) { |
| WalkTypeAndMembers( |
| param, param->Type(), param->Attributes(), |
| [this](const auto* p, const auto* t, const auto&) { |
| CheckNotBool(p, t, "entry point params can only be a bool for fragment shaders"); |
| }); |
| } |
| |
| // TODO(516717234): Move to functional |
| if (func->IsEntryPoint()) { |
| ValidateShaderIOAnnotations(param, param->Type(), param->BindingPoint(), |
| param->Attributes(), ShaderIOKind::kInputParam); |
| } else { |
| if (param->BindingPoint().has_value()) { |
| AddError(param) << "input param to non-entry point function has a binding point set"; |
| return false; |
| } |
| |
| if (param->Builtin().has_value()) { |
| AddError(param) << "builtins can only be decorated on entry point params"; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void Structural::ValidateIOAttributes(const Function* func) { |
| const auto stage = func->Stage(); |
| struct Task { |
| const CastableBase* anchor; |
| const core::type::Type* type; |
| const IOAttributes& attr; |
| IODirection dir; |
| ShaderIOKind io_kind; |
| }; |
| Vector<Task, 16> tasks; |
| |
| // Gather parameters. |
| for (auto* param : func->Params()) { |
| tasks.Push({param, param->Type(), param->Attributes(), IODirection::kInput, |
| ShaderIOKind::kInputParam}); |
| } |
| |
| // Gather return value. |
| tasks.Push({func, func->ReturnType(), func->ReturnAttributes(), IODirection::kOutput, |
| ShaderIOKind::kResultValue}); |
| |
| // Gather referenced module variables. |
| for (auto* var : referenced_module_vars_.TransitiveReferences(func)) { |
| auto* mv = var->Result()->Type()->As<core::type::MemoryView>(); |
| if (mv == nullptr) { |
| continue; |
| } |
| if (mv->AddressSpace() == AddressSpace::kIn || mv->AddressSpace() == AddressSpace::kOut || |
| mv->AddressSpace() == AddressSpace::kHandle) { |
| tasks.Push({var, mv->StoreType(), var->Attributes(), |
| validator::IODirectionFor(mv->AddressSpace()), |
| ShaderIOKind::kModuleScopeVar}); |
| } |
| } |
| |
| if (stage != Function::PipelineStage::kUndefined) { |
| // Shared context for blend_src and location validation |
| BlendSrcContext input_ctx{func->Stage(), {}, {}, nullptr, IODirection::kInput}; |
| BlendSrcContext output_ctx{func->Stage(), {}, {}, nullptr, IODirection::kOutput}; |
| |
| // First pass: pre-populate location hashes for blend_src. |
| for (const auto& task : tasks) { |
| auto& ctx = task.dir == IODirection::kInput ? input_ctx : output_ctx; |
| WalkTypeAndMembers( |
| ctx, task.type, task.attr, |
| [task](BlendSrcContext& c, const core::type::Type*, const IOAttributes& a) { |
| if (a.blend_src.has_value() && a.location.has_value()) { |
| c.locations.Add(a.location.value(), task.anchor); |
| } |
| }); |
| } |
| |
| // Second pass: validate blend_src usages. |
| for (const auto& task : tasks) { |
| auto& ctx = task.dir == IODirection::kInput ? input_ctx : output_ctx; |
| CheckBlendSrc(ctx, task.anchor, task.type, task.attr); |
| } |
| |
| if (!output_ctx.blend_srcs.IsEmpty()) { |
| if (output_ctx.blend_srcs.Count() != 2) { |
| AddError(func) << "if any @blend_src is used on an output, then @blend_src(0) and " |
| "@blend_src(1) must be used"; |
| } |
| } |
| |
| // Third pass: validate all non-blend_src location usages. |
| for (const auto& task : tasks) { |
| if (task.dir == IODirection::kInput) { |
| CheckLocation(input_ctx.locations, task.anchor, task.attr, func->Stage(), task.type, |
| task.dir); |
| } else if (task.dir == IODirection::kOutput) { |
| CheckLocation(output_ctx.locations, task.anchor, task.attr, func->Stage(), |
| task.type, task.dir); |
| } |
| } |
| } |
| |
| // Validate all the interpolation usages. |
| for (const auto& task : tasks) { |
| CheckInterpolation(task.anchor, task.type, task.attr, stage, task.dir); |
| } |
| |
| if (stage != Function::PipelineStage::kUndefined) { |
| // Validate all the binding_point usages, and ensure things that require binding_point have |
| // them. |
| for (const auto& task : tasks) { |
| CheckBindingPoint(task.anchor, task.type, task.attr, task.io_kind); |
| } |
| } |
| |
| IOAttributeContext impl_ctx{.input_builtins = {}, .output_builtins = {}}; |
| // Validate all remaining attributes on IO objects |
| for (const auto& task : tasks) { |
| ValidateIOAttributesImpl(impl_ctx, task.anchor, task.type, task.attr, stage, task.dir, |
| task.io_kind); |
| } |
| } |
| |
| void Structural::ValidateIOAttributesImpl(IOAttributeContext& ctx, |
| const CastableBase* msg_anchor, |
| const core::type::Type* ty, |
| const IOAttributes& attr, |
| Function::PipelineStage stage, |
| IODirection dir, |
| ShaderIOKind io_kind) { |
| bool skip_builtins = ir_.properties.Contains(Property::kAllowBackendSpecificShaderIO) && |
| io_kind == ShaderIOKind::kModuleScopeVar; |
| const IOAttributeUsage usage = IOAttributeUsageFor(stage, dir); |
| WalkTypeAndMembers( |
| *this, ty, attr, |
| [&ctx, msg_anchor, usage, io_kind, skip_builtins, dir]( |
| Structural& v, const core::type::Type* t, const IOAttributes& a) { |
| const auto checkers = IOAttributeCheckersFor(a, skip_builtins); |
| if (checkers.IsEmpty()) { |
| return; |
| } |
| |
| if (a.builtin.has_value() && !skip_builtins && |
| usage != IOAttributeUsage::kUndefinedUsage) { |
| const auto& builtin = a.builtin.value(); |
| |
| uint32_t count = 0; |
| switch (dir) { |
| case IODirection::kInput: |
| count = ++(ctx.input_builtins.GetOrAddZeroEntry(builtin).value); |
| break; |
| case IODirection::kOutput: |
| count = ++(ctx.output_builtins.GetOrAddZeroEntry(builtin).value); |
| break; |
| default: |
| // This shouldn't ever happen, but this will get caught later in the |
| // checker, so just ignoring |
| break; |
| } |
| if (v.ir_.properties.Contains(Property::kAllowClipDistancesOnF32ScalarAndVector) && |
| builtin == BuiltinValue::kClipDistances) { |
| if (count > 2) { |
| v.AddError(msg_anchor) |
| << "too many instances of builtin 'clip_distances' on entry point " |
| << ToString(dir) |
| << ", only two allowed with 'kAllowClipDistancesOnF32ScalarAndVector' " |
| "property enabled"; |
| } |
| } else { |
| if (count > 1) { |
| v.AddError(msg_anchor) |
| << "duplicate instance of builtin '" << ToString(builtin) |
| << "' on entry point " << ToString(dir) |
| << ", must be unique per entry point i/o direction"; |
| } |
| } |
| } |
| |
| auto failed = tint::Hashset<const IOAttributeChecker*, 4>(); |
| |
| if (usage != IOAttributeUsage::kUndefinedUsage) { |
| for (const auto* checker : checkers) { |
| if (!checker->valid_usages.Contains(usage)) { |
| failed.Add(checker); |
| |
| std::stringstream msg; |
| msg << ToString(checker->kind) << " IO attributes cannot be declared for a " |
| << ToString(usage) << ". "; |
| if (checker->valid_usages.Size() == 1) { |
| const auto& u = *checker->valid_usages.begin(); |
| msg << "They can only be used for a " << ToString(u) << "."; |
| } else { |
| msg << "They can only be used for " << ToString(checker->valid_usages); |
| } |
| v.AddError(msg_anchor) << msg.str(); |
| } |
| } |
| } |
| |
| for (const auto& checker : checkers) { |
| if (failed.Contains(checker)) { |
| continue; |
| } |
| |
| if (!checker->valid_io_kinds.Contains(io_kind)) { |
| failed.Add(checker); |
| |
| std::stringstream msg; |
| msg << ToString(checker->kind) << " IO attributes cannot be declared on a " |
| << ToString(io_kind) << ". "; |
| if (checker->valid_io_kinds.Size() == 1) { |
| const auto& k = *checker->valid_io_kinds.begin(); |
| msg << "They can only be used on a " << ToString(k) << "."; |
| } else { |
| msg << "They can only be used on " << ToString(checker->valid_io_kinds); |
| } |
| v.AddError(msg_anchor) << msg.str(); |
| } |
| } |
| |
| for (const auto& checker : checkers) { |
| if (failed.Contains(checker)) { |
| continue; |
| } |
| |
| if (!checker->type_check(t, v.ir_.properties)) { |
| failed.Add(checker); |
| v.AddError(msg_anchor) << ToString(checker->kind) << " " << checker->type_error; |
| } |
| } |
| |
| for (const auto& checker : checkers) { |
| if (failed.Contains(checker)) { |
| continue; |
| } |
| |
| if (auto res = checker->check(t, a, v.ir_.properties, usage); res != Success) { |
| failed.Add(checker); |
| v.AddError(msg_anchor) << res.Failure(); |
| } |
| } |
| }); |
| } |
| |
| void Structural::CheckFrontFacingIfBool(const CastableBase* msg_anchor, |
| const IOAttributes& attr, |
| const core::type::Type* ty, |
| const std::string& err) { |
| if (ty->Is<core::type::Bool>() && attr.builtin != BuiltinValue::kFrontFacing) { |
| AddError(msg_anchor) << err; |
| } |
| } |
| |
| void Structural::CheckNotBool(const CastableBase* msg_anchor, |
| const core::type::Type* ty, |
| const std::string& err) { |
| if (ty->Is<core::type::Bool>()) { |
| AddError(msg_anchor) << err; |
| } |
| } |
| |
| void Structural::CheckWorkgroupSize(const Function* func) { |
| if (!func->IsCompute()) { |
| if (func->WorkgroupSize().has_value()) { |
| AddError(func) << "@workgroup_size only valid on compute entry point"; |
| } |
| return; |
| } |
| |
| if (!func->WorkgroupSize().has_value()) { |
| AddError(func) << "compute entry point requires @workgroup_size"; |
| return; |
| } |
| |
| auto workgroup_sizes = func->WorkgroupSize().value(); |
| // The number parameters cannot be checked here, since it is stored internally as a 3 element |
| // array, so will always have 3 elements at this point. |
| TINT_ASSERT(workgroup_sizes.size() == 3); |
| |
| uint64_t total_size = 1; |
| |
| std::optional<const core::type::Type*> sizes_ty; |
| for (auto* size : workgroup_sizes) { |
| if (!size || !size->Type()) { |
| AddError(func) << "a @workgroup_size param is undefined or missing a type"; |
| return; |
| } |
| |
| auto* ty = size->Type(); |
| if (!ty->IsAnyOf<core::type::I32, core::type::U32>()) { |
| AddError(func) << "@workgroup_size params must be an 'i32' or 'u32', received " |
| << NameOf(ty); |
| return; |
| } |
| |
| if (!sizes_ty.has_value()) { |
| sizes_ty = ty; |
| } |
| |
| if (sizes_ty != ty) { |
| AddError(func) << "@workgroup_size params must be all 'i32's or all 'u32's"; |
| return; |
| } |
| |
| if (auto* c = size->As<ir::Constant>()) { |
| if (c->Value()->ValueAs<int64_t>() <= 0) { |
| AddError(func) << "@workgroup_size params must be greater than 0"; |
| return; |
| } |
| total_size *= c->Value()->ValueAs<uint64_t>(); |
| |
| constexpr uint64_t kMaxGridSize = 0xffffffff; |
| if (total_size > kMaxGridSize) { |
| AddError(func) << "workgroup grid size cannot exceed 0x" << std::hex |
| << kMaxGridSize; |
| } |
| continue; |
| } |
| |
| if (!ir_.properties.Contains(Property::kAllowOverrides)) { |
| AddError(func) << "@workgroup_size param is not a constant value, and IR property " |
| "'AllowOverrides' is not enabled"; |
| return; |
| } |
| |
| if (auto* r = size->As<ir::InstructionResult>()) { |
| if (!r->Instruction()) { |
| AddError(func) << "instruction for @workgroup_size param is not defined"; |
| return; |
| } |
| |
| if (r->Instruction()->Block() != ir_.root_block) { |
| AddError(func) << "@workgroup_size param defined by non-module scope value"; |
| return; |
| } |
| |
| // Since above, it is already checked if the value is in the root block, it is assumed |
| // to be pipeline creatable here, i.e. const/override or derived from consts and |
| // overrides. |
| // If that is not true, that indicates an issue in CheckRootBlock(). |
| continue; |
| } |
| |
| AddError(func) << "@workgroup_size must be an InstructionResult or a Constant"; |
| } |
| } |
| |
| void Structural::CheckSubgroupSize(const Function* func) { |
| // @subgroup_size is optional |
| if (!func->SubgroupSize().has_value()) { |
| return; |
| } |
| |
| if (!func->IsCompute()) { |
| AddError(func) << "@subgroup_size only valid on compute entry point"; |
| return; |
| } |
| |
| auto subgroup_size = func->SubgroupSize().value(); |
| if (subgroup_size == nullptr) { |
| AddError(func) << "a @subgroup_size param must have a value"; |
| return; |
| } |
| |
| if (!subgroup_size->Type()) { |
| AddError(func) << "a @subgroup_size param is missing a type"; |
| return; |
| } |
| |
| auto* ty = subgroup_size->Type(); |
| if (!ty->IsAnyOf<core::type::I32, core::type::U32>()) { |
| AddError(func) << "@subgroup_size param must be an 'i32' or 'u32', received " << NameOf(ty); |
| return; |
| } |
| |
| if (auto* c = subgroup_size->As<ir::Constant>()) { |
| int64_t value = c->Value()->ValueAs<int64_t>(); |
| if (value <= 0) { |
| AddError(func) << "@subgroup_size param must be greater than 0"; |
| return; |
| } |
| |
| if (!IsPowerOfTwo<int64_t>(value)) { |
| AddError(func) << "@subgroup_size param must be a power of 2"; |
| return; |
| } |
| |
| return; |
| } |
| |
| if (!ir_.properties.Contains(Property::kAllowOverrides)) { |
| AddError(func) << "@subgroup_size param is not a constant value, and IR property " |
| "'AllowOverrides' is not enabled"; |
| return; |
| } |
| |
| if (auto* r = subgroup_size->As<ir::InstructionResult>()) { |
| if (!r->Instruction()) { |
| AddError(func) << "instruction for @subgroup_size param is not defined"; |
| return; |
| } |
| |
| if (r->Instruction()->Block() != ir_.root_block) { |
| AddError(func) << "@subgroup_size param defined by non-module scope value"; |
| return; |
| } |
| |
| if (r->Instruction()->Is<core::ir::Override>()) { |
| return; |
| } |
| } |
| |
| AddError(func) << "@subgroup_size must be an InstructionResult or a Constant"; |
| } |
| |
| void Structural::ProcessTasks() { |
| while (!tasks_.IsEmpty()) { |
| tasks_.Pop()(); |
| } |
| } |
| |
| void Structural::QueueBlock(const Block* blk) { |
| tasks_.Push([this] { EndBlock(); }); |
| tasks_.Push([this, blk] { BeginBlock(blk); }); |
| } |
| |
| void Structural::BeginBlock(const Block* blk) { |
| scope_stack_.Push(); |
| block_stack_.Push(blk); |
| |
| if (auto* mb = blk->As<MultiInBlock>()) { |
| for (auto* param : mb->Params()) { |
| if (!param->Alive()) { |
| AddError(param) << "destroyed parameter found in block parameter list"; |
| return; |
| } |
| if (!param->Block()) { |
| AddError(param) << "block parameter has nullptr parent block"; |
| return; |
| } else if (param->Block() != mb) { |
| AddError(param) << "block parameter has incorrect parent block"; |
| AddNote(param->Block()) << "parent block declared here"; |
| return; |
| } |
| |
| CheckType(param->Type(), [&]() -> diag::Diagnostic& { return AddError(param); }); |
| |
| if (param->Type()->Is<core::type::Void>()) { |
| AddError(param) << "block parameter type cannot be void"; |
| } |
| if (param->Type()->Is<core::type::Reference>()) { |
| AddError(param) << "block parameter type cannot be a reference"; |
| } |
| |
| scope_stack_.Add(param); |
| } |
| } |
| |
| if (!blk->Terminator()) { |
| AddError(blk) << "block does not end in a terminator instruction"; |
| } |
| |
| // Validate the instructions w.r.t. the parent block |
| for (auto* inst : *blk) { |
| if (inst->Block() != blk) { |
| AddError(inst) << "block instruction does not have same block as parent"; |
| AddNote(blk) << "in block"; |
| } |
| } |
| |
| // Enqueue validation of the instructions of the block |
| if (!blk->IsEmpty()) { |
| QueueInstructions(blk->Instructions()); |
| } |
| } |
| |
| void Structural::EndBlock() { |
| scope_stack_.Pop(); |
| block_stack_.Pop(); |
| } |
| |
| void Structural::QueueInstructions(const Instruction* inst) { |
| if (diag_.ContainsErrors()) { |
| return; |
| } |
| |
| tasks_.Push([this, inst] { |
| // Tasks are processed LIFO, so push the next instruction to the stack before checking the |
| // current instruction, which may need to add more blocks to the stack itself. |
| if (inst->next) { |
| QueueInstructions(inst->next); |
| } |
| CheckInstruction(inst); |
| }); |
| } |
| |
| void Structural::CheckInstruction(const Instruction* inst) { |
| visited_instructions_.Add(inst); |
| if (!inst->Alive()) { |
| AddError(inst) << "destroyed instruction found in instruction list"; |
| return; |
| } |
| |
| auto results = inst->Results(); |
| for (size_t i = 0; i < results.Length(); ++i) { |
| auto* res = results[i]; |
| if (!res) { |
| continue; |
| } |
| |
| CheckType(res->Type(), [&]() -> diag::Diagnostic& { return AddResultError(inst, i); }); |
| } |
| |
| auto ops = inst->Operands(); |
| for (size_t i = 0; i < ops.Length(); ++i) { |
| auto* op = ops[i]; |
| if (!op) { |
| continue; |
| } |
| |
| CheckType(op->Type(), [&]() -> diag::Diagnostic& { return AddError(inst, i); }); |
| } |
| |
| // Push a task to add the results to the scope. |
| // This ensures that for control instructions, the results are only added to the scope |
| // after their nested blocks have been evaluated (since tasks are processed LIFO). |
| tasks_.Push([this, inst] { |
| for (auto* result : inst->Results()) { |
| if (result) { |
| scope_stack_.Add(result); |
| } |
| } |
| }); |
| |
| tint::Switch( |
| inst, // |
| [&](const Access* a) { CheckAccess(a); }, // |
| [&](const Binary* b) { CheckBinary(b); }, // |
| [&](const Call* c) { CheckCall(c); }, // |
| [&](const If* if_) { CheckIf(if_); }, // |
| [&](const Let* let) { CheckLet(let); }, // |
| [&](const Load* load) { CheckLoad(load); }, // |
| [&](const LoadVectorElement* l) { CheckLoadVectorElement(l); }, // |
| [&](const Loop* l) { CheckLoop(l); }, // |
| [&](const Phony* p) { CheckPhony(p); }, // |
| [&](const Store* s) { CheckStore(s); }, // |
| [&](const StoreVectorElement* s) { CheckStoreVectorElement(s); }, // |
| [&](const Switch* s) { CheckSwitch(s); }, // |
| [&](const Swizzle* s) { CheckSwizzle(s); }, // |
| [&](const Terminator* b) { CheckTerminator(b); }, // |
| [&](const Unary* u) { CheckUnary(u); }, // |
| [&](const Override* o) { CheckOverride(o); }, // |
| [&](const Var* var) { CheckVar(var); }, // |
| TINT_ICE_ON_NO_MATCH); |
| } |
| |
| void Structural::CheckOverride(const Override* o) { |
| // Intentionally not checking operands, since Override may have a null operand |
| if (!CheckResults(o, Override::kNumResults)) { |
| return; |
| } |
| |
| if (o->Block() != ir_.root_block) { |
| AddError(o) << "override must be declared at module scope"; |
| } |
| |
| if (o->Initializer()) { |
| CheckOperand(o, ir::Var::kInitializerOperandOffset); |
| } else if (o->Operands().Length() == 0) { |
| AddError(o) << "override is malformed, missing initializer operand"; |
| } |
| } |
| |
| void Structural::CheckVar(const Var* var) { |
| if (!CheckResultsAndOperands(var, Var::kNumResults, Var::kNumOperands)) { |
| return; |
| } |
| |
| // TODO(516717234): Remove when ValidateShaderIOAnnotations are moved to function validator |
| auto* result_type = var->Result()->Type(); |
| auto* mv = result_type->As<core::type::MemoryView>(); |
| if (!mv) { |
| AddError(var) << "result type " << NameOf(result_type) |
| << " must be a pointer or a reference"; |
| return; |
| } |
| const core::ir::type::ValueArrayCount* count = nullptr; |
| if (auto* ary = result_type->UnwrapPtr()->As<core::type::Array>()) { |
| count = ary->Count()->As<core::ir::type::ValueArrayCount>(); |
| } else if (auto* buf = result_type->UnwrapPtr()->As<core::type::Buffer>()) { |
| count = buf->Count()->As<core::ir::type::ValueArrayCount>(); |
| } |
| |
| if (count) { |
| if (!scope_stack_.Contains(count->value)) { |
| AddError(var) << NameOf(count->value) << " is not in scope"; |
| } |
| } |
| |
| if (var->Initializer()) { |
| if (!CheckOperand(var, ir::Var::kInitializerOperandOffset)) { |
| return; |
| } |
| } |
| |
| // TODO(516717234): Move to functional validator |
| CheckBindingPoint(var, var->Result(0)->Type(), var->Attributes(), |
| ShaderIOKind::kModuleScopeVar); |
| |
| auto address_space = mv->AddressSpace(); |
| if (address_space != AddressSpace::kIn && address_space != AddressSpace::kOut) { |
| CheckInterpolation(var, mv->StoreType(), var->Attributes(), |
| Function::PipelineStage::kUndefined, IODirection::kResource); |
| } |
| |
| // TODO(516717234): Move to functional validator |
| if (var->Block() == ir_.root_block) { |
| if (mv->AddressSpace() == AddressSpace::kIn || mv->AddressSpace() == AddressSpace::kOut) { |
| ValidateShaderIOAnnotations(var, var->Result()->Type(), var->BindingPoint(), |
| var->Attributes(), ShaderIOKind::kModuleScopeVar); |
| } |
| } |
| } |
| |
| const ir::Function* Structural::ContainingFunction(const ir::Instruction* inst) { |
| if (inst->Block() == ir_.root_block) { |
| return nullptr; |
| } |
| |
| return block_to_function_.GetOrAdd(inst->Block(), [&] { // |
| return ContainingFunction(inst->Block()->Parent()); |
| }); |
| } |
| |
| Hashset<const ir::Function*, 4> Structural::ContainingEndPoints(const ir::Function* f) { |
| if (!f) { |
| return {}; |
| } |
| |
| Hashset<const ir::Function*, 4> result{}; |
| Hashset<const ir::Function*, 4> visited{f}; |
| |
| auto call_sites = user_func_calls_.GetOr(f, Hashset<const ir::UserCall*, 4>()).Vector(); |
| while (!call_sites.IsEmpty()) { |
| auto call_site = call_sites.Pop(); |
| auto calling_function = ContainingFunction(call_site); |
| if (!calling_function) { |
| continue; |
| } |
| |
| if (visited.Contains(calling_function)) { |
| continue; |
| } |
| visited.Add(calling_function); |
| |
| if (calling_function->IsEntryPoint()) { |
| result.Add(calling_function); |
| } |
| |
| for (auto new_call_sites : user_func_calls_.GetOr(f, Hashset<const ir::UserCall*, 4>())) { |
| call_sites.Push(new_call_sites); |
| } |
| } |
| |
| return result; |
| } |
| |
| void Structural::CheckBlendSrc(BlendSrcContext& ctx, |
| const CastableBase* target, |
| const core::type::Type* ty, |
| const IOAttributes& attr) { |
| if (attr.blend_src.has_value()) { |
| if (!ir_.properties.Contains(Property::kAllowBackendSpecificShaderIO)) { |
| AddError(target) << "blend_src cannot be used on non-struct-member types"; |
| } |
| CheckBlendSrcImpl(ctx, target, ty, attr); |
| } |
| |
| if (auto* s = ty->As<core::type::Struct>()) { |
| if (s->Members().Any([](auto* m) { return m->Attributes().blend_src.has_value(); })) { |
| auto location_count = 0u; |
| for (const auto* mem : s->Members()) { |
| auto& mem_attr = mem->Attributes(); |
| if (mem_attr.location.has_value()) { |
| location_count++; |
| } |
| CheckBlendSrcImpl(ctx, target, mem->Type(), mem_attr); |
| } |
| |
| if (location_count != 2) { |
| AddError(target) |
| << "structs with blend_src members must have exactly 2 members with " |
| "location annotations"; |
| } |
| return; |
| } |
| } |
| |
| // Reject blend_src on nested members |
| if (!ir_.properties.Contains(Property::kAllowBackendSpecificShaderIO)) { |
| WalkTypeAndMembers( |
| ctx, ty, attr, |
| [&target, this](BlendSrcContext&, const core::type::Type*, const IOAttributes& a) { |
| if (a.blend_src.has_value()) { |
| AddError(target) |
| << "blend_src cannot be used on members of non-top level structs"; |
| } |
| }); |
| } |
| } |
| |
| void Structural::CheckBlendSrcImpl(BlendSrcContext& ctx, |
| const CastableBase* target, |
| const core::type::Type* ty, |
| const IOAttributes& attr) { |
| if (!attr.blend_src.has_value()) { |
| return; |
| } |
| |
| auto bs_val = attr.blend_src.value(); |
| if (bs_val != 0 && bs_val != 1) { |
| AddError(target) << "blend_src value must be 0 or 1"; |
| } |
| if (!ctx.blend_srcs.Add(bs_val)) { |
| AddError(target) << "duplicate blend_src(" << bs_val << ") on entry point " |
| << ToString(ctx.dir); |
| } |
| |
| if (ctx.dir != IODirection::kOutput || ctx.stage != Function::PipelineStage::kFragment) { |
| AddError(target) << "blend_src can only be used on fragment shader outputs"; |
| return; |
| } |
| if (!attr.location.has_value() || attr.location.value() != 0) { |
| AddError(target) << "struct members with blend_src must be located at 0"; |
| } |
| |
| if (!ctx.blend_src_type) { |
| if (!ty->IsNumericScalarOrVector()) { |
| AddError(target) << "blend_src must be a numeric scalar or vector, but has type " |
| << ty->FriendlyName(); |
| } |
| ctx.blend_src_type = ty; |
| } else if (ctx.blend_src_type != ty) { |
| AddError(target) << "blend_src type " << ty->FriendlyName() |
| << " does not match other blend_src type " |
| << ctx.blend_src_type->FriendlyName(); |
| } |
| } |
| |
| void Structural::CheckLocation(Hashmap<uint32_t, const CastableBase*, 4>& locations, |
| const CastableBase* target, |
| const IOAttributes& attr, |
| const Function::PipelineStage stage, |
| const core::type::Type* type, |
| const IODirection dir) { |
| struct WalkContext { |
| Structural* validator; |
| Hashmap<uint32_t, const CastableBase*, 4>& locations; |
| const CastableBase* target; |
| const Function::PipelineStage stage; |
| const IODirection dir; |
| }; |
| WalkContext ctx{this, locations, target, stage, dir}; |
| |
| WalkTypeAndMembers( |
| ctx, type, attr, |
| [](WalkContext& context, const core::type::Type* ty, const IOAttributes& attribute) { |
| if (ty->Is<core::type::Struct>()) { |
| return; |
| } |
| |
| if (attribute.blend_src) { |
| // locations associated with a blend_src usage should already be |
| // pre-populated in locations |
| return; |
| } |
| |
| if (attribute.location.has_value()) { |
| if (context.stage == Function::PipelineStage::kCompute && |
| context.dir == IODirection::kInput) { |
| context.validator->AddError(context.target) |
| << "location attribute is not valid for compute shader inputs"; |
| } |
| |
| auto loc = attribute.location.value(); |
| if (const auto conflict = context.locations.Get(loc)) { |
| context.validator->AddError(context.target) |
| << "duplicate location(" << loc << ") on entry point " |
| << ToString(context.dir); |
| context.validator->AddDeclarationNote(*conflict.value); |
| } else { |
| context.locations.Add(loc, context.target); |
| } |
| } |
| }); |
| } |
| |
| void Structural::CheckInterpolation(const CastableBase* anchor, |
| const core::type::Type* ty, |
| const IOAttributes& attr, |
| const Function::PipelineStage stage, |
| const IODirection dir) { |
| if (!ty) { |
| return; |
| } |
| |
| bool ctx = false; |
| |
| WalkTypeAndMembers( |
| ctx, ty, attr, |
| [this, anchor, stage, dir](bool& in_location_composite, const core::type::Type* t, |
| const IOAttributes& a) { |
| bool has_location = a.location.has_value() || in_location_composite; |
| if (!has_location) { |
| if (auto* str = t->As<core::type::Struct>()) { |
| has_location |= str->Members().All( |
| [](const auto* mem) { return mem->Attributes().location.has_value(); }); |
| } |
| } |
| |
| if (a.interpolation.has_value()) { |
| has_location |= (ir_.properties.Contains(Property::kAllowBackendSpecificShaderIO) && |
| a.builtin.has_value()); |
| |
| if (!ir_.properties.Contains(Property::kAllowLocationForNumericComposites) && |
| t->As<core::type::Struct>()) { |
| AddError(anchor) << "interpolation cannot be applied to a struct without " |
| "'kAllowLocationForNumericComposites' property"; |
| } |
| |
| if (t->IsIntegerScalarOrVector()) { |
| if (a.interpolation.value().type != InterpolationType::kFlat) { |
| AddError(anchor) |
| << "interpolation attribute type must be flat for integral types"; |
| } |
| } |
| |
| auto interp_type = a.interpolation.value().type; |
| auto interp_sampling = a.interpolation.value().sampling; |
| if (interp_sampling != InterpolationSampling::kUndefined) { |
| switch (interp_type) { |
| case InterpolationType::kFlat: |
| if (interp_sampling != InterpolationSampling::kFirst && |
| interp_sampling != InterpolationSampling::kEither) { |
| AddError(anchor) << "flat interpolation can only use 'first', " |
| "'either' or undefined sampling parameters"; |
| } |
| break; |
| case InterpolationType::kLinear: |
| case InterpolationType::kPerspective: |
| if (interp_sampling != InterpolationSampling::kCenter && |
| interp_sampling != InterpolationSampling::kCentroid && |
| interp_sampling != InterpolationSampling::kSample) { |
| AddError(anchor) << "linear and perspective interpolation can only " |
| "use 'center', 'centroid', 'sample', or " |
| "undefined sampling parameters"; |
| } |
| break; |
| case InterpolationType::kUndefined: |
| AddError(anchor) << "undefined interpolation should on have an " |
| "undefined sampling parameter"; |
| break; |
| default: |
| TINT_UNREACHABLE(); |
| } |
| } |
| |
| if (!has_location) { |
| if (!ir_.properties.Contains(Property::kAllowBackendSpecificShaderIO)) { |
| AddError(anchor) << "interpolation attribute requires a location attribute"; |
| } else { |
| AddError(anchor) << "interpolation attribute requires a location attribute " |
| "(or location-like shader I/O annotation)"; |
| } |
| } |
| } else if (has_location && t->IsIntegerScalarOrVector()) { |
| // Integral vertex outputs and fragment inputs require flat interpolation. |
| const bool needs_flat = |
| (stage == Function::PipelineStage::kVertex && dir == IODirection::kOutput) || |
| (stage == Function::PipelineStage::kFragment && dir == IODirection::kInput); |
| if (needs_flat) { |
| AddError(anchor) << "integral user-defined inputs and outputs must have an " |
| "@interpolate(flat) attribute"; |
| } |
| } |
| |
| if (t->IsAnyOf<core::type::Array, core::type::Struct>()) { |
| in_location_composite |= a.location.has_value(); |
| } |
| }); |
| } |
| |
| void Structural::CheckBindingPoint(const CastableBase* anchor, |
| const core::type::Type* ty, |
| const IOAttributes& attr, |
| const ShaderIOKind& io_kind) { |
| const auto& binding_point = attr.binding_point; |
| auto address_space = AddressSpace::kUndefined; |
| if (const auto* mv = ty->As<core::type::MemoryView>()) { |
| address_space = mv->AddressSpace(); |
| } else { |
| // ModuleScopeVars transform in MSL backends unwraps pointers to handles |
| if (ty->IsHandle()) { |
| address_space = AddressSpace::kHandle; |
| } |
| } |
| |
| if (binding_point.has_value() && io_kind != ShaderIOKind::kModuleScopeVar && |
| !ir_.properties.Contains(Property::kAllowMslEntryPointInterface)) { |
| AddError(anchor) << "binding_points are only valid on resource variables"; |
| } |
| |
| switch (address_space) { |
| case AddressSpace::kHandle: |
| if (!binding_point.has_value()) { |
| AddError(anchor) << "a " << ToString(address_space) |
| << " resource requires a binding point"; |
| } |
| break; |
| case AddressSpace::kStorage: |
| case AddressSpace::kUniform: |
| if (!binding_point.has_value()) { |
| AddError(anchor) << "a " << ToString(address_space) |
| << " resource requires a binding point"; |
| } |
| break; |
| default: |
| if (binding_point.has_value()) { |
| AddError(anchor) << "a " << ToString(address_space) |
| << " non-resource cannot have a binding point"; |
| } |
| break; |
| } |
| } |
| |
| void Structural::ValidateShaderIOAnnotations(const CastableBase* msg_anchor, |
| const core::type::Type* ty, |
| const std::optional<BindingPoint>& binding_point, |
| const IOAttributes& attr, |
| ShaderIOKind kind) { |
| EnumSet<IOAnnotation> annotations; |
| |
| // Since there is no entries in the set at this point, this should never fail. |
| TINT_ASSERT(AddIOAnnotationsFromIOAttributes(annotations, attr) == Success); |
| |
| if (binding_point.has_value()) { |
| annotations.Add(IOAnnotation::kBindingPoint); |
| } |
| |
| if (auto* mv = ty->As<core::type::MemoryView>()) { |
| if (mv->AddressSpace() == AddressSpace::kWorkgroup) { |
| annotations.Add(IOAnnotation::kWorkgroup); |
| } |
| } |
| |
| if (ty->Is<core::type::Void>()) { |
| if (!annotations.Empty()) { |
| AddError(msg_anchor) << ToString(kind) << " with void type should never be annotated"; |
| } |
| return; // Early return because later rules assume non-void types. |
| } |
| |
| if (attr.location.has_value()) { |
| if (ir_.properties.Contains(Property::kAllowLocationForNumericComposites)) { |
| std::function<bool(const core::type::Type*)> is_numeric = |
| [&is_numeric](const core::type::Type* t) -> bool { |
| t = t->UnwrapPtrOrRef(); |
| bool result = false; |
| tint::Switch( |
| t, |
| [&](const core::type::Struct* s) { |
| for (auto* m : s->Members()) { |
| if (!is_numeric(m->Type())) { |
| return; |
| } |
| } |
| result = true; |
| }, |
| [&](Default) { |
| auto* e = t->DeepestElement()->UnwrapPtrOrRef(); |
| tint::Switch( |
| e, // |
| [&](const core::type::Struct* s) { result = is_numeric(s); }, |
| [&](Default) { result = e->IsNumericScalarOrVector(); }); |
| }); |
| return result; |
| }; |
| if (!is_numeric(ty)) { |
| AddError(msg_anchor) |
| << ToString(kind) |
| << " with a location attribute must contain only numeric elements " |
| << ty->FriendlyName(); |
| return; |
| } |
| } else { |
| if (!ty->UnwrapPtrOrRef()->IsNumericScalarOrVector()) { |
| AddError(msg_anchor) << ToString(kind) |
| << " with a location attribute must be a numeric scalar or " |
| "vector, but has type " |
| << ty->FriendlyName(); |
| return; |
| } |
| } |
| } |
| |
| if (auto* ty_struct = ty->UnwrapPtrOrRef()->As<core::type::Struct>()) { |
| for (const auto* mem : ty_struct->Members()) { |
| EnumSet<IOAnnotation> mem_annotations = annotations; |
| auto add_result = AddIOAnnotationsFromIOAttributes(mem_annotations, mem->Attributes()); |
| if (add_result != Success) { |
| AddError(msg_anchor) |
| << ToString(kind) |
| << " struct member has same IO annotation, as top-level struct, '" |
| << ToString(add_result.Failure()) << "'"; |
| return; |
| } |
| |
| if (!CheckStructMemberAttributes(mem, [&]() -> diag::Diagnostic& { |
| return AddError(msg_anchor) << ToString(kind) << " "; |
| })) { |
| return; |
| } |
| |
| if (ir_.properties.Contains(Property::kAllowMslEntryPointInterface)) { |
| if (auto* mv = mem->Type()->As<core::type::MemoryView>()) { |
| if (mv->AddressSpace() == AddressSpace::kWorkgroup) { |
| mem_annotations.Add(IOAnnotation::kWorkgroup); |
| } |
| } |
| } |
| |
| if (mem_annotations.Empty()) { |
| AddError(msg_anchor) << ToString(kind) |
| << " struct members must have at least one IO annotation, " |
| "e.g. a binding point, a location, etc"; |
| } else if (mem_annotations.Size() > 1) { |
| AddError(msg_anchor) |
| << ToString(kind) << " struct member has more than one IO annotation, " |
| << ToString(mem_annotations); |
| } |
| } |
| } else { |
| if (annotations.Empty()) { |
| if (!(ir_.properties.Contains(Property::kAllowUnannotatedModuleIOVariables) && |
| kind == ShaderIOKind::kModuleScopeVar)) { |
| AddError(msg_anchor) << ToString(kind) |
| << " must have at least one IO annotation, e.g. a binding " |
| "point, a location, etc"; |
| } |
| } else if (annotations.Size() > 1) { |
| AddError(msg_anchor) << ToString(kind) << " has more than one IO annotation, " |
| << ToString(annotations); |
| } |
| } |
| } |
| |
| bool Structural::CheckStructMemberAttributes(const core::type::StructMember* member, |
| std::function<diag::Diagnostic&()> make_diag) { |
| const auto checkers = IOAttributeCheckersFor(member->Attributes(), /*skip_builtins*/ false); |
| for (const auto* checker : checkers) { |
| auto res = checker->check(member->Type(), member->Attributes(), ir_.properties, |
| IOAttributeUsage::kUndefinedUsage); |
| if (res != Success) { |
| make_diag() << res.Failure(); |
| return false; |
| } |
| if (!checker->type_check(member->Type(), ir_.properties)) { |
| make_diag() << ToString(checker->kind) << " " << checker->type_error; |
| return false; |
| } |
| } |
| |
| if (member->Attributes().location.has_value()) { |
| if (ir_.properties.Contains(Property::kAllowLocationForNumericComposites)) { |
| if (!member->Type()->UnwrapPtrOrRef()->IsNumericScalarOrVector() && |
| !member->Type()->UnwrapPtrOrRef()->Is<core::type::Struct>()) { |
| make_diag() << "struct member with a location attribute must be a numeric scalar, " |
| "a numeric vector or a struct, but has type " |
| << member->Type()->FriendlyName(); |
| return false; |
| } |
| } else { |
| if (!member->Type()->UnwrapPtrOrRef()->IsNumericScalarOrVector()) { |
| make_diag() << "struct member with a location attribute must be " |
| "a numeric scalar or vector, but has type " |
| << member->Type()->FriendlyName(); |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| void Structural::CheckLet(const Let* l) { |
| CheckResultsAndOperands(l, Let::kNumResults, Let::kNumOperands); |
| } |
| |
| void Structural::CheckCall(const Call* call) { |
| tint::Switch( |
| call, // |
| [&](const BuiltinCall* c) { CheckBuiltinCall(c); }, // |
| [&](const MemberBuiltinCall* c) { CheckMemberBuiltinCall(c); }, // |
| [&](const Construct* c) { CheckConstruct(c); }, // |
| [&](const Convert* c) { CheckConvert(c); }, // |
| [&](const Discard* d) { // |
| stage_restricted_instructions_.Add( |
| d, SupportedStages{Function::PipelineStage::kFragment}); // |
| CheckDiscard(d); // |
| }, // |
| [&](const UserCall* c) { // |
| if (c->Target()) { // |
| auto calls = // |
| user_func_calls_.GetOr(c->Target(), // |
| Hashset<const ir::UserCall*, 4>{}); // |
| calls.Add(c); // |
| user_func_calls_.Replace(c->Target(), calls); // |
| } |
| CheckUserCall(c); |
| }, |
| [&](Default) { |
| // Validation of custom IR instructions |
| }); |
| } |
| |
| void Structural::CheckBuiltinCall(const BuiltinCall* call) { |
| // This check cannot be more precise, since until intrinsic lookup below, it is unknown what |
| // number of operands are expected, but still need to enforce things are in scope, |
| // have types, etc. |
| if (!CheckResults(call, BuiltinCall::kNumResults) || !CheckOperands(call)) { |
| return; |
| } |
| |
| auto args = Transform<8>(call->Args(), [&](const ir::Value* v) { return v->Type(); }); |
| |
| intrinsic::Context context{call->TableData(), type_mgr_, symbols_}; |
| auto builtin = core::intrinsic::LookupFn(context, call->FriendlyName().c_str(), call->FuncId(), |
| call->ExplicitTemplateParams(), args, |
| core::EvaluationStage::kRuntime); |
| if (builtin != Success) { |
| AddError(call) << builtin.Failure(); |
| return; |
| } |
| |
| // Track the stages that this builtin call is limited to, so that we can check them against the |
| // entry points that they are used from. |
| SupportedStages stages; |
| if (builtin->info->flags.Contains(intrinsic::OverloadFlag::kSupportsComputePipeline)) { |
| stages.Add(Function::PipelineStage::kCompute); |
| } |
| if (builtin->info->flags.Contains(intrinsic::OverloadFlag::kSupportsFragmentPipeline)) { |
| stages.Add(Function::PipelineStage::kFragment); |
| } |
| if (builtin->info->flags.Contains(intrinsic::OverloadFlag::kSupportsVertexPipeline)) { |
| stages.Add(Function::PipelineStage::kVertex); |
| } |
| stage_restricted_instructions_.Add(call, stages); |
| |
| const core::ir::CoreBuiltinCall* bc = call->As<CoreBuiltinCall>(); |
| if (bc == nullptr) { |
| return; |
| } |
| CheckCoreBuiltinCall(bc); |
| } |
| |
| void Structural::CheckCoreBuiltinCall(const CoreBuiltinCall* call) { |
| if (ir_.properties.Contains(Property::kDisallowVectorMinMaxClamp)) { |
| switch (call->Func()) { |
| case core::BuiltinFn::kClamp: |
| case core::BuiltinFn::kMax: |
| case core::BuiltinFn::kMin: |
| if (call->Result()->Type()->Is<core::type::Vector>()) { |
| AddError(call) << "vector " << call->FriendlyName() |
| << " disallowed by the DisallowVectorMinMaxClamp property"; |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| void Structural::CheckMemberBuiltinCall(const MemberBuiltinCall* call) { |
| // This check cannot be more precise, since until intrinsic lookup below, it is unknown what |
| // number of operands are expected, but still need to enforce things are in scope, |
| // have types, etc. |
| CheckResults(call, MemberBuiltinCall::kNumResults) || !CheckOperands(call); |
| } |
| |
| void Structural::CheckConstruct(const Construct* construct) { |
| CheckResultsAndOperandRange(construct, Construct::kNumResults, Construct::kMinOperands); |
| } |
| |
| void Structural::CheckConvert(const Convert* convert) { |
| CheckResultsAndOperands(convert, Convert::kNumResults, Convert::kNumOperands); |
| } |
| |
| void Structural::CheckDiscard(const tint::core::ir::Discard* discard) { |
| CheckResultsAndOperands(discard, Discard::kNumResults, Discard::kNumOperands); |
| } |
| |
| void Structural::CheckUserCall(const UserCall* call) { |
| CheckResultsAndOperandRange(call, UserCall::kNumResults, UserCall::kMinOperands); |
| |
| if (!call->Target()) { |
| AddError(call, UserCall::kFunctionOperandOffset) << "target not defined or not a function"; |
| return; |
| } |
| } |
| |
| void Structural::CheckAccess(const Access* a) { |
| CheckResultsAndOperandRange(a, Access::kNumResults, Access::kMinNumOperands); |
| } |
| |
| void Structural::CheckBinary(const Binary* b) { |
| if (!CheckResultsAndOperands(b, Binary::kNumResults, Binary::kNumOperands)) { |
| return; |
| } |
| if (b->Op() == core::BinaryOp::kLogicalAnd) { |
| AddError(b) << "logical-and is not valid in the IR"; |
| return; |
| } |
| if (b->Op() == core::BinaryOp::kLogicalOr) { |
| AddError(b) << "logical-or is not valid in the IR"; |
| return; |
| } |
| } |
| |
| void Structural::CheckUnary(const Unary* u) { |
| CheckResultsAndOperands(u, Unary::kNumResults, Unary::kNumOperands); |
| } |
| |
| void Structural::CheckIf(const If* if_) { |
| CheckResults(if_); |
| CheckOperands(if_, If::kNumOperands); |
| |
| if (if_->False() && if_->False()->Is<core::ir::MultiInBlock>()) { |
| AddError(if_) << "if false block must be a block"; |
| } |
| if (if_->True() && if_->True()->Is<core::ir::MultiInBlock>()) { |
| AddError(if_) << "if true block must be a block"; |
| } |
| |
| tasks_.Push([this] { control_stack_.Pop(); }); |
| |
| if (!if_->False()->IsEmpty()) { |
| QueueBlock(if_->False()); |
| } |
| |
| QueueBlock(if_->True()); |
| |
| tasks_.Push([this, if_] { control_stack_.Push(if_); }); |
| } |
| |
| void Structural::CheckLoop(const Loop* l) { |
| CheckResults(l); |
| CheckOperands(l, 0); |
| |
| if (l->Initializer()->Is<core::ir::MultiInBlock>()) { |
| AddError(l->Initializer()) << "loop initializer must be a block"; |
| } |
| |
| if (!l->Initializer()->IsEmpty()) { |
| if (!l->Initializer()->Terminator() || |
| !l->Initializer()->Terminator()->Is<core::ir::NextIteration>()) { |
| AddError(l->Initializer()) << "loop initializer must have a NextIteration terminator"; |
| } |
| } |
| |
| // Note: Tasks are queued in reverse order of their execution |
| tasks_.Push([this] { control_stack_.Pop(); }); |
| if (!l->Initializer()->IsEmpty()) { |
| tasks_.Push([this] { EndBlock(); }); |
| } |
| tasks_.Push([this] { EndBlock(); }); |
| if (!l->Continuing()->IsEmpty()) { |
| tasks_.Push([this, l] { |
| if (!l->Continuing()->Terminator()->IsAnyOf<NextIteration, BreakIf>()) { |
| AddError(l->Continuing()) |
| << "loop continuing terminator can only be next_iteration or break_if"; |
| } |
| EndBlock(); |
| }); |
| } |
| |
| // ⎡Initializer ⎤ |
| // ⎢ ⎡Body ⎤⎥ |
| // ⎣ ⎣ [Continuing ] ⎦⎦ |
| |
| if (!l->Continuing()->IsEmpty()) { |
| tasks_.Push([this, l] { BeginBlock(l->Continuing()); }); |
| } else if (!l->Continuing()->Params().IsEmpty()) { |
| AddError(l) << "loop continuing block has parameters but is empty"; |
| } |
| |
| tasks_.Push([this, l] { |
| CheckLoopBody(l); |
| BeginBlock(l->Body()); |
| }); |
| if (!l->Initializer()->IsEmpty()) { |
| tasks_.Push([this, l] { BeginBlock(l->Initializer()); }); |
| } |
| tasks_.Push([this, l] { control_stack_.Push(l); }); |
| } |
| |
| void Structural::CheckLoopBody(const Loop* loop) { |
| // If the body block has parameters, there must be an initializer block. |
| if (!loop->Body()->Params().IsEmpty()) { |
| if (!loop->HasInitializer()) { |
| AddError(loop) << "loop with body block parameters must have an initializer"; |
| } |
| } |
| } |
| |
| void Structural::CheckSwitch(const Switch* s) { |
| CheckResults(s); |
| CheckOperands(s, Switch::kNumOperands); |
| |
| tasks_.Push([this] { control_stack_.Pop(); }); |
| |
| for (auto& cse : s->Cases()) { |
| if (cse.selectors.IsEmpty()) { |
| AddError(s) << "case does not have any selectors"; |
| } |
| if (cse.block->Is<core::ir::MultiInBlock>()) { |
| AddError(s) << "case block must be a block"; |
| } |
| QueueBlock(cse.block); |
| } |
| |
| tasks_.Push([this, s] { control_stack_.Push(s); }); |
| } |
| |
| void Structural::CheckSwizzle(const Swizzle* s) { |
| CheckResultsAndOperands(s, Swizzle::kNumResults, Swizzle::kNumOperands); |
| } |
| |
| void Structural::CheckTerminator(const Terminator* b) { |
| // All terminators should have zero results |
| if (!CheckResults(b, 0)) { |
| return; |
| } |
| |
| // Operands must be alive and in scope if they are not nullptr. |
| if (!CheckOperands(b)) { |
| return; |
| } |
| |
| tint::Switch( |
| b, // |
| [&](const ir::BreakIf* i) { CheckBreakIf(i); }, // |
| [&](const ir::Continue* c) { CheckContinue(c); }, // |
| [&](const ir::Exit* e) { CheckExit(e); }, // |
| [&](const ir::NextIteration* n) { CheckNextIteration(n); }, // |
| [&](const ir::Return* ret) { CheckReturn(ret); }, // |
| [&](const ir::TerminateInvocation*) {}, // |
| [&](const ir::Unreachable* u) { CheckUnreachable(u); }, // |
| TINT_ICE_ON_NO_MATCH); |
| |
| if (b->next) { |
| AddError(b) << "must be the last instruction in the block"; |
| } |
| } |
| |
| void Structural::CheckBreakIf(const BreakIf* b) { |
| auto* loop = b->Loop(); |
| if (loop == nullptr) { |
| AddError(b) << "has no associated loop"; |
| return; |
| } |
| if (b->Condition() == nullptr) { |
| AddError(b) << "break_if condition cannot be nullptr"; |
| return; |
| } |
| |
| auto next_iter_values = b->NextIterValues(); |
| if (auto* body = loop->Body()) { |
| CheckOperandsMatchTarget(b, b->ArgsOperandOffset(), next_iter_values.size(), body, |
| body->Params()); |
| } |
| |
| auto exit_values = b->ExitValues(); |
| CheckOperandsMatchTarget(b, b->ArgsOperandOffset() + next_iter_values.size(), |
| exit_values.size(), loop, loop->Results()); |
| } |
| |
| void Structural::CheckContinue(const Continue* c) { |
| auto* loop = c->Loop(); |
| if (loop == nullptr) { |
| AddError(c) << "has no associated loop"; |
| return; |
| } |
| if (!TransitivelyHolds(loop->Body(), c)) { |
| if (control_stack_.Any(Eq<const ControlInstruction*>(loop))) { |
| AddError(c) << "must only be called from loop body"; |
| } else { |
| AddError(c) << "called outside of associated loop"; |
| } |
| } |
| |
| if (auto* cont = loop->Continuing()) { |
| CheckOperandsMatchTarget(c, Continue::kArgsOperandOffset, c->Args().size(), cont, |
| cont->Params()); |
| } |
| } |
| |
| void Structural::CheckExit(const Exit* e) { |
| if (control_stack_.IsEmpty()) { |
| AddError(e) << "found outside all control instructions"; |
| return; |
| } |
| if (e->ControlInstruction() == nullptr) { |
| AddError(e) << "has no parent control instruction"; |
| return; |
| } |
| |
| auto args = e->Args(); |
| CheckOperandsMatchTarget(e, e->ArgsOperandOffset(), args.size(), e->ControlInstruction(), |
| e->ControlInstruction()->Results()); |
| |
| tint::Switch( |
| e, // |
| [&](const ir::ExitIf* i) { CheckExitIf(i); }, // |
| [&](const ir::ExitLoop* l) { CheckExitLoop(l); }, // |
| [&](const ir::ExitSwitch* s) { CheckExitSwitch(s); }, // |
| TINT_ICE_ON_NO_MATCH); |
| } |
| |
| void Structural::CheckNextIteration(const NextIteration* n) { |
| auto* loop = n->Loop(); |
| if (loop == nullptr) { |
| AddError(n) << "has no associated loop"; |
| return; |
| } |
| |
| if (loop->Initializer() != n->Block() && loop->Continuing() != n->Block()) { |
| if (control_stack_.Any(Eq<const ControlInstruction*>(loop))) { |
| AddError(n) << "must only be called directly from loop initializer or continuing"; |
| } else { |
| AddError(n) << "called outside of associated loop"; |
| } |
| } |
| |
| if (auto* body = loop->Body()) { |
| CheckOperandsMatchTarget(n, NextIteration::kArgsOperandOffset, n->Args().size(), body, |
| body->Params()); |
| } |
| } |
| |
| void Structural::CheckExitIf(const ExitIf* e) { |
| if (control_stack_.Back() != e->If()) { |
| AddError(e) << "if target jumps over other control instructions"; |
| AddNote(control_stack_.Back()) << "first control instruction jumped"; |
| } |
| } |
| |
| void Structural::CheckReturn(const Return* ret) { |
| if (!CheckOperands(ret, Return::kMinOperands, Return::kMaxOperands)) { |
| return; |
| } |
| |
| auto* func = ret->Func(); |
| if (func == nullptr) { |
| // Func() returning nullptr after CheckResultsAndOperandRange is due to the first |
| // operand being not a function |
| AddError(ret) << "expected function for first operand"; |
| return; |
| } |
| |
| if (func != ContainingFunction(ret)) { |
| AddError(ret) << "function operand does not match containing function"; |
| return; |
| } |
| } |
| |
| void Structural::CheckUnreachable(const Unreachable* u) { |
| CheckResultsAndOperands(u, Unreachable::kNumResults, Unreachable::kNumOperands); |
| } |
| |
| void Structural::CheckControlsAllowingIf(const Exit* exit, const Instruction* control) { |
| bool found = false; |
| for (auto ctrl : tint::Reverse(control_stack_)) { |
| if (ctrl == control) { |
| found = true; |
| break; |
| } |
| // A exit switch can step over if instructions, but no others. |
| if (!ctrl->Is<ir::If>()) { |
| AddError(exit) << control->FriendlyName() |
| << " target jumps over other control instructions"; |
| AddNote(ctrl) << "first control instruction jumped"; |
| return; |
| } |
| } |
| if (!found) { |
| AddError(exit) << control->FriendlyName() << " not found in parent control instructions"; |
| } |
| } |
| |
| void Structural::CheckExitSwitch(const ExitSwitch* s) { |
| CheckControlsAllowingIf(s, s->ControlInstruction()); |
| } |
| |
| void Structural::CheckExitLoop(const ExitLoop* l) { |
| CheckControlsAllowingIf(l, l->ControlInstruction()); |
| } |
| |
| void Structural::CheckLoad(const Load* l) { |
| CheckResultsAndOperands(l, Load::kNumResults, Load::kNumOperands); |
| } |
| |
| void Structural::CheckStore(const Store* s) { |
| CheckResultsAndOperands(s, Store::kNumResults, Store::kNumOperands); |
| } |
| |
| void Structural::CheckLoadVectorElement(const LoadVectorElement* l) { |
| CheckResultsAndOperands(l, LoadVectorElement::kNumResults, LoadVectorElement::kNumOperands); |
| } |
| |
| void Structural::CheckStoreVectorElement(const StoreVectorElement* s) { |
| CheckResultsAndOperands(s, StoreVectorElement::kNumResults, StoreVectorElement::kNumOperands); |
| } |
| |
| void Structural::CheckPhony(const Phony* p) { |
| if (!ir_.properties.Contains(Property::kAllowPhonyInstructions)) { |
| AddError(p) << "missing property 'kAllowPhonyInstructions'"; |
| return; |
| } |
| |
| if (!CheckResultsAndOperands(p, Phony::kNumResults, Phony::kNumOperands)) { |
| return; |
| } |
| } |
| |
| void Structural::CheckOperandsMatchTarget(const Instruction* source_inst, |
| size_t source_operand_offset, |
| size_t source_operand_count, |
| const CastableBase* target, |
| VectorRef<const Value*> target_values) { |
| if (source_operand_count != target_values.Length()) { |
| auto values = [&](size_t n) { return n == 1 ? " value" : " values"; }; |
| AddError(source_inst) << "provides " << source_operand_count << values(source_operand_count) |
| << " but " << NameOf(target) << " expects " << target_values.Length() |
| << values(target_values.Length()); |
| AddDeclarationNote(target); |
| } |
| size_t count = std::min(source_operand_count, target_values.Length()); |
| for (size_t i = 0; i < count; i++) { |
| auto* source_value = source_inst->Operand(source_operand_offset + i); |
| auto* target_value = target_values[i]; |
| if (!source_value || !target_value) { |
| continue; // Caller should be checking operands are not null |
| } |
| auto* source_type = source_value->Type(); |
| auto* target_type = target_value->Type(); |
| if (source_type != target_type) { |
| AddError(source_inst, source_operand_offset + i) |
| << "operand with type " << NameOf(source_type) << " does not match " |
| << NameOf(target) << " target type " << NameOf(target_type); |
| AddDeclarationNote(target_value); |
| } |
| } |
| } |
| |
| } // namespace tint::core::ir::validator |