From 767dcc2e5d78ebbf55a22bd32f411a455ffd7744 Mon Sep 17 00:00:00 2001 From: Daniel Kirchner Date: Thu, 11 Mar 2021 16:10:13 +0100 Subject: [PATCH] Add ZeroByReturndatasizeReplacer. --- libevmasm/SemanticInformation.cpp | 19 ++ libevmasm/SemanticInformation.h | 1 + libsolidity/interface/OptimiserSettings.h | 2 +- libyul/CMakeLists.txt | 2 + libyul/SideEffects.h | 13 +- libyul/backends/evm/EVMDialect.cpp | 1 + libyul/optimiser/Suite.cpp | 5 +- .../ZeroByReturndatasizeReplacer.cpp | 166 ++++++++++++++++++ .../optimiser/ZeroByReturndatasizeReplacer.h | 76 ++++++++ .../indirect_call.yul | 20 +++ .../indirect_call2.yul | 20 +++ .../zeroByReturndatasizeReplacer/loop.yul | 22 +++ .../loop_call.yul | 39 ++++ .../multiple_calls.yul | 23 +++ .../recursive_call.yul | 21 +++ .../simple_call.yul | 13 ++ .../zeroByReturndatasizeReplacer/stub.yul | 11 ++ 17 files changed, 449 insertions(+), 5 deletions(-) create mode 100644 libyul/optimiser/ZeroByReturndatasizeReplacer.cpp create mode 100644 libyul/optimiser/ZeroByReturndatasizeReplacer.h create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call2.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop_call.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/multiple_calls.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/recursive_call.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/simple_call.yul create mode 100644 test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/stub.yul diff --git a/libevmasm/SemanticInformation.cpp b/libevmasm/SemanticInformation.cpp index a6f8d7a9f..db1859135 100644 --- a/libevmasm/SemanticInformation.cpp +++ b/libevmasm/SemanticInformation.cpp @@ -324,6 +324,25 @@ SemanticInformation::Effect SemanticInformation::storage(Instruction _instructio } } +SemanticInformation::Effect SemanticInformation::returndata(Instruction _instruction) +{ + switch (_instruction) + { + case Instruction::CALL: + case Instruction::CALLCODE: + case Instruction::DELEGATECALL: + case Instruction::CREATE: + case Instruction::CREATE2: + case Instruction::STATICCALL: + return SemanticInformation::Write; + case Instruction::RETURNDATACOPY: + case Instruction::RETURNDATASIZE: + return SemanticInformation::Read; + default: + return SemanticInformation::None; + } +} + SemanticInformation::Effect SemanticInformation::otherState(Instruction _instruction) { switch (_instruction) diff --git a/libevmasm/SemanticInformation.h b/libevmasm/SemanticInformation.h index 679766e66..2639d9f84 100644 --- a/libevmasm/SemanticInformation.h +++ b/libevmasm/SemanticInformation.h @@ -82,6 +82,7 @@ struct SemanticInformation static bool canBeRemovedIfNoMSize(Instruction _instruction); static Effect memory(Instruction _instruction); static Effect storage(Instruction _instruction); + static Effect returndata(Instruction _instruction); static Effect otherState(Instruction _instruction); static bool invalidInPureFunctions(Instruction _instruction); static bool invalidInViewFunctions(Instruction _instruction); diff --git a/libsolidity/interface/OptimiserSettings.h b/libsolidity/interface/OptimiserSettings.h index 3df99325e..2d46f00c4 100644 --- a/libsolidity/interface/OptimiserSettings.h +++ b/libsolidity/interface/OptimiserSettings.h @@ -47,7 +47,7 @@ struct OptimiserSettings "gvif" // Run full inliner "CTUcarrLsTFOtfDncarrIulc" // SSA plus simplify "]" - "jmuljuljul VcTOcul jmul"; // Make source short and pretty + "jmuljuljul VcTOcul jmulz"; // Make source short and pretty /// No optimisations at all - not recommended. static OptimiserSettings none() diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index f33067bbb..71df4ba8d 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -198,6 +198,8 @@ add_library(yul optimiser/VarDeclInitializer.h optimiser/VarNameCleaner.cpp optimiser/VarNameCleaner.h + optimiser/ZeroByReturndatasizeReplacer.cpp + optimiser/ZeroByReturndatasizeReplacer.h ) target_link_libraries(yul PUBLIC evmasm solutil langutil smtutil) diff --git a/libyul/SideEffects.h b/libyul/SideEffects.h index 7433e40d2..3d4118f05 100644 --- a/libyul/SideEffects.h +++ b/libyul/SideEffects.h @@ -77,10 +77,15 @@ struct SideEffects /// effect on `msize()`. Effect memory = None; + /// Can write, read or have no effect on returndata, when the value of `memory` is `Write`, `Read` + /// or `None` respectively. Note that, when the value is `Read`, the expression can have an + /// effect on `returndatasize()`. + Effect returndata = None; + /// @returns the worst-case side effects. static SideEffects worst() { - return SideEffects{false, false, false, false, false, Write, Write, Write}; + return SideEffects{false, false, false, false, false, Write, Write, Write, Write}; } /// @returns the combined side effects of two pieces of code. @@ -94,7 +99,8 @@ struct SideEffects cannotLoop && _other.cannotLoop, otherState + _other.otherState, storage + _other.storage, - memory + _other.memory + memory + _other.memory, + returndata + _other.returndata }; } @@ -115,7 +121,8 @@ struct SideEffects cannotLoop == _other.cannotLoop && otherState == _other.otherState && storage == _other.storage && - memory == _other.memory; + memory == _other.memory && + returndata == _other.returndata; } }; diff --git a/libyul/backends/evm/EVMDialect.cpp b/libyul/backends/evm/EVMDialect.cpp index eb21bd1b9..4ae81d968 100644 --- a/libyul/backends/evm/EVMDialect.cpp +++ b/libyul/backends/evm/EVMDialect.cpp @@ -339,6 +339,7 @@ SideEffects EVMDialect::sideEffectsOfInstruction(evmasm::Instruction _instructio translate(evmasm::SemanticInformation::otherState(_instruction)), translate(evmasm::SemanticInformation::storage(_instruction)), translate(evmasm::SemanticInformation::memory(_instruction)), + translate(evmasm::SemanticInformation::returndata(_instruction)) }; } diff --git a/libyul/optimiser/Suite.cpp b/libyul/optimiser/Suite.cpp index f11a02860..4a33bf12f 100644 --- a/libyul/optimiser/Suite.cpp +++ b/libyul/optimiser/Suite.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -206,7 +207,8 @@ map> const& OptimiserSuite::allSteps() StructuralSimplifier, UnusedFunctionParameterPruner, UnusedPruner, - VarDeclInitializer + VarDeclInitializer, + ZeroByReturndatasizeReplacer >(); // Does not include VarNameCleaner because it destroys the property of unique names. // Does not include NameSimplifier. @@ -247,6 +249,7 @@ map const& OptimiserSuite::stepNameToAbbreviationMap() {UnusedFunctionParameterPruner::name, 'p'}, {UnusedPruner::name, 'u'}, {VarDeclInitializer::name, 'd'}, + {ZeroByReturndatasizeReplacer::name, 'z'}, }; yulAssert(lookupTable.size() == allSteps().size(), ""); yulAssert(( diff --git a/libyul/optimiser/ZeroByReturndatasizeReplacer.cpp b/libyul/optimiser/ZeroByReturndatasizeReplacer.cpp new file mode 100644 index 000000000..3a91ffcb6 --- /dev/null +++ b/libyul/optimiser/ZeroByReturndatasizeReplacer.cpp @@ -0,0 +1,166 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Optimisation stage that replaces the literal 0 by returndatasize() in case there could not have been a call yet. + */ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +using namespace std; +using namespace solidity; +using namespace solidity::yul; + +namespace +{ +bool invalidatesReturndata(Dialect const& _dialect, map const& _functionSideEffects, FunctionCall const& _funCall) { + if (BuiltinFunction const* builtin = _dialect.builtin(_funCall.functionName.name)) + if (builtin->sideEffects.returndata == SideEffects::Write) + return true; + if (auto const* sideEffects = util::valueOrNullptr(_functionSideEffects, _funCall.functionName.name)) + if (sideEffects->returndata == SideEffects::Write) + return true; + return false; +} + +class CallTracer: public ASTModifier +{ +public: + CallTracer( + Dialect const& _dialect, + map const& _functionSideEffects, + map> const& _functionCalls + ): m_dialect(_dialect), m_functionSideEffects(move(_functionSideEffects)), m_functionCalls(move(_functionCalls)) + {} + + using ASTModifier::operator(); + void operator()(FunctionCall& _funCall) override + { + ASTModifier::operator()(_funCall); + if (!m_hasZeroReturndata) + util::BreadthFirstSearch{{_funCall.functionName.name}, m_badFunctions}.run([&](YulString _fn, auto&& _addChild) { + m_badFunctions.insert(_fn); + if (set const* callees = util::valueOrNullptr(m_functionCalls, _fn)) + for (YulString f: *callees) + _addChild(f); + }); + + if (invalidatesReturndata(m_dialect, m_functionSideEffects, _funCall)) + m_hasZeroReturndata = false; + } + void operator()(ForLoop& _loop) override + { + bool hadZeroReturndata = m_hasZeroReturndata; + ASTModifier::operator()(_loop); + if (hadZeroReturndata && !m_hasZeroReturndata) + { + m_badLoops.insert(&_loop); + ASTModifier::operator()(_loop); + } + } + void operator()(FunctionDefinition& _funDef) override + { + bool hadZeroReturndata = m_hasZeroReturndata; + m_hasZeroReturndata = true; + ASTModifier::operator()(_funDef); + m_hasZeroReturndata = hadZeroReturndata; + } + + set const& badFunctions() const { return m_badFunctions; } + set const& badLoops() const { return m_badLoops; } +private: + set m_badFunctions; + set m_badLoops; + Dialect const& m_dialect; + map const& m_functionSideEffects; + map> const& m_functionCalls; + bool m_hasZeroReturndata = true; +}; +} + +void ZeroByReturndatasizeReplacer::operator()(FunctionCall& _funCall) +{ + BuiltinFunction const* builtin = m_dialect.builtin(_funCall.functionName.name); + for (auto&& [index, arg]: ranges::views::enumerate(_funCall.arguments) | ranges::views::reverse) + if (!builtin || !builtin->literalArgument(index)) + visit(arg); + if (invalidatesReturndata(m_dialect, m_functionSideEffects, _funCall)) + m_hasZeroReturndata = false; +} + +void ZeroByReturndatasizeReplacer::operator()(FunctionDefinition& _funDef) +{ + if (!m_badFunctions.count(_funDef.name)) + { + bool hadZeroReturndata = m_hasZeroReturndata; + m_hasZeroReturndata = true; + ASTModifier::operator()(_funDef); + m_hasZeroReturndata = hadZeroReturndata; + } +} + +void ZeroByReturndatasizeReplacer::operator()(ForLoop& _loop) +{ + if (m_badLoops.count(&_loop)) + m_hasZeroReturndata = false; + else + ASTModifier::operator()(_loop); +} + + +void ZeroByReturndatasizeReplacer::visit(Expression& _e) +{ + if ( + Literal* literal = std::get_if(&_e); + m_hasZeroReturndata && literal && valueOfLiteral(*literal) == 0 + ) + _e = FunctionCall{ + literal->location, + Identifier{literal->location, "returndatasize"_yulstring}, + {} + }; + else + ASTModifier::visit(_e); +} + +void ZeroByReturndatasizeReplacer::run(OptimiserStepContext& _context, Block& _ast) +{ + if ( + EVMDialect const* dialect = dynamic_cast(&_context.dialect); + !dialect || !dialect->providesObjectAccess() || !dialect->evmVersion().supportsReturndata() + ) + return; + + CallGraph callGraph = CallGraphGenerator::callGraph(_ast); + auto sideEffects = SideEffectsPropagator::sideEffects(_context.dialect, callGraph); + CallTracer tracer{_context.dialect, sideEffects, callGraph.functionCalls}; + tracer(_ast); + ZeroByReturndatasizeReplacer{_context.dialect, sideEffects, tracer.badFunctions(), tracer.badLoops()}(_ast); +} diff --git a/libyul/optimiser/ZeroByReturndatasizeReplacer.h b/libyul/optimiser/ZeroByReturndatasizeReplacer.h new file mode 100644 index 000000000..4e50b55d4 --- /dev/null +++ b/libyul/optimiser/ZeroByReturndatasizeReplacer.h @@ -0,0 +1,76 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Optimisation stage that replaces the literal 0 by returndatasize() in case there could not have been a call yet. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace solidity::yul +{ + +/** + * Optimisation stage that replaces the literal 0 by returndatasize() in case there could not have been a call yet. + * + * Only works for an EVMDialect that supports returndata and only works for a dialect with object access, since + * the analysis requires the entire contract to be available as Yul AST. + * + * Prerequisite: Disambiguator + */ +class ZeroByReturndatasizeReplacer: public ASTModifier +{ +public: + static constexpr char const* name{"ZeroByReturndatasizeReplacer"}; + static void run(OptimiserStepContext& _context, Block& _ast); + +protected: + using ASTModifier::operator(); + using ASTModifier::visit; + + void operator()(FunctionCall& _funCall) override; + void operator()(FunctionDefinition& _funDef) override; + void operator()(ForLoop& _loop) override; + + void visit(Expression& _e) override; + +private: + explicit ZeroByReturndatasizeReplacer( + Dialect const& _dialect, + std::map const& _functionSideEffects, + std::set const& _badFunctions, + std::set const& _badLoops + ): + m_dialect(_dialect), + m_functionSideEffects(_functionSideEffects), + m_badFunctions(_badFunctions), + m_badLoops(_badLoops) + {} + bool m_hasZeroReturndata = true; + Dialect const& m_dialect; + std::map const& m_functionSideEffects; + std::set const& m_badFunctions; + std::set const& m_badLoops; +}; + +} diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call.yul new file mode 100644 index 000000000..858494453 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call.yul @@ -0,0 +1,20 @@ +{ + let x := 0 + function f() -> r { + r := call(0, 0, 0, 0, 0, 0, 0) + r := add(r, call(0, 0, 0, 0, 0, 0, 0)) + } + sstore(0, f()) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// function f() -> r +// { +// r := call(returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize()) +// r := add(r, call(0, 0, 0, 0, 0, 0, 0)) +// } +// sstore(0, f()) +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call2.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call2.yul new file mode 100644 index 000000000..58aeb0228 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/indirect_call2.yul @@ -0,0 +1,20 @@ +{ + let x := 0 + function f() -> r { + r := call(0, 0, 0, 0, 0, 0, 0) + r := add(r, call(0, 0, 0, 0, 0, 0, 0)) + } + sstore(f(), 0) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// function f() -> r +// { +// r := call(returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize()) +// r := add(r, call(0, 0, 0, 0, 0, 0, 0)) +// } +// sstore(f(), returndatasize()) +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop.yul new file mode 100644 index 000000000..422486850 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop.yul @@ -0,0 +1,22 @@ +{ + for { let i := 0 } lt(i, 4) { i := add(i, 1) } { + mstore(i, 0) + } + + for { let i := 0 } lt(i, 4) { i := add(i, 1) } { + sstore(i, 0) + sstore(0, call(0, 0, 0, 0, 0, 0, 0)) + } +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// for { let i := returndatasize() } lt(i, 4) { i := add(i, 1) } +// { mstore(i, returndatasize()) } +// for { let i_1 := 0 } lt(i_1, 4) { i_1 := add(i_1, 1) } +// { +// sstore(i_1, 0) +// sstore(0, call(0, 0, 0, 0, 0, 0, 0)) +// } +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop_call.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop_call.yul new file mode 100644 index 000000000..795cda700 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/loop_call.yul @@ -0,0 +1,39 @@ +{ + for { let i := 0 } lt(i, 4) { i := add(i, 1) } { + g() + mstore(i, 0) + } + + for { let i := 0 } lt(i, 4) { i := add(i, 1) } { + f() + sstore(0, call(0, 0, 0, 0, 0, 0, 0)) + } + + function f() { + sstore(0, 0) + } + function g() { + sstore(0, 0) + } +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// for { let i := returndatasize() } lt(i, 4) { i := add(i, 1) } +// { +// g() +// mstore(i, returndatasize()) +// } +// for { let i_1 := 0 } lt(i_1, 4) { i_1 := add(i_1, 1) } +// { +// f() +// sstore(0, call(0, 0, 0, 0, 0, 0, 0)) +// } +// function f() +// { sstore(0, 0) } +// function g() +// { +// sstore(returndatasize(), returndatasize()) +// } +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/multiple_calls.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/multiple_calls.yul new file mode 100644 index 000000000..1b7e37bf5 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/multiple_calls.yul @@ -0,0 +1,23 @@ +{ + let x := 0 + function f() -> r { + // cannot replace zeroes due to the second call to f below + r := call(0, 0, 0, 0, 0, 0, 0) + r := add(r, call(0, 0, 0, 0, 0, 0, 0)) + } + pop(f()) + pop(f()) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// function f() -> r +// { +// r := call(0, 0, 0, 0, 0, 0, 0) +// r := add(r, call(0, 0, 0, 0, 0, 0, 0)) +// } +// pop(f()) +// pop(f()) +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/recursive_call.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/recursive_call.yul new file mode 100644 index 000000000..efbf14b91 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/recursive_call.yul @@ -0,0 +1,21 @@ +{ + let x := 0 + function f() -> r { + // cannot replace zeroes due to the recursive call to f + r := call(0, 0, 0, 0, 0, 0, 0) + pop(f()) + } + pop(f()) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// function f() -> r +// { +// r := call(0, 0, 0, 0, 0, 0, 0) +// pop(f()) +// } +// pop(f()) +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/simple_call.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/simple_call.yul new file mode 100644 index 000000000..0b3d84245 --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/simple_call.yul @@ -0,0 +1,13 @@ +{ + let x := 0 + let y := call(0, 0, 0, 0, 0, 0, 0) + sstore(y, 0) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// let y := call(returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize(), returndatasize()) +// sstore(y, 0) +// } diff --git a/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/stub.yul b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/stub.yul new file mode 100644 index 000000000..97243f13c --- /dev/null +++ b/test/libyul/yulOptimizerTests/zeroByReturndatasizeReplacer/stub.yul @@ -0,0 +1,11 @@ +{ + let x := 0 + sstore(x,x) +} +// ---- +// step: zeroByReturndatasizeReplacer +// +// { +// let x := returndatasize() +// sstore(x, x) +// }