From 468c26b233cd368d05b8d2831790bb6099390c09 Mon Sep 17 00:00:00 2001 From: David Neto Date: Tue, 2 Jun 2020 13:25:06 +0000 Subject: [PATCH] [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 --- src/reader/spirv/function.cc | 322 +++++++++++++++- src/reader/spirv/function.h | 88 ++++- src/reader/spirv/function_cfg_test.cc | 509 +++++++++++++++++++++++++- 3 files changed, 890 insertions(+), 29 deletions(-) diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc index ff4d80b739..50c947a243 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 @@ FunctionEmitter::FunctionEmitter(ParserImpl* pi, 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 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 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 @@ bool FunctionEmitter::Emit() { "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 @@ bool FunctionEmitter::EmitBody() { return false; } + // TODO(dneto): register phis + // TODO(dneto): register SSA values which need to be hoisted + if (!EmitFunctionVariables()) { return false; } @@ -1244,8 +1271,8 @@ bool FunctionEmitter::FindIfSelectionInternalHeaders() { 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 @@ bool FunctionEmitter::FindIfSelectionInternalHeaders() { 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 @@ bool FunctionEmitter::FindIfSelectionInternalHeaders() { << " 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 @@ TypedExpression FunctionEmitter::MakeExpression(uint32_t id) { } 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 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())->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( + 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 @@ bool FunctionEmitter::EmitStatementsInBasicBlock( 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 917425969e..a7fec4c323 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 #include #include #include @@ -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 @@ struct BlockInfo { 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 @@ class FunctionEmitter { /// @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 @@ class FunctionEmitter { BlockInfo* HeaderIfBreakable(const Construct* c); /// Appends a new statement to the top of the statement stack. - void AddStatement(std::unique_ptr statement); + /// @param statement the new statement + /// @returns a pointer to the statement. + ast::Statement* AddStatement(std::unique_ptr statement); + + /// @returns the last statetment in the top of the statement stack. + ast::Statement* LastStatement(); + + struct StatementBlock; + using CompletionAction = std::function; + + // 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 @@ class FunctionEmitter { // 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 statements_stack_; + // The |construct| member for the 0th element is only valid during the + // lifetime of the EmitFunctionBodyStatements method. + std::vector statements_stack_; // The set of IDs that have already had an identifier name generated for it. std::unordered_set identifier_values_; diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc index 7f4b81eae3..e10b3b014b 100644 --- a/src/reader/spirv/function_cfg_test.cc +++ b/src/reader/spirv/function_cfg_test.cc @@ -27,6 +27,9 @@ namespace reader { namespace spirv { namespace { +using ::testing::Eq; +using ::testing::HasSubstr; + std::string Dump(const std::vector& v) { std::ostringstream o; o << "{"; @@ -46,16 +49,29 @@ std::string CommonTypes() { 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 @@ TEST_F(SpvParserTest, RegisterMerges_BadMergeBlock) { 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 @@ TEST_F(SpvParserTest, 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, DISABLED_BlockIsContinueForMoreThanOneHeader) { )"; } +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