Merge pull request #12091 from ethereum/applyControlFlowSideEffectsUserDefined

Use side effects of user-defined functions in other optimizer steps.
This commit is contained in:
chriseth 2021-11-02 15:50:37 +01:00 committed by GitHub
commit a7b137829f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 45 deletions

View File

@ -12,6 +12,7 @@ Compiler Features:
* SMTChecker: Report contract invariants and reentrancy properties. This can be enabled via the CLI option ``--model-checker-invariants`` or the Standard JSON option ``settings.modelChecker.invariants``. * SMTChecker: Report contract invariants and reentrancy properties. This can be enabled via the CLI option ``--model-checker-invariants`` or the Standard JSON option ``settings.modelChecker.invariants``.
* Standard JSON: Accept nested brackets in step sequences passed to ``settings.optimizer.details.yulDetails.optimizerSteps``. * Standard JSON: Accept nested brackets in step sequences passed to ``settings.optimizer.details.yulDetails.optimizerSteps``.
* Standard JSON: Add ``settings.debug.debugInfo`` option for selecting how much extra debug information should be included in the produced EVM assembly and Yul code. * Standard JSON: Add ``settings.debug.debugInfo`` option for selecting how much extra debug information should be included in the produced EVM assembly and Yul code.
* Yul Optimizer: Take control-flow side-effects of user-defined functions into account in various optimizer steps.
Bugfixes: Bugfixes:

View File

@ -19,6 +19,7 @@
#include <libyul/optimiser/Semantics.h> #include <libyul/optimiser/Semantics.h>
#include <libyul/AST.h> #include <libyul/AST.h>
#include <libyul/optimiser/NameCollector.h> #include <libyul/optimiser/NameCollector.h>
#include <libyul/ControlFlowSideEffectsCollector.h>
#include <libsolutil/CommonData.h> #include <libsolutil/CommonData.h>
using namespace std; using namespace std;
@ -26,6 +27,12 @@ using namespace solidity;
using namespace solidity::yul; using namespace solidity::yul;
using namespace solidity::util; using namespace solidity::util;
void ConditionalSimplifier::run(OptimiserStepContext& _context, Block& _ast)
{
ControlFlowSideEffectsCollector sideEffects(_context.dialect, _ast);
ConditionalSimplifier{_context.dialect, sideEffects.functionSideEffects()}(_ast);
}
void ConditionalSimplifier::operator()(Switch& _switch) void ConditionalSimplifier::operator()(Switch& _switch)
{ {
visit(*_switch.expression); visit(*_switch.expression);
@ -65,7 +72,7 @@ void ConditionalSimplifier::operator()(Block& _block)
if ( if (
holds_alternative<Identifier>(*_if.condition) && holds_alternative<Identifier>(*_if.condition) &&
!_if.body.statements.empty() && !_if.body.statements.empty() &&
TerminationFinder(m_dialect).controlFlowKind(_if.body.statements.back()) != TerminationFinder(m_dialect, &m_functionSideEffects).controlFlowKind(_if.body.statements.back()) !=
TerminationFinder::ControlFlow::FlowOut TerminationFinder::ControlFlow::FlowOut
) )
{ {

View File

@ -44,7 +44,6 @@ namespace solidity::yul
* *
* Future features: * Future features:
* - allow replacements by "1" * - allow replacements by "1"
* - take termination of user-defined functions into account
* *
* Works best with SSA form and if dead code removal has run before. * Works best with SSA form and if dead code removal has run before.
* *
@ -54,20 +53,21 @@ class ConditionalSimplifier: public ASTModifier
{ {
public: public:
static constexpr char const* name{"ConditionalSimplifier"}; static constexpr char const* name{"ConditionalSimplifier"};
static void run(OptimiserStepContext& _context, Block& _ast) static void run(OptimiserStepContext& _context, Block& _ast);
{
ConditionalSimplifier{_context.dialect}(_ast);
}
using ASTModifier::operator(); using ASTModifier::operator();
void operator()(Switch& _switch) override; void operator()(Switch& _switch) override;
void operator()(Block& _block) override; void operator()(Block& _block) override;
private: private:
explicit ConditionalSimplifier(Dialect const& _dialect): explicit ConditionalSimplifier(
m_dialect(_dialect) Dialect const& _dialect,
std::map<YulString, ControlFlowSideEffects> const& _sideEffects
):
m_dialect(_dialect), m_functionSideEffects(_sideEffects)
{} {}
Dialect const& m_dialect; Dialect const& m_dialect;
std::map<YulString, ControlFlowSideEffects> const& m_functionSideEffects;
}; };
} }

View File

@ -20,6 +20,7 @@
#include <libyul/AST.h> #include <libyul/AST.h>
#include <libyul/Utilities.h> #include <libyul/Utilities.h>
#include <libyul/optimiser/NameCollector.h> #include <libyul/optimiser/NameCollector.h>
#include <libyul/ControlFlowSideEffectsCollector.h>
#include <libsolutil/CommonData.h> #include <libsolutil/CommonData.h>
using namespace std; using namespace std;
@ -27,6 +28,12 @@ using namespace solidity;
using namespace solidity::yul; using namespace solidity::yul;
using namespace solidity::util; using namespace solidity::util;
void ConditionalUnsimplifier::run(OptimiserStepContext& _context, Block& _ast)
{
ControlFlowSideEffectsCollector sideEffects(_context.dialect, _ast);
ConditionalUnsimplifier{_context.dialect, sideEffects.functionSideEffects()}(_ast);
}
void ConditionalUnsimplifier::operator()(Switch& _switch) void ConditionalUnsimplifier::operator()(Switch& _switch)
{ {
visit(*_switch.expression); visit(*_switch.expression);
@ -78,7 +85,7 @@ void ConditionalUnsimplifier::operator()(Block& _block)
YulString condition = std::get<Identifier>(*_if.condition).name; YulString condition = std::get<Identifier>(*_if.condition).name;
if ( if (
holds_alternative<Assignment>(_stmt2) && holds_alternative<Assignment>(_stmt2) &&
TerminationFinder(m_dialect).controlFlowKind(_if.body.statements.back()) != TerminationFinder(m_dialect, &m_functionSideEffects).controlFlowKind(_if.body.statements.back()) !=
TerminationFinder::ControlFlow::FlowOut TerminationFinder::ControlFlow::FlowOut
) )
{ {

View File

@ -33,20 +33,21 @@ class ConditionalUnsimplifier: public ASTModifier
{ {
public: public:
static constexpr char const* name{"ConditionalUnsimplifier"}; static constexpr char const* name{"ConditionalUnsimplifier"};
static void run(OptimiserStepContext& _context, Block& _ast) static void run(OptimiserStepContext& _context, Block& _ast);
{
ConditionalUnsimplifier{_context.dialect}(_ast);
}
using ASTModifier::operator(); using ASTModifier::operator();
void operator()(Switch& _switch) override; void operator()(Switch& _switch) override;
void operator()(Block& _block) override; void operator()(Block& _block) override;
private: private:
explicit ConditionalUnsimplifier(Dialect const& _dialect): explicit ConditionalUnsimplifier(
m_dialect(_dialect) Dialect const& _dialect,
std::map<YulString, ControlFlowSideEffects> const& _sideEffects
):
m_dialect(_dialect), m_functionSideEffects(_sideEffects)
{} {}
Dialect const& m_dialect; Dialect const& m_dialect;
std::map<YulString, ControlFlowSideEffects> const& m_functionSideEffects;
}; };
} }

View File

@ -22,6 +22,7 @@
#include <libyul/optimiser/DeadCodeEliminator.h> #include <libyul/optimiser/DeadCodeEliminator.h>
#include <libyul/optimiser/Semantics.h> #include <libyul/optimiser/Semantics.h>
#include <libyul/optimiser/OptimiserStep.h> #include <libyul/optimiser/OptimiserStep.h>
#include <libyul/ControlFlowSideEffectsCollector.h>
#include <libyul/AST.h> #include <libyul/AST.h>
#include <libevmasm/SemanticInformation.h> #include <libevmasm/SemanticInformation.h>
@ -36,7 +37,11 @@ using namespace solidity::yul;
void DeadCodeEliminator::run(OptimiserStepContext& _context, Block& _ast) void DeadCodeEliminator::run(OptimiserStepContext& _context, Block& _ast)
{ {
DeadCodeEliminator{_context.dialect}(_ast); ControlFlowSideEffectsCollector sideEffects(_context.dialect, _ast);
DeadCodeEliminator{
_context.dialect,
sideEffects.functionSideEffects()
}(_ast);
} }
void DeadCodeEliminator::operator()(ForLoop& _for) void DeadCodeEliminator::operator()(ForLoop& _for)
@ -49,7 +54,7 @@ void DeadCodeEliminator::operator()(Block& _block)
{ {
TerminationFinder::ControlFlow controlFlowChange; TerminationFinder::ControlFlow controlFlowChange;
size_t index; size_t index;
tie(controlFlowChange, index) = TerminationFinder{m_dialect}.firstUnconditionalControlFlowChange(_block.statements); tie(controlFlowChange, index) = TerminationFinder{m_dialect, &m_functionSideEffects}.firstUnconditionalControlFlowChange(_block.statements);
// Erase everything after the terminating statement that is not a function definition. // Erase everything after the terminating statement that is not a function definition.
if (controlFlowChange != TerminationFinder::ControlFlow::FlowOut && index != std::numeric_limits<size_t>::max()) if (controlFlowChange != TerminationFinder::ControlFlow::FlowOut && index != std::numeric_limits<size_t>::max())

View File

@ -31,12 +31,15 @@ namespace solidity::yul
{ {
struct Dialect; struct Dialect;
struct OptimiserStepContext; struct OptimiserStepContext;
struct ControlFlowSideEffects;
/** /**
* Optimisation stage that removes unreachable code * Optimisation stage that removes unreachable code
* *
* Unreachable code is any code within a block which is preceded by a * Unreachable code is any code within a block which is preceded by a
* leave, return, invalid, break, continue, selfdestruct or revert. * leave, return, invalid, break, continue, selfdestruct or revert or
* a call to a user-defined function that never returns (either due to
* recursion or a call to return / revert / stop).
* *
* Function definitions are retained as they might be called by earlier * Function definitions are retained as they might be called by earlier
* code and thus are considered reachable. * code and thus are considered reachable.
@ -57,9 +60,13 @@ public:
void operator()(Block& _block) override; void operator()(Block& _block) override;
private: private:
DeadCodeEliminator(Dialect const& _dialect): m_dialect(_dialect) {} DeadCodeEliminator(
Dialect const& _dialect,
std::map<YulString, ControlFlowSideEffects> const& _sideEffects
): m_dialect(_dialect), m_functionSideEffects(_sideEffects) {}
Dialect const& m_dialect; Dialect const& m_dialect;
std::map<YulString, ControlFlowSideEffects> const& m_functionSideEffects;
}; };
} }

View File

@ -182,8 +182,19 @@ pair<TerminationFinder::ControlFlow, size_t> TerminationFinder::firstUncondition
TerminationFinder::ControlFlow TerminationFinder::controlFlowKind(Statement const& _statement) TerminationFinder::ControlFlow TerminationFinder::controlFlowKind(Statement const& _statement)
{ {
if ( if (
holds_alternative<VariableDeclaration>(_statement) &&
std::get<VariableDeclaration>(_statement).value &&
containsNonContinuingFunctionCall(*std::get<VariableDeclaration>(_statement).value)
)
return ControlFlow::Terminate;
else if (
holds_alternative<Assignment>(_statement) &&
containsNonContinuingFunctionCall(*std::get<Assignment>(_statement).value)
)
return ControlFlow::Terminate;
else if (
holds_alternative<ExpressionStatement>(_statement) && holds_alternative<ExpressionStatement>(_statement) &&
isTerminatingBuiltin(std::get<ExpressionStatement>(_statement)) containsNonContinuingFunctionCall(std::get<ExpressionStatement>(_statement).expression)
) )
return ControlFlow::Terminate; return ControlFlow::Terminate;
else if (holds_alternative<Break>(_statement)) else if (holds_alternative<Break>(_statement))
@ -196,10 +207,18 @@ TerminationFinder::ControlFlow TerminationFinder::controlFlowKind(Statement cons
return ControlFlow::FlowOut; return ControlFlow::FlowOut;
} }
bool TerminationFinder::isTerminatingBuiltin(ExpressionStatement const& _exprStmnt) bool TerminationFinder::containsNonContinuingFunctionCall(Expression const& _expr)
{ {
if (holds_alternative<FunctionCall>(_exprStmnt.expression)) if (auto functionCall = std::get_if<FunctionCall>(&_expr))
if (auto instruction = toEVMInstruction(m_dialect, std::get<FunctionCall>(_exprStmnt.expression).functionName.name)) {
return evmasm::SemanticInformation::terminatesControlFlow(*instruction); for (auto const& arg: functionCall->arguments)
if (containsNonContinuingFunctionCall(arg))
return true;
if (auto builtin = m_dialect.builtin(functionCall->functionName.name))
return !builtin->controlFlowSideEffects.canContinue;
else if (m_functionSideEffects && m_functionSideEffects->count(functionCall->functionName.name))
return !m_functionSideEffects->at(functionCall->functionName.name).canContinue;
}
return false; return false;
} }

View File

@ -205,22 +205,31 @@ private:
std::set<YulString> m_variableReferences; std::set<YulString> m_variableReferences;
}; };
struct ControlFlowSideEffects;
/** /**
* Helper class to find "irregular" control flow. * Helper class to find "irregular" control flow.
* This includes termination, break and continue. * This includes termination, break, continue and leave.
* In general, it is applied only to "simple" statements. The control-flow
* of loops, switches and if statements is always "FlowOut" with the assumption
* that the caller will descend into them.
*/ */
class TerminationFinder class TerminationFinder
{ {
public: public:
// TODO check all uses of TerminationFinder! /// "Terminate" here means that there is no continuing control-flow.
/// If this is applied to a function that can revert or stop, but can also
/// exit regularly, the property is set to "FlowOut".
enum class ControlFlow { FlowOut, Break, Continue, Terminate, Leave }; enum class ControlFlow { FlowOut, Break, Continue, Terminate, Leave };
TerminationFinder(Dialect const& _dialect): m_dialect(_dialect) {} TerminationFinder(
Dialect const& _dialect,
std::map<YulString, ControlFlowSideEffects> const* _functionSideEffects = nullptr
): m_dialect(_dialect), m_functionSideEffects(_functionSideEffects) {}
/// @returns the index of the first statement in the provided sequence /// @returns the index of the first statement in the provided sequence
/// that is an unconditional ``break``, ``continue``, ``leave`` or a /// that is an unconditional ``break``, ``continue``, ``leave`` or a
/// call to a terminating builtin function. /// call to a terminating function.
/// If control flow can continue at the end of the list, /// If control flow can continue at the end of the list,
/// returns `FlowOut` and ``size_t(-1)``. /// returns `FlowOut` and ``size_t(-1)``.
/// The function might return ``FlowOut`` even though control /// The function might return ``FlowOut`` even though control
@ -233,13 +242,14 @@ public:
/// This function could return FlowOut even if control flow never continues. /// This function could return FlowOut even if control flow never continues.
ControlFlow controlFlowKind(Statement const& _statement); ControlFlow controlFlowKind(Statement const& _statement);
/// @returns true if the expression statement is a direct /// @returns true if the expression contains a
/// call to a builtin terminating function like /// call to a terminating function, i.e. a function that does not have
/// ``stop``, ``revert`` or ``return``. /// a regular "flow out" control-flow (it might also be recursive).
bool isTerminatingBuiltin(ExpressionStatement const& _exprStmnt); bool containsNonContinuingFunctionCall(Expression const& _expr);
private: private:
Dialect const& m_dialect; Dialect const& m_dialect;
std::map<YulString, ControlFlowSideEffects> const* m_functionSideEffects;
}; };
} }

View File

@ -0,0 +1,38 @@
{
function recursive() { recursive() }
function terminating() { stop() }
function maybeReverting() { if calldataload(0) { revert(0, 0) } }
let a := calldataload(7)
if a { recursive() }
a := calldataload(a)
if a { maybeReverting() }
a := calldataload(a)
if a { terminating() }
sstore(0, a)
}
// ----
// step: conditionalSimplifier
//
// {
// function recursive()
// { recursive() }
// function terminating()
// { stop() }
// function maybeReverting()
// {
// if calldataload(0) { revert(0, 0) }
// }
// let a := calldataload(7)
// if a { recursive() }
// a := 0
// a := calldataload(a)
// if a { maybeReverting() }
// a := calldataload(a)
// if a { terminating() }
// a := 0
// sstore(0, a)
// }

View File

@ -0,0 +1,38 @@
{
function recursive() { recursive() }
function terminating() { stop() }
function maybeReverting() { if calldataload(0) { revert(0, 0) } }
let a := calldataload(7)
if a { recursive() }
a := 0
a := calldataload(a)
if a { maybeReverting() }
a := calldataload(a)
if a { terminating() }
a := 0
sstore(0, a)
}
// ----
// step: conditionalUnsimplifier
//
// {
// function recursive()
// { recursive() }
// function terminating()
// { stop() }
// function maybeReverting()
// {
// if calldataload(0) { revert(0, 0) }
// }
// let a := calldataload(7)
// if a { recursive() }
// a := calldataload(a)
// if a { maybeReverting() }
// a := calldataload(a)
// if a { terminating() }
// sstore(0, a)
// }

View File

@ -0,0 +1,44 @@
{
switch calldataload(0)
case 0 {
recursive()
sstore(0, 1)
}
case 1 {
terminating()
sstore(0, 7)
}
case 2 {
reverting()
sstore(0, 7)
}
function recursive()
{
recursive()
}
function terminating()
{
return(0, 0)
}
function reverting()
{
revert(0, 0)
}
}
// ----
// step: deadCodeEliminator
//
// {
// switch calldataload(0)
// case 0 { recursive() }
// case 1 { terminating() }
// case 2 { reverting() }
// function recursive()
// { recursive() }
// function terminating()
// { return(0, 0) }
// function reverting()
// { revert(0, 0) }
// }

View File

@ -5,9 +5,7 @@
function fun() function fun()
{ {
return(1, 1) sstore(0, 1)
pop(sub(10, 5))
} }
pop(add(1, 1)) pop(add(1, 1))
@ -19,5 +17,5 @@
// fun() // fun()
// revert(0, 0) // revert(0, 0)
// function fun() // function fun()
// { return(1, 1) } // { sstore(0, 1) }
// } // }

View File

@ -1,11 +1,11 @@
{ {
// This function name can be shortened, the other cannot. // This function name can be shortened, the other cannot.
function nonmstore_(x) { function nonmstore_(x) {
nonmstore_(x) if calldataload(0) { nonmstore_(x) }
sstore(10, calldataload(2)) sstore(10, calldataload(2))
} }
function mstore_(x) -> y { function mstore_(x) -> y {
let t3_3_ := mstore_(x) if calldataload(0) { let t3_3_ := mstore_(x) }
y := 8 y := 8
sstore(y, calldataload(y)) sstore(y, calldataload(y))
} }
@ -22,12 +22,12 @@
// } // }
// function nonmstore(x) // function nonmstore(x)
// { // {
// nonmstore(x) // if calldataload(0) { nonmstore(x) }
// sstore(10, calldataload(2)) // sstore(10, calldataload(2))
// } // }
// function mstore_(x) -> y // function mstore_(x) -> y
// { // {
// pop(mstore_(x)) // if calldataload(0) { pop(mstore_(x)) }
// y := 8 // y := 8
// sstore(y, calldataload(y)) // sstore(y, calldataload(y))
// } // }

View File

@ -5,7 +5,9 @@
sstore(1, l) sstore(1, l)
function f(a, b, c) -> x, y, z function f(a, b, c) -> x, y, z
{ {
x, y, z := f(1, 2, 3) if calldataload(0) {
x, y, z := f(1, 2, 3)
}
x := add(x, 1) x := add(x, 1)
} }
} }
@ -21,9 +23,13 @@
// } // }
// function f() -> x, y, z // function f() -> x, y, z
// { // {
// let x_1, y_1, z_1 := f() // if calldataload(0)
// y := y_1 // {
// z := z_1 // let x_1, y_1, z_1 := f()
// x := add(x_1, 1) // x := x_1
// y := y_1
// z := z_1
// }
// x := add(x, 1)
// } // }
// } // }