Skip structured control flow chekcs for non-shader capability.
Fix infinite loop in dominator algorithm when there's an
unreachable block.
BasicBlock::BasicBlock(uint32_t id)
: id_(id),
immediate_dominator_(nullptr),
+ immediate_post_dominator_(nullptr),
predecessors_(),
successors_(),
+ type_(0),
reachable_(false) {}
void BasicBlock::SetImmediateDominator(BasicBlock* dom_block) {
immediate_dominator_ = dom_block;
}
+void BasicBlock::SetImmediatePostDominator(BasicBlock* pdom_block) {
+ immediate_post_dominator_ = pdom_block;
+}
+
const BasicBlock* BasicBlock::GetImmediateDominator() const {
return immediate_dominator_;
}
+const BasicBlock* BasicBlock::GetImmediatePostDominator() const {
+ return immediate_post_dominator_;
+}
+
BasicBlock* BasicBlock::GetImmediateDominator() { return immediate_dominator_; }
+BasicBlock* BasicBlock::GetImmediatePostDominator() {
+ return immediate_post_dominator_;
+}
-void BasicBlock::RegisterSuccessors(vector<BasicBlock*> next_blocks) {
+void BasicBlock::RegisterSuccessors(const vector<BasicBlock*>& next_blocks) {
for (auto& block : next_blocks) {
block->predecessors_.push_back(this);
successors_.push_back(block);
- if (block->reachable_ == false) block->set_reachability(reachable_);
+ if (block->reachable_ == false) block->set_reachable(reachable_);
}
}
}
BasicBlock::DominatorIterator::DominatorIterator() : current_(nullptr) {}
-BasicBlock::DominatorIterator::DominatorIterator(const BasicBlock* block)
- : current_(block) {}
+
+BasicBlock::DominatorIterator::DominatorIterator(
+ const BasicBlock* block,
+ std::function<const BasicBlock*(const BasicBlock*)> dominator_func)
+ : current_(block), dom_func_(dominator_func) {}
BasicBlock::DominatorIterator& BasicBlock::DominatorIterator::operator++() {
- if (current_ == current_->GetImmediateDominator()) {
+ if (current_ == dom_func_(current_)) {
current_ = nullptr;
} else {
- current_ = current_->GetImmediateDominator();
+ current_ = dom_func_(current_);
}
return *this;
}
const BasicBlock::DominatorIterator BasicBlock::dom_begin() const {
- return DominatorIterator(this);
+ return DominatorIterator(
+ this, [](const BasicBlock* b) { return b->GetImmediateDominator(); });
}
BasicBlock::DominatorIterator BasicBlock::dom_begin() {
- return DominatorIterator(this);
+ return DominatorIterator(
+ this, [](const BasicBlock* b) { return b->GetImmediateDominator(); });
}
const BasicBlock::DominatorIterator BasicBlock::dom_end() const {
return DominatorIterator();
}
+const BasicBlock::DominatorIterator BasicBlock::pdom_begin() const {
+ return DominatorIterator(
+ this, [](const BasicBlock* b) { return b->GetImmediatePostDominator(); });
+}
+
+BasicBlock::DominatorIterator BasicBlock::pdom_begin() {
+ return DominatorIterator(
+ this, [](const BasicBlock* b) { return b->GetImmediatePostDominator(); });
+}
+
+const BasicBlock::DominatorIterator BasicBlock::pdom_end() const {
+ return DominatorIterator();
+}
+
+BasicBlock::DominatorIterator BasicBlock::pdom_end() {
+ return DominatorIterator();
+}
+
bool operator==(const BasicBlock::DominatorIterator& lhs,
const BasicBlock::DominatorIterator& rhs) {
return lhs.current_ == rhs.current_;
#include "spirv/1.1/spirv.h"
#include <cstdint>
+
+#include <bitset>
+#include <functional>
#include <vector>
namespace libspirv {
+enum BlockType : uint32_t {
+ kBlockTypeUndefined,
+ kBlockTypeHeader,
+ kBlockTypeLoop,
+ kBlockTypeMerge,
+ kBlockTypeBreak,
+ kBlockTypeContinue,
+ kBlockTypeReturn,
+ kBlockTypeCOUNT ///< Total number of block types. (must be the last element)
+};
+
// This class represents a basic block in a SPIR-V module
class BasicBlock {
public:
/// Returns the successors of the BasicBlock
std::vector<BasicBlock*>* get_successors() { return &successors_; }
- /// Returns true if the block should be reachable in the CFG
+ /// Returns true if the block is reachable in the CFG
bool is_reachable() const { return reachable_; }
- void set_reachability(bool reachability) { reachable_ = reachability; }
+ /// Returns true if BasicBlock is of the given type
+ bool is_type(BlockType type) const {
+ if (type == kBlockTypeUndefined) return type_.none();
+ return type_.test(type);
+ }
+
+ /// Sets the reachability of the basic block in the CFG
+ void set_reachable(bool reachability) { reachable_ = reachability; }
+
+ /// Sets the type of the BasicBlock
+ void set_type(BlockType type) {
+ if (type == kBlockTypeUndefined)
+ type_.reset();
+ else
+ type_.set(type);
+ }
/// Sets the immedate dominator of this basic block
///
/// @param[in] dom_block The dominator block
void SetImmediateDominator(BasicBlock* dom_block);
+ /// Sets the immedate post dominator of this basic block
+ ///
+ /// @param[in] pdom_block The post dominator block
+ void SetImmediatePostDominator(BasicBlock* pdom_block);
+
/// Returns the immedate dominator of this basic block
BasicBlock* GetImmediateDominator();
/// Returns the immedate dominator of this basic block
const BasicBlock* GetImmediateDominator() const;
+ /// Returns the immedate post dominator of this basic block
+ BasicBlock* GetImmediatePostDominator();
+
+ /// Returns the immedate post dominator of this basic block
+ const BasicBlock* GetImmediatePostDominator() const;
+
/// Ends the block without a successor
void RegisterBranchInstruction(SpvOp branch_instruction);
/// Adds @p next BasicBlocks as successors of this BasicBlock
- void RegisterSuccessors(std::vector<BasicBlock*> next = {});
+ void RegisterSuccessors(const std::vector<BasicBlock*>& next = {});
/// Returns true if the id of the BasicBlock matches
bool operator==(const BasicBlock& other) const { return other.id_ == id_; }
/// @brief A BasicBlock dominator iterator class
///
- /// This iterator will iterate over the dominators of the block
+ /// This iterator will iterate over the (post)dominators of the block
class DominatorIterator
: public std::iterator<std::forward_iterator_tag, BasicBlock*> {
public:
/// @brief Constructs an iterator for the given block which points to
/// @p block
///
- /// @param block The block which is referenced by the iterator
- explicit DominatorIterator(const BasicBlock* block);
+ /// @param block The block which is referenced by the iterator
+ /// @param dominator_func This function will be called to get the immediate
+ /// (post)dominator of the current block
+ DominatorIterator(
+ const BasicBlock* block,
+ std::function<const BasicBlock*(const BasicBlock*)> dominator_func);
/// @brief Advances the iterator
DominatorIterator& operator++();
private:
const BasicBlock* current_;
+ std::function<const BasicBlock*(const BasicBlock*)> dom_func_;
};
- /// Returns an iterator which points to the current block
+ /// Returns a dominator iterator which points to the current block
const DominatorIterator dom_begin() const;
+
+ /// Returns a dominator iterator which points to the current block
DominatorIterator dom_begin();
- /// Returns an iterator which points to one element past the first block
+ /// Returns a dominator iterator which points to one element past the first
+ /// block
const DominatorIterator dom_end() const;
+
+ /// Returns a dominator iterator which points to one element past the first
+ /// block
DominatorIterator dom_end();
+ /// Returns a post dominator iterator which points to the current block
+ const DominatorIterator pdom_begin() const;
+ /// Returns a post dominator iterator which points to the current block
+ DominatorIterator pdom_begin();
+
+ /// Returns a post dominator iterator which points to one element past the
+ /// last block
+ const DominatorIterator pdom_end() const;
+
+ /// Returns a post dominator iterator which points to one element past the
+ /// last block
+ DominatorIterator pdom_end();
+
private:
/// Id of the BasicBlock
const uint32_t id_;
/// Pointer to the immediate dominator of the BasicBlock
BasicBlock* immediate_dominator_;
+ /// Pointer to the immediate dominator of the BasicBlock
+ BasicBlock* immediate_post_dominator_;
+
/// The set of predecessors of the BasicBlock
std::vector<BasicBlock*> predecessors_;
/// The set of successors of the BasicBlock
std::vector<BasicBlock*> successors_;
+ /// The type of the block
+ std::bitset<kBlockTypeCOUNT - 1> type_;
+
+ /// True if the block is reachable in the CFG
bool reachable_;
};
#include "val/Construct.h"
+#include <cassert>
+#include <cstddef>
+
namespace libspirv {
-Construct::Construct(BasicBlock* header_block, BasicBlock* merge_block,
- BasicBlock* continue_block)
- : header_block_(header_block),
- merge_block_(merge_block),
- continue_block_(continue_block) {}
+Construct::Construct(ConstructType type, BasicBlock* entry,
+ BasicBlock* exit, std::vector<Construct*> constructs)
+ : type_(type),
+ corresponding_constructs_(constructs),
+ entry_block_(entry),
+ exit_block_(exit) {}
+
+ConstructType Construct::get_type() const { return type_; }
+
+const std::vector<Construct*>& Construct::get_corresponding_constructs() const {
+ return corresponding_constructs_;
+}
+std::vector<Construct*>& Construct::get_corresponding_constructs() {
+ return corresponding_constructs_;
+}
+
+bool ValidateConstructSize(ConstructType type, size_t size) {
+ switch (type) {
+ case ConstructType::kSelection: return size == 0;
+ case ConstructType::kContinue: return size == 1;
+ case ConstructType::kLoop: return size == 1;
+ case ConstructType::kCase: return size >= 1;
+ default: assert(1 == 0 && "Type not defined");
+ }
+ return false;
+}
+
+void Construct::set_corresponding_constructs(
+ std::vector<Construct*> constructs) {
+ assert(ValidateConstructSize(type_, constructs.size()));
+ corresponding_constructs_ = constructs;
+}
+
+const BasicBlock* Construct::get_entry() const { return entry_block_; }
+BasicBlock* Construct::get_entry() { return entry_block_; }
-const BasicBlock* Construct::get_header() const { return header_block_; }
-const BasicBlock* Construct::get_merge() const { return merge_block_; }
-const BasicBlock* Construct::get_continue() const { return continue_block_; }
+const BasicBlock* Construct::get_exit() const { return exit_block_; }
+BasicBlock* Construct::get_exit() { return exit_block_; }
-BasicBlock* Construct::get_header() { return header_block_; }
-BasicBlock* Construct::get_merge() { return merge_block_; }
-BasicBlock* Construct::get_continue() { return continue_block_; }
+void Construct::set_exit(BasicBlock* exit_block) {
+ exit_block_ = exit_block;
}
+} /// namespace libspirv
#define LIBSPIRV_VAL_CONSTRUCT_H_
#include <cstdint>
+#include <vector>
namespace libspirv {
+enum class ConstructType {
+ kNone,
+ /// The set of blocks dominated by a selection header, minus the set of blocks
+ /// dominated by the header's merge block
+ kSelection,
+ /// The set of blocks dominated by an OpLoopMerge's Continue Target and post
+ /// dominated by the corresponding back
+ kContinue,
+ /// The set of blocks dominated by a loop header, minus the set of blocks
+ /// dominated by the loop's merge block, minus the loop's corresponding
+ /// continue construct
+ kLoop,
+ /// The set of blocks dominated by an OpSwitch's Target or Default, minus the
+ /// set of blocks dominated by the OpSwitch's merge block (this construct is
+ /// only defined for those OpSwitch Target or Default that are not equal to
+ /// the OpSwitch's corresponding merge block)
+ kCase
+};
+
class BasicBlock;
/// @brief This class tracks the CFG constructs as defined in the SPIR-V spec
class Construct {
public:
- Construct(BasicBlock* header_block, BasicBlock* merge_block,
- BasicBlock* continue_block = nullptr);
+ Construct(ConstructType type, BasicBlock* dominator,
+ BasicBlock* exit = nullptr,
+ std::vector<Construct*> constructs = {});
+
+ /// Returns the type of the construct
+ ConstructType get_type() const;
+
+ const std::vector<Construct*>& get_corresponding_constructs() const;
+ std::vector<Construct*>& get_corresponding_constructs();
+ void set_corresponding_constructs(std::vector<Construct*> constructs);
+
+ /// Returns the dominator block of the construct.
+ ///
+ /// This is usually the header block or the first block of the construct.
+ const BasicBlock* get_entry() const;
- const BasicBlock* get_header() const;
- const BasicBlock* get_merge() const;
- const BasicBlock* get_continue() const;
+ /// Returns the dominator block of the construct.
+ ///
+ /// This is usually the header block or the first block of the construct.
+ BasicBlock* get_entry();
- BasicBlock* get_header();
- BasicBlock* get_merge();
- BasicBlock* get_continue();
+ /// Returns the exit block of the construct.
+ ///
+ /// For a continue construct it is the backedge block of the corresponding
+ /// loop construct. For the case construct it is the block that branches to
+ /// the OpSwitch merge block or other case blocks. Otherwise it is the merge
+ /// block of the corresponding header block
+ const BasicBlock* get_exit() const;
+
+ /// Returns the exit block of the construct.
+ ///
+ /// For a continue construct it is the backedge block of the corresponding
+ /// loop construct. For the case construct it is the block that branches to
+ /// the OpSwitch merge block or other case blocks. Otherwise it is the merge
+ /// block of the corresponding header block
+ BasicBlock* get_exit();
+
+ /// Sets the exit block for this construct. This is useful for continue
+ /// constructs which do not know the back-edge block during construction
+ void set_exit(BasicBlock* exit_block);
private:
- BasicBlock* header_block_; ///< The header block of a loop or selection
- BasicBlock* merge_block_; ///< The merge block of a loop or selection
- BasicBlock* continue_block_; ///< The continue block of a loop block
+ /// The type of the construct
+ ConstructType type_;
+
+ /// These are the constructs that are related to this construct. These
+ /// constructs can be the continue construct, for the corresponding loop
+ /// construct, the case construct that are part of the same OpSwitch
+ /// instruction
+ ///
+ /// Here is a table that describes what constructs are included in
+ /// @p corresponding_constructs_
+ /// | this construct | corresponding construct |
+ /// |----------------|----------------------------------|
+ /// | loop | continue |
+ /// | continue | loop |
+ /// | case | other cases in the same OpSwitch |
+ ///
+ /// kContinue and kLoop constructs will always have corresponding
+ /// constructs even if they are represented by the same block
+ std::vector<Construct*> corresponding_constructs_;
+
+ /// @brief Dominator block for the construct
+ ///
+ /// The dominator block for the construct. Depending on the construct this may
+ /// be a selection header, a continue target of a loop, a loop header or a
+ /// Target or Default block of a switch
+ BasicBlock* entry_block_;
+
+ /// @brief Exiting block for the construct
+ ///
+ /// The exit block for the construct. This can be a merge block for the loop
+ /// and selection constructs, a back-edge block for a continue construct, or
+ /// the branching block for the case construct
+ BasicBlock* exit_block_;
};
} /// namespace libspirv
#include <cassert>
#include <algorithm>
+#include <utility>
#include "val/BasicBlock.h"
#include "val/Construct.h"
#include "val/ValidationState.h"
+using std::ignore;
using std::list;
+using std::make_pair;
+using std::pair;
using std::string;
+using std::tie;
using std::vector;
namespace libspirv {
declaration_type_(FunctionDecl::kFunctionDeclUnknown),
blocks_(),
current_block_(nullptr),
+ pseudo_exit_block_(kInvalidId),
cfg_constructs_(),
variable_ids_(),
parameter_ids_() {}
uint32_t continue_id) {
RegisterBlock(merge_id, false);
RegisterBlock(continue_id, false);
- cfg_constructs_.emplace_back(get_current_block(), &blocks_.at(merge_id),
- &blocks_.at(continue_id));
+ BasicBlock& merge_block = blocks_.at(merge_id);
+ BasicBlock& continue_block = blocks_.at(continue_id);
+ assert(current_block_ &&
+ "RegisterLoopMerge must be called when called within a block");
+
+ current_block_->set_type(kBlockTypeLoop);
+ merge_block.set_type(kBlockTypeMerge);
+ continue_block.set_type(kBlockTypeContinue);
+ cfg_constructs_.emplace_back(ConstructType::kLoop, current_block_,
+ &merge_block);
+ Construct& loop_construct = cfg_constructs_.back();
+ cfg_constructs_.emplace_back(ConstructType::kContinue, &continue_block);
+ Construct& continue_construct = cfg_constructs_.back();
+ continue_construct.set_corresponding_constructs({&loop_construct});
+ loop_construct.set_corresponding_constructs({&continue_construct});
return SPV_SUCCESS;
}
spv_result_t Function::RegisterSelectionMerge(uint32_t merge_id) {
RegisterBlock(merge_id, false);
- cfg_constructs_.emplace_back(get_current_block(), &blocks_.at(merge_id));
+ BasicBlock& merge_block = blocks_.at(merge_id);
+ current_block_->set_type(kBlockTypeHeader);
+ merge_block.set_type(kBlockTypeMerge);
+
+ cfg_constructs_.emplace_back(ConstructType::kSelection, get_current_block(),
+ &merge_block);
return SPV_SUCCESS;
}
undefined_blocks_.erase(id);
current_block_ = &inserted_block->second;
ordered_blocks_.push_back(current_block_);
- if (IsFirstBlock(id)) current_block_->set_reachability(true);
+ if (IsFirstBlock(id)) current_block_->set_reachable(true);
} else if (success) { // Block doesn't exsist but this is not a definition
undefined_blocks_.insert(id);
}
next_blocks.push_back(&inserted_block->second);
}
+ if (branch_instruction == SpvOpReturn ||
+ branch_instruction == SpvOpReturnValue) {
+ assert(next_blocks.empty());
+ next_blocks.push_back(&pseudo_exit_block_);
+ }
current_block_->RegisterBranchInstruction(branch_instruction);
current_block_->RegisterSuccessors(next_blocks);
current_block_ = nullptr;
const BasicBlock* Function::get_current_block() const { return current_block_; }
BasicBlock* Function::get_current_block() { return current_block_; }
+BasicBlock* Function::get_pseudo_exit_block() { return &pseudo_exit_block_; }
+const BasicBlock* Function::get_pseudo_exit_block() const {
+ return &pseudo_exit_block_;
+}
+
const list<Construct>& Function::get_constructs() const {
return cfg_constructs_;
}
return ordered_blocks_[0];
}
-bool Function::IsMergeBlock(uint32_t merge_block_id) const {
- const auto b = blocks_.find(merge_block_id);
+bool Function::IsBlockType(uint32_t merge_block_id, BlockType type) const {
+ bool ret = false;
+ const BasicBlock* block;
+ tie(block, ignore) = GetBlock(merge_block_id);
+ if (block) {
+ ret = block->is_type(type);
+ }
+ return ret;
+}
+
+pair<const BasicBlock*, bool> Function::GetBlock(uint32_t id) const {
+ const auto b = blocks_.find(id);
if (b != end(blocks_)) {
- return cfg_constructs_.end() !=
- find_if(begin(cfg_constructs_), end(cfg_constructs_),
- [&](const Construct& construct) {
- return construct.get_merge() == &b->second;
- });
+ const BasicBlock* block = &(b->second);
+ bool defined =
+ undefined_blocks_.find(block->get_id()) == end(undefined_blocks_);
+ return make_pair(block, defined);
} else {
- return false;
+ return make_pair(nullptr, false);
}
}
+pair<BasicBlock*, bool> Function::GetBlock(uint32_t id) {
+ const BasicBlock* out;
+ bool defined;
+ tie(out, defined) = const_cast<const Function*>(this)->GetBlock(id);
+ return make_pair(const_cast<BasicBlock*>(out), defined);
+}
} /// namespace libspirv
#define LIBSPIRV_VAL_FUNCTION_H_
#include <list>
-#include <vector>
-#include <unordered_set>
#include <unordered_map>
+#include <unordered_set>
+#include <vector>
#include "spirv/1.1/spirv.h"
#include "spirv-tools/libspirv.h"
void RegisterBlockEnd(std::vector<uint32_t> successors_list,
SpvOp branch_instruction);
- /// Returns true if the \p merge_block_id is a merge block
- bool IsMergeBlock(uint32_t merge_block_id) const;
-
- /// Returns true if the \p id is the first block of this function
+ /// Returns true if the \p id block is the first block of this function
bool IsFirstBlock(uint32_t id) const;
+ /// Returns true if the \p merge_block_id is a BlockType of \p type
+ bool IsBlockType(uint32_t merge_block_id, BlockType type) const;
+
+ /// Returns a pair consisting of the BasicBlock with \p id and a bool
+ /// which is true if the block has been defined, and false if it is
+ /// declared but not defined. This function will return nullptr if the
+ /// \p id was not declared and not defined at the current point in the binary
+ std::pair<const BasicBlock*, bool> GetBlock(uint32_t id) const;
+ std::pair<BasicBlock*, bool> GetBlock(uint32_t id);
+
/// Returns the first block of the current function
const BasicBlock* get_first_block() const;
/// Returns the block that is currently being parsed in the binary
const BasicBlock* get_current_block() const;
+ /// Returns the psudo exit block
+ BasicBlock* get_pseudo_exit_block();
+
+ /// Returns the psudo exit block
+ const BasicBlock* get_pseudo_exit_block() const;
+
/// Prints a GraphViz digraph of the CFG of the current funciton
void printDotGraph() const;
/// The block that is currently being parsed
BasicBlock* current_block_;
+ /// A pseudo exit block that is the successor to all return blocks
+ BasicBlock pseudo_exit_block_;
+
/// The constructs that are available in this function
std::list<Construct> cfg_constructs_;
} /// namespace libspirv
-
#endif /// LIBSPIRV_VAL_FUNCTION_H_
namespace libspirv {
+// Universal Limit of ResultID + 1
+static const uint32_t kInvalidId = 0x400000;
+
// Info about a result ID.
typedef struct spv_id_info_t {
/// Id value.
#include <algorithm>
#include <array>
+#include <functional>
#include <list>
#include <map>
#include <string>
class ValidationState_t;
-/// @brief Calculates dominator edges of a root basic block
+/// A function that returns a vector of BasicBlocks given a BasicBlock. Used to
+/// get the successor and predecessor nodes of a CFG block
+using get_blocks_func =
+ std::function<const std::vector<BasicBlock*>*(const BasicBlock*)>;
+
+/// @brief Calculates dominator edges for a set of blocks
///
-/// This function calculates the dominator edges form a root BasicBlock. Uses
-/// the dominator algorithm by Cooper et al.
+/// This function calculates the dominator edges for a set of blocks in the CFG.
+/// Uses the dominator algorithm by Cooper et al.
///
-/// @param[in] first_block the root or entry BasicBlock of a function
+/// @param[in] postorder A vector of blocks in post order traversal order
+/// in a CFG
+/// @param[in] predecessor_func Function used to get the predecessor nodes of a
+/// block
///
/// @return a set of dominator edges represented as a pair of blocks
std::vector<std::pair<BasicBlock*, BasicBlock*>> CalculateDominators(
- const BasicBlock& first_block);
+ const std::vector<const BasicBlock*>& postorder,
+ get_blocks_func predecessor_func);
/// @brief Performs the Control Flow Graph checks
///
/// provided by the @p dom_edges parameter
///
/// @param[in,out] dom_edges The edges of the dominator tree
+/// @param[in] set_func This function will be called to updated the Immediate
+/// dominator
void UpdateImmediateDominators(
- std::vector<std::pair<BasicBlock*, BasicBlock*>>& dom_edges);
+ const std::vector<std::pair<BasicBlock*, BasicBlock*>>& dom_edges,
+ std::function<void(BasicBlock*, BasicBlock*)> set_func);
/// @brief Prints all of the dominators of a BasicBlock
///
#include <algorithm>
#include <functional>
+#include <set>
+#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
using std::find;
using std::function;
using std::get;
+using std::ignore;
using std::make_pair;
using std::numeric_limits;
using std::pair;
+using std::set;
+using std::string;
+using std::tie;
using std::transform;
using std::unordered_map;
using std::unordered_set;
using cbb_ptr = const BasicBlock*;
using bb_iter = vector<BasicBlock*>::const_iterator;
-using get_blocks_func = function<const vector<BasicBlock*>*(const BasicBlock*)>;
-
struct block_info {
cbb_ptr block; ///< pointer to the block
bb_iter iter; ///< Iterator to the current child node being processed
/// @param[in] entry The root BasicBlock of a CFG tree
/// @param[in] successor_func A function which will return a pointer to the
/// successor nodes
-/// @param[in] preorder A function that will be called for every block in a CFG
-/// following preorder traversal semantics
+/// @param[in] preorder A function that will be called for every block in a
+/// CFG following preorder traversal semantics
/// @param[in] postorder A function that will be called for every block in a
/// CFG following postorder traversal semantics
/// @param[in] backedge A function that will be called when a backedge is
return b->get_successors();
}
+const vector<BasicBlock*>* predecessor(const BasicBlock* b) {
+ return b->get_predecessors();
+}
+
} // namespace
vector<pair<BasicBlock*, BasicBlock*>> CalculateDominators(
- vector<cbb_ptr>& postorder) {
+ const vector<cbb_ptr>& postorder, get_blocks_func predecessor_func) {
struct block_detail {
size_t dominator; ///< The index of blocks's dominator in post order array
size_t postorder_index; ///< The index of the block in the post order array
};
-
- const size_t undefined_dom = static_cast<size_t>(postorder.size());
+ const size_t undefined_dom = postorder.size();
unordered_map<cbb_ptr, block_detail> idoms;
for (size_t i = 0; i < postorder.size(); i++) {
idoms[postorder[i]] = {undefined_dom, i};
}
-
idoms[postorder.back()].dominator = idoms[postorder.back()].postorder_index;
bool changed = true;
while (changed) {
changed = false;
for (auto b = postorder.rbegin() + 1; b != postorder.rend(); b++) {
- size_t& b_dom = idoms[*b].dominator;
- const vector<BasicBlock*>* predecessors = (*b)->get_predecessors();
-
- // first processed predecessor
+ const vector<BasicBlock*>* predecessors = predecessor_func(*b);
+ // first processed/reachable predecessor
auto res = find_if(begin(*predecessors), end(*predecessors),
[&idoms, undefined_dom](BasicBlock* pred) {
- return idoms[pred].dominator != undefined_dom;
+ return idoms[pred].dominator != undefined_dom &&
+ pred->is_reachable();
});
- assert(res != end(*predecessors));
+ if (res == end(*predecessors)) continue;
BasicBlock* idom = *res;
size_t idom_idx = idoms[idom].postorder_index;
// all other predecessors
for (auto p : *predecessors) {
- if (idom == p || p->is_reachable() == false) {
- continue;
- }
+ if (idom == p || p->is_reachable() == false) continue;
if (idoms[p].dominator != undefined_dom) {
size_t finger1 = idoms[p].postorder_index;
size_t finger2 = idom_idx;
idom_idx = finger1;
}
}
- if (b_dom != idom_idx) {
- b_dom = idom_idx;
+ if (idoms[*b].dominator != idom_idx) {
+ idoms[*b].dominator = idom_idx;
changed = true;
}
}
return out;
}
-void UpdateImmediateDominators(vector<pair<bb_ptr, bb_ptr>>& dom_edges) {
+void UpdateImmediateDominators(
+ const vector<pair<bb_ptr, bb_ptr>>& dom_edges,
+ function<void(BasicBlock*, BasicBlock*)> set_func) {
for (auto& edge : dom_edges) {
- get<0>(edge)->SetImmediateDominator(get<1>(edge));
+ set_func(get<0>(edge), get<1>(edge));
}
}
-void printDominatorList(BasicBlock& b) {
+void printDominatorList(const BasicBlock& b) {
std::cout << b.get_id() << " is dominated by: ";
const BasicBlock* bb = &b;
while (bb->GetImmediateDominator() != bb) {
}
spv_result_t MergeBlockAssert(ValidationState_t& _, uint32_t merge_block) {
- if (_.get_current_function().IsMergeBlock(merge_block)) {
+ if (_.get_current_function().IsBlockType(merge_block, kBlockTypeMerge)) {
return _.diag(SPV_ERROR_INVALID_CFG)
<< "Block " << _.getIdName(merge_block)
<< " is already a merge block for another header";
return SPV_SUCCESS;
}
+/// Update the continue construct's exit blocks once the backedge blocks are
+/// identified in the CFG.
+void UpdateContinueConstructExitBlocks(
+ Function& function, const vector<pair<uint32_t, uint32_t>>& back_edges) {
+ auto& constructs = function.get_constructs();
+ // TODO(umar): Think of a faster way to do this
+ for (auto& edge : back_edges) {
+ uint32_t back_edge_block_id;
+ uint32_t loop_header_block_id;
+ tie(back_edge_block_id, loop_header_block_id) = edge;
+
+ auto is_this_header = [=](Construct& c) {
+ return c.get_type() == ConstructType::kLoop &&
+ c.get_entry()->get_id() == loop_header_block_id;
+ };
+
+ for (auto construct : constructs) {
+ if (is_this_header(construct)) {
+ Construct* continue_construct =
+ construct.get_corresponding_constructs().back();
+ assert(continue_construct->get_type() == ConstructType::kContinue);
+
+ BasicBlock* back_edge_block;
+ tie(back_edge_block, ignore) = function.GetBlock(back_edge_block_id);
+ continue_construct->set_exit(back_edge_block);
+ }
+ }
+ }
+}
+
+/// Constructs an error message for construct validation errors
+string ConstructErrorString(const Construct& construct,
+ const string& header_string,
+ const string& exit_string,
+ bool post_dominate = false) {
+ string construct_name;
+ string header_name;
+ string exit_name;
+ string dominate_text;
+ if (post_dominate) {
+ dominate_text = "is not post dominated by";
+ } else {
+ dominate_text = "does not dominate";
+ }
+
+ switch (construct.get_type()) {
+ case ConstructType::kSelection:
+ construct_name = "selection";
+ header_name = "selection header";
+ exit_name = "merge block";
+ break;
+ case ConstructType::kLoop:
+ construct_name = "loop";
+ header_name = "loop header";
+ exit_name = "merge block";
+ break;
+ case ConstructType::kContinue:
+ construct_name = "continue";
+ header_name = "continue target";
+ exit_name = "back-edge block";
+ break;
+ case ConstructType::kCase:
+ construct_name = "case";
+ header_name = "case block";
+ exit_name = "exit block"; // TODO(umar): there has to be a better name
+ break;
+ default:
+ assert(1 == 0 && "Not defined type");
+ }
+ // TODO(umar): Add header block for continue constructs to error message
+ return "The " + construct_name + " construct with the " + header_name + " " +
+ header_string + " " + dominate_text + " the " + exit_name + " " +
+ exit_string;
+}
+
+spv_result_t StructuredControlFlowChecks(
+ const ValidationState_t& _, const Function& function,
+ const vector<pair<uint32_t, uint32_t>>& back_edges) {
+ /// Check all backedges target only loop headers and have exactly one
+ /// back-edge branching to it
+ set<uint32_t> loop_headers;
+ for (auto back_edge : back_edges) {
+ uint32_t back_edge_block;
+ uint32_t header_block;
+ tie(back_edge_block, header_block) = back_edge;
+ if (!function.IsBlockType(header_block, kBlockTypeLoop)) {
+ return _.diag(SPV_ERROR_INVALID_CFG)
+ << "Back-edges (" << _.getIdName(back_edge_block) << " -> "
+ << _.getIdName(header_block)
+ << ") can only be formed between a block and a loop header.";
+ }
+ bool success;
+ tie(ignore, success) = loop_headers.insert(header_block);
+ if (!success) {
+ // TODO(umar): List the back-edge blocks that are branching to loop
+ // header
+ return _.diag(SPV_ERROR_INVALID_CFG)
+ << "Loop header " << _.getIdName(header_block)
+ << " targeted by multiple back-edges";
+ }
+ }
+
+ // Check construct rules
+ for (const Construct& construct : function.get_constructs()) {
+ auto header = construct.get_entry();
+ auto merge = construct.get_exit();
+
+ // if the merge block is reachable then it's dominated by the header
+ if (merge->is_reachable() &&
+ find(merge->dom_begin(), merge->dom_end(), header) ==
+ merge->dom_end()) {
+ return _.diag(SPV_ERROR_INVALID_CFG)
+ << ConstructErrorString(construct, _.getIdName(header->get_id()),
+ _.getIdName(merge->get_id()));
+ }
+ if (construct.get_type() == ConstructType::kContinue) {
+ if (find(header->pdom_begin(), header->pdom_end(), merge) ==
+ merge->pdom_end()) {
+ return _.diag(SPV_ERROR_INVALID_CFG)
+ << ConstructErrorString(construct, _.getIdName(header->get_id()),
+ _.getIdName(merge->get_id()), true);
+ }
+ }
+ // TODO(umar): an OpSwitch block dominates all its defined case
+ // constructs
+ // TODO(umar): each case construct has at most one branch to another
+ // case construct
+ // TODO(umar): each case construct is branched to by at most one other
+ // case construct
+ // TODO(umar): if Target T1 branches to Target T2, or if Target T1
+ // branches to the Default and the Default branches to Target T2, then
+ // T1 must immediately precede T2 in the list of the OpSwitch Target
+ // operands
+ }
+ return SPV_SUCCESS;
+}
+
spv_result_t PerformCfgChecks(ValidationState_t& _) {
for (auto& function : _.get_functions()) {
+ // Check all referenced blocks are defined within a function
+ if (function.get_undefined_block_count() != 0) {
+ string undef_blocks("{");
+ for (auto undefined_block : function.get_undefined_blocks()) {
+ undef_blocks += _.getIdName(undefined_block) + " ";
+ }
+ return _.diag(SPV_ERROR_INVALID_CFG)
+ << "Block(s) " << undef_blocks << "\b}"
+ << " are referenced but not defined in function "
+ << _.getIdName(function.get_id());
+ }
+
// Updates each blocks immediate dominators
vector<const BasicBlock*> postorder;
+ vector<const BasicBlock*> postdom_postorder;
vector<pair<uint32_t, uint32_t>> back_edges;
if (auto* first_block = function.get_first_block()) {
+ /// calculate dominators
DepthFirstTraversal(*first_block, successor, [](cbb_ptr) {},
[&](cbb_ptr b) { postorder.push_back(b); },
[&](cbb_ptr from, cbb_ptr to) {
back_edges.emplace_back(from->get_id(),
to->get_id());
});
- auto edges = libspirv::CalculateDominators(postorder);
- libspirv::UpdateImmediateDominators(edges);
+ auto edges = libspirv::CalculateDominators(postorder, predecessor);
+ libspirv::UpdateImmediateDominators(
+ edges, [](bb_ptr block, bb_ptr dominator) {
+ block->SetImmediateDominator(dominator);
+ });
+
+ /// calculate post dominators
+ auto exit_block = function.get_pseudo_exit_block();
+ DepthFirstTraversal(*exit_block, predecessor, [](cbb_ptr) {},
+ [&](cbb_ptr b) { postdom_postorder.push_back(b); },
+ [&](cbb_ptr, cbb_ptr) {});
+ auto postdom_edges =
+ libspirv::CalculateDominators(postdom_postorder, successor);
+ libspirv::UpdateImmediateDominators(
+ postdom_edges, [](bb_ptr block, bb_ptr dominator) {
+ block->SetImmediatePostDominator(dominator);
+ });
}
+ UpdateContinueConstructExitBlocks(function, back_edges);
// Check if the order of blocks in the binary appear before the blocks they
// dominate
}
}
- // Check all referenced blocks are defined within a function
- if (function.get_undefined_block_count() != 0) {
- std::stringstream ss;
- ss << "{";
- for (auto undefined_block : function.get_undefined_blocks()) {
- ss << _.getIdName(undefined_block) << " ";
- }
- return _.diag(SPV_ERROR_INVALID_CFG)
- << "Block(s) " << ss.str() << "\b}"
- << " are referenced but not defined in function "
- << _.getIdName(function.get_id());
+ /// Structured control flow checks are only required for shader capabilities
+ if (_.hasCapability(SpvCapabilityShader)) {
+ spvCheckReturn(StructuredControlFlowChecks(_, function, back_edges));
}
-
- // Check all headers dominate their merge blocks
- for (Construct& construct : function.get_constructs()) {
- auto header = construct.get_header();
- auto merge = construct.get_merge();
- // auto cont = construct.get_continue();
-
- if (merge->is_reachable() &&
- find(merge->dom_begin(), merge->dom_end(), header) ==
- merge->dom_end()) {
- return _.diag(SPV_ERROR_INVALID_CFG)
- << "Header block " << _.getIdName(header->get_id())
- << " doesn't dominate its merge block "
- << _.getIdName(merge->get_id());
- }
- }
-
- // TODO(umar): All CFG back edges must branch to a loop header, with each
- // loop header having exactly one back edge branching to it
-
- // TODO(umar): For a given loop, its back-edge block must post dominate the
- // OpLoopMerge's Continue Target, and that Continue Target must dominate the
- // back-edge block
}
return SPV_SUCCESS;
}
spvCheckReturn(_.get_current_function().RegisterBlock(inst->result_id));
break;
case SpvOpLoopMerge: {
- // TODO(umar): mark current block as a loop header
uint32_t merge_block = inst->words[inst->operands[0].offset];
uint32_t continue_block = inst->words[inst->operands[1].offset];
CFG_ASSERT(MergeBlockAssert, merge_block);
using libspirv::BasicBlock;
using libspirv::ValidationState_t;
-using ValidateCFG = spvtest::ValidateBase<bool>;
+using ValidateCFG = spvtest::ValidateBase<SpvCapability>;
using spvtest::ScopedContext;
namespace {
return lhs;
}
-string header =
- "OpCapability Shader\n"
- "OpMemoryModel Logical GLSL450\n";
+const char* header(SpvCapability cap) {
+ static const char* shader_header =
+ "OpCapability Shader\n"
+ "OpMemoryModel Logical GLSL450\n";
-string types_consts =
- "%voidt = OpTypeVoid\n"
- "%boolt = OpTypeBool\n"
- "%intt = OpTypeInt 32 1\n"
- "%one = OpConstant %intt 1\n"
- "%two = OpConstant %intt 2\n"
- "%ptrt = OpTypePointer Function %intt\n"
- "%funct = OpTypeFunction %voidt\n";
+ static const char* kernel_header =
+ "OpCapability Kernel\n"
+ "OpMemoryModel Logical OpenCL\n";
-TEST_F(ValidateCFG, Simple) {
- Block first("first");
+ return (cap == SpvCapabilityShader) ? shader_header : kernel_header;
+}
+
+const char* types_consts() {
+ static const char* types =
+ "%voidt = OpTypeVoid\n"
+ "%boolt = OpTypeBool\n"
+ "%intt = OpTypeInt 32 1\n"
+ "%one = OpConstant %intt 1\n"
+ "%two = OpConstant %intt 2\n"
+ "%ptrt = OpTypePointer Function %intt\n"
+ "%funct = OpTypeFunction %voidt\n";
+
+ return types;
+}
+
+INSTANTIATE_TEST_CASE_P(StructuredControlFlow, ValidateCFG,
+ ::testing::Values(SpvCapabilityShader,
+ SpvCapabilityKernel));
+
+TEST_P(ValidateCFG, Simple) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
Block loop("loop", SpvOpBranchConditional);
Block cont("cont");
Block merge("merge", SpvOpReturn);
- loop.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpLoopMerge %merge %cont None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) {
+ loop.setBody("OpLoopMerge %merge %cont None\n");
+ }
- string str = header + nameOps("loop", "first", "cont", "merge",
- make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + nameOps("loop", "entry", "cont", "merge",
+ make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
- str += first >> loop;
+ str += entry >> loop;
str += loop >> vector<Block>({cont, merge});
str += cont >> loop;
str += merge;
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, Variable) {
+TEST_P(ValidateCFG, Variable) {
Block entry("entry");
Block cont("cont");
Block exit("exit", SpvOpReturn);
entry.setBody("%var = OpVariable %ptrt Function\n");
- string str = header + nameOps(make_pair("func", "Main")) + types_consts +
- " %func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + nameOps(make_pair("func", "Main")) +
+ types_consts() + " %func = OpFunction %voidt None %funct\n";
str += entry >> cont;
str += cont >> exit;
str += exit;
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, VariableNotInFirstBlockBad) {
+TEST_P(ValidateCFG, VariableNotInFirstBlockBad) {
Block entry("entry");
Block cont("cont");
Block exit("exit", SpvOpReturn);
// This operation should only be performed in the entry block
cont.setBody("%var = OpVariable %ptrt Function\n");
- string str = header + nameOps(make_pair("func", "Main")) + types_consts +
- " %func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + nameOps(make_pair("func", "Main")) +
+ types_consts() + " %func = OpFunction %voidt None %funct\n";
str += entry >> cont;
str += cont >> exit;
"Variables can only be defined in the first block of a function"));
}
-TEST_F(ValidateCFG, BlockAppearsBeforeDominatorBad) {
+TEST_P(ValidateCFG, BlockAppearsBeforeDominatorBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block cont("cont");
Block branch("branch", SpvOpBranchConditional);
Block merge("merge", SpvOpReturn);
- branch.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %merge None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) branch.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("cont", "branch", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("cont", "branch", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> branch;
str += cont >> merge; // cont appears before its dominator
"before its dominator .\\[branch\\]"));
}
-TEST_F(ValidateCFG, MergeBlockTargetedByMultipleHeaderBlocksBad) {
+TEST_P(ValidateCFG, MergeBlockTargetedByMultipleHeaderBlocksBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block loop("loop");
Block selection("selection", SpvOpBranchConditional);
Block merge("merge", SpvOpReturn);
- loop.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- " OpLoopMerge %merge %loop None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody(" OpLoopMerge %merge %loop None\n");
+
// cannot share the same merge
- selection.setBody("OpSelectionMerge %merge None\n");
+ if (is_shader) selection.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("merge", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("merge", make_pair("func", "Main")) + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
str += entry >> loop;
str += loop >> selection;
str += "OpFunctionEnd\n";
CompileSuccessfully(str);
- ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
- EXPECT_THAT(getDiagnosticString(),
- MatchesRegex("Block .\\[merge\\] is already a merge block "
- "for another header"));
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("Block .\\[merge\\] is already a merge block "
+ "for another header"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
}
-TEST_F(ValidateCFG, MergeBlockTargetedByMultipleHeaderBlocksSelectionBad) {
+TEST_P(ValidateCFG, MergeBlockTargetedByMultipleHeaderBlocksSelectionBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block loop("loop", SpvOpBranchConditional);
Block selection("selection", SpvOpBranchConditional);
Block merge("merge", SpvOpReturn);
- selection.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- " OpSelectionMerge %merge None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) selection.setBody(" OpSelectionMerge %merge None\n");
+
// cannot share the same merge
- loop.setBody(" OpLoopMerge %merge %loop None\n");
+ if (is_shader) loop.setBody(" OpLoopMerge %merge %loop None\n");
- string str = header + nameOps("merge", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("merge", make_pair("func", "Main")) + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
str += entry >> selection;
str += selection >> vector<Block>({merge, loop});
str += "OpFunctionEnd\n";
CompileSuccessfully(str);
- ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
- EXPECT_THAT(getDiagnosticString(),
- MatchesRegex("Block .\\[merge\\] is already a merge block "
- "for another header"));
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("Block .\\[merge\\] is already a merge block "
+ "for another header"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
}
-TEST_F(ValidateCFG, BranchTargetFirstBlockBad) {
+TEST_P(ValidateCFG, BranchTargetFirstBlockBad) {
Block entry("entry");
Block bad("bad");
Block end("end", SpvOpReturn);
- string str = header + nameOps("entry", "bad", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("entry", "bad", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> bad;
str += bad >> entry; // Cannot target entry block
"is targeted by block .\\[bad\\]"));
}
-TEST_F(ValidateCFG, BranchConditionalTrueTargetFirstBlockBad) {
+TEST_P(ValidateCFG, BranchConditionalTrueTargetFirstBlockBad) {
Block entry("entry");
Block bad("bad", SpvOpBranchConditional);
Block exit("exit", SpvOpReturn);
- bad.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- " OpLoopMerge %entry %exit None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ bad.setBody(" OpLoopMerge %entry %exit None\n");
- string str = header + nameOps("entry", "bad", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("entry", "bad", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> bad;
str += bad >> vector<Block>({entry, exit}); // cannot target entry block
"is targeted by block .\\[bad\\]"));
}
-TEST_F(ValidateCFG, BranchConditionalFalseTargetFirstBlockBad) {
+TEST_P(ValidateCFG, BranchConditionalFalseTargetFirstBlockBad) {
Block entry("entry");
Block bad("bad", SpvOpBranchConditional);
Block t("t");
Block merge("merge");
Block end("end", SpvOpReturn);
- bad.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpLoopMerge %merge %cont None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ bad.setBody("OpLoopMerge %merge %cont None\n");
- string str = header + nameOps("entry", "bad", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("entry", "bad", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> bad;
str += bad >> vector<Block>({t, entry});
"is targeted by block .\\[bad\\]"));
}
-TEST_F(ValidateCFG, SwitchTargetFirstBlockBad) {
+TEST_P(ValidateCFG, SwitchTargetFirstBlockBad) {
Block entry("entry");
Block bad("bad", SpvOpSwitch);
Block block1("block1");
Block merge("merge");
Block end("end", SpvOpReturn);
- bad.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %merge None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ bad.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("entry", "bad", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("entry", "bad", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> bad;
str += bad >> vector<Block>({def, block1, block2, block3, entry});
"is targeted by block .\\[bad\\]"));
}
-TEST_F(ValidateCFG, BranchToBlockInOtherFunctionBad) {
+TEST_P(ValidateCFG, BranchToBlockInOtherFunctionBad) {
Block entry("entry");
Block middle("middle", SpvOpBranchConditional);
Block end("end", SpvOpReturn);
- middle.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %end None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ middle.setBody("OpSelectionMerge %end None\n");
Block entry2("entry2");
Block middle2("middle2");
Block end2("end2", SpvOpReturn);
- string str = header + nameOps("middle2", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("middle2", make_pair("func", "Main")) + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
str += entry >> middle;
str += middle >> vector<Block>({end, middle2});
"defined in function .\\[Main\\]"));
}
-TEST_F(ValidateCFG, HeaderDoesntDominatesMergeBad) {
+TEST_P(ValidateCFG, HeaderDoesntDominatesMergeBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block head("head", SpvOpBranchConditional);
Block f("f");
entry.setBody("%cond = OpSLessThan %intt %one %two\n");
- head.setBody("OpSelectionMerge %merge None\n");
+ if (is_shader) head.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("head", "merge", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("head", "merge", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> merge;
str += head >> vector<Block>({merge, f});
str += merge;
CompileSuccessfully(str);
- ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
- EXPECT_THAT(
- getDiagnosticString(),
- MatchesRegex("Header block .\\[head\\] doesn't dominate its merge block "
- ".\\[merge\\]"));
+
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(
+ getDiagnosticString(),
+ MatchesRegex("The selection construct with the selection header "
+ ".\\[head\\] does not dominate the merge block "
+ ".\\[merge\\]"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
}
-TEST_F(ValidateCFG, UnreachableMerge) {
+TEST_P(ValidateCFG, UnreachableMerge) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block branch("branch", SpvOpBranchConditional);
Block t("t", SpvOpReturn);
Block f("f", SpvOpReturn);
Block merge("merge", SpvOpReturn);
- branch.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %merge None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) branch.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("branch", "merge", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("branch", "merge", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> branch;
str += branch >> vector<Block>({t, f});
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, UnreachableMergeDefinedByOpUnreachable) {
+TEST_P(ValidateCFG, UnreachableMergeDefinedByOpUnreachable) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block branch("branch", SpvOpBranchConditional);
Block t("t", SpvOpReturn);
Block f("f", SpvOpReturn);
Block merge("merge", SpvOpUnreachable);
- branch.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %merge None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) branch.setBody("OpSelectionMerge %merge None\n");
- string str = header + nameOps("branch", "merge", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("branch", "merge", make_pair("func", "Main")) +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> branch;
str += branch >> vector<Block>({t, f});
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, UnreachableBlock) {
+TEST_P(ValidateCFG, UnreachableBlock) {
Block entry("entry");
Block unreachable("unreachable");
Block exit("exit", SpvOpReturn);
- string str = header +
+ string str = header(GetParam()) +
nameOps("unreachable", "exit", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> exit;
str += unreachable >> exit;
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, UnreachableBranch) {
+TEST_P(ValidateCFG, UnreachableBranch) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block unreachable("unreachable", SpvOpBranchConditional);
Block unreachablechildt("unreachablechildt");
Block merge("merge");
Block exit("exit", SpvOpReturn);
- unreachable.setBody(
- " %cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %merge None\n");
- string str = header +
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) unreachable.setBody("OpSelectionMerge %merge None\n");
+ string str = header(GetParam()) +
nameOps("unreachable", "exit", make_pair("func", "Main")) +
- types_consts + "%func = OpFunction %voidt None %funct\n";
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> exit;
str += unreachable >> vector<Block>({unreachablechildt, unreachablechildf});
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, EmptyFunction) {
- string str = header + types_consts +
+TEST_P(ValidateCFG, EmptyFunction) {
+ string str = header(GetParam()) + string(types_consts()) +
"%func = OpFunction %voidt None %funct\n" + "OpFunctionEnd\n";
CompileSuccessfully(str);
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, SingleBlockLoop) {
+TEST_P(ValidateCFG, SingleBlockLoop) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block loop("loop", SpvOpBranchConditional);
Block exit("exit", SpvOpReturn);
- loop.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpLoopMerge %exit %loop None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody("OpLoopMerge %exit %loop None\n");
- string str =
- header + types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + string(types_consts()) +
+ "%func = OpFunction %voidt None %funct\n";
str += entry >> loop;
str += loop >> vector<Block>({loop, exit});
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, NestedLoops) {
+TEST_P(ValidateCFG, NestedLoops) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block loop1("loop1");
Block loop1_cont_break_block("loop1_cont_break_block",
Block loop1_merge("loop1_merge");
Block exit("exit", SpvOpReturn);
- loop1.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpLoopMerge %loop1_merge %loop2 None\n");
-
- loop2.setBody("OpLoopMerge %loop2_merge %loop2 None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) {
+ loop1.setBody("OpLoopMerge %loop1_merge %loop2 None\n");
+ loop2.setBody("OpLoopMerge %loop2_merge %loop2 None\n");
+ }
- string str =
- header + types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + nameOps("loop2", "loop2_merge") +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> loop1;
str += loop1 >> loop1_cont_break_block;
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-TEST_F(ValidateCFG, NestedSelection) {
+TEST_P(ValidateCFG, NestedSelection) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
const int N = 256;
vector<Block> if_blocks;
vector<Block> merge_blocks;
Block inner("inner");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+
if_blocks.emplace_back("if0", SpvOpBranchConditional);
- if_blocks[0].setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpSelectionMerge %if_merge0 None\n");
+
+ if (is_shader) if_blocks[0].setBody("OpSelectionMerge %if_merge0 None\n");
merge_blocks.emplace_back("if_merge0", SpvOpReturn);
for (int i = 1; i < N; i++) {
stringstream ss;
ss << i;
if_blocks.emplace_back("if" + ss.str(), SpvOpBranchConditional);
- if_blocks[i].setBody("OpSelectionMerge %if_merge" + ss.str() + " None\n");
+ if (is_shader)
+ if_blocks[i].setBody("OpSelectionMerge %if_merge" + ss.str() + " None\n");
merge_blocks.emplace_back("if_merge" + ss.str(), SpvOpBranch);
}
- string str =
- header + types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) + string(types_consts()) +
+ "%func = OpFunction %voidt None %funct\n";
+ str += entry >> if_blocks[0];
for (int i = 0; i < N - 1; i++) {
str += if_blocks[i] >> vector<Block>({if_blocks[i + 1], merge_blocks[i]});
}
ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
-// TODO(umar): enable this test
-TEST_F(ValidateCFG, DISABLED_BackEdgeBlockDoesntPostDominateContinueTargetBad) {
+TEST_P(ValidateCFG, BackEdgeBlockDoesntPostDominateContinueTargetBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
Block entry("entry");
Block loop1("loop1", SpvOpBranchConditional);
Block loop2("loop2", SpvOpBranchConditional);
- Block loop2_merge("loop2_merge");
- Block loop1_merge("loop1_merge", SpvOpBranchConditional);
+ Block loop2_merge("loop2_merge", SpvOpBranchConditional);
+ Block be_block("be_block");
Block exit("exit", SpvOpReturn);
- loop1.setBody(
- "%cond = OpSLessThan %intt %one %two\n"
- "OpLoopMerge %loop1_merge %loop2 None\n");
-
- loop2.setBody("OpLoopMerge %loop2_merge %loop2 None\n");
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) {
+ loop1.setBody("OpLoopMerge %exit %loop2_merge None\n");
+ loop2.setBody("OpLoopMerge %loop2_merge %loop2 None\n");
+ }
- string str =
- header + types_consts + "%func = OpFunction %voidt None %funct\n";
+ string str = header(GetParam()) +
+ nameOps("loop1", "loop2", "be_block", "loop2_merge") +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
str += entry >> loop1;
- str += loop1 >> vector<Block>({loop2, loop1_merge});
+ str += loop1 >> vector<Block>({loop2, exit});
str += loop2 >> vector<Block>({loop2, loop2_merge});
- str += loop2_merge >> loop1_merge;
- str += loop1_merge >> vector<Block>({loop1, exit});
+ str += loop2_merge >> vector<Block>({be_block, exit});
+ str += be_block >> loop1;
str += exit;
str += "OpFunctionEnd";
CompileSuccessfully(str);
- ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ if (GetParam() == SpvCapabilityShader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("The continue construct with the continue target "
+ ".\\[loop2_merge\\] is not post dominated by the "
+ "back-edge block .\\[be_block\\]"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, BranchingToNonLoopHeaderBlockBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block split("split", SpvOpBranchConditional);
+ Block t("t");
+ Block f("f");
+ Block exit("exit", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) split.setBody("OpSelectionMerge %exit None\n");
+
+ string str = header(GetParam()) + nameOps("split", "f") + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> split;
+ str += split >> vector<Block>({t, f});
+ str += t >> exit;
+ str += f >> split;
+ str += exit;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(
+ getDiagnosticString(),
+ MatchesRegex("Back-edges \\(.\\[f\\] -> .\\[split\\]\\) can only "
+ "be formed between a block and a loop header."));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, BranchingToSameNonLoopHeaderBlockBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block split("split", SpvOpBranchConditional);
+ Block exit("exit", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) split.setBody("OpSelectionMerge %exit None\n");
+
+ string str = header(GetParam()) + nameOps("split") + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> split;
+ str += split >> vector<Block>({split, exit});
+ str += exit;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex(
+ "Back-edges \\(.\\[split\\] -> .\\[split\\]\\) can only be "
+ "formed between a block and a loop header."));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, MultipleBackEdgesToLoopHeaderBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block loop("loop", SpvOpBranchConditional);
+ Block cont("cont", SpvOpBranchConditional);
+ Block merge("merge", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody("OpLoopMerge %merge %loop None\n");
+
+ string str = header(GetParam()) + nameOps("cont", "loop") + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> loop;
+ str += loop >> vector<Block>({cont, merge});
+ str += cont >> vector<Block>({loop, loop});
+ str += merge;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex(
+ "Loop header .\\[loop\\] targeted by multiple back-edges"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, ContinueTargetMustBePostDominatedByBackEdge) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block loop("loop", SpvOpBranchConditional);
+ Block cheader("cheader", SpvOpBranchConditional);
+ Block be_block("be_block");
+ Block merge("merge", SpvOpReturn);
+ Block exit("exit", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody("OpLoopMerge %merge %cheader None\n");
+
+ string str = header(GetParam()) + nameOps("cheader", "be_block") +
+ types_consts() + "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> loop;
+ str += loop >> vector<Block>({cheader, merge});
+ str += cheader >> vector<Block>({exit, be_block});
+ str += exit; // Branches out of a continue construct
+ str += be_block >> loop;
+ str += merge;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("The continue construct with the continue target "
+ ".\\[cheader\\] is not post dominated by the "
+ "back-edge block .\\[be_block\\]"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, BranchOutOfConstructToMergeBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block loop("loop", SpvOpBranchConditional);
+ Block cont("cont", SpvOpBranchConditional);
+ Block merge("merge", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody("OpLoopMerge %merge %loop None\n");
+
+ string str = header(GetParam()) + nameOps("cont", "loop") + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> loop;
+ str += loop >> vector<Block>({cont, merge});
+ str += cont >> vector<Block>({loop, merge});
+ str += merge;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("The continue construct with the continue target "
+ ".\\[loop\\] is not post dominated by the "
+ "back-edge block .\\[cont\\]"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_P(ValidateCFG, BranchOutOfConstructBad) {
+ bool is_shader = GetParam() == SpvCapabilityShader;
+ Block entry("entry");
+ Block loop("loop", SpvOpBranchConditional);
+ Block cont("cont", SpvOpBranchConditional);
+ Block merge("merge");
+ Block exit("exit", SpvOpReturn);
+
+ entry.setBody("%cond = OpSLessThan %intt %one %two\n");
+ if (is_shader) loop.setBody("OpLoopMerge %merge %loop None\n");
+
+ string str = header(GetParam()) + nameOps("cont", "loop") + types_consts() +
+ "%func = OpFunction %voidt None %funct\n";
+
+ str += entry >> loop;
+ str += loop >> vector<Block>({cont, merge});
+ str += cont >> vector<Block>({loop, exit});
+ str += merge >> exit;
+ str += exit;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ if (is_shader) {
+ ASSERT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+ EXPECT_THAT(getDiagnosticString(),
+ MatchesRegex("The continue construct with the continue target "
+ ".\\[loop\\] is not post dominated by the "
+ "back-edge block .\\[cont\\]"));
+ } else {
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+ }
+}
+
+TEST_F(ValidateCFG, OpSwitchToUnreachableBlock) {
+ Block entry("entry", SpvOpSwitch);
+ Block case0("case0");
+ Block case1("case1");
+ Block case2("case2");
+ Block def("default", SpvOpUnreachable);
+ Block phi("phi", SpvOpReturn);
+
+ string str = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %id
+OpExecutionMode %main LocalSize 1 1 1
+OpSource GLSL 430
+OpName %main "main"
+OpDecorate %id BuiltIn GlobalInvocationId
+%void = OpTypeVoid
+%voidf = OpTypeFunction %void
+%u32 = OpTypeInt 32 0
+%f32 = OpTypeFloat 32
+%uvec3 = OpTypeVector %u32 3
+%fvec3 = OpTypeVector %f32 3
+%uvec3ptr = OpTypePointer Input %uvec3
+%id = OpVariable %uvec3ptr Input
+%one = OpConstant %u32 1
+%three = OpConstant %u32 3
+%main = OpFunction %void None %voidf
+)";
+
+ entry.setBody(
+ "%idval = OpLoad %uvec3 %id\n"
+ "%x = OpCompositeExtract %u32 %idval 0\n"
+ "%selector = OpUMod %u32 %x %three\n"
+ "OpSelectionMerge %phi None\n");
+ str += entry >> vector<Block>({def, case0, case1, case2});
+ str += case1 >> phi;
+ str += def;
+ str += phi;
+ str += case0 >> phi;
+ str += case2 >> phi;
+ str += "OpFunctionEnd";
+
+ CompileSuccessfully(str);
+ ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
}
/// TODO(umar): Switch instructions
-/// TODO(umar): CFG branching outside of CFG construct
/// TODO(umar): Nested CFG constructs
-}
+} /// namespace
template class spvtest::ValidateBase<
std::tuple<int, std::tuple<std::string, std::function<spv_result_t(int)>,
std::function<spv_result_t(int)>>>>;
+template class spvtest::ValidateBase<SpvCapability>;
}