Skip to content

Commit 2ca6163

Browse files
authored
Merge pull request #16750 from argotorg/ssa-cfg-slg-populates-spill-set
SSA CFG: Stack Layout generator populates spill set
2 parents 7af01a4 + 86e929f commit 2ca6163

20 files changed

Lines changed: 543 additions & 54 deletions

libyul/backends/evm/ssa/CodeTransform.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include <libyul/backends/evm/ssa/CodeTransform.h>
2020

21+
#include <libyul/backends/evm/ssa/CallGraph.h>
2122
#include <libyul/backends/evm/ssa/StackLayoutGenerator.h>
2223
#include <libyul/backends/evm/ssa/StackShuffler.h>
2324
#include <libyul/backends/evm/ssa/StackUtils.h>
@@ -52,6 +53,7 @@ void CodeTransform::run
5253
ControlFlowGraphs const& controlFlowGraphs = _controlFlowLiveness.controlFlowGraphs.get();
5354
yulAssert(controlFlowGraphs.functionGraphs.size() == _controlFlowLiveness.cfgLiveness.size());
5455
FunctionLabels const functionLabels = registerFunctionLabels(_assembly, controlFlowGraphs);
56+
CallGraph const callGraph(controlFlowGraphs);
5557

5658
for (std::size_t functionIndex = 0; functionIndex < controlFlowGraphs.functionGraphs.size(); ++functionIndex)
5759
{
@@ -62,15 +64,16 @@ void CodeTransform::run
6264
auto const& liveness = _controlFlowLiveness.cfgLiveness[functionIndex];
6365
yulAssert(liveness);
6466
auto const graphID = static_cast<ControlFlowGraphs::FunctionGraphID>(functionIndex);
65-
auto const& stackLayout = StackLayoutGenerator::generate(*liveness, callSites, graphID);
67+
bool const spillingAllowed = !callGraph.isRecursive(graphID);
68+
auto const stackLayoutGeneratorResult = StackLayoutGenerator::generate(*liveness, callSites, graphID, spillingAllowed);
6669
CodeTransform transform(
6770
_assembly,
6871
_builtinContext,
6972
controlFlowGraphs,
7073
functionLabels,
7174
callSites,
7275
cfg,
73-
stackLayout,
76+
stackLayoutGeneratorResult.layout,
7477
graphID
7578
);
7679
transform(cfg.entry);

libyul/backends/evm/ssa/StackLayoutGenerator.cpp

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,29 @@ void declareJunk(StackType& _stack, LivenessAnalysis::LivenessData const& _live)
8282

8383
}
8484

85-
SSACFGStackLayout StackLayoutGenerator::generate(
85+
StackLayoutGenerator::Result StackLayoutGenerator::generate(
8686
LivenessAnalysis const& _liveness,
8787
CallSites const& _callSites,
88-
ControlFlowGraphs::FunctionGraphID const _graphID
88+
ControlFlowGraphs::FunctionGraphID const _graphID,
89+
bool const _spillingAllowed
8990
)
9091
{
91-
return StackLayoutGenerator(_liveness, _callSites, _graphID).m_resultLayout;
92+
StackLayoutGenerator generator(_liveness, _callSites, _graphID, _spillingAllowed);
93+
return Result{std::move(generator.m_resultLayout), std::move(generator.m_spillSet)};
9294
}
9395

9496
StackLayoutGenerator::StackLayoutGenerator(
9597
LivenessAnalysis const& _liveness,
9698
CallSites const& _callSites,
97-
ControlFlowGraphs::FunctionGraphID const _graphID
99+
ControlFlowGraphs::FunctionGraphID const _graphID,
100+
bool const _spillingAllowed
98101
):
99102
m_cfg(_liveness.cfg()),
100103
m_liveness(_liveness),
101104
m_callSites(_callSites),
102105
m_graphID(_graphID),
103106
m_hasFunctionReturnLabel(!_liveness.cfg().isMainGraph() && _liveness.cfg().canContinue),
107+
m_spillingAllowed(_spillingAllowed),
104108
m_junkAdmittingBlocksFinder(std::make_unique<JunkAdmittingBlocksFinder>(_liveness.cfg(), _liveness.topologicalSort())),
105109
m_inputStackProposalsPerBlock(m_cfg.numBlocks()),
106110
m_resultLayout(m_cfg.numBlocks())
@@ -199,7 +203,8 @@ void StackLayoutGenerator::defineStackIn(SSACFG::BlockId const& _blockId)
199203
stack,
200204
proposals[i],
201205
{},
202-
proposals[i].size()
206+
proposals[i].size(),
207+
&m_spillSet
203208
);
204209
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
205210
cumulativeCost += stack.callbacks().opGas;
@@ -258,15 +263,20 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId)
258263

259264
StackSlotLiveness const opLiveOutSlots = toStackSlotLiveness(m_cfg, opLiveOutWithoutOutputs);
260265
{
261-
StackData const target = findOptimalTarget(
266+
auto [target, plannedSpillSet] = findOptimalTarget(
262267
stack.data(),
263268
requiredStackTop,
264269
opLiveOutSlots,
265270
junkCanBeAdded,
266-
m_hasFunctionReturnLabel
271+
m_hasFunctionReturnLabel,
272+
m_spillSet,
273+
m_spillingAllowed
267274
);
268-
auto const shuffleResult = StackShuffler<StackType::Callbacks>::shuffle(stack, target);
275+
auto const spillCountBefore = m_spillSet.numSpilled();
276+
m_spillSet = std::move(plannedSpillSet);
277+
auto const shuffleResult = shuffleWithSpillDiscovery(currentStackData, target, m_spillSet);
269278
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
279+
yulAssert(m_spillingAllowed || m_spillSet.numSpilled() == spillCountBefore, "Spilling not allowed, stack too deep.");
270280
}
271281

272282
blockLayout.operationIn.push_back(currentStackData);
@@ -293,15 +303,20 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId)
293303
{
294304
auto const condition = Slot::makeValue(m_cfg, _cJump.condition);
295305
StackSlotLiveness const blockLiveOutSlots = toStackSlotLiveness(m_cfg, blockLiveOut);
296-
StackData const target = findOptimalTarget(
306+
auto [target, plannedSpillSet] = findOptimalTarget(
297307
stack.data(),
298308
{condition},
299309
blockLiveOutSlots,
300310
false,
301-
m_hasFunctionReturnLabel
311+
m_hasFunctionReturnLabel,
312+
m_spillSet,
313+
m_spillingAllowed
302314
);
303-
auto const shuffleResult = StackShuffler<StackType::Callbacks>::shuffle(stack, target);
315+
auto const spillCountBefore = m_spillSet.numSpilled();
316+
m_spillSet = std::move(plannedSpillSet);
317+
auto const shuffleResult = shuffleWithSpillDiscovery(currentStackData, target, m_spillSet);
304318
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
319+
yulAssert(m_spillingAllowed || m_spillSet.numSpilled() == spillCountBefore, "Spilling not allowed, stack too deep.");
305320
}
306321

307322
yulAssert(!stack.empty() && stack.top().isValue() && stack.top().value() == _cJump.condition);
@@ -322,8 +337,10 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId)
322337
// in case there are return values, let's bring the function return label to the top
323338
StackData returnStack = _functionReturn.returnValues | ranges::views::transform([this](InstId const _id) { return StackSlot::makeValue(m_cfg, _id); }) | ranges::to<std::vector>;
324339
returnStack.push_back(StackSlot::makeFunctionReturnLabel(m_graphID));
325-
auto const shuffleResult = StackShuffler<StackType::Callbacks>::shuffle(stack, returnStack);
340+
auto const spillCountBefore = m_spillSet.numSpilled();
341+
auto const shuffleResult = shuffleWithSpillDiscovery(currentStackData, returnStack, m_spillSet);
326342
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
343+
yulAssert(m_spillingAllowed || m_spillSet.numSpilled() == spillCountBefore, "Spilling not allowed, stack too deep.");
327344
blockLayout.exitIn = currentStackData;
328345
},
329346
[&](SSACFG::BasicBlock::Jump const& _jump) {

libyul/backends/evm/ssa/StackLayoutGenerator.h

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#pragma once
2020

21+
#include <libyul/backends/evm/ssa/spill/SpillSet.h>
22+
2123
#include <libyul/backends/evm/ssa/Stack.h>
2224
#include <libyul/backends/evm/ssa/StackLayout.h>
2325

@@ -32,17 +34,27 @@ class StackLayoutGenerator
3234
{
3335
public:
3436
using Slot = StackSlot;
35-
static SSACFGStackLayout generate(
37+
38+
/// the per-block stack layout plus the set of values the layout generator decided to spill to memory
39+
struct Result
40+
{
41+
SSACFGStackLayout layout;
42+
spill::SpillSet spillSet;
43+
};
44+
45+
static Result generate(
3646
LivenessAnalysis const& _liveness,
3747
CallSites const& _callSites,
38-
ControlFlowGraphs::FunctionGraphID _graphID
48+
ControlFlowGraphs::FunctionGraphID _graphID,
49+
bool _spillingAllowed
3950
);
4051

4152
private:
4253
explicit StackLayoutGenerator(
4354
LivenessAnalysis const& _liveness,
4455
CallSites const& _callSites,
45-
ControlFlowGraphs::FunctionGraphID _graphID
56+
ControlFlowGraphs::FunctionGraphID _graphID,
57+
bool _spillingAllowed
4658
);
4759

4860
void defineStackIn(SSACFG::BlockId const& _blockId);
@@ -53,13 +65,15 @@ class StackLayoutGenerator
5365
CallSites const& m_callSites;
5466
ControlFlowGraphs::FunctionGraphID m_graphID;
5567
bool m_hasFunctionReturnLabel;
68+
bool m_spillingAllowed;
5669

5770
std::unique_ptr<JunkAdmittingBlocksFinder> m_junkAdmittingBlocksFinder;
5871
// Per-edge stack proposals for defineStackIn.
5972
// m_inputStackProposalsPerBlock[blockId] contains (predecessorId, edgeStack) pairs
6073
// representing the stack state flowing from each predecessor into the block.
6174
std::vector<std::vector<std::pair<SSACFG::BlockId, StackData>>> m_inputStackProposalsPerBlock;
6275
SSACFGStackLayout m_resultLayout;
76+
spill::SpillSet m_spillSet;
6377
};
6478

6579
}

libyul/backends/evm/ssa/StackShuffler.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,4 +951,49 @@ class StackShuffler
951951
}
952952
};
953953

954+
[[nodiscard]] inline StackShufflerResult shuffleWithSpillDiscovery(
955+
StackData& _data,
956+
StackData const& _args,
957+
StackSlotLiveness const& _liveOut,
958+
std::size_t const _targetStackSize,
959+
spill::SpillSet& _spilledVariables
960+
)
961+
{
962+
StackData const initialData = _data;
963+
StackShufflerResult result;
964+
do
965+
{
966+
_data = initialData;
967+
Stack<> stack(_data, {});
968+
result = StackShuffler<NoOpStackManipulationCallbacks>::shuffle(stack, _args, _liveOut, _targetStackSize, &_spilledVariables);
969+
switch (result.status)
970+
{
971+
case StackShufflerResult::Status::Continue:
972+
yulAssert(false);
973+
case StackShufflerResult::Status::Admissible:
974+
break;
975+
case StackShufflerResult::Status::StackTooDeep:
976+
{
977+
yulAssert(result.culprit.isValue() && !result.culprit.isLiteralValue());
978+
yulAssert(!_spilledVariables.isSpilled(result.culprit.value()));
979+
_spilledVariables.add(result.culprit.value());
980+
break;
981+
}
982+
case StackShufflerResult::Status::MaxIterationsReached:
983+
break;
984+
}
985+
}
986+
while (result.status == StackShufflerResult::Status::StackTooDeep);
987+
return result;
988+
}
989+
990+
[[nodiscard]] inline StackShufflerResult shuffleWithSpillDiscovery(
991+
StackData& _data,
992+
StackData const& _target,
993+
spill::SpillSet& _spilledVariables)
994+
{
995+
return shuffleWithSpillDiscovery(_data, _target, {}, _target.size(), _spilledVariables);
996+
}
997+
998+
954999
}

libyul/backends/evm/ssa/StackUtils.cpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,19 @@ void GasAccumulatingCallbacks::push(StackSlot const& _slot)
5454
auto const op = cfg.evmDialect.evmVersion().hasPush0() ? evmasm::Instruction::PUSH0 : evmasm::Instruction::CODESIZE;
5555
opGas += evmasm::GasMeter::runGas(op, cfg.evmDialect.evmVersion());
5656
}
57-
else
57+
else if (_slot.isFunctionCallReturnLabel())
5858
{
59-
yulAssert(_slot.isFunctionCallReturnLabel(), "we can only push literals, junk, and function call return labels");
6059
// this is a jump dest, we don't really know yet how big it is going to be, just assume that it fits into
6160
// a 2-byte number
6261
opGas += evmasm::GasMeter::runGas(evmasm::Instruction::PUSH2, cfg.evmDialect.evmVersion());
6362
}
63+
else
64+
{
65+
// Spilled SSA value
66+
yulAssert(_slot.isValue(), "unexpected slot kind in GasAccumulatingCallbacks::push");
67+
opGas += evmasm::GasMeter::runGas(evmasm::Instruction::PUSH32, cfg.evmDialect.evmVersion());
68+
opGas += evmasm::GasMeter::runGas(evmasm::Instruction::MLOAD, cfg.evmDialect.evmVersion());
69+
}
6470
}
6571

6672
void GasAccumulatingCallbacks::pop()
@@ -80,13 +86,15 @@ StackData solidity::yul::ssa::stackPreImage(SSACFG const& _cfg, StackData _stack
8086
return _stack;
8187
}
8288

83-
StackData solidity::yul::ssa::findOptimalTarget
89+
OptimalTarget solidity::yul::ssa::findOptimalTarget
8490
(
8591
StackData const& _stackData,
8692
StackData const& _targetArgs,
8793
StackSlotLiveness const& _targetLiveOut,
8894
bool const _canIntroduceJunk,
89-
bool const _hasFunctionReturnLabel
95+
bool const _hasFunctionReturnLabel,
96+
spill::SpillSet const& _spillSet,
97+
bool const _spillingAllowed
9098
)
9199
{
92100
std::size_t const minSize = _targetLiveOut.size() + _targetArgs.size() + (_hasFunctionReturnLabel ? 1 : 0);
@@ -105,10 +113,11 @@ StackData solidity::yul::ssa::findOptimalTarget
105113

106114
StackData data;
107115
data.reserve(startSize + maxUpwardExpansion);
116+
spill::SpillSet spillSet;
108117
auto const evaluateCost = [&](std::size_t const _targetSize) -> std::size_t
109118
{
110119
StackShufflerResult result;
111-
spill::SpillSet spillSet;
120+
spillSet = _spillSet;
112121
OpsCountingCallbacks callbacks;
113122
do
114123
{
@@ -142,6 +151,7 @@ StackData solidity::yul::ssa::findOptimalTarget
142151

143152
std::size_t bestCost = evaluateCost(startSize);
144153
StackData bestData = data;
154+
spill::SpillSet bestSpillSet = spillSet;
145155

146156
// On non-reverting paths, only search downward from pivot to avoid growing the stack.
147157
// On reverting paths, search in both directions since stack cleanup doesn't matter.
@@ -158,6 +168,7 @@ StackData solidity::yul::ssa::findOptimalTarget
158168
{
159169
bestCost = cost;
160170
bestData = data;
171+
bestSpillSet = spillSet;
161172
consecutiveIncreases = 0;
162173
}
163174
else if (++consecutiveIncreases >= stopAfter)
@@ -175,14 +186,16 @@ StackData solidity::yul::ssa::findOptimalTarget
175186
{
176187
bestCost = cost;
177188
bestData = data;
189+
bestSpillSet = spillSet;
178190
consecutiveIncreases = 0;
179191
}
180192
else if (++consecutiveIncreases >= stopAfter)
181193
break;
182194
}
183195
}
184196

185-
return bestData;
197+
yulAssert(_spillingAllowed || bestSpillSet.numSpilled() == _spillSet.numSpilled(), "Spilling not allowed, stack too deep.");
198+
return OptimalTarget{std::move(bestData), std::move(bestSpillSet)};
186199
}
187200

188201
CallSites solidity::yul::ssa::gatherCallSites(SSACFG const& _cfg)

libyul/backends/evm/ssa/StackUtils.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#pragma once
2020

21+
#include <libyul/backends/evm/ssa/spill/SpillSet.h>
22+
2123
#include <libyul/backends/evm/ssa/PhiInverse.h>
2224
#include <libyul/backends/evm/ssa/Stack.h>
2325
#include <libyul/backends/evm/ssa/StackSlotLiveness.h>
@@ -63,15 +65,26 @@ struct GasAccumulatingCallbacks
6365
/// Transform stack data by replacing all its phi variables with their respective preimages.
6466
StackData stackPreImage(SSACFG const& _cfg, StackData _stack, PhiInverse const& _phiInverse);
6567

68+
struct OptimalTarget
69+
{
70+
StackData data;
71+
spill::SpillSet spillSet;
72+
};
73+
6674
/// Searches for the cheapest target stack size for shuffling _stackData towards
67-
/// (_targetArgs on top, _targetLiveOut anywhere in the tail), then realizes that size and
68-
/// returns the resolved post-shuffle StackData
69-
StackData findOptimalTarget(
75+
/// (_targetArgs on top, _targetLiveOut anywhere in the tail). Realizes that size and returns the resolved post-shuffle
76+
/// StackData together with the spill set that was needed to reach it.
77+
/// Callers must use the returned spill set for any subsequent shuffle of the real stack to this target, otherwise
78+
/// live values that were planned-spilled here will silently disappear from the real stack while
79+
/// staying outside the caller's spill set.
80+
OptimalTarget findOptimalTarget(
7081
StackData const& _stackData,
7182
StackData const& _targetArgs,
7283
StackSlotLiveness const& _targetLiveOut,
7384
bool _canIntroduceJunk,
74-
bool _hasFunctionReturnLabel
85+
bool _hasFunctionReturnLabel,
86+
spill::SpillSet const& _spillSet,
87+
bool _spillingAllowed
7588
);
7689

7790
CallSites gatherCallSites(SSACFG const& _cfg);

0 commit comments

Comments
 (0)