| // 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/promote_side_effects_to_decl.h" |
| |
| #include <memory> |
| #include <string> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #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/if_statement.h" |
| #include "src/tint/sem/member_accessor_expression.h" |
| #include "src/tint/sem/variable.h" |
| #include "src/tint/sem/while_statement.h" |
| #include "src/tint/transform/manager.h" |
| #include "src/tint/transform/utils/get_insertion_point.h" |
| #include "src/tint/transform/utils/hoist_to_decl_before.h" |
| #include "src/tint/utils/scoped_assignment.h" |
| |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::PromoteSideEffectsToDecl); |
| |
| namespace tint::transform { |
| namespace { |
| |
| // Base state class for common members |
| class StateBase { |
| protected: |
| CloneContext& ctx; |
| ProgramBuilder& b; |
| const sem::Info& sem; |
| |
| explicit StateBase(CloneContext& ctx_in) |
| : ctx(ctx_in), b(*ctx_in.dst), sem(ctx_in.src->Sem()) {} |
| }; |
| |
| // This first transform converts side-effecting for-loops to loops and else-ifs |
| // to else {if}s so that the next transform, DecomposeSideEffects, can insert |
| // hoisted expressions above their current location. |
| struct SimplifySideEffectStatements : Castable<PromoteSideEffectsToDecl, Transform> { |
| ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override; |
| }; |
| |
| Transform::ApplyResult SimplifySideEffectStatements::Apply(const Program* src, |
| const DataMap&, |
| DataMap&) const { |
| ProgramBuilder b; |
| CloneContext ctx{&b, src, /* auto_clone_symbols */ true}; |
| |
| bool made_changes = false; |
| |
| HoistToDeclBefore hoist_to_decl_before(ctx); |
| for (auto* node : ctx.src->ASTNodes().Objects()) { |
| if (auto* expr = node->As<ast::Expression>()) { |
| auto* sem_expr = src->Sem().Get(expr); |
| if (!sem_expr || !sem_expr->HasSideEffects()) { |
| continue; |
| } |
| |
| hoist_to_decl_before.Prepare(sem_expr); |
| made_changes = true; |
| } |
| } |
| |
| if (!made_changes) { |
| return SkipTransform; |
| } |
| |
| ctx.Clone(); |
| return Program(std::move(b)); |
| } |
| |
| // Decomposes side-effecting expressions to ensure order of evaluation. This |
| // handles both breaking down logical binary expressions for short-circuit |
| // evaluation, as well as hoisting expressions to ensure order of evaluation. |
| struct DecomposeSideEffects : Castable<PromoteSideEffectsToDecl, Transform> { |
| class CollectHoistsState; |
| class DecomposeState; |
| ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override; |
| }; |
| |
| // CollectHoistsState traverses the AST top-down, identifying which expressions |
| // need to be hoisted to ensure order of evaluation, both those that give |
| // side-effects, as well as those that receive, and returns a set of these |
| // expressions. |
| using ToHoistSet = std::unordered_set<const ast::Expression*>; |
| class DecomposeSideEffects::CollectHoistsState : public StateBase { |
| // Expressions to hoist because they either cause or receive side-effects. |
| ToHoistSet to_hoist; |
| |
| // Used to mark expressions as not or no longer having side-effects. |
| std::unordered_set<const ast::Expression*> no_side_effects; |
| |
| // Returns true if `expr` has side-effects. Unlike invoking |
| // sem::Expression::HasSideEffects(), this function takes into account whether |
| // `expr` has been hoisted, returning false in that case. Furthermore, it |
| // returns the correct result on parent expression nodes by traversing the |
| // expression tree, memoizing the results to ensure O(1) amortized lookup. |
| bool HasSideEffects(const ast::Expression* expr) { |
| if (no_side_effects.count(expr)) { |
| return false; |
| } |
| |
| return Switch( |
| expr, |
| [&](const ast::CallExpression* e) -> bool { return sem.Get(e)->HasSideEffects(); }, |
| [&](const ast::BinaryExpression* e) { |
| if (HasSideEffects(e->lhs) || HasSideEffects(e->rhs)) { |
| return true; |
| } |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::IndexAccessorExpression* e) { |
| if (HasSideEffects(e->object) || HasSideEffects(e->index)) { |
| return true; |
| } |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::MemberAccessorExpression* e) { |
| if (HasSideEffects(e->structure) || HasSideEffects(e->member)) { |
| return true; |
| } |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::BitcastExpression* e) { // |
| if (HasSideEffects(e->expr)) { |
| return true; |
| } |
| no_side_effects.insert(e); |
| return false; |
| }, |
| |
| [&](const ast::UnaryOpExpression* e) { // |
| if (HasSideEffects(e->expr)) { |
| return true; |
| } |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::IdentifierExpression* e) { |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::LiteralExpression* e) { |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](const ast::PhonyExpression* e) { |
| no_side_effects.insert(e); |
| return false; |
| }, |
| [&](Default) { |
| TINT_ICE(Transform, b.Diagnostics()) << "Unhandled expression type"; |
| return false; |
| }); |
| } |
| |
| // Adds `e` to `to_hoist` for hoisting to a let later on. |
| void Hoist(const ast::Expression* e) { |
| no_side_effects.insert(e); |
| to_hoist.emplace(e); |
| } |
| |
| // Hoists any expressions in `maybe_hoist` and clears it |
| template <size_t N> |
| void Flush(tint::utils::Vector<const ast::Expression*, N>& maybe_hoist) { |
| for (auto* m : maybe_hoist) { |
| Hoist(m); |
| } |
| maybe_hoist.Clear(); |
| } |
| |
| // Recursive function that processes expressions for side-effects. It |
| // traverses the expression tree child before parent, left-to-right. Each call |
| // returns whether the input expression should maybe be hoisted, allowing the |
| // parent node to decide whether to hoist or not. Generally: |
| // * When 'true' is returned, the expression is added to the maybe_hoist list. |
| // * When a side-effecting expression is met, we flush the expressions in the |
| // maybe_hoist list, as they are potentially receivers of the side-effects. |
| // * For index and member accessor expressions, special care is taken to not |
| // over-hoist the lhs expressions, as these may be be chained to refer to a |
| // single memory location. |
| template <size_t N> |
| bool ProcessExpression(const ast::Expression* expr, |
| tint::utils::Vector<const ast::Expression*, N>& maybe_hoist) { |
| auto process = [&](const ast::Expression* e) -> bool { |
| return ProcessExpression(e, maybe_hoist); |
| }; |
| |
| auto default_process = [&](const ast::Expression* e) { |
| auto maybe = process(e); |
| if (maybe) { |
| maybe_hoist.Push(e); |
| } |
| if (HasSideEffects(e)) { |
| Flush(maybe_hoist); |
| } |
| return false; |
| }; |
| |
| auto binary_process = [&](auto* lhs, auto* rhs) { |
| // If neither side causes side-effects, but at least one receives them, |
| // let parent node hoist. This avoids over-hoisting side-effect receivers |
| // of compound binary expressions (e.g. for "((a && b) && c) && f()", we |
| // don't want to hoist each of "a", "b", and "c" separately, but want to |
| // hoist "((a && b) && c)". |
| if (!HasSideEffects(lhs) && !HasSideEffects(rhs)) { |
| auto lhs_maybe = process(lhs); |
| auto rhs_maybe = process(rhs); |
| if (lhs_maybe || rhs_maybe) { |
| return true; |
| } |
| return false; |
| } |
| |
| default_process(lhs); |
| default_process(rhs); |
| return false; |
| }; |
| |
| auto accessor_process = [&](auto* lhs, auto* rhs) { |
| auto maybe = process(lhs); |
| // If lhs is a variable, let parent node hoist otherwise flush it right |
| // away. This is to avoid over-hoisting the lhs of accessor chains (e.g. |
| // for "v[a][b][c] + g()" we want to hoist all of "v[a][b][c]", not "t1 = |
| // v[a]", then "t2 = t1[b]" then "t3 = t2[c]"). |
| if (maybe && HasSideEffects(lhs)) { |
| maybe_hoist.Push(lhs); |
| Flush(maybe_hoist); |
| maybe = false; |
| } |
| default_process(rhs); |
| return maybe; |
| }; |
| |
| return Switch( |
| expr, |
| [&](const ast::CallExpression* e) -> bool { |
| // We eagerly flush any variables in maybe_hoist for the current |
| // call expression. Then we scope maybe_hoist to the processing of |
| // the call args. This ensures that given: g(c, a(0), d) we hoist |
| // 'c' because of 'a(0)', but not 'd' because there's no need, since |
| // the call to g() will be hoisted if necessary. |
| if (HasSideEffects(e)) { |
| Flush(maybe_hoist); |
| } |
| |
| TINT_SCOPED_ASSIGNMENT(maybe_hoist, {}); |
| for (auto* a : e->args) { |
| default_process(a); |
| } |
| |
| // Always hoist this call, even if it has no side-effects to ensure |
| // left-to-right order of evaluation. |
| // E.g. for "no_side_effects() + side_effects()", we want to hoist |
| // no_side_effects() first. |
| return true; |
| }, |
| [&](const ast::IdentifierExpression* e) { |
| if (auto* sem_e = sem.Get(e)) { |
| if (auto* var_user = sem_e->As<sem::VariableUser>()) { |
| // Don't hoist constants. |
| if (var_user->ConstantValue()) { |
| return false; |
| } |
| // Don't hoist read-only variables as they cannot receive side-effects. |
| if (var_user->Variable()->Access() == ast::Access::kRead) { |
| return false; |
| } |
| // Don't hoist textures / samplers as they can't be placed into a let, nor |
| // can they have side effects. |
| if (var_user->Variable()->Type()->IsAnyOf<sem::Texture, sem::Sampler>()) { |
| return false; |
| } |
| return true; |
| } |
| } |
| return false; |
| }, |
| [&](const ast::BinaryExpression* e) { |
| if (e->IsLogical() && HasSideEffects(e)) { |
| // Don't hoist children of logical binary expressions with |
| // side-effects. These will be handled by DecomposeState. |
| process(e->lhs); |
| process(e->rhs); |
| return false; |
| } |
| return binary_process(e->lhs, e->rhs); |
| }, |
| [&](const ast::BitcastExpression* e) { // |
| return process(e->expr); |
| }, |
| [&](const ast::UnaryOpExpression* e) { // |
| auto r = process(e->expr); |
| // Don't hoist address-of expressions. |
| // E.g. for "g(&b, a(0))", we hoist "a(0)" only. |
| if (e->op == ast::UnaryOp::kAddressOf) { |
| return false; |
| } |
| return r; |
| }, |
| [&](const ast::IndexAccessorExpression* e) { |
| return accessor_process(e->object, e->index); |
| }, |
| [&](const ast::MemberAccessorExpression* e) { |
| return accessor_process(e->structure, e->member); |
| }, |
| [&](const ast::LiteralExpression*) { |
| // Leaf |
| return false; |
| }, |
| [&](const ast::PhonyExpression*) { |
| // Leaf |
| return false; |
| }, |
| [&](Default) { |
| TINT_ICE(Transform, b.Diagnostics()) << "Unhandled expression type"; |
| return false; |
| }); |
| } |
| |
| // Starts the recursive processing of a statement's expression(s) to hoist |
| // side-effects to lets. |
| void ProcessStatement(const ast::Expression* expr) { |
| if (!expr) { |
| return; |
| } |
| |
| tint::utils::Vector<const ast::Expression*, 8> maybe_hoist; |
| ProcessExpression(expr, maybe_hoist); |
| } |
| |
| // Special case for processing assignment statement expressions, as we must |
| // evaluate the rhs before the lhs, and possibly hoist the rhs expression. |
| void ProcessAssignment(const ast::Expression* lhs, const ast::Expression* rhs) { |
| // Evaluate rhs before lhs |
| tint::utils::Vector<const ast::Expression*, 8> maybe_hoist; |
| if (ProcessExpression(rhs, maybe_hoist)) { |
| maybe_hoist.Push(rhs); |
| } |
| |
| // If the rhs has side-effects, it may affect the lhs, so hoist it right |
| // away. e.g. "b[c] = a(0);" |
| if (HasSideEffects(rhs)) { |
| // Technically, we can always hoist rhs, but don't bother doing so when |
| // the lhs is just a variable or phony. |
| if (!lhs->IsAnyOf<ast::IdentifierExpression, ast::PhonyExpression>()) { |
| Flush(maybe_hoist); |
| } |
| } |
| |
| // If maybe_hoist still has values, it means they are potential side-effect |
| // receivers. We pass this in while processing the lhs, in which case they |
| // may get hoisted if the lhs has side-effects. E.g. "b[a(0)] = c;". |
| ProcessExpression(lhs, maybe_hoist); |
| } |
| |
| public: |
| explicit CollectHoistsState(CloneContext& ctx_in) : StateBase(ctx_in) {} |
| |
| ToHoistSet Run() { |
| // Traverse all statements, recursively processing their expression tree(s) |
| // to hoist side-effects to lets. |
| for (auto* node : ctx.src->ASTNodes().Objects()) { |
| auto* stmt = node->As<ast::Statement>(); |
| if (!stmt) { |
| continue; |
| } |
| |
| Switch( |
| stmt, [&](const ast::AssignmentStatement* s) { ProcessAssignment(s->lhs, s->rhs); }, |
| [&](const ast::CallStatement* s) { // |
| ProcessStatement(s->expr); |
| }, |
| [&](const ast::ForLoopStatement* s) { ProcessStatement(s->condition); }, |
| [&](const ast::WhileStatement* s) { ProcessStatement(s->condition); }, |
| [&](const ast::IfStatement* s) { // |
| ProcessStatement(s->condition); |
| }, |
| [&](const ast::ReturnStatement* s) { // |
| ProcessStatement(s->value); |
| }, |
| [&](const ast::SwitchStatement* s) { ProcessStatement(s->condition); }, |
| [&](const ast::VariableDeclStatement* s) { |
| ProcessStatement(s->variable->initializer); |
| }); |
| } |
| |
| return std::move(to_hoist); |
| } |
| }; |
| |
| // DecomposeState performs the actual transforming of the AST to ensure order of |
| // evaluation, using the set of expressions to hoist collected by |
| // CollectHoistsState. |
| class DecomposeSideEffects::DecomposeState : public StateBase { |
| ToHoistSet to_hoist; |
| |
| // Returns true if `binary_expr` should be decomposed for short-circuit eval. |
| bool IsLogicalWithSideEffects(const ast::BinaryExpression* binary_expr) { |
| return binary_expr->IsLogical() && (sem.Get(binary_expr->lhs)->HasSideEffects() || |
| sem.Get(binary_expr->rhs)->HasSideEffects()); |
| } |
| |
| // Recursive function used to decompose an expression for short-circuit eval. |
| template <size_t N> |
| const ast::Expression* Decompose(const ast::Expression* expr, |
| tint::utils::Vector<const ast::Statement*, N>* curr_stmts) { |
| // Helper to avoid passing in same args. |
| auto decompose = [&](auto& e) { return Decompose(e, curr_stmts); }; |
| |
| // Clones `expr`, possibly hoisting it to a let. |
| auto clone_maybe_hoisted = [&](const ast::Expression* e) -> const ast::Expression* { |
| if (to_hoist.count(e)) { |
| auto name = b.Symbols().New(); |
| auto* v = b.Let(name, ctx.Clone(e)); |
| auto* decl = b.Decl(v); |
| curr_stmts->Push(decl); |
| return b.Expr(name); |
| } |
| return ctx.Clone(e); |
| }; |
| |
| return Switch( |
| expr, |
| [&](const ast::BinaryExpression* bin_expr) -> const ast::Expression* { |
| if (!IsLogicalWithSideEffects(bin_expr)) { |
| // No short-circuit, emit usual binary expr |
| ctx.Replace(bin_expr->lhs, decompose(bin_expr->lhs)); |
| ctx.Replace(bin_expr->rhs, decompose(bin_expr->rhs)); |
| return clone_maybe_hoisted(bin_expr); |
| } |
| |
| // Decompose into ifs to implement short-circuiting |
| // For example, 'let r = a && b' becomes: |
| // |
| // var temp = a; |
| // if (temp) { |
| // temp = b; |
| // } |
| // let r = temp; |
| // |
| // and similarly, 'let r = a || b' becomes: |
| // |
| // var temp = a; |
| // if (!temp) { |
| // temp = b; |
| // } |
| // let r = temp; |
| // |
| // Further, compound logical binary expressions are also handled |
| // recursively, for example, 'let r = (a && (b && c))' becomes: |
| // |
| // var temp = a; |
| // if (temp) { |
| // var temp2 = b; |
| // if (temp2) { |
| // temp2 = c; |
| // } |
| // temp = temp2; |
| // } |
| // let r = temp; |
| |
| auto name = b.Sym(); |
| curr_stmts->Push(b.Decl(b.Var(name, decompose(bin_expr->lhs)))); |
| |
| const ast::Expression* if_cond = nullptr; |
| if (bin_expr->IsLogicalOr()) { |
| if_cond = b.Not(name); |
| } else { |
| if_cond = b.Expr(name); |
| } |
| |
| const ast::BlockStatement* if_body = nullptr; |
| { |
| tint::utils::Vector<const ast::Statement*, N> stmts; |
| TINT_SCOPED_ASSIGNMENT(curr_stmts, &stmts); |
| auto* new_rhs = decompose(bin_expr->rhs); |
| curr_stmts->Push(b.Assign(name, new_rhs)); |
| if_body = b.Block(std::move(*curr_stmts)); |
| } |
| |
| curr_stmts->Push(b.If(if_cond, if_body)); |
| |
| return b.Expr(name); |
| }, |
| [&](const ast::IndexAccessorExpression* idx) { |
| ctx.Replace(idx->object, decompose(idx->object)); |
| ctx.Replace(idx->index, decompose(idx->index)); |
| return clone_maybe_hoisted(idx); |
| }, |
| [&](const ast::BitcastExpression* bitcast) { |
| ctx.Replace(bitcast->expr, decompose(bitcast->expr)); |
| return clone_maybe_hoisted(bitcast); |
| }, |
| [&](const ast::CallExpression* call) { |
| if (call->target.name) { |
| ctx.Replace(call->target.name, decompose(call->target.name)); |
| } |
| for (auto* a : call->args) { |
| ctx.Replace(a, decompose(a)); |
| } |
| return clone_maybe_hoisted(call); |
| }, |
| [&](const ast::MemberAccessorExpression* member) { |
| ctx.Replace(member->structure, decompose(member->structure)); |
| ctx.Replace(member->member, decompose(member->member)); |
| return clone_maybe_hoisted(member); |
| }, |
| [&](const ast::UnaryOpExpression* unary) { |
| ctx.Replace(unary->expr, decompose(unary->expr)); |
| return clone_maybe_hoisted(unary); |
| }, |
| [&](const ast::LiteralExpression* lit) { |
| return clone_maybe_hoisted(lit); // Leaf expression, just clone as is |
| }, |
| [&](const ast::IdentifierExpression* id) { |
| return clone_maybe_hoisted(id); // Leaf expression, just clone as is |
| }, |
| [&](const ast::PhonyExpression* phony) { |
| return clone_maybe_hoisted(phony); // Leaf expression, just clone as is |
| }, |
| [&](Default) { |
| TINT_ICE(AST, b.Diagnostics()) |
| << "unhandled expression type: " << expr->TypeInfo().name; |
| return nullptr; |
| }); |
| } |
| |
| // Inserts statements in `stmts` before `stmt` |
| template <size_t N> |
| void InsertBefore(tint::utils::Vector<const ast::Statement*, N>& stmts, |
| const ast::Statement* stmt) { |
| if (!stmts.IsEmpty()) { |
| auto ip = utils::GetInsertionPoint(ctx, stmt); |
| for (auto* s : stmts) { |
| ctx.InsertBefore(ip.first->Declaration()->statements, ip.second, s); |
| } |
| } |
| } |
| |
| // Decomposes expressions of `stmt`, returning a replacement statement or |
| // nullptr if not replacing it. |
| const ast::Statement* DecomposeStatement(const ast::Statement* stmt) { |
| return Switch( |
| stmt, |
| [&](const ast::AssignmentStatement* s) -> const ast::Statement* { |
| if (!sem.Get(s->lhs)->HasSideEffects() && !sem.Get(s->rhs)->HasSideEffects()) { |
| return nullptr; |
| } |
| // rhs before lhs |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->rhs, Decompose(s->rhs, &stmts)); |
| ctx.Replace(s->lhs, Decompose(s->lhs, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::CallStatement* s) -> const ast::Statement* { |
| if (!sem.Get(s->expr)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->expr, Decompose(s->expr, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::ForLoopStatement* s) -> const ast::Statement* { |
| if (!s->condition || !sem.Get(s->condition)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->condition, Decompose(s->condition, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::WhileStatement* s) -> const ast::Statement* { |
| if (!sem.Get(s->condition)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->condition, Decompose(s->condition, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::IfStatement* s) -> const ast::Statement* { |
| if (!sem.Get(s->condition)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->condition, Decompose(s->condition, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::ReturnStatement* s) -> const ast::Statement* { |
| if (!s->value || !sem.Get(s->value)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->value, Decompose(s->value, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::SwitchStatement* s) -> const ast::Statement* { |
| if (!sem.Get(s->condition)) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(s->condition, Decompose(s->condition, &stmts)); |
| InsertBefore(stmts, s); |
| return ctx.CloneWithoutTransform(s); |
| }, |
| [&](const ast::VariableDeclStatement* s) -> const ast::Statement* { |
| auto* var = s->variable; |
| if (!var->initializer || !sem.Get(var->initializer)->HasSideEffects()) { |
| return nullptr; |
| } |
| tint::utils::Vector<const ast::Statement*, 8> stmts; |
| ctx.Replace(var->initializer, Decompose(var->initializer, &stmts)); |
| InsertBefore(stmts, s); |
| return b.Decl(ctx.CloneWithoutTransform(var)); |
| }, |
| [](Default) -> const ast::Statement* { |
| // Other statement types don't have expressions |
| return nullptr; |
| }); |
| } |
| |
| public: |
| explicit DecomposeState(CloneContext& ctx_in, ToHoistSet to_hoist_in) |
| : StateBase(ctx_in), to_hoist(std::move(to_hoist_in)) {} |
| |
| void Run() { |
| // We replace all BlockStatements as this allows us to iterate over the |
| // block statements and ctx.InsertBefore hoisted declarations on them. |
| ctx.ReplaceAll([&](const ast::BlockStatement* block) -> const ast::Statement* { |
| for (auto* stmt : block->statements) { |
| if (auto* new_stmt = DecomposeStatement(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 = DecomposeStatement(fl->initializer)) { |
| ctx.Replace(fl->initializer, new_stmt); |
| } |
| if (auto* new_stmt = DecomposeStatement(fl->continuing)) { |
| ctx.Replace(fl->continuing, new_stmt); |
| } |
| } |
| } |
| return nullptr; |
| }); |
| } |
| }; |
| |
| Transform::ApplyResult DecomposeSideEffects::Apply(const Program* src, |
| const DataMap&, |
| DataMap&) const { |
| ProgramBuilder b; |
| CloneContext ctx{&b, src, /* auto_clone_symbols */ true}; |
| |
| // First collect side-effecting expressions to hoist |
| CollectHoistsState collect_hoists_state{ctx}; |
| auto to_hoist = collect_hoists_state.Run(); |
| |
| // Now decompose these expressions |
| DecomposeState decompose_state{ctx, std::move(to_hoist)}; |
| decompose_state.Run(); |
| |
| ctx.Clone(); |
| return Program(std::move(b)); |
| } |
| |
| } // namespace |
| |
| PromoteSideEffectsToDecl::PromoteSideEffectsToDecl() = default; |
| PromoteSideEffectsToDecl::~PromoteSideEffectsToDecl() = default; |
| |
| Transform::ApplyResult PromoteSideEffectsToDecl::Apply(const Program* src, |
| const DataMap& inputs, |
| DataMap& outputs) const { |
| transform::Manager manager; |
| manager.Add<SimplifySideEffectStatements>(); |
| manager.Add<DecomposeSideEffects>(); |
| return manager.Apply(src, inputs, outputs); |
| } |
| |
| } // namespace tint::transform |