Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions libyul/backends/evm/ssa/Stack.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <range/v3/view/reverse.hpp>

#include <cstdint>
#include <map>
#include <type_traits>

namespace solidity::yul
Expand Down Expand Up @@ -129,6 +130,7 @@ static_assert(std::is_standard_layout_v<StackSlot>, "Want to have a predictable
static_assert(std::is_trivial_v<StackSlot>, "Want to have no init/cpy overhead");

using StackData = std::vector<StackSlot>;
using SpilledVariables = std::map<SSACFG::ValueId, uint32_t>;
std::string slotToString(StackSlot const& _slot);
std::string stackToString(StackData const& _stackData);

Expand Down Expand Up @@ -273,11 +275,22 @@ class Stack
return Depth{static_cast<size_t>(std::distance(ranges::begin(rview), it))};
}

static bool constexpr canBeFreelyGenerated(Slot const& _slot)
bool canBeFreelyGenerated(Slot const& _slot) const
{
return _slot.isLiteralValueID() || _slot.isJunk() || _slot.isFunctionCallReturnLabel();
return
_slot.isLiteralValueID() ||
_slot.isJunk() ||
_slot.isFunctionCallReturnLabel() ||
(
_slot.isValueID() &&
m_spilledVariables &&
m_spilledVariables->contains(_slot.valueID())
);
}

void setSpilledVariables(SpilledVariables const* _spilledVariables) { m_spilledVariables = _spilledVariables; }
SpilledVariables const* spilledVariables() const noexcept { return m_spilledVariables; }

Slot const& operator[](Offset const& _index) const noexcept { return (*m_data)[_index.value]; }
auto begin() const { return ranges::begin(*m_data); }
auto end() const { return ranges::end(*m_data); }
Expand Down Expand Up @@ -305,6 +318,7 @@ class Stack
private:
Data* m_data;
Callbacks m_callbacks;
SpilledVariables const* m_spilledVariables = nullptr;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced this should live in the Stack.
It feels like an auxiliary information that should be recorded somewhere in the algorithm rather than directly in the Stack data structure.

};

}
Expand Down
28 changes: 23 additions & 5 deletions libyul/backends/evm/ssa/StackShuffler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,33 @@
using namespace solidity::yul::ssa;
using namespace solidity::yul::ssa::detail;

Target::Target(StackData const& _args, LivenessAnalysis::LivenessData const& _liveOut, std::size_t const _targetSize):
Target::Target(
StackData const& _args,
LivenessAnalysis::LivenessData const& _liveOut,
std::size_t const _targetSize,
SpilledVariables const* _spilledVariables
):
args(_args),
liveOut(_liveOut),
spilledVariables(_spilledVariables),
size(_targetSize),
tailSize(_targetSize - _args.size())
{
// Spilled values can be materialized on demand, so they contribute no stack-distribution
// requirement and must not count toward minCount (otherwise the shuffler would chase a
// deficit it can't resolve on stack).
auto const isSpilled = [_spilledVariables](SSACFG::ValueId const _id)
{
return _spilledVariables && _spilledVariables->contains(_id);
};

minCount.reserve(_args.size() + _liveOut.size());
for (auto const& arg: _args)
if (!arg.isJunk())
if (!arg.isJunk() && !(arg.isValueID() && isSpilled(arg.valueID())))
++minCount[arg];
for (auto const& _liveValueId: _liveOut | ranges::views::keys)
++minCount[StackSlot::makeValueID(_liveValueId)];
for (auto const& liveValueId: _liveOut | ranges::views::keys)
if (!isSpilled(liveValueId))
++minCount[StackSlot::makeValueID(liveValueId)];
}

State::State(StackData const& _stackData, Target const& _target, std::size_t const _reachableStackDepth):
Expand Down Expand Up @@ -125,7 +140,10 @@ bool State::requiredInArgs(StackSlot const& _slot) const

bool State::requiredInTail(StackSlot const& _slot) const
{
return _slot.isValueID() && m_target.liveOut.contains(_slot.valueID());
if (!_slot.isValueID() || !m_target.liveOut.contains(_slot.valueID()))
return false;
// Spilled values can be rematerialized, so they need not occupy a tail slot.
return !m_target.spilledVariables || !m_target.spilledVariables->contains(_slot.valueID());
}

bool State::offsetInTargetArgsRegion(StackOffset const _offset) const
Expand Down
34 changes: 29 additions & 5 deletions libyul/backends/evm/ssa/StackShuffler.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ namespace detail
/// provide a lower bound for the slot distribution.
struct Target
{
Target(StackData const& _args, LivenessAnalysis::LivenessData const& _liveOut, std::size_t _targetSize);
Target(
StackData const& _args,
LivenessAnalysis::LivenessData const& _liveOut,
std::size_t _targetSize,
SpilledVariables const* _spilledVariables = nullptr
);

StackData const& args;
LivenessAnalysis::LivenessData const& liveOut;
SpilledVariables const* const spilledVariables;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Target really need to store the spilledVariables?

std::size_t const size;
std::size_t const tailSize;
boost::container::flat_map<StackSlot, size_t> minCount;
Expand Down Expand Up @@ -158,8 +164,11 @@ class StackShuffler
std::size_t _targetStackSize
)
{
detail::Target const target(_args, _liveOut, _targetStackSize);
yulAssert(_liveOut.size() <= target.size, "not enough tail space");
detail::Target const target(_args, _liveOut, _targetStackSize, _stack.spilledVariables());
// If the caller has wired up a spill set, the shuffler can reduce the effective liveOut
// size by spilling; otherwise the liveOut must fit into the target up front.
if (!_stack.spilledVariables())
yulAssert(_liveOut.size() <= target.size, "not enough tail space");
{
// check that all required values are on stack
detail::State const state(_stack.data(), target, ReachableStackDepth);
Expand Down Expand Up @@ -272,6 +281,21 @@ class StackShuffler
if (shrinkStack(_stack, _state))
return {StackShufflerResult::Status::Continue};

// if we couldn't shrink the stack we surface this failed state as stack too deep
for (StackOffset const offset: _state.stackRange() | ranges::views::reverse)
{
Slot const& candidate = _stack[offset];
if (
candidate.isValueID() &&
!candidate.isLiteralValueID() &&
(
!_stack.spilledVariables() ||
!_stack.spilledVariables()->contains(candidate.valueID())
)
)
return {StackShufflerResult::Status::StackTooDeep, candidate};
}

yulAssert(false, "reached final and forbidden state");
}

Expand Down Expand Up @@ -810,7 +834,7 @@ class StackShuffler
}
else
{
if (_stack.isBeyondSwapRange(*depth))
if (_stack.isBeyondSwapRange(*depth) && !_stack.canBeFreelyGenerated(_state.targetArg(offset)))
return _stack.depthToOffset(*depth);
}
}
Expand All @@ -828,7 +852,7 @@ class StackShuffler
std::optional<StackDepth> depth = _stack.findSlotDepth(slotAtOffset);
// it must exist
yulAssert(depth);
if (!_stack.dupReachable(*depth))
if (!_stack.dupReachable(*depth) && !_stack.canBeFreelyGenerated(slotAtOffset))
return _stack.depthToOffset(*depth);
}
}
Expand Down
38 changes: 32 additions & 6 deletions libyul/backends/evm/ssa/StackUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,38 @@ std::size_t solidity::yul::ssa::findOptimalTargetSize
data.reserve(startSize + maxUpwardExpansion);
auto const evaluateCost = [&](std::size_t const _targetSize) -> std::size_t
{
data = _stackData;
Stack<OpsCountingCallbacks> countOpsStack(data, {});
auto const shuffleResult = StackShuffler<OpsCountingCallbacks>::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize);
yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible);
yulAssert(countOpsStack.size() == _targetSize);
return countOpsStack.callbacks().numOps;
StackShufflerResult result;
SpilledVariables spillSet;
OpsCountingCallbacks callbacks;
do
{
data = _stackData;
Stack<OpsCountingCallbacks> countOpsStack(data, {});
countOpsStack.setSpilledVariables(&spillSet);
result = StackShuffler<OpsCountingCallbacks>::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize);
callbacks = countOpsStack.callbacks();
switch (result.status)
{
case StackShufflerResult::Status::Continue:
yulAssert(false);
case StackShufflerResult::Status::Admissible:
break;
case StackShufflerResult::Status::StackTooDeep:
{
yulAssert(result.culprit.isValueID() && !result.culprit.isLiteralValueID());
yulAssert(!spillSet.contains(result.culprit.valueID()));
spillSet.emplace(result.culprit.valueID(), 0);
break;
}
case StackShufflerResult::Status::MaxIterationsReached:
break;
}
}
while (result.status == StackShufflerResult::Status::StackTooDeep);
yulAssert(data.size() == _targetSize);
yulAssert(result.status == StackShufflerResult::Status::Admissible);
std::size_t const cost = callbacks.numOps + 1000 * spillSet.size();
return cost;
};

std::size_t bestCost = evaluateCost(startSize);
Expand Down
57 changes: 57 additions & 0 deletions test/libyul/ssa/StackShufflerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ std::string_view constexpr parserKeyInitialStack {"initial"};
std::string_view constexpr parserKeyStackTop {"targetStackTop"};
std::string_view constexpr parserKeyTailSet {"targetStackTailSet"};
std::string_view constexpr parserKeyStackSize {"targetStackSize"};
std::string_view constexpr parserKeyAllowSpilling {"allowSpilling"};
std::string_view constexpr parserKeyInitialSpilled {"initialSpilledSet"};

using Liveness = LivenessAnalysis::LivenessData;
using Slot = StackSlot;
Expand Down Expand Up @@ -178,6 +180,8 @@ struct ShuffleTestInput
std::optional<TestStack::Data> targetStackTop;
Liveness targetStackTailSet{};
std::optional<size_t> targetStackSize;
bool allowSpilling = false;
SpilledVariables initialSpilledSet{};

bool valid() const
{
Expand Down Expand Up @@ -236,6 +240,18 @@ struct ShuffleTestInput
else
throw std::runtime_error(fmt::format("Couldn't parse targetStackSize: {}", value));
}
else if (key == parserKeyAllowSpilling)
{
if (value == "true")
result.allowSpilling = true;
else if (value == "false")
result.allowSpilling = false;
else
throw std::runtime_error(fmt::format("Couldn't parse allowSpilling: {}", value));
}
else if (key == parserKeyInitialSpilled)
for (auto const& [valueId, _]: parseLiveness(value))
result.initialSpilledSet.emplace(valueId, 0u);

}

Expand Down Expand Up @@ -475,13 +491,42 @@ explicitly provided.)";
auto stackData = *testConfig.initial;
std::ostringstream oss;
StackShufflerResult shuffleResult;
SpilledVariables spillSet = testConfig.initialSpilledSet;

// First, when spilling is allowed, run the shuffler repeatedly without recording to determine
// the final spill set. Each iteration starts from the initial stack and adds the culprit of a
// recoverable StackTooDeep to the spill set.
if (testConfig.allowSpilling)
while (true)
{
auto scratch = *testConfig.initial;
TestStack stack(scratch, {});
stack.setSpilledVariables(&spillSet);
auto const result = StackShuffler<StackManipulationCallbacks>::shuffle(
stack,
*testConfig.targetStackTop,
testConfig.targetStackTailSet,
*testConfig.targetStackSize
);
if (
result.status != StackShufflerResult::Status::StackTooDeep ||
!result.culprit.isValueID() ||
result.culprit.isLiteralValueID() ||
spillSet.contains(result.culprit.valueID())
)
break;
spillSet.emplace(result.culprit.valueID(), 0u);
}

// Final shuffle with the (possibly pre-populated) spill set, recording the trace.
{
TraceRecorder trace(oss, *testConfig.targetStackTop, testConfig.targetStackTailSet, *testConfig.targetStackSize);
trace.record("(initial)", *testConfig.initial);
TestStack stack(stackData, {.hook = [&](std::string const& op)
{
trace.record(op, stackData);
}});
stack.setSpilledVariables(&spillSet);
shuffleResult = StackShuffler<StackManipulationCallbacks>::shuffle(
stack,
*testConfig.targetStackTop,
Expand All @@ -506,6 +551,16 @@ explicitly provided.)";
case StackShufflerResult::Status::Continue:
yulAssert(false, "Unexpected Continue status from shuffle()");
}
if (testConfig.allowSpilling)
oss << fmt::format(
"Spilled: {{{}}}\n",
fmt::join(
spillSet | ranges::views::keys | ranges::views::transform(
[](auto const& id) { return slotToString(StackSlot::makeValueID(id)); }
),
", "
)
);
// check stack data
if (shuffleResult.status == StackShufflerResult::Status::Admissible)
{
Expand All @@ -514,6 +569,8 @@ explicitly provided.)";
yulAssert(stackData.size() == *testConfig.targetStackSize);
for (const auto& valueID: testConfig.targetStackTailSet | ranges::views::keys)
{
if (spillSet.contains(valueID))
continue;
auto const findIt = ranges::find(
stackData.begin(),
stackData.begin() + static_cast<std::ptrdiff_t>(tailSize),
Expand Down
16 changes: 16 additions & 0 deletions test/libyul/ssa/stackShuffler/initial_spill_set.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Seeds the spill set with v1 from the start. Even though v1 is in the tail set and not on the stack,
// the shuffler treats it as freely generatable and finishes admissible without retries.
allowSpilling: true
initialSpilledSet: {v1}
initial: [v3, v4, v5]
targetStackTop: [v3, v4, v5]
targetStackTailSet: {v1}
targetStackSize: 3
// ----
// | 0 1 2
// +---------------------
// (initial)| v3 v4 v5
// +---------------------
// (target)| v3 v4 v5
// Status: Admissible
// Spilled: {v1}
17 changes: 17 additions & 0 deletions test/libyul/ssa/stackShuffler/spill_arg_in_liveout.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// v1 is needed both as an arg AND in the liveOut tail set, but appears only once on the stack at the top.
// The deep tail slots (depth 17 = offset 0) are beyond swap range, so v1 cannot be duplicated into the tail.
// With allowSpilling, the shuffler reports StackTooDeep with culprit v1, the test driver adds v1 to the spill
// set and retries; v1 is now freely generatable so the second iteration succeeds.
allowSpilling: true
initial: [JUNK, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v1]
targetStackTop: [v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v1]
targetStackTailSet: {v1}
targetStackSize: 18
// ----
// | 0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// +------- +-----------------------------------------------------------------------------------------------------------------------
// (initial)| * | v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v1
// +------- +-----------------------------------------------------------------------------------------------------------------------
// (target)| {v1} | v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a little bit confusing to me that here v1 is in (target), but in the previous example, v1 is not present in the (target), even though both test cases specify targetStackTailSet as {v1}.

I am not sure if this is just a visualization issue, or something about the semantics of a target.

// Status: Admissible
// Spilled: {v1}
13 changes: 13 additions & 0 deletions test/libyul/ssa/stackShuffler/spill_disabled_default.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Same setup as spill_arg_in_liveout but without allowSpilling: confirms the default behaviour is
// preserved and the shuffler reports StackTooDeep without retrying.
Comment on lines +1 to +2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explicitly say allowSpilling: false here?

initial: [JUNK, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v1]
targetStackTop: [v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v1]
targetStackTailSet: {v1}
targetStackSize: 18
// ----
// | 0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// +------- +-----------------------------------------------------------------------------------------------------------------------
// (initial)| * | v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v1
// +------- +-----------------------------------------------------------------------------------------------------------------------
// (target)| {v1} | v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v1
// Status: StackTooDeep (culprit: v1)
17 changes: 17 additions & 0 deletions test/libyul/ssa/stackShuffler/spill_many_for_small_target.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Every target arg is also in the tail set, but the tail region has zero room (tail size = 0).
// Each fix-tail iteration reports the deepest still-needed value as StackTooDeep; with allowSpilling
// the driver spills it and retries. After every value has been spilled, the stack is admissible
// because the spilled values can be regenerated freely.
allowSpilling: true
initial: [v1, v2, v3, v4, v5, v6, v7, v8]
targetStackTop: [v1, v2, v3, v4, v5, v6, v7, v8]
targetStackTailSet: {v1, v2, v3, v4, v5, v6, v7, v8}
targetStackSize: 8
// ----
// | 0 1 2 3 4 5 6 7
// +--------------------------------------------------------
// (initial)| v1 v2 v3 v4 v5 v6 v7 v8
// +--------------------------------------------------------
// (target)| v1 v2 v3 v4 v5 v6 v7 v8
// Status: Admissible
// Spilled: {v1, v2, v3, v4, v5, v6, v7, v8}
Loading