[spirv-reader] Emit control flow: if/then/else
Bug: tint:3
Change-Id: Ief0544415f27842913a6234a962d163ecedb48df
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/21821
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index ff4d80b..50c947a 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -27,15 +27,20 @@
#include "src/ast/as_expression.h"
#include "src/ast/assignment_statement.h"
#include "src/ast/binary_expression.h"
+#include "src/ast/else_statement.h"
#include "src/ast/identifier_expression.h"
+#include "src/ast/if_statement.h"
+#include "src/ast/loop_statement.h"
#include "src/ast/member_accessor_expression.h"
#include "src/ast/scalar_constructor_expression.h"
#include "src/ast/storage_class.h"
+#include "src/ast/switch_statement.h"
#include "src/ast/uint_literal.h"
#include "src/ast/unary_op.h"
#include "src/ast/unary_op_expression.h"
#include "src/ast/variable.h"
#include "src/ast/variable_decl_statement.h"
+#include "src/reader/spirv/construct.h"
#include "src/reader/spirv/fail_stream.h"
#include "src/reader/spirv/parser_impl.h"
@@ -368,19 +373,37 @@
fail_stream_(pi->fail_stream()),
namer_(pi->namer()),
function_(function) {
- statements_stack_.emplace_back(ast::StatementList{});
+ PushNewStatementBlock(nullptr, 0, nullptr);
}
FunctionEmitter::~FunctionEmitter() = default;
-const ast::StatementList& FunctionEmitter::ast_body() {
- assert(!statements_stack_.empty());
- return statements_stack_[0];
+void FunctionEmitter::PushNewStatementBlock(const Construct* construct,
+ uint32_t end_id,
+ CompletionAction action) {
+ statements_stack_.emplace_back(StatementBlock{construct, end_id, action,
+ ast::StatementList{},
+ ast::CaseStatementList{}});
}
-void FunctionEmitter::AddStatement(std::unique_ptr<ast::Statement> statement) {
+const ast::StatementList& FunctionEmitter::ast_body() {
assert(!statements_stack_.empty());
- statements_stack_.back().emplace_back(std::move(statement));
+ return statements_stack_[0].statements;
+}
+
+ast::Statement* FunctionEmitter::AddStatement(
+ std::unique_ptr<ast::Statement> statement) {
+ assert(!statements_stack_.empty());
+ auto* result = statement.get();
+ statements_stack_.back().statements.emplace_back(std::move(statement));
+ return result;
+}
+
+ast::Statement* FunctionEmitter::LastStatement() {
+ assert(!statements_stack_.empty());
+ const auto& statement_list = statements_stack_.back().statements;
+ assert(!statement_list.empty());
+ return statement_list.back().get();
}
bool FunctionEmitter::Emit() {
@@ -406,10 +429,11 @@
"element but has "
<< statements_stack_.size();
}
- ast::StatementList body(std::move(statements_stack_[0]));
+ ast::StatementList body(std::move(statements_stack_[0].statements));
parser_impl_.get_module().functions().back()->set_body(std::move(body));
// Maintain the invariant by repopulating the one and only element.
- statements_stack_[0] = ast::StatementList{};
+ statements_stack_.clear();
+ PushNewStatementBlock(constructs_[0].get(), 0, nullptr);
return success();
}
@@ -502,6 +526,9 @@
return false;
}
+ // TODO(dneto): register phis
+ // TODO(dneto): register SSA values which need to be hoisted
+
if (!EmitFunctionVariables()) {
return false;
}
@@ -1244,8 +1271,8 @@
if (construct->kind != Construct::kIfSelection) {
continue;
}
- const auto* branch =
- GetBlockInfo(construct->begin_id)->basic_block->terminator();
+ auto* if_header_info = GetBlockInfo(construct->begin_id);
+ const auto* branch = if_header_info->basic_block->terminator();
const auto true_head = branch->GetSingleWordInOperand(1);
const auto false_head = branch->GetSingleWordInOperand(2);
@@ -1259,9 +1286,11 @@
if (contains_true) {
true_head_info->true_head_for = construct.get();
+ if_header_info->true_head = true_head_info;
}
if (contains_false) {
false_head_info->false_head_for = construct.get();
+ if_header_info->false_head = false_head_info;
}
if ((!contains_true) && contains_false) {
false_head_info->exclusive_false_head_for = construct.get();
@@ -1337,7 +1366,9 @@
<< " going to " << premerge_id << " and " << dest_id;
}
premerge_id = dest_id;
- GetBlockInfo(dest_id)->premerge_head_for = construct.get();
+ auto* dest_block_info = GetBlockInfo(dest_id);
+ dest_block_info->premerge_head_for = construct.get();
+ if_header_info->premerge_head = dest_block_info;
}
break;
}
@@ -1427,14 +1458,269 @@
}
bool FunctionEmitter::EmitFunctionBodyStatements() {
- // TODO(dneto): For now, emit only regular statements in the entry block.
- // We'll use assignments as markers in the tests, to be able to tell where
- // code is placed in control flow. First prove that we can emit assignments.
- return EmitStatementsInBasicBlock(*function_.entry());
+ // Dump the basic blocks in order, grouped by construct.
+
+ // We maintain a stack of StatementBlock objects, where new statements
+ // are always written to the topmost entry of the stack. By this point in
+ // processing, we have already recorded the interesting control flow
+ // boundaries in the BlockInfo and associated Construct objects. As we
+ // enter a new statement grouping, we push onto the stack, and also schedule
+ // the statement block's completion and removal at a future block's ID.
+
+ // Upon entry, the statement stack has one entry representing the whole
+ // function.
+ assert(!constructs_.empty());
+ Construct* function_construct = constructs_[0].get();
+ assert(function_construct != nullptr);
+ assert(function_construct->kind == Construct::kFunction);
+ // Make the first entry valid by filling in the construct field, which
+ // had not been computed at the time the entry was first created.
+ // TODO(dneto): refactor how the first construct is created vs.
+ // this statements stack entry is populated.
+ assert(statements_stack_.size() == 1);
+ statements_stack_[0].construct = function_construct;
+
+ for (auto block_id : block_order()) {
+ if (!EmitBasicBlock(*GetBlockInfo(block_id))) {
+ return false;
+ }
+ }
+ return success();
}
-bool FunctionEmitter::EmitStatementsInBasicBlock(
- const spvtools::opt::BasicBlock& bb) {
+bool FunctionEmitter::EmitBasicBlock(const BlockInfo& block_info) {
+ // Close off previous constructs.
+ while (!statements_stack_.empty() &&
+ (statements_stack_.back().end_id == block_info.id)) {
+ StatementBlock& sb = statements_stack_.back();
+ sb.completion_action(&sb);
+ statements_stack_.pop_back();
+ }
+ if (statements_stack_.empty()) {
+ return Fail() << "internal error: statements stack empty at block "
+ << block_info.id;
+ }
+
+ // Enter new constructs.
+
+ std::vector<const Construct*> entering_constructs; // inner most comes first
+ {
+ auto* here = block_info.construct;
+ auto* const top_construct = statements_stack_.back().construct;
+ while (here != top_construct) {
+ // Only enter a construct at its header block.
+ if (here->begin_id == block_info.id) {
+ entering_constructs.push_back(here);
+ }
+ here = here->parent;
+ }
+ }
+ // What constructs can we have entered?
+ // - It can't be kFunction, because there is only one of those, and it was
+ // already on the stack at the outermost level.
+ // - We have at most one of kIfSelection, kSwitchSelection, or kLoop because
+ // each of those is headed by a block with a merge instruction, and the
+ // kIfSelection and kSwitchSelection header blocks end in different branch
+ // instructions.
+ // - A kContinue can contain a kContinue
+ // This is possible in Vulkan SPIR-V, but Tint disallows this by the rule
+ // that a block can be continue target for at most one header block. See
+ // test DISABLED_BlockIsContinueForMoreThanOneHeader. If we generalize this,
+ // then by a dominance argument, the inner loop continue target can only be
+ // a single-block loop.
+ // TODO(dneto): Handle this case.
+ // - All that's left is a kContinue and one of kIfSelection, kSwitchSelection,
+ // kLoop.
+ //
+ // The kContinue can be the parent of the other. For example, a selection
+ // starting at the first block of a continue construct.
+ //
+ // The kContinue can't be the child of the other because either:
+ // - Either it would be a single block loop but in that case there is no
+ // kLoop construct for it, by construction.
+ // - The kContinue is in a loop that is not single-block; and the
+ // selection contains the kContinue block but not the loop block. That
+ // breaks dominance rules. That is, the continue target is dominated by
+ // that loop header, and so gets found on the outside before the
+ // selection is found. The selection is inside the outer loop.
+ //
+ // So we fall into one of the following cases:
+ // - We are entering 0 or 1 constructs, or
+ // - We are entering 2 constructs, with the outer one being a kContinue, the
+ // inner one is not a continue.
+ if (entering_constructs.size() > 2) {
+ return Fail() << "internal error: bad construct nesting found";
+ }
+ if (entering_constructs.size() == 2) {
+ auto inner_kind = entering_constructs[0]->kind;
+ auto outer_kind = entering_constructs[1]->kind;
+ if (outer_kind != Construct::kContinue) {
+ return Fail() << "internal error: bad construct nesting. Only Continue "
+ "construct can be outer construct on same block";
+ }
+ if (inner_kind == Construct::kContinue) {
+ return Fail() << "internal error: unsupported construct nesting: "
+ "Continue around Continue";
+ }
+ if (inner_kind != Construct::kIfSelection &&
+ inner_kind != Construct::kSwitchSelection &&
+ inner_kind != Construct::kLoop) {
+ return Fail() << "internal error: bad construct nesting. Continue around "
+ "something other than if, switch, or loop";
+ }
+ }
+
+ // Enter constructs from outermost to innermost.
+ // kLoop and kContinue push a new statement-block onto the stack before
+ // emitting statements in the block.
+ // kIfSelection and kSwitchSelection emit statements in the block and then
+ // emit push a new statement-block. Only emit the statements in the block
+ // once.
+
+ // Have we emitted the statements for this block?
+ bool emitted = false;
+ for (auto iter = entering_constructs.rbegin();
+ iter != entering_constructs.rend(); ++iter) {
+ const Construct* construct = *iter;
+
+ switch (construct->kind) {
+ case Construct::kFunction:
+ return Fail() << "internal error: nested function construct";
+
+ case Construct::kLoop:
+ return Fail() << "unhandled: loop construct";
+
+ case Construct::kContinue:
+ return Fail() << "unhandled: continue construct";
+
+ case Construct::kIfSelection:
+ if (!EmitStatementsInBasicBlock(block_info, &emitted)) {
+ return false;
+ }
+ if (!EmitIfStart(block_info)) {
+ return false;
+ }
+ break;
+
+ case Construct::kSwitchSelection:
+ return Fail() << "unhandled: switch construct";
+ }
+ }
+
+ // If we aren't starting or transitioning, then emit the normal
+ // statements now.
+ if (!EmitStatementsInBasicBlock(block_info, &emitted)) {
+ return false;
+ }
+
+ if (!EmitNormalTerminator(block_info)) {
+ return false;
+ }
+ return success();
+}
+
+bool FunctionEmitter::EmitIfStart(const BlockInfo& block_info) {
+ // The block is the if-header block. So its construct is the if construct.
+ auto* construct = block_info.construct;
+ assert(construct->kind == Construct::kIfSelection);
+ assert(construct->begin_id == block_info.id);
+
+ const auto* const false_head = block_info.false_head;
+ const auto* const premerge_head = block_info.premerge_head;
+
+ auto* const if_stmt =
+ AddStatement(std::make_unique<ast::IfStatement>())->AsIf();
+ const auto condition_id =
+ block_info.basic_block->terminator()->GetSingleWordInOperand(0);
+ // Generate the code for the condition.
+ if_stmt->set_condition(std::move(MakeExpression(condition_id).expr));
+
+ // Compute the block IDs that should end the then-clause and the else-clause.
+
+ // We need to know where the *emitted* selection should end, i.e. the intended
+ // merge block id. That should be the current premerge block, if it exists,
+ // or otherwise the declared merge block.
+ //
+ // This is another way to think about it:
+ // If there is a premerge, then there are three cases:
+ // - premerge_head is different from the true_head and false_head:
+ // - Premerge comes last. In effect, move the selection merge up
+ // to where the premerge begins.
+ // - premerge_head is the same as the false_head
+ // - This is really an if-then without an else clause.
+ // Move the merge up to where the premerge is.
+ // - premerge_head is the same as the true_head
+ // - This is really an if-else without an then clause.
+ // Emit it as: if (cond) {} else {....}
+ // Move the merge up to where the premerge is.
+ const uint32_t intended_merge =
+ premerge_head ? premerge_head->id : construct->end_id;
+
+ // then-clause:
+ // If true_head exists:
+ // spans from true head to the earlier of the false head (if it exists)
+ // or the selection merge.
+ // Otherwise:
+ // ends at from the false head (if it exists), otherwise the selection
+ // end.
+ const uint32_t then_end = false_head ? false_head->id : intended_merge;
+
+ // else-clause:
+ // ends at the premerge head (if it exists) or at the selection end.
+ const uint32_t else_end = premerge_head ? premerge_head->id : intended_merge;
+
+ // Push statement blocks for the then-clause and the else-clause.
+ // But make sure we do it in the right order.
+
+ auto push_then = [this, if_stmt, then_end, construct]() {
+ // Push the then clause onto the stack.
+ PushNewStatementBlock(construct, then_end, [if_stmt](StatementBlock* s) {
+ // The "then" consists of the statement list
+ // from the top of statments stack, without an
+ // elseif condition.
+ if_stmt->set_body(std::move(s->statements));
+ });
+ };
+
+ auto push_else = [this, if_stmt, else_end, construct]() {
+ // Push the else clause onto the stack first.
+ PushNewStatementBlock(construct, else_end, [if_stmt](StatementBlock* s) {
+ // The "else" consists of the statement list from the top of statments
+ // stack, without an elseif condition.
+ ast::ElseStatementList else_stmts;
+ else_stmts.emplace_back(std::make_unique<ast::ElseStatement>(
+ nullptr, std::move(s->statements)));
+ if_stmt->set_else_statements(std::move(else_stmts));
+ });
+ };
+
+ if (GetBlockInfo(else_end)->pos < GetBlockInfo(then_end)->pos) {
+ // Process the else-clause first. The then-clause will be empty so avoid
+ // pushing onto the stack at all.
+ push_else();
+ } else {
+ // Blocks for the then-clause appear before blocks for the else-clause.
+ // So push the else-clause handling onto the stack first. The else-clause
+ // might be empty, but this works anyway.
+ push_else();
+ push_then();
+ }
+
+ return success();
+}
+
+bool FunctionEmitter::EmitNormalTerminator(const BlockInfo&) {
+ // TODO(dneto): emit fallthrough, break, continue, return, kill
+ return true;
+}
+
+bool FunctionEmitter::EmitStatementsInBasicBlock(const BlockInfo& block_info,
+ bool* already_emitted) {
+ if (*already_emitted) {
+ // Only emit this part of the basic block once.
+ return true;
+ }
+ const spvtools::opt::BasicBlock& bb = *(block_info.basic_block);
const auto* terminator = bb.terminator();
const auto* merge = bb.GetMergeInst(); // Might be nullptr
// Emit regular statements.
@@ -1447,7 +1733,7 @@
return false;
}
}
- // TODO(dneto): Handle the terminator
+ *already_emitted = true;
return true;
}
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index 9174259..a7fec4c 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -15,6 +15,7 @@
#ifndef SRC_READER_SPIRV_FUNCTION_H_
#define SRC_READER_SPIRV_FUNCTION_H_
+#include <functional>
#include <memory>
#include <ostream>
#include <unordered_map>
@@ -27,8 +28,10 @@
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/type_manager.h"
+#include "src/ast/case_statement.h"
#include "src/ast/expression.h"
#include "src/ast/module.h"
+#include "src/ast/statement.h"
#include "src/reader/spirv/construct.h"
#include "src/reader/spirv/fail_stream.h"
#include "src/reader/spirv/namer.h"
@@ -138,7 +141,21 @@
const Construct* premerge_head_for = nullptr;
/// The construct for which this block is the false head, and that construct
/// does not have a true head.
+ /// TODO(dneto): I think we can remove |exclusive_false_head_for|
const Construct* exclusive_false_head_for = nullptr;
+ /// If not null, then this block is an if-selection header, and |true_head| is
+ /// the target of the true branch on the OpBranchConditional.
+ /// In particular, true_head->true_head_for == this
+ const BlockInfo* true_head = nullptr;
+ /// If not null, then this block is an if-selection header, and |false_head|
+ /// is the target of the false branch on the OpBranchConditional.
+ /// In particular, false_head->false_head_for == this
+ const BlockInfo* false_head = nullptr;
+ /// If not null, then this block is an if-selection header, and when following
+ /// the flow via the true and false branches, control first reconverges at
+ /// |premerge_head|, and |premerge_head| is still inside the if-selection.
+ /// In particular, premerge_head->premerge_head_for == this
+ const BlockInfo* premerge_head = nullptr;
};
inline std::ostream& operator<<(std::ostream& o, const BlockInfo& bi) {
@@ -268,10 +285,35 @@
/// @returns false if emission failed.
bool EmitFunctionBodyStatements();
- /// Emits a basic block
- /// @param bb internal representation of the basic block
+ /// Emits a basic block.
+ /// @param block_info the block to emit
/// @returns false if emission failed.
- bool EmitStatementsInBasicBlock(const spvtools::opt::BasicBlock& bb);
+ bool EmitBasicBlock(const BlockInfo& block_info);
+
+ /// Emits an IfStatement, including its condition expression, and sets
+ /// up the statement stack to accumulate subsequent basic blocks into
+ /// the "then" and "else" clauses.
+ /// @param block_info the if-selection header block
+ /// @returns false if emission failed.
+ bool EmitIfStart(const BlockInfo& block_info);
+
+ /// Emits the non-control-flow parts of a basic block, but only once.
+ /// The |already_emitted| parameter indicates whether the code has already
+ /// been emitted, and is used to signal that this invocation actually emitted
+ /// it.
+ /// @param block_info the block to emit
+ /// @param already_emitted the block to emit
+ /// @returns false if the code had not yet been emitted, but emission failed
+ bool EmitStatementsInBasicBlock(const BlockInfo& block_info,
+ bool* already_emitted);
+
+ /// Emits code for terminators, but that aren't part of entering or
+ /// resolving structured control flow. That is, if the basic block
+ /// terminator calls for it, emit the fallthrough, break, continue, return,
+ /// or kill commands.
+ /// @param block_info the block with the terminator to emit (if any)
+ /// @returns false if emission failed
+ bool EmitNormalTerminator(const BlockInfo& block_info);
/// Emits a normal instruction: not a terminator, label, or variable
/// declaration.
@@ -347,7 +389,41 @@
BlockInfo* HeaderIfBreakable(const Construct* c);
/// Appends a new statement to the top of the statement stack.
- void AddStatement(std::unique_ptr<ast::Statement> statement);
+ /// @param statement the new statement
+ /// @returns a pointer to the statement.
+ ast::Statement* AddStatement(std::unique_ptr<ast::Statement> statement);
+
+ /// @returns the last statetment in the top of the statement stack.
+ ast::Statement* LastStatement();
+
+ struct StatementBlock;
+ using CompletionAction = std::function<void(StatementBlock*)>;
+
+ // A StatementBlock represents a braced-list of statements while it is being
+ // constructed.
+ struct StatementBlock {
+ // The construct to which this construct constributes.
+ const Construct* construct;
+ // The ID of the block at which the completion action should be triggerd
+ // and this statement block discarded. This is often the |end_id| of
+ // |construct| itself.
+ uint32_t end_id;
+ // The completion action finishes processing this statement block.
+ CompletionAction completion_action;
+
+ // Only one of |statements| or |cases| is active.
+
+ // The list of statements being built.
+ ast::StatementList statements;
+ // The list of cases being built, for a switch.
+ ast::CaseStatementList cases;
+ };
+
+ /// Pushes an empty statement block onto the statements stack.
+ /// @param action the completion action for this block
+ void PushNewStatementBlock(const Construct* construct,
+ uint32_t end_id,
+ CompletionAction action);
ParserImpl& parser_impl_;
ast::Module& ast_module_;
@@ -362,7 +438,9 @@
// A stack of statement lists. Each list is contained in a construct in
// the next deeper element of stack. The 0th entry represents the statements
// for the entire function. This stack is never empty.
- std::vector<ast::StatementList> statements_stack_;
+ // The |construct| member for the 0th element is only valid during the
+ // lifetime of the EmitFunctionBodyStatements method.
+ std::vector<StatementBlock> statements_stack_;
// The set of IDs that have already had an identifier name generated for it.
std::unordered_set<uint32_t> identifier_values_;
diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc
index 7f4b81e..e10b3b0 100644
--- a/src/reader/spirv/function_cfg_test.cc
+++ b/src/reader/spirv/function_cfg_test.cc
@@ -27,6 +27,9 @@
namespace spirv {
namespace {
+using ::testing::Eq;
+using ::testing::HasSubstr;
+
std::string Dump(const std::vector<uint32_t>& v) {
std::ostringstream o;
o << "{";
@@ -46,16 +49,29 @@
OpCapability Shader
OpMemoryModel Logical Simple
+ OpName %var "var"
+
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%bool = OpTypeBool
- %cond = OpUndef %bool
- %cond2 = OpUndef %bool
- %cond3 = OpUndef %bool
+ %cond = OpConstantNull %bool
+ %cond2 = OpConstantTrue %bool
+ %cond3 = OpConstantFalse %bool
%uint = OpTypeInt 32 0
- %selector = OpUndef %uint
+ %selector = OpConstant %uint 42
+
+ %uint_0 = OpConstant %uint 0
+ %uint_1 = OpConstant %uint 1
+ %uint_2 = OpConstant %uint 2
+ %uint_3 = OpConstant %uint 3
+ %uint_4 = OpConstant %uint 4
+ %uint_5 = OpConstant %uint 5
+ %uint_6 = OpConstant %uint 6
+
+ %ptr_Private_uint = OpTypePointer Private %uint
+ %var = OpVariable %ptr_Private_uint Private
%999 = OpConstant %uint 999
)";
@@ -677,7 +693,7 @@
fe.RegisterBasicBlocks();
EXPECT_FALSE(fe.RegisterMerges());
EXPECT_THAT(p->error(),
- Eq("Structured header block 10 declares invalid merge block 1"));
+ Eq("Structured header block 10 declares invalid merge block 2"));
}
TEST_F(SpvParserTest, RegisterMerges_HeaderIsItsOwnMerge) {
@@ -6642,7 +6658,6 @@
OpFunctionEnd
)";
auto* p = parser(test::Assemble(assembly));
- std::cout << assembly;
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
FunctionEmitter fe(p, *spirv_function(100));
EXPECT_FALSE(FlowFindIfSelectionInternalHeaders(&fe));
@@ -6729,6 +6744,488 @@
)";
}
+TEST_F(SpvParserTest, EmitBody_If_Empty) {
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %99 %99
+
+ %99 = OpLabel
+ OpReturn
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ }
+}
+Else{
+ {
+ }
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Then_NoElse) {
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %30 %99
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %99
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Else{
+ {
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_NoThen_Else) {
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %99 %30
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %99
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ }
+}
+Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Then_Else) {
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %30 %40
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %99
+
+ %40 = OpLabel
+ OpStore %var %uint_2
+ OpBranch %99
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{2}
+ }
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Then_Else_Premerge) {
+ // TODO(dneto): This should get an extra if(true) around
+ // the premerge code.
+ // See https://bugs.chromium.org/p/tint/issues/detail?id=82
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %30 %40
+
+ %80 = OpLabel ; premerge
+ OpStore %var %uint_3
+ OpBranch %99
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %80
+
+ %40 = OpLabel
+ OpStore %var %uint_2
+ OpBranch %80
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{2}
+ }
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{3}
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Then_Premerge) {
+ // The premerge *is* the else.
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %30 %80
+
+ %80 = OpLabel ; premerge
+ OpStore %var %uint_3
+ OpBranch %99
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %80
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Else{
+ {
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{3}
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Else_Premerge) {
+ // The premerge *is* the then-clause.
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %80 %30
+
+ %80 = OpLabel ; premerge
+ OpStore %var %uint_3
+ OpBranch %99
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ %30 = OpLabel
+ OpStore %var %uint_1
+ OpBranch %80
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ }
+}
+Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{3}
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
+TEST_F(SpvParserTest, EmitBody_If_Nest_If) {
+ auto* p = parser(test::Assemble(CommonTypes() + R"(
+ %100 = OpFunction %void None %voidfn
+
+ %10 = OpLabel
+ OpStore %var %uint_0
+ OpSelectionMerge %99 None
+ OpBranchConditional %cond %30 %40
+
+ %30 = OpLabel ;; inner if #1
+ OpStore %var %uint_1
+ OpSelectionMerge %39 None
+ OpBranchConditional %cond2 %33 %39
+
+ %33 = OpLabel
+ OpStore %var %uint_2
+ OpBranch %39
+
+ %39 = OpLabel ;; inner merge
+ OpStore %var %uint_3
+ OpBranch %99
+
+ %40 = OpLabel ;; inner if #2
+ OpStore %var %uint_4
+ OpSelectionMerge %49 None
+ OpBranchConditional %cond2 %49 %43
+
+ %43 = OpLabel
+ OpStore %var %uint_5
+ OpBranch %49
+
+ %49 = OpLabel ;; 2nd inner merge
+ OpStore %var %uint_6
+ OpBranch %99
+
+ %99 = OpLabel
+ OpStore %var %999
+ OpReturn
+
+ OpFunctionEnd
+ )"));
+ ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+ FunctionEmitter fe(p, *spirv_function(100));
+ EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+ EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+ Identifier{var}
+ ScalarConstructor{0}
+}
+If{
+ (
+ ScalarConstructor{false}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{1}
+ }
+ If{
+ (
+ ScalarConstructor{true}
+ )
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{2}
+ }
+ }
+ }
+ Else{
+ {
+ }
+ }
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{3}
+ }
+ }
+}
+Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{4}
+ }
+ If{
+ (
+ ScalarConstructor{true}
+ )
+ {
+ }
+ }
+ Else{
+ {
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{5}
+ }
+ }
+ }
+ Assignment{
+ Identifier{var}
+ ScalarConstructor{6}
+ }
+ }
+}
+Assignment{
+ Identifier{var}
+ ScalarConstructor{999}
+}
+)"));
+}
+
} // namespace
} // namespace spirv
} // namespace reader