blob: b15f3a093cd7c2a622d69fad202f294aa267180c [file] [log] [blame]
// Copyright 2022 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/transform/unwind_discard_functions.h"
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "src/tint/ast/discard_statement.h"
#include "src/tint/ast/return_statement.h"
#include "src/tint/ast/traverse_expressions.h"
#include "src/tint/sem/block_statement.h"
#include "src/tint/sem/call.h"
#include "src/tint/sem/for_loop_statement.h"
#include "src/tint/sem/function.h"
#include "src/tint/sem/if_statement.h"
#include "src/tint/transform/utils/get_insertion_point.h"
TINT_INSTANTIATE_TYPEINFO(tint::transform::UnwindDiscardFunctions);
namespace tint::transform {
namespace {
class State {
private:
CloneContext& ctx;
ProgramBuilder& b;
const sem::Info& sem;
Symbol module_discard_var_name; // Use ModuleDiscardVarName() to read
Symbol module_discard_func_name; // Use ModuleDiscardFuncName() to read
// If `block`'s parent is of type TO, returns pointer to it.
template <typename TO>
const TO* ParentAs(const ast::BlockStatement* block) {
if (auto* sem_block = sem.Get(block)) {
return As<TO>(sem_block->Parent());
}
return nullptr;
}
// Returns true if `sem_expr` contains a call expression that may
// (transitively) execute a discard statement.
bool MayDiscard(const sem::Expression* sem_expr) {
return sem_expr && sem_expr->Behaviors().Contains(sem::Behavior::kDiscard);
}
// Lazily creates and returns the name of the module bool variable for whether
// to discard: "tint_discard".
Symbol ModuleDiscardVarName() {
if (!module_discard_var_name.IsValid()) {
module_discard_var_name = b.Symbols().New("tint_discard");
ctx.dst->Global(module_discard_var_name, b.ty.bool_(), b.Expr(false),
ast::StorageClass::kPrivate);
}
return module_discard_var_name;
}
// Lazily creates and returns the name of the function that contains a single
// discard statement: "tint_discard_func".
// We do this to avoid having multiple discard statements in a single program,
// which causes problems in certain backends (see crbug.com/1118).
Symbol ModuleDiscardFuncName() {
if (!module_discard_func_name.IsValid()) {
module_discard_func_name = b.Symbols().New("tint_discard_func");
b.Func(module_discard_func_name, {}, b.ty.void_(), {b.Discard()});
}
return module_discard_func_name;
}
// Creates "return <default return value>;" based on the return type of
// `stmt`'s owning function.
const ast::ReturnStatement* Return(const ast::Statement* stmt) {
const ast::Expression* ret_val = nullptr;
auto* ret_type = sem.Get(stmt)->Function()->Declaration()->return_type;
if (!ret_type->Is<ast::Void>()) {
ret_val = b.Construct(ctx.Clone(ret_type));
}
return b.Return(ret_val);
}
// Returns true if the function `stmt` is in is an entry point
bool IsInEntryPointFunc(const ast::Statement* stmt) {
return sem.Get(stmt)->Function()->Declaration()->IsEntryPoint();
}
// Creates "tint_discard_func();"
const ast::CallStatement* CallDiscardFunc() {
auto func_name = ModuleDiscardFuncName();
return b.CallStmt(b.Call(func_name));
}
// Creates and returns a new if-statement of the form:
//
// if (tint_discard) {
// return <default value>;
// }
//
// or if `stmt` is in a entry point function:
//
// if (tint_discard) {
// tint_discard_func();
// return <default value>;
// }
//
const ast::IfStatement* IfDiscardReturn(const ast::Statement* stmt) {
ast::StatementList stmts;
// For entry point functions, also emit the discard statement
if (IsInEntryPointFunc(stmt)) {
stmts.emplace_back(CallDiscardFunc());
}
stmts.emplace_back(Return(stmt));
auto var_name = ModuleDiscardVarName();
return b.If(var_name, b.Block(stmts));
}
// Hoists `sem_expr` to a let followed by an `IfDiscardReturn` before `stmt`.
// For example, if `stmt` is:
//
// return f();
//
// This function will transform this to:
//
// let t1 = f();
// if (tint_discard) {
// return;
// }
// return t1;
//
const ast::Statement* HoistAndInsertBefore(const ast::Statement* stmt,
const sem::Expression* sem_expr) {
auto* expr = sem_expr->Declaration();
auto ip = utils::GetInsertionPoint(ctx, stmt);
auto var_name = b.Sym();
auto* decl = b.Decl(b.Var(var_name, nullptr, ctx.Clone(expr)));
ctx.InsertBefore(ip.first->Declaration()->statements, ip.second, decl);
ctx.InsertBefore(ip.first->Declaration()->statements, ip.second,
IfDiscardReturn(stmt));
auto* var_expr = b.Expr(var_name);
// Special handling for CallStatement as we can only replace its expression
// with a CallExpression.
if (stmt->Is<ast::CallStatement>()) {
// We could replace the call statement with no statement, but we can't do
// that with transforms (yet), so just return a phony assignment.
return b.Assign(b.Phony(), var_expr);
}
ctx.Replace(expr, var_expr);
return ctx.CloneWithoutTransform(stmt);
}
// Returns true if `stmt` is a for-loop initializer statement.
bool IsForLoopInitStatement(const ast::Statement* stmt) {
if (auto* sem_stmt = sem.Get(stmt)) {
if (auto* sem_fl = As<sem::ForLoopStatement>(sem_stmt->Parent())) {
return sem_fl->Declaration()->initializer == stmt;
}
}
return false;
}
// Inserts an `IfDiscardReturn` after `stmt` if possible (i.e. `stmt` is not
// in a for-loop init), otherwise falls back to HoistAndInsertBefore, hoisting
// `sem_expr` to a let followed by an `IfDiscardReturn` before `stmt`.
//
// For example, if `stmt` is:
//
// let r = f();
//
// This function will transform this to:
//
// let r = f();
// if (tint_discard) {
// return;
// }
const ast::Statement* TryInsertAfter(const ast::Statement* stmt,
const sem::Expression* sem_expr) {
// If `stmt` is the init of a for-loop, hoist and insert before instead.
if (IsForLoopInitStatement(stmt)) {
return HoistAndInsertBefore(stmt, sem_expr);
}
auto ip = utils::GetInsertionPoint(ctx, stmt);
ctx.InsertAfter(ip.first->Declaration()->statements, ip.second,
IfDiscardReturn(stmt));
return nullptr; // Don't replace current statement
}
// Replaces the input discard statement with either setting the module level
// discard bool ("tint_discard = true"), or calling the discard function
// ("tint_discard_func()"), followed by a default return statement.
//
// Replaces "discard;" with:
//
// tint_discard = true;
// return;
//
// Or if `stmt` is a entry point function, replaces with:
//
// tint_discard_func();
// return;
//
const ast::Statement* ReplaceDiscardStatement(
const ast::DiscardStatement* stmt) {
const ast::Statement* to_insert = nullptr;
if (IsInEntryPointFunc(stmt)) {
to_insert = CallDiscardFunc();
} else {
auto var_name = ModuleDiscardVarName();
to_insert = b.Assign(var_name, true);
}
auto ip = utils::GetInsertionPoint(ctx, stmt);
ctx.InsertBefore(ip.first->Declaration()->statements, ip.second, to_insert);
return Return(stmt);
}
// Handle statement
const ast::Statement* Statement(const ast::Statement* stmt) {
return Switch(
stmt,
[&](const ast::DiscardStatement* s) -> const ast::Statement* {
return ReplaceDiscardStatement(s);
},
[&](const ast::AssignmentStatement* s) -> const ast::Statement* {
auto* sem_lhs = sem.Get(s->lhs);
auto* sem_rhs = sem.Get(s->rhs);
if (MayDiscard(sem_lhs)) {
if (MayDiscard(sem_rhs)) {
TINT_ICE(Transform, b.Diagnostics())
<< "Unexpected: both sides of assignment statement may "
"discard. Make sure transform::PromoteSideEffectsToDecl "
"was run first.";
}
return TryInsertAfter(s, sem_lhs);
} else if (MayDiscard(sem_rhs)) {
return TryInsertAfter(s, sem_rhs);
}
return nullptr;
},
[&](const ast::CallStatement* s) -> const ast::Statement* {
auto* sem_expr = sem.Get(s->expr);
if (!MayDiscard(sem_expr)) {
return nullptr;
}
return TryInsertAfter(s, sem_expr);
},
[&](const ast::ElseStatement* s) -> const ast::Statement* {
if (MayDiscard(sem.Get(s->condition))) {
TINT_ICE(Transform, b.Diagnostics())
<< "Unexpected ElseIf condition that may discard. Make sure "
"transform::PromoteSideEffectsToDecl was run first.";
}
return nullptr;
},
[&](const ast::ForLoopStatement* s) -> const ast::Statement* {
if (MayDiscard(sem.Get(s->condition))) {
TINT_ICE(Transform, b.Diagnostics())
<< "Unexpected ForLoopStatement condition that may discard. "
"Make sure transform::PromoteSideEffectsToDecl was run "
"first.";
}
return nullptr;
},
[&](const ast::IfStatement* s) -> const ast::Statement* {
auto* sem_expr = sem.Get(s->condition);
if (!MayDiscard(sem_expr)) {
return nullptr;
}
return HoistAndInsertBefore(s, sem_expr);
},
[&](const ast::ReturnStatement* s) -> const ast::Statement* {
auto* sem_expr = sem.Get(s->value);
if (!MayDiscard(sem_expr)) {
return nullptr;
}
return HoistAndInsertBefore(s, sem_expr);
},
[&](const ast::SwitchStatement* s) -> const ast::Statement* {
auto* sem_expr = sem.Get(s->condition);
if (!MayDiscard(sem_expr)) {
return nullptr;
}
return HoistAndInsertBefore(s, sem_expr);
},
[&](const ast::VariableDeclStatement* s) -> const ast::Statement* {
auto* var = s->variable;
if (!var->constructor) {
return nullptr;
}
auto* sem_expr = sem.Get(var->constructor);
if (!MayDiscard(sem_expr)) {
return nullptr;
}
return TryInsertAfter(s, sem_expr);
});
}
public:
/// Constructor
/// @param ctx_in the context
explicit State(CloneContext& ctx_in)
: ctx(ctx_in), b(*ctx_in.dst), sem(ctx_in.src->Sem()) {}
/// Runs the transform
void Run() {
ctx.ReplaceAll(
[&](const ast::BlockStatement* block) -> const ast::Statement* {
// If this block is for an else-if statement, process the else-if now
// before processing its block statements.
// NOTE: we can't replace else statements at this point - this would
// need to be done when replacing the parent if-statement. However, in
// this transform, we don't ever expect to need to do this as else-ifs
// are converted to else { if } by PromoteSideEffectsToDecl, so this
// is only for validation.
if (auto* sem_else = ParentAs<sem::ElseStatement>(block)) {
if (auto* new_stmt = Statement(sem_else->Declaration())) {
TINT_ASSERT(Transform, new_stmt == nullptr);
return nullptr;
}
}
// Iterate block statements and replace them as needed.
for (auto* stmt : block->statements) {
if (auto* new_stmt = Statement(stmt)) {
ctx.Replace(stmt, new_stmt);
}
// Handle for loops, as they are the only other AST node that
// contains statements outside of BlockStatements.
if (auto* fl = stmt->As<ast::ForLoopStatement>()) {
if (auto* new_stmt = Statement(fl->initializer)) {
ctx.Replace(fl->initializer, new_stmt);
}
if (auto* new_stmt = Statement(fl->continuing)) {
// NOTE: Should never reach here as we cannot discard in a
// continuing block.
ctx.Replace(fl->continuing, new_stmt);
}
}
}
return nullptr;
});
ctx.Clone();
}
};
} // namespace
UnwindDiscardFunctions::UnwindDiscardFunctions() = default;
UnwindDiscardFunctions::~UnwindDiscardFunctions() = default;
void UnwindDiscardFunctions::Run(CloneContext& ctx,
const DataMap&,
DataMap&) const {
State state(ctx);
state.Run();
}
bool UnwindDiscardFunctions::ShouldRun(const Program* program,
const DataMap& /*data*/) const {
auto& sem = program->Sem();
for (auto* f : program->AST().Functions()) {
if (sem.Get(f)->Behaviors().Contains(sem::Behavior::kDiscard)) {
return true;
}
}
return false;
}
} // namespace tint::transform