blob: 4e106a8b70ca0240f553d61e6c1d24aebc33d321 [file] [log] [blame]
/*
* Copyright 2021 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkSpan.h"
#include "include/core/SkTypes.h"
#include "include/private/base/SkTArray.h"
#include "src/sksl/SkSLCompiler.h"
#include "src/sksl/SkSLDefines.h"
#include "src/sksl/analysis/SkSLProgramUsage.h"
#include "src/sksl/ir/SkSLFunctionDefinition.h"
#include "src/sksl/ir/SkSLIRNode.h"
#include "src/sksl/ir/SkSLIfStatement.h"
#include "src/sksl/ir/SkSLNop.h"
#include "src/sksl/ir/SkSLProgram.h"
#include "src/sksl/ir/SkSLProgramElement.h"
#include "src/sksl/ir/SkSLStatement.h"
#include "src/sksl/ir/SkSLSwitchCase.h"
#include "src/sksl/ir/SkSLSwitchStatement.h"
#include "src/sksl/transform/SkSLProgramWriter.h"
#include "src/sksl/transform/SkSLTransform.h"
#include <memory>
#include <vector>
using namespace skia_private;
namespace SkSL {
class Expression;
static void eliminate_unreachable_code(SkSpan<std::unique_ptr<ProgramElement>> elements,
ProgramUsage* usage) {
class UnreachableCodeEliminator : public ProgramWriter {
public:
UnreachableCodeEliminator(ProgramUsage* usage) : fUsage(usage) {
fFoundFunctionExit.push_back(false);
fFoundBlockExit.push_back(false);
}
bool visitExpressionPtr(std::unique_ptr<Expression>& expr) override {
// We don't need to look inside expressions at all.
return false;
}
bool visitStatementPtr(std::unique_ptr<Statement>& stmt) override {
if (fFoundFunctionExit.back() || fFoundBlockExit.back()) {
// If we already found an exit in this section, anything beyond it is dead code.
if (!stmt->is<Nop>()) {
// Eliminate the dead statement by substituting a Nop.
fUsage->remove(stmt.get());
stmt = Nop::Make();
}
return false;
}
switch (stmt->kind()) {
case Statement::Kind::kReturn:
case Statement::Kind::kDiscard:
// We found a function exit on this path.
fFoundFunctionExit.back() = true;
break;
case Statement::Kind::kBreak:
// A `break` statement can either be breaking out of a loop or terminating an
// individual switch case. We treat both cases the same way: they only apply
// to the statements associated with the parent statement (i.e. enclosing loop
// block / preceding case label).
case Statement::Kind::kContinue:
fFoundBlockExit.back() = true;
break;
case Statement::Kind::kExpression:
case Statement::Kind::kNop:
case Statement::Kind::kVarDeclaration:
// These statements don't affect control flow.
break;
case Statement::Kind::kBlock:
// Blocks are on the straight-line path and don't affect control flow.
return INHERITED::visitStatementPtr(stmt);
case Statement::Kind::kDo: {
// Function-exits are allowed to propagate outside of a do-loop, because it
// always executes its body at least once.
fFoundBlockExit.push_back(false);
bool result = INHERITED::visitStatementPtr(stmt);
fFoundBlockExit.pop_back();
return result;
}
case Statement::Kind::kFor: {
// Function-exits are not allowed to propagate out, because a for-loop or while-
// loop could potentially run zero times.
fFoundFunctionExit.push_back(false);
fFoundBlockExit.push_back(false);
bool result = INHERITED::visitStatementPtr(stmt);
fFoundBlockExit.pop_back();
fFoundFunctionExit.pop_back();
return result;
}
case Statement::Kind::kIf: {
// This statement is conditional and encloses two inner sections of code.
// If both sides contain a function-exit or loop-exit, that exit is allowed to
// propagate out.
IfStatement& ifStmt = stmt->as<IfStatement>();
fFoundFunctionExit.push_back(false);
fFoundBlockExit.push_back(false);
bool result = (ifStmt.ifTrue() && this->visitStatementPtr(ifStmt.ifTrue()));
bool foundFunctionExitOnTrue = fFoundFunctionExit.back();
bool foundLoopExitOnTrue = fFoundBlockExit.back();
fFoundFunctionExit.pop_back();
fFoundBlockExit.pop_back();
fFoundFunctionExit.push_back(false);
fFoundBlockExit.push_back(false);
result |= (ifStmt.ifFalse() && this->visitStatementPtr(ifStmt.ifFalse()));
bool foundFunctionExitOnFalse = fFoundFunctionExit.back();
bool foundLoopExitOnFalse = fFoundBlockExit.back();
fFoundFunctionExit.pop_back();
fFoundBlockExit.pop_back();
fFoundFunctionExit.back() |= foundFunctionExitOnTrue &&
foundFunctionExitOnFalse;
fFoundBlockExit.back() |= foundLoopExitOnTrue &&
foundLoopExitOnFalse;
return result;
}
case Statement::Kind::kSwitch: {
// In switch statements we consider unreachable code on a per-case basis.
SwitchStatement& sw = stmt->as<SwitchStatement>();
bool result = false;
// Tracks whether we found at least one case that doesn't lead to a return
// statement (potentially via fallthrough).
bool foundCaseWithoutReturn = false;
bool hasDefault = false;
for (std::unique_ptr<Statement>& c : sw.cases()) {
// We eliminate unreachable code within the statements of the individual
// case. Breaks are not allowed to propagate outside the case statement
// itself. Function returns are allowed to propagate out only if all cases
// have a return AND one of the cases is default (so that we know at least
// one of the branches will be taken). This is similar to how we handle if
// statements above.
fFoundFunctionExit.push_back(false);
fFoundBlockExit.push_back(false);
SwitchCase& sc = c->as<SwitchCase>();
result |= this->visitStatementPtr(sc.statement());
// When considering whether a case has a return we can propagate, we
// assume the following:
// 1. The default case is always placed last in a switch statement and
// it is the last possible label reachable via fallthrough. Thus if
// it does not contain a return statement, then we don't propagate a
// function return.
// 2. In all other cases we prevent the return from propagating only if
// we encounter a break statement. If no return or break is found,
// we defer the decision to the fallthrough case. We won't propagate
// a return unless we eventually encounter a default label.
//
// See resources/sksl/shared/SwitchWithEarlyReturn.sksl for test cases that
// exercise this.
if (sc.isDefault()) {
foundCaseWithoutReturn |= !fFoundFunctionExit.back();
hasDefault = true;
} else {
// We can only be sure that a case does not lead to a return if it
// doesn't fallthrough.
foundCaseWithoutReturn |=
(!fFoundFunctionExit.back() && fFoundBlockExit.back());
}
fFoundFunctionExit.pop_back();
fFoundBlockExit.pop_back();
}
fFoundFunctionExit.back() |= !foundCaseWithoutReturn && hasDefault;
return result;
}
case Statement::Kind::kSwitchCase:
// We should never hit this case as switch cases are handled in the previous
// case.
SkUNREACHABLE;
}
return false;
}
ProgramUsage* fUsage;
STArray<32, bool> fFoundFunctionExit;
STArray<32, bool> fFoundBlockExit;
using INHERITED = ProgramWriter;
};
for (std::unique_ptr<ProgramElement>& pe : elements) {
if (pe->is<FunctionDefinition>()) {
UnreachableCodeEliminator visitor{usage};
visitor.visitStatementPtr(pe->as<FunctionDefinition>().body());
}
}
}
void Transform::EliminateUnreachableCode(Module& module, ProgramUsage* usage) {
return eliminate_unreachable_code(SkSpan(module.fElements), usage);
}
void Transform::EliminateUnreachableCode(Program& program) {
return eliminate_unreachable_code(SkSpan(program.fOwnedElements), program.fUsage.get());
}
} // namespace SkSL