blob: d02dcb728105ab028aabe1aa75f979d0b1585077 [file] [log] [blame]
// Copyright 2023 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "src/tint/lang/core/ir/transform/merge_return.h"
#include <utility>
#include "src/tint/lang/core/ir/builder.h"
#include "src/tint/lang/core/ir/module.h"
#include "src/tint/utils/containers/reverse.h"
#include "src/tint/utils/containers/transform.h"
#include "src/tint/utils/rtti/switch.h"
TINT_INSTANTIATE_TYPEINFO(tint::ir::transform::MergeReturn);
using namespace tint::builtin::fluent_types; // NOLINT
using namespace tint::number_suffixes; // NOLINT
namespace tint::ir::transform {
MergeReturn::MergeReturn() = default;
MergeReturn::~MergeReturn() = default;
/// PIMPL state for the transform, for a single function.
struct MergeReturn::State {
/// The IR module.
Module* ir = nullptr;
/// The IR builder.
Builder b{*ir};
/// The type manager.
type::Manager& ty{ir->Types()};
/// The "has not returned" flag.
Var* continue_execution = nullptr;
/// The variable that holds the return value.
/// Null when the function does not return a value.
Var* return_val = nullptr;
/// The final return at the end of the function block.
/// May be null when the function returns in all blocks of a control instruction.
Return* fn_return = nullptr;
/// A set of control instructions that transitively hold a return instruction
utils::Hashset<ControlInstruction*, 8> holds_return_;
/// Constructor
/// @param mod the module
explicit State(Module* mod) : ir(mod) {}
/// Process the function.
/// @param fn the function to process
void Process(Function* fn) {
// Find all of the nested return instructions in the function.
for (const auto& usage : fn->Usages()) {
if (auto* ret = usage.instruction->As<Return>()) {
TransitivelyMarkAsReturning(ret->Block()->Parent());
}
}
if (holds_return_.IsEmpty()) {
// No control instructions hold a return statement, so nothing needs to be done.
return;
}
// Create a boolean variable that can be used to check whether the function is returning.
continue_execution = b.Var("continue_execution", ty.ptr<function, bool>());
continue_execution->SetInitializer(b.Constant(true));
fn->Block()->Prepend(continue_execution);
// Create a variable to hold the return value if needed.
if (!fn->ReturnType()->Is<type::Void>()) {
return_val = b.Var("return_value", ty.ptr(function, fn->ReturnType()));
fn->Block()->Prepend(return_val);
}
// Look to see if the function ends with a return
fn_return = tint::As<Return>(fn->Block()->Terminator());
// Process the function's block.
// This will traverse into control instructions that hold returns, and apply the necessary
// changes to remove returns.
ProcessBlock(fn->Block());
// If the function didn't end with a return, add one
if (!fn_return) {
AppendFinalReturn(fn);
}
// Cleanup - if 'continue_execution' was only ever assigned, remove it.
continue_execution->DestroyIfOnlyAssigned();
}
/// Marks all the control instructions from ctrl to the function as holding a return.
/// @param ctrl the control instruction to mark as returning, along with all ancestor control
/// instructions.
void TransitivelyMarkAsReturning(ControlInstruction* ctrl) {
for (; ctrl; ctrl = ctrl->Block()->Parent()) {
if (!holds_return_.Add(ctrl)) {
return;
}
}
}
/// Walks the instructions of @p block, processing control instructions that are marked as
/// holding a return instruction. After processing a control instruction with a return, the
/// instructions following the control instruction will be wrapped in a 'if' that only executes
/// if a return was not reached.
/// @param block the block to process
void ProcessBlock(Block* block) {
If* inner_if = nullptr;
for (auto* inst = *block->begin(); inst;) { // For each instruction in 'block'
// As we're modifying the block that we're iterating over, grab the pointer to the next
// instruction before (potentially) moving 'inst' to another block.
auto* next = inst->next;
TINT_DEFER(inst = next);
if (auto* ret = inst->As<Return>()) {
// Note: Return instructions are processed without being moved into the 'if' block.
ProcessReturn(ret, inner_if);
break; // All instructions processed.
}
if (inst->Is<Unreachable>()) {
// Unreachable can become reachable once returns are turned into exits.
// As this is the terminator for the block, simply stop processing the
// instructions. A appropriate terminator will be created for this block below.
inst->Remove();
break;
}
// If we've already passed a control instruction holding a return, then we need to move
// the instructions that follow the control instruction into the inner-most 'if'.
if (inner_if) {
inst->Remove();
inner_if->True()->Append(inst);
}
// Control instructions holding a return need to be processed, and then a new 'if' needs
// to be created to hold the instructions that are between the control instruction and
// the block's terminating instruction.
if (auto* ctrl = inst->As<ControlInstruction>()) {
if (holds_return_.Contains(ctrl)) {
// Control instruction transitively holds a return.
ctrl->ForeachBlock([&](Block* ctrl_block) { ProcessBlock(ctrl_block); });
if (next && next != fn_return && !utils::IsAnyOf<Exit, Unreachable>(next)) {
inner_if = CreateIfContinueExecution(ctrl);
}
}
}
}
if (inner_if) {
// new_value_with_type returns a new RuntimeValue with the same type as 'v'
auto new_value_with_type = [&](Value* v) { return b.InstructionResult(v->Type()); };
if (inner_if->True()->HasTerminator()) {
if (auto* exit_if = inner_if->True()->Terminator()->As<ExitIf>()) {
// Ensure the associated 'if' is updated.
exit_if->SetIf(inner_if);
if (!exit_if->Args().IsEmpty()) {
// Inner-most 'if' has a 'exit_if' that returns values.
// These need propagating through the if stack.
inner_if->SetResults(
utils::Transform<8>(exit_if->Args(), new_value_with_type));
}
}
} else {
// Inner-most if doesn't have a terminating instruction. Add an 'exit_if'.
inner_if->True()->Append(b.ExitIf(inner_if));
}
// Loop over the 'if' instructions, starting with the inner-most, and add any missing
// terminating instructions to the blocks holding the 'if'.
for (auto* i = inner_if; i; i = tint::As<If>(i->Block()->Parent())) {
if (!i->Block()->HasTerminator()) {
// Append the exit instruction to the block holding the 'if'.
utils::Vector<InstructionResult*, 8> exit_args = i->Results();
if (!i->HasResults()) {
i->SetResults(utils::Transform(exit_args, new_value_with_type));
}
auto* exit = b.Exit(i->Block()->Parent(), std::move(exit_args));
i->Block()->Append(exit);
}
}
}
}
/// Transforms a return instruction.
/// @param ret the return instruction
/// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
/// @note unlike other instructions, return instructions are not automatically moved into the
/// 'if(continue_execution)' block.
void ProcessReturn(Return* ret, If* cond) {
if (ret == fn_return) {
// 'ret' is the final instruction for the function.
ProcessFunctionBlockReturn(ret, cond);
} else {
// Return is in a nested block
ProcessNestedReturn(ret, cond);
}
}
/// Transforms the return instruction that is the last instruction in the function's block.
/// @param ret the return instruction
/// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
void ProcessFunctionBlockReturn(Return* ret, If* cond) {
if (!return_val) {
return; // No need to transform non-value, end-of-function returns
}
// Assign the return's value to 'return_val' inside a 'if(continue_execution)'
if (!cond) {
cond = CreateIfContinueExecution(ret->prev);
}
cond->True()->Append(b.Store(return_val, ret->Value()));
cond->True()->Append(b.ExitIf(cond));
// Change the function return to unconditionally load 'return_val' and return it
auto* load = b.Load(return_val);
load->InsertBefore(ret);
ret->SetValue(load->Result());
}
/// Transforms the return instruction that is found in a control instruction.
/// @param ret the return instruction
/// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
void ProcessNestedReturn(Return* ret, If* cond) {
// If we have a 'if(continue_execution)' block, then insert instructions into that,
// otherwise insert into the block holding the return.
auto* block = cond ? cond->True() : ret->Block();
// Set the 'continue_execution' flag to false, and store the return value into 'return_val',
// if present.
block->Append(b.Store(continue_execution, false));
if (return_val) {
block->Append(b.Store(return_val, ret->Args()[0]));
}
// If the outermost control instruction is expecting exit values, then return them as
// 'undef' values.
auto* ctrl = block->Parent();
utils::Vector<Value*, 8> exit_args;
exit_args.Resize(ctrl->Results().Length());
// Replace the return instruction with an exit instruction.
block->Append(b.Exit(ctrl, std::move(exit_args)));
ret->Destroy();
}
/// Builds instructions to create a 'if(continue_execution)' conditional.
/// @param after new instructions will be inserted after this instruction
/// @return the 'If' control instruction
If* CreateIfContinueExecution(Instruction* after) {
auto* load = b.Load(continue_execution);
auto* cond = b.If(load);
load->InsertAfter(after);
cond->InsertAfter(load);
return cond;
}
/// Adds a final return instruction to the end of @p fn
/// @param fn the function
void AppendFinalReturn(Function* fn) {
b.With(fn->Block(), [&] {
if (return_val) {
b.Return(fn, b.Load(return_val));
} else {
b.Return(fn);
}
});
}
};
void MergeReturn::Run(Module* ir, const DataMap&, DataMap&) const {
// Process each function.
for (auto* fn : ir->functions) {
State{ir}.Process(fn);
}
}
} // namespace tint::ir::transform