[val] Split instruction validation to separate file Move the structural instruction validation to validator_instructions file. Bug: 555255421 Change-Id: I730b748b6d0572365457304571e0e8263c54c09c Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/338155 Reviewed-by: James Price <jrprice@google.com> Commit-Queue: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel index b22592c..0587fb1 100644 --- a/src/tint/lang/core/ir/BUILD.bazel +++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -101,6 +101,7 @@ "user_call.cc", "validator.cc", "validator_function.cc", + "validator_instructions.cc", "validator_io.cc", "validator_types.cc", "value.cc",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake index 461e79a..cb73924 100644 --- a/src/tint/lang/core/ir/BUILD.cmake +++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -163,6 +163,7 @@ lang/core/ir/validator.cc lang/core/ir/validator.h lang/core/ir/validator_function.cc + lang/core/ir/validator_instructions.cc lang/core/ir/validator_io.cc lang/core/ir/validator_types.cc lang/core/ir/value.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn index d515d8e..ec16705 100644 --- a/src/tint/lang/core/ir/BUILD.gn +++ b/src/tint/lang/core/ir/BUILD.gn
@@ -164,6 +164,7 @@ "validator.cc", "validator.h", "validator_function.cc", + "validator_instructions.cc", "validator_io.cc", "validator_types.cc", "value.cc",
diff --git a/src/tint/lang/core/ir/structural_validator.cc b/src/tint/lang/core/ir/structural_validator.cc index 008d95a..05ffb76 100644 --- a/src/tint/lang/core/ir/structural_validator.cc +++ b/src/tint/lang/core/ir/structural_validator.cc
@@ -28,29 +28,18 @@ #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/bool.h" #include "src/tint/lang/core/type/pointer.h" #include "src/tint/lang/core/type/reference.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/internal_limits.h" #include "src/tint/utils/macros/defer.h" -#include "src/tint/utils/result.h" #include "src/tint/utils/rtti/switch.h" #include "src/tint/utils/text/text_style.h" @@ -62,27 +51,6 @@ } while (false) 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; -} - -} // namespace Structural::Structural(const Module& ir, diag::List& diagnostics) : ir_(ir), diag_(diagnostics), referenced_module_vars_(ir) {} @@ -360,7 +328,7 @@ return true; } -bool Structural::CheckResults(const ir::Instruction* inst, std::optional<size_t> count = {}) { +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 " @@ -387,7 +355,7 @@ bool Structural::CheckResultsAndOperandRange(const ir::Instruction* inst, size_t num_results, size_t min_operands, - std::optional<size_t> max_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); @@ -497,7 +465,7 @@ return passed; } -bool Structural::CheckOperands(const ir::Instruction* inst, std::optional<size_t> count = {}) { +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 " @@ -901,586 +869,4 @@ 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) { - if (!CheckResultsAndOperands(o, Override::kNumResults, Override::kNumOperands)) { - return; - } - - if (o->Block() != ir_.root_block) { - AddError(o) << "override must be declared at module scope"; - } -} - -void Structural::CheckVar(const Var* var) { - if (!CheckResultsAndOperands(var, Var::kNumResults, Var::kNumOperands)) { - return; - } - - 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; - } - } - - 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); - } - - 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); - } - } -} - -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; - } - } - if (ir_.properties.Contains(Property::kAllowBufferTypes)) { - switch (call->Func()) { - case core::BuiltinFn::kBufferArrayView: - if (call->Result()->Type()->UnwrapPtr()->HasFixedFootprint()) { - AddError(call) - << call->FriendlyName() << " result type must not have a fixed footprint"; - } - 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"; - } - - if (auto* constexpr_if = if_->As<core::ir::ConstExprIf>()) { - if (constexpr_if->Results().Length() != 1) { - AddError(constexpr_if) << "constexpr_if must have exactly one result"; - } else if (!constexpr_if->Result(0)->Type()->Is<core::type::Bool>()) { - AddError(constexpr_if) << "constexpr_if result type must be 'bool'"; - } - if (constexpr_if->False()->IsEmpty()) { - AddError(constexpr_if) << "constexpr_if must have a false block"; - } else if (!constexpr_if->False()->Terminator() || - !constexpr_if->False()->Terminator()->Is<core::ir::ExitIf>()) { - AddError(constexpr_if->False()) - << "constexpr_if false block terminator must be an exit_if"; - } - if (!constexpr_if->True()->Terminator() || - !constexpr_if->True()->Terminator()->Is<core::ir::ExitIf>()) { - AddError(constexpr_if->True()) - << "constexpr_if true block terminator must be an exit_if"; - } - } - - QueueTasks( - PushControlStack(if_), - [this, if_] { - if (!if_->False()->IsEmpty()) { - QueueBlock(if_->False()); - } - QueueBlock(if_->True()); - }, - PopControlStack()); -} - -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"; - } - } - - if (!l->Body()->Params().IsEmpty()) { - if (!l->HasInitializer()) { - AddError(l) << "loop with body block parameters must have an initializer"; - } - } - - if (l->Body()->IsEmpty()) { - AddError(l->Body()) << "loop body block must not be empty"; - } - - if (l->Continuing()->IsEmpty()) { - if (!l->Continuing()->Params().IsEmpty()) { - AddError(l) << "loop continuing block has parameters but is empty"; - } - } else if (!l->Continuing()->Terminator()->IsAnyOf<NextIteration, BreakIf>()) { - AddError(l->Continuing()) - << "loop continuing terminator can only be next_iteration or break_if"; - } - - // ⎡Initializer ⎤ - // ⎢ ⎡Body ⎤⎥ - // ⎣ ⎣ [Continuing ] ⎦⎦ - QueueTasks(PushControlStack(l), - QueueNestedTasks( // - BeginBlockTask(l->Initializer()), - QueueNestedTasks( // - BeginBlockTask(l->Body()), - QueueNestedTasks( // - BeginBlockTask(l->Continuing()), // - [] {}, // - EndBlockTask(l->Continuing())), - EndBlockTask(l->Body())), - EndBlockTask(l->Initializer())), - PopControlStack()); -} - -void Structural::CheckSwitch(const Switch* s) { - CheckResults(s); - CheckOperands(s, Switch::kNumOperands); - - QueueTasks( - PushControlStack(s), - [this, s] { - 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); - } - }, - PopControlStack()); -} - -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; - } -} - } // namespace tint::core::ir::validator
diff --git a/src/tint/lang/core/ir/structural_validator.h b/src/tint/lang/core/ir/structural_validator.h index 6d22259..c7c2e53 100644 --- a/src/tint/lang/core/ir/structural_validator.h +++ b/src/tint/lang/core/ir/structural_validator.h
@@ -307,7 +307,7 @@ /// @param inst the instruction /// @param count the number of results to check /// @returns true if the results count is as expected and none are null - bool CheckResults(const ir::Instruction* inst, std::optional<size_t> count); + bool CheckResults(const ir::Instruction* inst, std::optional<size_t> count = {}); /// Checks the given operand is not null and its type is not null /// @param inst the instruction @@ -331,7 +331,7 @@ /// @param inst the instruction /// @param count the number of operands to check /// @returns true if the operands count is as expected and none are null - bool CheckOperands(const ir::Instruction* inst, std::optional<size_t> count); + bool CheckOperands(const ir::Instruction* inst, std::optional<size_t> count = {}); /// Checks the number of results for @p inst are exactly equal to @p num_results and the number /// of operands is correctly. Both results and operands are confirmed to be non-null. @@ -344,7 +344,7 @@ bool CheckResultsAndOperandRange(const ir::Instruction* inst, size_t num_results, size_t min_operands, - std::optional<size_t> max_operands); + std::optional<size_t> max_operands = {}); /// Checks the number of results and operands for @p inst are exactly equal to num_results /// and num_operands, respectively, and that none of them are null.
diff --git a/src/tint/lang/core/ir/validator_instructions.cc b/src/tint/lang/core/ir/validator_instructions.cc new file mode 100644 index 0000000..d4d177b --- /dev/null +++ b/src/tint/lang/core/ir/validator_instructions.cc
@@ -0,0 +1,648 @@ +// 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 <string_view> + +#include "src/tint/lang/core/binary_op.h" +#include "src/tint/lang/core/intrinsic/table.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/structural_validator.h" +#include "src/tint/lang/core/ir/terminate_invocation.h" +#include "src/tint/lang/core/type/array.h" +#include "src/tint/lang/core/type/bool.h" +#include "src/tint/lang/core/type/vector.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/result.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; +} + +} // namespace +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) { + if (!CheckResultsAndOperands(o, Override::kNumResults, Override::kNumOperands)) { + return; + } + + if (o->Block() != ir_.root_block) { + AddError(o) << "override must be declared at module scope"; + } +} + +void Structural::CheckVar(const Var* var) { + if (!CheckResultsAndOperands(var, Var::kNumResults, Var::kNumOperands)) { + return; + } + + 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; + } + } + + 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); + } + + 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); + } + } +} + +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; + } + } + if (ir_.properties.Contains(Property::kAllowBufferTypes)) { + switch (call->Func()) { + case core::BuiltinFn::kBufferArrayView: + if (call->Result()->Type()->UnwrapPtr()->HasFixedFootprint()) { + AddError(call) + << call->FriendlyName() << " result type must not have a fixed footprint"; + } + 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"; + } + + if (auto* constexpr_if = if_->As<core::ir::ConstExprIf>()) { + if (constexpr_if->Results().Length() != 1) { + AddError(constexpr_if) << "constexpr_if must have exactly one result"; + } else if (!constexpr_if->Result(0)->Type()->Is<core::type::Bool>()) { + AddError(constexpr_if) << "constexpr_if result type must be 'bool'"; + } + if (constexpr_if->False()->IsEmpty()) { + AddError(constexpr_if) << "constexpr_if must have a false block"; + } else if (!constexpr_if->False()->Terminator() || + !constexpr_if->False()->Terminator()->Is<core::ir::ExitIf>()) { + AddError(constexpr_if->False()) + << "constexpr_if false block terminator must be an exit_if"; + } + if (!constexpr_if->True()->Terminator() || + !constexpr_if->True()->Terminator()->Is<core::ir::ExitIf>()) { + AddError(constexpr_if->True()) + << "constexpr_if true block terminator must be an exit_if"; + } + } + + QueueTasks( + PushControlStack(if_), + [this, if_] { + if (!if_->False()->IsEmpty()) { + QueueBlock(if_->False()); + } + QueueBlock(if_->True()); + }, + PopControlStack()); +} + +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"; + } + } + + if (!l->Body()->Params().IsEmpty()) { + if (!l->HasInitializer()) { + AddError(l) << "loop with body block parameters must have an initializer"; + } + } + + if (l->Body()->IsEmpty()) { + AddError(l->Body()) << "loop body block must not be empty"; + } + + if (l->Continuing()->IsEmpty()) { + if (!l->Continuing()->Params().IsEmpty()) { + AddError(l) << "loop continuing block has parameters but is empty"; + } + } else if (!l->Continuing()->Terminator()->IsAnyOf<NextIteration, BreakIf>()) { + AddError(l->Continuing()) + << "loop continuing terminator can only be next_iteration or break_if"; + } + + // ⎡Initializer ⎤ + // ⎢ ⎡Body ⎤⎥ + // ⎣ ⎣ [Continuing ] ⎦⎦ + QueueTasks(PushControlStack(l), + QueueNestedTasks( // + BeginBlockTask(l->Initializer()), + QueueNestedTasks( // + BeginBlockTask(l->Body()), + QueueNestedTasks( // + BeginBlockTask(l->Continuing()), // + [] {}, // + EndBlockTask(l->Continuing())), + EndBlockTask(l->Body())), + EndBlockTask(l->Initializer())), + PopControlStack()); +} + +void Structural::CheckSwitch(const Switch* s) { + CheckResults(s); + CheckOperands(s, Switch::kNumOperands); + + QueueTasks( + PushControlStack(s), + [this, s] { + 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); + } + }, + PopControlStack()); +} + +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; + } +} + +} // namespace tint::core::ir::validator