Skip to content

Commit d91dc99

Browse files
authored
Merge pull request #10015 from ethereum/moveFunctionArgumentsToMemoryNew
Stack Limit Evader: Move function arguments and return values to memory (v2)
2 parents f5d659d + c341445 commit d91dc99

28 files changed

+1217
-93
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Language Features:
44

55

66
Compiler Features:
7+
* Yul EVM Code Transform: Do not reuse stack slots that immediately become unreachable.
8+
* Yul EVM Code Transform: Also pop unused argument slots for functions without return variables (under the same restrictions as for functions with return variables).
9+
* Yul Optimizer: Move function arguments and return variables to memory with the experimental Stack Limit Evader (which is not enabled by default).
710

811

912
Bugfixes:

libyul/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ add_library(yul
129129
optimiser/FullInliner.h
130130
optimiser/FunctionCallFinder.cpp
131131
optimiser/FunctionCallFinder.h
132+
optimiser/FunctionDefinitionCollector.cpp
133+
optimiser/FunctionDefinitionCollector.h
132134
optimiser/FunctionGrouper.cpp
133135
optimiser/FunctionGrouper.h
134136
optimiser/FunctionHoister.cpp

libyul/backends/evm/EVMCodeTransform.cpp

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,24 @@ void CodeTransform::operator()(VariableDeclaration const& _varDecl)
182182
else
183183
m_variablesScheduledForDeletion.insert(&var);
184184
}
185-
else if (m_unusedStackSlots.empty())
186-
atTopOfStack = false;
187185
else
188186
{
189-
auto slot = static_cast<size_t>(*m_unusedStackSlots.begin());
190-
m_unusedStackSlots.erase(m_unusedStackSlots.begin());
191-
m_context->variableStackHeights[&var] = slot;
192-
if (size_t heightDiff = variableHeightDiff(var, varName, true))
193-
m_assembly.appendInstruction(evmasm::swapInstruction(static_cast<unsigned>(heightDiff - 1)));
194-
m_assembly.appendInstruction(evmasm::Instruction::POP);
187+
bool foundUnusedSlot = false;
188+
for (auto it = m_unusedStackSlots.begin(); it != m_unusedStackSlots.end(); ++it)
189+
{
190+
if (m_assembly.stackHeight() - *it > 17)
191+
continue;
192+
foundUnusedSlot = true;
193+
auto slot = static_cast<size_t>(*it);
194+
m_unusedStackSlots.erase(it);
195+
m_context->variableStackHeights[&var] = slot;
196+
if (size_t heightDiff = variableHeightDiff(var, varName, true))
197+
m_assembly.appendInstruction(evmasm::swapInstruction(static_cast<unsigned>(heightDiff - 1)));
198+
m_assembly.appendInstruction(evmasm::Instruction::POP);
199+
break;
200+
}
201+
if (!foundUnusedSlot)
202+
atTopOfStack = false;
195203
}
196204
}
197205
}
@@ -404,7 +412,7 @@ void CodeTransform::operator()(FunctionDefinition const& _function)
404412
subTransform.deleteVariable(var);
405413
}
406414

407-
if (!m_allowStackOpt || _function.returnVariables.empty())
415+
if (!m_allowStackOpt)
408416
subTransform.setupReturnVariablesAndFunctionExit();
409417

410418
subTransform(_function.body);
@@ -594,6 +602,7 @@ void CodeTransform::visitExpression(Expression const& _expression)
594602

595603
void CodeTransform::setupReturnVariablesAndFunctionExit()
596604
{
605+
yulAssert(isInsideFunction(), "");
597606
yulAssert(!returnVariablesAndFunctionExitAreSetup(), "");
598607
yulAssert(m_scope, "");
599608

@@ -656,7 +665,8 @@ void CodeTransform::visitStatements(vector<Statement> const& _statements)
656665
{
657666
freeUnusedVariables();
658667
if (
659-
!m_delayedReturnVariables.empty() &&
668+
isInsideFunction() &&
669+
!returnVariablesAndFunctionExitAreSetup() &&
660670
statementNeedsReturnVariableSetup(statement, m_delayedReturnVariables)
661671
)
662672
setupReturnVariablesAndFunctionExit();

libyul/backends/evm/EVMCodeTransform.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ class CodeTransform
181181
{
182182
return m_functionExitStackHeight.has_value();
183183
}
184+
bool isInsideFunction() const
185+
{
186+
return m_functionExitLabel.has_value();
187+
}
184188

185189
AbstractAssembly& m_assembly;
186190
AsmAnalysisInfo& m_info;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
This file is part of solidity.
3+
4+
solidity is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
solidity is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with solidity. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include <libyul/optimiser/FunctionDefinitionCollector.h>
19+
#include <libyul/AST.h>
20+
21+
using namespace std;
22+
using namespace solidity;
23+
using namespace solidity::yul;
24+
25+
map<YulString, FunctionDefinition const*> FunctionDefinitionCollector::run(Block& _block)
26+
{
27+
FunctionDefinitionCollector functionDefinitionCollector;
28+
functionDefinitionCollector(_block);
29+
return functionDefinitionCollector.m_functionDefinitions;
30+
}
31+
32+
void FunctionDefinitionCollector::operator()(FunctionDefinition const& _functionDefinition)
33+
{
34+
m_functionDefinitions[_functionDefinition.name] = &_functionDefinition;
35+
ASTWalker::operator()(_functionDefinition);
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
This file is part of solidity.
3+
4+
solidity is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
solidity is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with solidity. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
/**
18+
* AST walker that finds all function definitions and stores them into a map indexed by the function names.
19+
*/
20+
#pragma once
21+
22+
#include <libyul/optimiser/ASTWalker.h>
23+
24+
#include <map>
25+
26+
namespace solidity::yul
27+
{
28+
29+
/**
30+
* AST walker that finds all function definitions and stores them into a map indexed by the function names.
31+
*
32+
* Prerequisite: Disambiguator
33+
*/
34+
class FunctionDefinitionCollector: ASTWalker
35+
{
36+
public:
37+
static std::map<YulString, FunctionDefinition const*> run(Block& _block);
38+
private:
39+
using ASTWalker::operator();
40+
void operator()(FunctionDefinition const& _functionDefinition) override;
41+
std::map<YulString, FunctionDefinition const*> m_functionDefinitions;
42+
};
43+
44+
}

libyul/optimiser/StackLimitEvader.cpp

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <libyul/optimiser/StackLimitEvader.h>
1919
#include <libyul/optimiser/CallGraphGenerator.h>
2020
#include <libyul/optimiser/FunctionCallFinder.h>
21+
#include <libyul/optimiser/FunctionDefinitionCollector.h>
2122
#include <libyul/optimiser/NameDispenser.h>
2223
#include <libyul/optimiser/StackToMemoryMover.h>
2324
#include <libyul/backends/evm/EVMDialect.h>
@@ -28,6 +29,9 @@
2829
#include <libsolutil/Algorithms.h>
2930
#include <libsolutil/CommonData.h>
3031

32+
#include <range/v3/view/concat.hpp>
33+
#include <range/v3/view/take.hpp>
34+
3135
using namespace std;
3236
using namespace solidity;
3337
using namespace solidity::yul;
@@ -62,25 +66,42 @@ struct MemoryOffsetAllocator
6266
for (YulString child: callGraph.at(_function))
6367
requiredSlots = std::max(run(child), requiredSlots);
6468

65-
if (unreachableVariables.count(_function))
69+
if (auto const* unreachables = util::valueOrNullptr(unreachableVariables, _function))
6670
{
67-
yulAssert(!slotAllocations.count(_function), "");
68-
for (YulString variable: unreachableVariables.at(_function))
69-
if (variable.empty())
70-
{
71-
// TODO: Too many function arguments or return parameters.
72-
}
73-
else
71+
if (FunctionDefinition const* functionDefinition = util::valueOrDefault(functionDefinitions, _function, nullptr, util::allow_copy))
72+
if (
73+
size_t totalArgCount = functionDefinition->returnVariables.size() + functionDefinition->parameters.size();
74+
totalArgCount > 16
75+
)
76+
for (TypedName const& var: ranges::concat_view(
77+
functionDefinition->parameters,
78+
functionDefinition->returnVariables
79+
) | ranges::views::take(totalArgCount - 16))
80+
slotAllocations[var.name] = requiredSlots++;
81+
82+
// Assign slots for all variables that become unreachable in the function body, if the above did not
83+
// assign a slot for them already.
84+
for (YulString variable: *unreachables)
85+
// The empty case is a function with too many arguments or return values,
86+
// which was already handled above.
87+
if (!variable.empty() && !slotAllocations.count(variable))
7488
slotAllocations[variable] = requiredSlots++;
7589
}
7690

7791
return slotsRequiredForFunction[_function] = requiredSlots;
7892
}
7993

94+
/// Maps function names to the set of unreachable variables in that function.
95+
/// An empty variable name means that the function has too many arguments or return variables.
8096
map<YulString, set<YulString>> const& unreachableVariables;
97+
/// The graph of immediate function calls of all functions.
8198
map<YulString, set<YulString>> const& callGraph;
99+
/// Maps the name of each user-defined function to its definition.
100+
map<YulString, FunctionDefinition const*> const& functionDefinitions;
82101

102+
/// Maps variable names to the memory slot the respective variable is assigned.
83103
map<YulString, uint64_t> slotAllocations{};
104+
/// Maps function names to the number of memory slots the respective function requires.
84105
map<YulString, uint64_t> slotsRequiredForFunction{};
85106
};
86107

@@ -116,6 +137,8 @@ void StackLimitEvader::run(
116137

117138
// Make sure all calls to ``memoryguard`` we found have the same value as argument (otherwise, abort).
118139
u256 reservedMemory = literalArgumentValue(*memoryGuardCalls.front());
140+
yulAssert(reservedMemory < u256(1) << 32 - 1, "");
141+
119142
for (FunctionCall const* memoryGuardCall: memoryGuardCalls)
120143
if (reservedMemory != literalArgumentValue(*memoryGuardCall))
121144
return;
@@ -127,12 +150,14 @@ void StackLimitEvader::run(
127150
if (_unreachableVariables.count(function))
128151
return;
129152

130-
MemoryOffsetAllocator memoryOffsetAllocator{_unreachableVariables, callGraph.functionCalls};
153+
map<YulString, FunctionDefinition const*> functionDefinitions = FunctionDefinitionCollector::run(*_object.code);
154+
155+
MemoryOffsetAllocator memoryOffsetAllocator{_unreachableVariables, callGraph.functionCalls, functionDefinitions};
131156
uint64_t requiredSlots = memoryOffsetAllocator.run();
157+
yulAssert(requiredSlots < (uint64_t(1) << 32) - 1, "");
132158

133159
StackToMemoryMover::run(_context, reservedMemory, memoryOffsetAllocator.slotAllocations, requiredSlots, *_object.code);
134160

135-
yulAssert(requiredSlots < std::numeric_limits<uint64_t>::max() / 32, "");
136161
reservedMemory += 32 * requiredSlots;
137162
for (FunctionCall* memoryGuardCall: FunctionCallFinder::run(*_object.code, "memoryguard"_yulstring))
138163
{

0 commit comments

Comments
 (0)