[spirv-reader] Classify CFG edges
Classify CFG edges: - loop backedge - a structured exit: - loop break - loop continue - selection break - fallthrough - forward (any of the rest) Also error out when there should have been a merge instruction. (More than one unique fallthrough or forward edge). Includes lots of tests. Bug: tint:3 Change-Id: I70f27680bdf098213056522abf04ac58a6b478ab Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/20481 Reviewed-by: dan sinclair <dsinclair@google.com>
This commit is contained in:
parent
32b41b79da
commit
054927d7eb
|
@ -443,6 +443,11 @@ bool FunctionEmitter::EmitBody() {
|
|||
if (!FindSwitchCaseHeaders()) {
|
||||
return false;
|
||||
}
|
||||
if (!ClassifyCFGEdges()) {
|
||||
return false;
|
||||
}
|
||||
// TODO(dneto): FindIfSelectionHeaders
|
||||
// TODO(dneto): FindInvalidNestingBetweenSelections
|
||||
|
||||
if (!EmitFunctionVariables()) {
|
||||
return false;
|
||||
|
@ -915,6 +920,237 @@ bool FunctionEmitter::FindSwitchCaseHeaders() {
|
|||
return success();
|
||||
}
|
||||
|
||||
BlockInfo* FunctionEmitter::HeaderForLoopOrContinue(const Construct* c) {
|
||||
if (c->kind == Construct::kLoop) {
|
||||
return GetBlockInfo(c->begin_id);
|
||||
}
|
||||
if (c->kind == Construct::kContinue) {
|
||||
auto* continue_block = GetBlockInfo(c->begin_id);
|
||||
return GetBlockInfo(continue_block->header_for_continue);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FunctionEmitter::ClassifyCFGEdges() {
|
||||
if (failed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks validity of CFG edges leaving each basic block. This implicitly
|
||||
// checks dominance rules for headers and continue constructs.
|
||||
//
|
||||
// For each branch encountered, classify each edge (S,T) as:
|
||||
// - a back-edge
|
||||
// - a structured exit (specific ways of branching to enclosing construct)
|
||||
// - a normal (forward) edge
|
||||
//
|
||||
// If more than one block is targeted by a normal edge, then S must be a
|
||||
// structured header.
|
||||
//
|
||||
// Term: NEC(B) is the nearest enclosing construct for B.
|
||||
//
|
||||
// If edge (S,T) is a normal edge, and NEC(S) != NEC(T), then
|
||||
// T is the header block of its NEC(T), and
|
||||
// NEC(S) is the parent of NEC(T).
|
||||
|
||||
for (const auto src : block_order_) {
|
||||
assert(src > 0);
|
||||
auto* src_info = GetBlockInfo(src);
|
||||
assert(src_info);
|
||||
const auto src_pos = src_info->pos;
|
||||
const auto& src_construct = *(src_info->construct);
|
||||
|
||||
// Compute the ordered list of unique successors.
|
||||
std::vector<uint32_t> successors;
|
||||
{
|
||||
std::unordered_set<uint32_t> visited;
|
||||
src_info->basic_block->ForEachSuccessorLabel(
|
||||
[&successors, &visited](const uint32_t succ) {
|
||||
if (visited.count(succ) == 0) {
|
||||
successors.push_back(succ);
|
||||
visited.insert(succ);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// There should only be one backedge per backedge block.
|
||||
uint32_t num_backedges = 0;
|
||||
|
||||
// Track destinations for normal forward edges.
|
||||
std::vector<uint32_t> normal_forward_edges;
|
||||
|
||||
if (successors.empty() && src_construct.enclosing_continue) {
|
||||
// Kill and return are not allowed in a continue construct.
|
||||
return Fail() << "Invalid function exit at block " << src
|
||||
<< " from continue construct starting at "
|
||||
<< src_construct.enclosing_continue->begin_id;
|
||||
}
|
||||
|
||||
for (const auto dest : successors) {
|
||||
const auto* dest_info = GetBlockInfo(dest);
|
||||
// We've already checked terminators are sane.
|
||||
assert(dest_info);
|
||||
const auto dest_pos = dest_info->pos;
|
||||
|
||||
// Insert the edge kind entry and keep a handle to update
|
||||
// its classification.
|
||||
EdgeKind& edge_kind = src_info->succ_edge[dest];
|
||||
|
||||
if (src_pos >= dest_pos) {
|
||||
// This is a backedge.
|
||||
edge_kind = EdgeKind::kBack;
|
||||
num_backedges++;
|
||||
const auto* continue_construct = src_construct.enclosing_continue;
|
||||
if (!continue_construct) {
|
||||
return Fail() << "Invalid backedge (" << src << "->" << dest
|
||||
<< "): " << src << " is not in a continue construct";
|
||||
}
|
||||
if (src_pos != continue_construct->end_pos - 1) {
|
||||
return Fail() << "Invalid exit (" << src << "->" << dest
|
||||
<< ") from continue construct: " << src
|
||||
<< " is not the last block in the continue construct "
|
||||
"starting at "
|
||||
<< src_construct.begin_id
|
||||
<< " (violates post-dominance rule)";
|
||||
}
|
||||
const auto* ct_info = GetBlockInfo(continue_construct->begin_id);
|
||||
assert(ct_info);
|
||||
if (ct_info->header_for_continue != dest) {
|
||||
return Fail()
|
||||
<< "Invalid backedge (" << src << "->" << dest
|
||||
<< "): does not branch to the corresponding loop header, "
|
||||
"expected "
|
||||
<< ct_info->header_for_continue;
|
||||
}
|
||||
} else {
|
||||
// This is a forward edge.
|
||||
// For now, classify it that way, but we might update it.
|
||||
edge_kind = EdgeKind::kForward;
|
||||
|
||||
// Exit from a continue construct can only be from the last block.
|
||||
const auto* continue_construct = src_construct.enclosing_continue;
|
||||
if (continue_construct != nullptr) {
|
||||
if (continue_construct->ContainsPos(src_pos) &&
|
||||
!continue_construct->ContainsPos(dest_pos) &&
|
||||
(src_pos != continue_construct->end_pos - 1)) {
|
||||
return Fail() << "Invalid exit (" << src << "->" << dest
|
||||
<< ") from continue construct: " << src
|
||||
<< " is not the last block in the continue construct "
|
||||
"starting at "
|
||||
<< continue_construct->begin_id
|
||||
<< " (violates post-dominance rule)";
|
||||
}
|
||||
}
|
||||
|
||||
// Check valid structured exit cases.
|
||||
|
||||
const auto& header_info = *GetBlockInfo(src_construct.begin_id);
|
||||
if (dest == header_info.merge_for_header) {
|
||||
// Branch to construct's merge block.
|
||||
const bool src_is_loop_header = header_info.continue_for_header != 0;
|
||||
const bool src_is_continue_header =
|
||||
header_info.header_for_continue != 0;
|
||||
edge_kind = (src_is_loop_header || src_is_continue_header)
|
||||
? EdgeKind::kLoopBreak
|
||||
: EdgeKind::kToMerge;
|
||||
} else {
|
||||
const auto* loop_or_continue_construct =
|
||||
src_construct.enclosing_loop_or_continue;
|
||||
if (loop_or_continue_construct != nullptr) {
|
||||
// Check for break block or continue block.
|
||||
const auto* loop_header_info =
|
||||
HeaderForLoopOrContinue(loop_or_continue_construct);
|
||||
if (loop_header_info == nullptr) {
|
||||
return Fail() << "internal error: invalid construction of loop "
|
||||
"related to block "
|
||||
<< loop_or_continue_construct->begin_id;
|
||||
}
|
||||
if (dest == loop_header_info->merge_for_header) {
|
||||
// It's a break block for the innermost loop.
|
||||
edge_kind = EdgeKind::kLoopBreak;
|
||||
} else if (dest == loop_header_info->continue_for_header) {
|
||||
// It's a continue block for the innermost loop construct.
|
||||
// In this case loop_or_continue_construct can't be a continue
|
||||
// construct, because then the branch to the continue target is
|
||||
// a backedge, and this code is only looking at forward edges.
|
||||
edge_kind = EdgeKind::kLoopContinue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A forward edge into a case construct that comes from something
|
||||
// other than the OpSwitch is actually a fallthrough.
|
||||
if (edge_kind == EdgeKind::kForward) {
|
||||
const auto* switch_construct =
|
||||
(dest_info->case_head_for ? dest_info->case_head_for
|
||||
: dest_info->default_head_for);
|
||||
if (switch_construct != nullptr) {
|
||||
if (src != switch_construct->begin_id) {
|
||||
edge_kind = EdgeKind::kCaseFallThrough;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((edge_kind == EdgeKind::kForward) ||
|
||||
(edge_kind == EdgeKind::kCaseFallThrough)) {
|
||||
// Check for an invalid forward exit out of this construct.
|
||||
if (dest_info->pos >= src_construct.end_pos) {
|
||||
return Fail()
|
||||
<< "Branch from block " << src << " to block " << dest
|
||||
<< " is an invalid exit from construct starting at block "
|
||||
<< src_construct.begin_id << "; branch bypasses block "
|
||||
<< src_construct.end_id;
|
||||
}
|
||||
|
||||
normal_forward_edges.push_back(dest);
|
||||
|
||||
// Check dominance.
|
||||
|
||||
// Look for edges that violate the dominance condition: a branch
|
||||
// from X to Y where:
|
||||
// If Y is in a nearest enclosing continue construct headed by
|
||||
// CT:
|
||||
// Y is not CT, and
|
||||
// In the structured order, X appears before CT order or
|
||||
// after CT's backedge block.
|
||||
// Otherwise, if Y is in a nearest enclosing construct
|
||||
// headed by H:
|
||||
// Y is not H, and
|
||||
// In the structured order, X appears before H or after H's
|
||||
// merge block.
|
||||
|
||||
const auto& dest_construct = *(dest_info->construct);
|
||||
if (dest != dest_construct.begin_id &&
|
||||
!dest_construct.ContainsPos(src_pos)) {
|
||||
return Fail() << "Branch from " << src << " to " << dest
|
||||
<< " bypasses "
|
||||
<< (dest_construct.kind == Construct::kContinue
|
||||
? "continue target "
|
||||
: "header ")
|
||||
<< dest_construct.begin_id
|
||||
<< " (dominance rule violated)";
|
||||
}
|
||||
}
|
||||
} // end forward edge
|
||||
} // end successor
|
||||
|
||||
if (num_backedges > 1) {
|
||||
return Fail() << "Block " << src
|
||||
<< " has too many backedges: " << num_backedges;
|
||||
}
|
||||
if ((normal_forward_edges.size() > 1) &&
|
||||
(src_info->merge_for_header == 0)) {
|
||||
return Fail() << "Control flow diverges at block " << src << " (to "
|
||||
<< normal_forward_edges[0] << ", "
|
||||
<< normal_forward_edges[1]
|
||||
<< ") but it is not a structured header (it has no merge "
|
||||
"instruction)";
|
||||
}
|
||||
}
|
||||
|
||||
return success();
|
||||
}
|
||||
|
||||
bool FunctionEmitter::EmitFunctionVariables() {
|
||||
if (failed()) {
|
||||
return false;
|
||||
|
|
|
@ -38,6 +38,31 @@ namespace tint {
|
|||
namespace reader {
|
||||
namespace spirv {
|
||||
|
||||
/// Kinds of CFG edges.
|
||||
enum class EdgeKind {
|
||||
// A back-edge: An edge from a node to one of its ancestors in a depth-first
|
||||
// search from the entry block.
|
||||
kBack,
|
||||
// An edge from a node to the merge block of the nearest enclosing structured
|
||||
// construct. We write "ToMerge" to make it clear the destination block is
|
||||
// the merge block, not the source block.
|
||||
kToMerge,
|
||||
// An edge from a node to the merge block of the nearest enclosing loop.
|
||||
// The source block is a "break block" as defined by SPIR-V.
|
||||
kLoopBreak,
|
||||
// An edge from a node in a loop body to the associated continue target, where
|
||||
// there are no other intervening loops or switches.
|
||||
// The source block is a "continue block" as defined by SPIR-V.
|
||||
kLoopContinue,
|
||||
// An edge from one switch case to the next sibling switch case.
|
||||
kCaseFallThrough,
|
||||
// None of the above. By structured control flow rules, there can be at most
|
||||
// one forward edge leaving a basic block. Otherwise there must have been a
|
||||
// merge instruction declaring the divergence and associated reconvergence
|
||||
// point.
|
||||
kForward
|
||||
};
|
||||
|
||||
/// Bookkeeping info for a basic block.
|
||||
struct BlockInfo {
|
||||
/// Constructor
|
||||
|
@ -87,6 +112,9 @@ struct BlockInfo {
|
|||
bool default_is_merge = false;
|
||||
/// The list of switch values that cause a branch to this block.
|
||||
std::unique_ptr<std::vector<uint64_t>> case_values;
|
||||
|
||||
/// Maps the ID of a successor block (in the CFG) to its edge classification.
|
||||
std::unordered_map<uint32_t, EdgeKind> succ_edge;
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& o, const BlockInfo& bi) {
|
||||
|
@ -127,6 +155,9 @@ class FunctionEmitter {
|
|||
/// @returns a FailStream on which to emit diagnostics.
|
||||
FailStream& Fail() { return fail_stream_.Fail(); }
|
||||
|
||||
/// @returns the parser implementation
|
||||
ParserImpl* parser() { return &parser_impl_; }
|
||||
|
||||
/// Emits the declaration, which comprises the name, parameters, and
|
||||
/// return type. The function AST node is appended to the module
|
||||
/// AST node.
|
||||
|
@ -184,6 +215,14 @@ class FunctionEmitter {
|
|||
/// @returns false on failure
|
||||
bool FindSwitchCaseHeaders();
|
||||
|
||||
/// Classifies the successor CFG edges for the ordered basic blocks.
|
||||
/// Also checks validity of each edge (populates the |succ_edge| field of
|
||||
/// BlockInfo). Implicitly checks dominance rules for headers and continue
|
||||
/// constructs. Assumes each block has been labeled with its control flow
|
||||
/// construct.
|
||||
/// @returns false on failure
|
||||
bool ClassifyCFGEdges();
|
||||
|
||||
/// Emits declarations of function variables.
|
||||
/// @returns false if emission failed.
|
||||
bool EmitFunctionVariables();
|
||||
|
@ -243,6 +282,13 @@ class FunctionEmitter {
|
|||
ast::type::Type* GetVariableStoreType(
|
||||
const spvtools::opt::Instruction& var_decl_inst);
|
||||
|
||||
/// Finds the loop header block for a loop construct or continue construct.
|
||||
/// The loop header block is the block with the corresponding OpLoopMerge
|
||||
/// instruction.
|
||||
/// @param c the loop or continue construct
|
||||
/// @returns the block info for the loop header.
|
||||
BlockInfo* HeaderForLoopOrContinue(const Construct* c);
|
||||
|
||||
ParserImpl& parser_impl_;
|
||||
ast::Module& ast_module_;
|
||||
spvtools::opt::IRContext& ir_context_;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue