[spirv-reader] Classify kSwitchBreak from deep in control flow
This also refactors break detection.
Bug: tint:3
Change-Id: I3a3e01c8d76d7c6fc2a14b3dbff136acd487e802
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/21220
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/construct.cc b/src/reader/spirv/construct.cc
index b1ef7de..ded3d71 100644
--- a/src/reader/spirv/construct.cc
+++ b/src/reader/spirv/construct.cc
@@ -26,18 +26,36 @@
uint32_t the_begin_pos,
uint32_t the_end_pos)
: parent(the_parent),
+ enclosing_loop(
+ // Compute the enclosing loop construct. Doing this in the
+ // constructor member list lets us make the member const.
+ // Compare parent depth because loop and continue are siblings and
+ // it's incidental which will appear on the stack first.
+ the_kind == kLoop
+ ? this
+ : ((parent && parent->depth < the_depth) ? parent->enclosing_loop
+ : nullptr)),
enclosing_continue(
// Compute the enclosing continue construct. Doing this in the
// constructor member list lets us make the member const.
- the_kind == kContinue
+ // Compare parent depth because loop and continue are siblings and
+ // it's incidental which will appear on the stack first.
+ the_kind == kContinue ? this
+ : ((parent && parent->depth < the_depth)
+ ? parent->enclosing_continue
+ : nullptr)),
+ enclosing_loop_or_continue_or_switch(
+ // Compute the enclosing loop or continue or switch construct.
+ // Doing this in the constructor member list lets us make the
+ // member const.
+ // Compare parent depth because loop and continue are siblings and
+ // it's incidental which will appear on the stack first.
+ (the_kind == kLoop || the_kind == kContinue ||
+ the_kind == kSwitchSelection)
? this
- : (parent ? parent->enclosing_continue : nullptr)),
- enclosing_loop_or_continue(
- // Compute the enclosing loop or continue construct. Doing this
- // in the constructor member list lets us make the member const.
- (the_kind == kLoop || the_kind == kContinue)
- ? this
- : (parent ? parent->enclosing_loop_or_continue : nullptr)),
+ : ((parent && parent->depth < the_depth)
+ ? parent->enclosing_loop_or_continue_or_switch
+ : nullptr)),
depth(the_depth),
kind(the_kind),
begin_id(the_begin_id),
diff --git a/src/reader/spirv/construct.h b/src/reader/spirv/construct.h
index a63437d..55ea5fc 100644
--- a/src/reader/spirv/construct.h
+++ b/src/reader/spirv/construct.h
@@ -68,12 +68,18 @@
/// The nearest enclosing construct (other than itself), or nullptr if
/// this construct represents the entire function.
const Construct* const parent = nullptr;
- /// The nearest continue construct, if one exists. A construct encloses
- /// itself.
+ /// The nearest enclosing loop construct, if one exists. A construct
+ /// encloses itself.
+ const Construct* const enclosing_loop = nullptr;
+ /// The nearest enclosing continue construct, if one exists. A construct
+ /// encloses itself.
const Construct* const enclosing_continue = nullptr;
- /// The nearest enclosing loop or continue construct, if one exists.
+ /// The nearest enclosing loop construct or continue construct or
+ /// switch-selection construct, if one exists. The signficance is
+ /// that a high level language "break" will branch to the merge block
+ /// of such an enclosing construct.
/// A construct encloses itself.
- const Construct* const enclosing_loop_or_continue = nullptr;
+ const Construct* const enclosing_loop_or_continue_or_switch = nullptr;
/// Control flow nesting depth. The entry block is at nesting depth 0.
const int depth = 0;
@@ -136,12 +142,17 @@
o << " parent:" << ToStringBrief(c.parent);
+ if (c.enclosing_loop) {
+ o << " in-l:" << ToStringBrief(c.enclosing_loop);
+ }
+
if (c.enclosing_continue) {
o << " in-c:" << ToStringBrief(c.enclosing_continue);
}
- if (c.enclosing_loop_or_continue != c.enclosing_continue) {
- o << " in-c-l:" << ToStringBrief(c.enclosing_loop_or_continue);
+ if ((c.enclosing_loop_or_continue_or_switch != c.enclosing_loop) &&
+ (c.enclosing_loop_or_continue_or_switch != c.enclosing_continue)) {
+ o << " in-c-l-s:" << ToStringBrief(c.enclosing_loop_or_continue_or_switch);
}
o << " }";
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 40292df..2176aae 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -927,13 +927,20 @@
return success();
}
-BlockInfo* FunctionEmitter::HeaderForLoopOrContinue(const Construct* c) {
- if (c->kind == Construct::kLoop) {
- return GetBlockInfo(c->begin_id);
+BlockInfo* FunctionEmitter::HeaderIfBreakable(const Construct* c) {
+ if (c == nullptr) {
+ return nullptr;
}
- if (c->kind == Construct::kContinue) {
- auto* continue_block = GetBlockInfo(c->begin_id);
- return GetBlockInfo(continue_block->header_for_continue);
+ switch (c->kind) {
+ case Construct::kLoop:
+ case Construct::kSwitchSelection:
+ return GetBlockInfo(c->begin_id);
+ case Construct::kContinue: {
+ const auto* continue_target = GetBlockInfo(c->begin_id);
+ return GetBlockInfo(continue_target->header_for_continue);
+ }
+ default:
+ break;
}
return nullptr;
}
@@ -1051,44 +1058,42 @@
// 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;
- if (src_is_loop_header || src_is_continue_header) {
- edge_kind = EdgeKind::kLoopBreak;
- } else if (src_construct.kind == Construct::kSwitchSelection) {
- edge_kind = EdgeKind::kSwitchBreak;
- } else {
- edge_kind = 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 (edge_kind == EdgeKind::kForward) {
+ // Check for a 'break' from a loop or from a switch.
+ const auto* breakable_header = HeaderIfBreakable(
+ src_construct.enclosing_loop_or_continue_or_switch);
+ if (breakable_header != nullptr) {
+ if (dest == breakable_header->merge_for_header) {
+ // It's a break.
+ edge_kind = (breakable_header->construct->kind ==
+ Construct::kSwitchSelection)
+ ? EdgeKind::kSwitchBreak
+ : EdgeKind::kLoopBreak;
}
- 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.
+ }
+ }
+
+ if (edge_kind == EdgeKind::kForward) {
+ // Check for a 'continue' from within a loop.
+ const auto* loop_header =
+ HeaderIfBreakable(src_construct.enclosing_loop);
+ if (loop_header != nullptr) {
+ if (dest == loop_header->continue_for_header) {
+ // It's a continue.
edge_kind = EdgeKind::kLoopContinue;
}
}
}
+ if (edge_kind == EdgeKind::kForward) {
+ const auto& header_info = *GetBlockInfo(src_construct.begin_id);
+ if (dest == header_info.merge_for_header) {
+ // Branch to construct's merge block. The loop break and
+ // switch break cases have already been covered.
+ edge_kind = EdgeKind::kToMerge;
+ }
+ }
+
// A forward edge into a case construct that comes from something
// other than the OpSwitch is actually a fallthrough.
if (edge_kind == EdgeKind::kForward) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index c2e8b1c..51c32c5 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -289,12 +289,17 @@
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);
+ /// Finds the header block for a structured construct that we can "break"
+ /// out from, from deeply nested control flow, if such a block exists.
+ /// If the construct is:
+ /// - a switch selection: return the selection header (ending in OpSwitch)
+ /// - a loop construct: return the loop header block
+ /// - a continue construct: return the loop header block
+ /// Otherwise, return nullptr.
+ /// @param c a structured construct, or nullptr
+ /// @returns the block info for the structured header we can "break" from,
+ /// or nullptr
+ BlockInfo* HeaderIfBreakable(const Construct* c);
ParserImpl& parser_impl_;
ast::Module& ast_module_;
diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc
index e1d96d3..2db7687 100644
--- a/src/reader/spirv/function_cfg_test.cc
+++ b/src/reader/spirv/function_cfg_test.cc
@@ -2938,7 +2938,7 @@
EXPECT_EQ(constructs.size(), 2u);
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
- Construct{ SwitchSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 }
+ Construct{ SwitchSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3018,12 +3018,10 @@
fe.RegisterMerges();
EXPECT_TRUE(fe.LabelControlFlowConstructs());
const auto& constructs = fe.constructs();
- // A single-block loop consists *only* of a continue target with one block in
- // it.
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ Continue [3,5) begin_id:40 end_id:99 depth:1 parent:Function@10 in-c:Continue@40 }
- Construct{ Loop [1,3) begin_id:20 end_id:40 depth:1 parent:Function@10 in-c-l:Loop@20 }
+ Construct{ Loop [1,3) begin_id:20 end_id:40 depth:1 parent:Function@10 in-l:Loop@20 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3119,7 +3117,7 @@
Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ IfSelection [0,2) begin_id:10 end_id:50 depth:1 parent:Function@10 }
Construct{ Continue [3,4) begin_id:60 end_id:99 depth:1 parent:Function@10 in-c:Continue@60 }
- Construct{ Loop [2,3) begin_id:50 end_id:60 depth:1 parent:Function@10 in-c-l:Loop@50 }
+ Construct{ Loop [2,3) begin_id:50 end_id:60 depth:1 parent:Function@10 in-l:Loop@50 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3237,9 +3235,9 @@
// The ordering among siblings depends on the computed block order.
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
- Construct{ SwitchSelection [0,7) begin_id:10 end_id:99 depth:1 parent:Function@10 }
- Construct{ IfSelection [1,3) begin_id:50 end_id:89 depth:2 parent:SwitchSelection@10 }
- Construct{ IfSelection [4,6) begin_id:20 end_id:49 depth:2 parent:SwitchSelection@10 }
+ Construct{ SwitchSelection [0,7) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
+ Construct{ IfSelection [1,3) begin_id:50 end_id:89 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
+ Construct{ IfSelection [4,6) begin_id:20 end_id:49 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3287,7 +3285,7 @@
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ IfSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 }
- Construct{ SwitchSelection [1,3) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 }
+ Construct{ SwitchSelection [1,3) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 in-c-l-s:SwitchSelection@20 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3341,8 +3339,8 @@
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ Continue [4,6) begin_id:50 end_id:89 depth:1 parent:Function@10 in-c:Continue@50 }
- Construct{ Loop [1,4) begin_id:20 end_id:50 depth:1 parent:Function@10 in-c-l:Loop@20 }
- Construct{ Continue [2,3) begin_id:30 end_id:40 depth:2 parent:Loop@20 in-c:Continue@30 }
+ Construct{ Loop [1,4) begin_id:20 end_id:50 depth:1 parent:Function@10 in-l:Loop@20 }
+ Construct{ Continue [2,3) begin_id:30 end_id:40 depth:2 parent:Loop@20 in-l:Loop@20 in-c:Continue@30 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3396,8 +3394,8 @@
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ Continue [5,6) begin_id:80 end_id:99 depth:1 parent:Function@10 in-c:Continue@80 }
- Construct{ Loop [1,5) begin_id:20 end_id:80 depth:1 parent:Function@10 in-c-l:Loop@20 }
- Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Loop@20 in-c-l:Loop@20 }
+ Construct{ Loop [1,5) begin_id:20 end_id:80 depth:1 parent:Function@10 in-l:Loop@20 }
+ Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Loop@20 in-l:Loop@20 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3447,7 +3445,7 @@
EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ Continue [2,5) begin_id:30 end_id:99 depth:1 parent:Function@10 in-c:Continue@30 }
- Construct{ Loop [1,2) begin_id:20 end_id:30 depth:1 parent:Function@10 in-c-l:Loop@20 }
+ Construct{ Loop [1,2) begin_id:20 end_id:30 depth:1 parent:Function@10 in-l:Loop@20 }
Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Continue@30 in-c:Continue@30 }
})")) << constructs;
// The block records the nearest enclosing construct.
@@ -3541,7 +3539,7 @@
Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
Construct{ IfSelection [0,6) begin_id:10 end_id:99 depth:1 parent:Function@10 }
Construct{ Continue [3,5) begin_id:40 end_id:89 depth:2 parent:IfSelection@10 in-c:Continue@40 }
- Construct{ Loop [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 in-c-l:Loop@20 }
+ Construct{ Loop [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 in-l:Loop@20 }
})")) << constructs;
// The block records the nearest enclosing construct.
EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -4715,6 +4713,135 @@
EXPECT_EQ(bi->succ_edge[99], EdgeKind::kSwitchBreak);
}
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedIf_Unconditional) {
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpSelectionMerge %99 None
+ OpSwitch %selector %99 20 %20
+
+ %20 = OpLabel
+ OpSelectionMerge %80 None
+ OpBranchConditional %cond %30 %80
+
+ %30 = OpLabel
+ OpBranch %99
+
+ %80 = OpLabel ; inner merge
+ OpBranch %99
+
+ %99 = OpLabel ; outer merge
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+
+ auto* bi = fe.GetBlockInfo(30);
+ ASSERT_NE(bi, nullptr);
+ EXPECT_EQ(bi->succ_edge.count(99), 1);
+ EXPECT_EQ(bi->succ_edge[99], EdgeKind::kSwitchBreak);
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedIf_Conditional) {
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpSelectionMerge %99 None
+ OpSwitch %selector %99 20 %20
+
+ %20 = OpLabel
+ OpSelectionMerge %80 None
+ OpBranchConditional %cond %30 %80
+
+ %30 = OpLabel
+ OpBranchConditional %cond2 %99 %80 ; break-if
+
+ %80 = OpLabel ; inner merge
+ OpBranch %99
+
+ %99 = OpLabel ; outer merge
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+
+ auto* bi = fe.GetBlockInfo(30);
+ ASSERT_NE(bi, nullptr);
+ EXPECT_EQ(bi->succ_edge.count(99), 1);
+ EXPECT_EQ(bi->succ_edge[99], EdgeKind::kSwitchBreak);
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedLoop_IsError) {
+ // It's an error because the break can only go as far as the loop.
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpSelectionMerge %99 None
+ OpSwitch %selector %99 20 %20
+
+ %20 = OpLabel
+ OpLoopMerge %80 %70 None
+ OpBranchConditional %cond %30 %80
+
+ %30 = OpLabel ; in loop construct
+ OpBranch %99 ; break
+
+ %70 = OpLabel
+ OpBranch %20
+
+ %80 = OpLabel
+ OpBranch %99
+
+ %99 = OpLabel ; outer merge
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_FALSE(FlowClassifyCFGEdges(&fe));
+ EXPECT_THAT(p->error(),
+ Eq("Branch from block 30 to block 99 is an invalid exit from "
+ "construct starting at block 20; branch bypasses block 70"));
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedSwitch_IsError) {
+ // It's an error because the break can only go as far as inner switch
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpSelectionMerge %99 None
+ OpSwitch %selector %99 20 %20
+
+ %20 = OpLabel
+ OpSelectionMerge %80 None
+ OpSwitch %selector %80 30 %30
+
+ %30 = OpLabel
+ OpBranch %99 ; break
+
+ %80 = OpLabel
+ OpBranch %99
+
+ %99 = OpLabel ; outer merge
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_FALSE(FlowClassifyCFGEdges(&fe));
+ EXPECT_THAT(p->error(),
+ Eq("Branch from block 30 to block 99 is an invalid exit from "
+ "construct starting at block 20; branch bypasses block 80"));
+}
+
TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromLoopBody) {
auto assembly = CommonTypes() + R"(
%100 = OpFunction %void None %voidfn
@@ -4811,6 +4938,125 @@
EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
}
+TEST_F(SpvParserTest,
+ ClassifyCFGEdges_LoopBreak_FromLoopBodyNestedSelection_Unconditional) {
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpBranch %20
+
+ %20 = OpLabel
+ OpLoopMerge %99 %80 None
+ OpBranchConditional %cond %30 %99
+
+ %30 = OpLabel
+ OpSelectionMerge %50 None
+ OpBranchConditional %cond2 %40 %50
+
+ %40 = OpLabel
+ OpBranch %99 ; deeply nested break
+
+ %50 = OpLabel ; inner merge
+ OpBranch %80
+
+ %80 = OpLabel
+ OpBranch %20 ; backedge
+
+ %99 = OpLabel
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+
+ auto* bi = fe.GetBlockInfo(40);
+ ASSERT_NE(bi, nullptr);
+ EXPECT_EQ(bi->succ_edge.count(99), 1);
+ EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
+}
+
+TEST_F(SpvParserTest,
+ ClassifyCFGEdges_LoopBreak_FromLoopBodyNestedSelection_Conditional) {
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpBranch %20
+
+ %20 = OpLabel
+ OpLoopMerge %99 %80 None
+ OpBranchConditional %cond %30 %99
+
+ %30 = OpLabel
+ OpSelectionMerge %50 None
+ OpBranchConditional %cond2 %40 %50
+
+ %40 = OpLabel
+ OpBranchConditional %cond3 %99 %50 ; break-if
+
+ %50 = OpLabel ; inner merge
+ OpBranch %80
+
+ %80 = OpLabel
+ OpBranch %20 ; backedge
+
+ %99 = OpLabel
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+
+ auto* bi = fe.GetBlockInfo(40);
+ ASSERT_NE(bi, nullptr);
+ EXPECT_EQ(bi->succ_edge.count(99), 1);
+ EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
+}
+
+TEST_F(SpvParserTest,
+ ClassifyCFGEdges_LoopBreak_FromContinueConstructNestedFlow_IsError) {
+ auto assembly = CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpBranch %20
+
+ %20 = OpLabel
+ OpLoopMerge %99 %40 None
+ OpBranchConditional %cond %30 %99
+
+ %30 = OpLabel
+ OpBranch %40
+
+ %40 = OpLabel ; continue construct
+ OpSelectionMerge %79 None
+ OpBranchConditional %cond2 %50 %79
+
+ %50 = OpLabel
+ OpBranchConditional %cond3 %99 %79 ; attempt to break to 99 should fail
+
+ %79 = OpLabel
+ OpBranch %80 ; inner merge
+
+ %80 = OpLabel
+ OpBranch %20 ; backedge
+
+ %99 = OpLabel
+ OpReturn
+)";
+ auto* p = parser(test::Assemble(assembly));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_FALSE(FlowClassifyCFGEdges(&fe));
+ EXPECT_THAT(p->error(),
+ Eq("Invalid exit (50->99) from continue construct: 50 is not the "
+ "last block in the continue construct starting at 40 "
+ "(violates post-dominance rule)"));
+}
+
TEST_F(SpvParserTest, ClassifyCFGEdges_LoopContinue_LoopBodyToContinue) {
auto assembly = CommonTypes() + R"(
%100 = OpFunction %void None %voidfn
@@ -4949,7 +5195,7 @@
auto* p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
FunctionEmitter fe(p, *spirv_function(100));
- EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe)) << p->error();
auto* bi = fe.GetBlockInfo(40);
ASSERT_NE(bi, nullptr);
@@ -5064,7 +5310,7 @@
auto* p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
FunctionEmitter fe(p, *spirv_function(100));
- EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+ EXPECT_TRUE(FlowClassifyCFGEdges(&fe)) << p->error();
auto* bi = fe.GetBlockInfo(40);
ASSERT_NE(bi, nullptr);