diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index 8e8ec56dfac6..80584bf7729d 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -99,6 +99,7 @@ add_library(yul backends/evm/ssa/StackLayoutGenerator.h backends/evm/ssa/StackShuffler.cpp backends/evm/ssa/StackShuffler.h + backends/evm/ssa/StackToMemorySpilling.h backends/evm/ssa/StackUtils.cpp backends/evm/ssa/StackUtils.h backends/evm/ssa/io/DotExporterBase.cpp diff --git a/libyul/backends/evm/ssa/StackShuffler.cpp b/libyul/backends/evm/ssa/StackShuffler.cpp index 4d5c5dbc6301..a9416a088a0f 100644 --- a/libyul/backends/evm/ssa/StackShuffler.cpp +++ b/libyul/backends/evm/ssa/StackShuffler.cpp @@ -24,23 +24,31 @@ 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()) { minCount.reserve(_args.size() + _liveOut.size()); for (auto const& arg: _args) - if (!arg.isJunk()) + if (!arg.isJunk() && !slotIsSpilled(arg, _spilledVariables)) ++minCount[arg]; - for (auto const& _liveValueId: _liveOut | ranges::views::keys) - ++minCount[StackSlot::makeValueID(_liveValueId)]; + for (auto const& liveValueId: _liveOut | ranges::views::keys) + if (!(_spilledVariables && _spilledVariables->isSpilled(liveValueId))) + ++minCount[StackSlot::makeValueID(liveValueId)]; } -State::State(StackData const& _stackData, Target const& _target, std::size_t const _reachableStackDepth): +State::State(StackData const& _stackData, Target const& _target, SpilledVariables const* const _spilledVariables, std::size_t const _reachableStackDepth): m_stackData(_stackData), m_target(_target), + m_spilledVariables(_spilledVariables), m_reachableStackDepth(_reachableStackDepth) { m_histogram.reserve(_stackData.size()); @@ -125,7 +133,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 !slotIsSpilled(_slot); } bool State::offsetInTargetArgsRegion(StackOffset const _offset) const diff --git a/libyul/backends/evm/ssa/StackShuffler.h b/libyul/backends/evm/ssa/StackShuffler.h index 6ccb629cb1ad..ac095346e76b 100644 --- a/libyul/backends/evm/ssa/StackShuffler.h +++ b/libyul/backends/evm/ssa/StackShuffler.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include #include @@ -36,14 +37,31 @@ namespace solidity::yul::ssa namespace detail { + +inline bool slotIsSpilled(StackSlot const& _slot, SpilledVariables const* const _spilledVariables) +{ + return _spilledVariables && _slot.isValueID() && _spilledVariables->isSpilled(_slot.valueID()); +} + +inline bool slotCanBeLoadedOrPushed(StackSlot const& _slot, SpilledVariables const* const _spilledVariables) +{ + return Stack<>::canBeFreelyGenerated(_slot) || slotIsSpilled(_slot, _spilledVariables); +} + /// Contains information about the shuffling target, aggregates over args and live out to /// 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; std::size_t const size; std::size_t const tailSize; boost::container::flat_map minCount; @@ -52,7 +70,7 @@ struct Target class State { public: - State(StackData const& _stackData, Target const& _target, std::size_t _reachableStackDepth); + State(StackData const& _stackData, Target const& _target, SpilledVariables const* const _spilledVariables, std::size_t _reachableStackDepth); std::size_t size() const; /// How many of `_slot` do we have on stack @@ -128,9 +146,20 @@ class State /// Depth of the deepest arg slot incompatible with target or Nothing for no incompatibility in current state std::optional findDeepestIncorrectArgSlot() const; + bool slotCanBeLoadedOrPushed(StackSlot const& _slot) const + { + return detail::slotCanBeLoadedOrPushed(_slot, m_spilledVariables); + } + + bool slotIsSpilled(StackSlot const& _slot) const + { + return detail::slotIsSpilled(_slot, m_spilledVariables); + } + private: StackData const& m_stackData; Target const& m_target; + SpilledVariables const* const m_spilledVariables; std::size_t const m_reachableStackDepth; boost::container::flat_map m_histogramTail; boost::container::flat_map m_histogramArgs; @@ -156,25 +185,32 @@ class StackShuffler Stack& _stack, StackData const& _args, LivenessAnalysis::LivenessData const& _liveOut, - std::size_t _targetStackSize + std::size_t _targetStackSize, + SpilledVariables const* const _spilledVariables = nullptr ) { - detail::Target const target(_args, _liveOut, _targetStackSize); - yulAssert(_liveOut.size() <= target.size, "not enough tail space"); + detail::Target const target(_args, _liveOut, _targetStackSize, _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 (!_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); + detail::State const state(_stack.data(), target, _spilledVariables, ReachableStackDepth); for (auto const& liveVariable: _liveOut | ranges::views::keys | ranges::views::transform(Slot::makeValueID)) - yulAssert(!_stack.canBeFreelyGenerated(liveVariable) && ranges::contains(_stack.data(), liveVariable)); + yulAssert( + !_stack.canBeFreelyGenerated(liveVariable) && + (ranges::contains(_stack.data(), liveVariable) || detail::slotIsSpilled(liveVariable, _spilledVariables)) + ); for (auto const& arg: _args) - yulAssert(_stack.canBeFreelyGenerated(arg) || ranges::contains(_stack.data(), arg)); + yulAssert(detail::slotCanBeLoadedOrPushed(arg, _spilledVariables) || ranges::contains(_stack.data(), arg)); } static std::size_t constexpr maxIterations = 1000; std::size_t i = 0; while (true) { - detail::State const state(_stack.data(), target, ReachableStackDepth); + detail::State const state(_stack.data(), target, _spilledVariables, ReachableStackDepth); auto result = shuffleStep(_stack, state); if (result.status == StackShufflerResult::Status::Admissible) { @@ -228,13 +264,13 @@ class StackShuffler return {StackShufflerResult::Status::Continue}; // after this, all current slots are either in acceptable positions or at least dup-reachable - if (auto unreachableOffset = allNecessarySlotsReachableOrFinal(_stack, _state)) + if (auto culprit = allNecessarySlotsReachableOrFinal(_stack, _state)) { // !allNecessarySlotsReachableOrFinal(ops) ≡ ¬(∀s: reachable(s) ∨ final(s)) ≡ ∃s: ¬reachable(s) ∧ ¬final(s) if (shrinkStack(_stack, _state)) return {StackShufflerResult::Status::Continue}; - return {StackShufflerResult::Status::StackTooDeep, _stack[*unreachableOffset]}; + return {StackShufflerResult::Status::StackTooDeep, _stack.top()}; } // this will either grow the tail as needed, swap down something from args that needs to be in the tail, @@ -273,6 +309,18 @@ 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() && + !_state.slotIsSpilled(candidate) + ) + return {StackShufflerResult::Status::StackTooDeep, candidate}; + } + yulAssert(false, "reached final and forbidden state"); } @@ -580,10 +628,10 @@ class StackShuffler _stack.dup(*sourceDepth); return {ShuffleHelperResult::Status::StackModified}; } - if (!_stack.canBeFreelyGenerated(arg)) + if (!_state.slotCanBeLoadedOrPushed(arg)) return {ShuffleHelperResult::Status::StackTooDeep, arg}; } - yulAssert(_stack.canBeFreelyGenerated(arg)); + yulAssert(_state.slotCanBeLoadedOrPushed(arg)); _stack.push(arg); return {ShuffleHelperResult::Status::StackModified}; } @@ -621,7 +669,7 @@ class StackShuffler } else { - if (!_stack.canBeFreelyGenerated(arg)) + if (!_state.slotCanBeLoadedOrPushed(arg)) return {ShuffleHelperResult::Status::StackTooDeep, arg}; auto result = dupDeepSlotIfRequired(_stack, _state); if (result.status == ShuffleHelperResult::Status::StackTooDeep) @@ -671,7 +719,7 @@ class StackShuffler for (StackOffset tailOffset: _state.stackTailRange()) if ( _stack.isValidSwapTarget(tailOffset) && - _stack.canBeFreelyGenerated(_stack[tailOffset]) && + _state.slotCanBeLoadedOrPushed(_stack[tailOffset]) && !_stack[tailOffset].isLiteralValueID() ) { @@ -830,8 +878,11 @@ class StackShuffler return false; } - /// Checks if all current slots are either in a position that is compatible with the target or, if not, are dup-reachable. - static std::optional allNecessarySlotsReachableOrFinal(Stack const& _stack, detail::State const& _state) + /// Checks if all current slots are either in a position that is compatible with the target or, if not, are + /// dup-reachable. + /// Returns the culprit slot (guaranteed to be non-junk) that cannot be placed or duplicated, or `std::nullopt` + /// if every slot is reachable-or-final. + static std::optional allNecessarySlotsReachableOrFinal(Stack const& _stack, detail::State const& _state) { // check that args are either in position or reachable for (StackOffset offset{_state.target().tailSize}; offset < _state.target().size; ++offset.value) @@ -846,19 +897,19 @@ class StackShuffler // the target offset itself is out of swap range, we must shrink to reach it if (offset.value < _stack.size() && _stack.isBeyondSwapRange(offset)) - return offset; + return targetArg; // find first occurrence of the slot std::optional const depth = _stack.findSlotDepth(targetArg); if (!depth) { // if there is no occurrence of the slot anywhere, we must be able to freely generate it - yulAssert(_stack.canBeFreelyGenerated(targetArg)); + yulAssert(_state.slotCanBeLoadedOrPushed(targetArg)); } else { - if (_stack.isBeyondSwapRange(*depth)) - return _stack.depthToOffset(*depth); + if (_stack.isBeyondSwapRange(*depth) && !_state.slotCanBeLoadedOrPushed(targetArg)) + return targetArg; } } // distribution check: all we have to dup can be duped @@ -875,8 +926,8 @@ class StackShuffler std::optional depth = _stack.findSlotDepth(slotAtOffset); // it must exist yulAssert(depth); - if (!_stack.dupReachable(*depth)) - return _stack.depthToOffset(*depth); + if (!_stack.dupReachable(*depth) && !_state.slotCanBeLoadedOrPushed(slotAtOffset)) + return slotAtOffset; } } diff --git a/libyul/backends/evm/ssa/StackToMemorySpilling.h b/libyul/backends/evm/ssa/StackToMemorySpilling.h new file mode 100644 index 000000000000..f4d869a9e669 --- /dev/null +++ b/libyul/backends/evm/ssa/StackToMemorySpilling.h @@ -0,0 +1,59 @@ +/* +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 + +#pragma once + +#include + +#include + +#include +#include + +namespace solidity::yul::ssa +{ +class SpilledVariables +{ +public: + + void spill(SSACFG::ValueId const _valueId) + { + bool const emplaced = m_spillSet.try_emplace(_valueId, m_currentSpillSlot).second; + yulAssert(emplaced, fmt::format("can't spill a value ({}) twice", _valueId)); + ++m_currentSpillSlot; + } + + bool isSpilled(SSACFG::ValueId const _valueId) const + { + return m_spillSet.contains(_valueId); + } + + std::size_t numSpilled() const + { + return m_spillSet.size(); + } + + auto spilledValues() const + { + return m_spillSet | ranges::views::keys; + } +private: + std::uint32_t m_currentSpillSlot{0}; + std::map m_spillSet; // map valueId -> ssa-cfg-local slot +}; +} diff --git a/libyul/backends/evm/ssa/StackUtils.cpp b/libyul/backends/evm/ssa/StackUtils.cpp index dbc9b1541e9e..883a1f3b8316 100644 --- a/libyul/backends/evm/ssa/StackUtils.cpp +++ b/libyul/backends/evm/ssa/StackUtils.cpp @@ -104,12 +104,37 @@ 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 countOpsStack(data, {}); - auto const shuffleResult = StackShuffler::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 countOpsStack(data, {}); + result = StackShuffler::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize, &spillSet); + 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.isSpilled(result.culprit.valueID())); + spillSet.spill(result.culprit.valueID()); + 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.numSpilled(); + return cost; }; std::size_t bestCost = evaluateCost(startSize); diff --git a/test/libyul/ssa/StackShufflerTest.cpp b/test/libyul/ssa/StackShufflerTest.cpp index 993ba4bb97f7..045b755c2b67 100644 --- a/test/libyul/ssa/StackShufflerTest.cpp +++ b/test/libyul/ssa/StackShufflerTest.cpp @@ -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; @@ -178,6 +180,8 @@ struct ShuffleTestInput std::optional targetStackTop; Liveness targetStackTailSet{}; std::optional targetStackSize; + bool allowSpilling = false; + SpilledVariables initialSpilledSet{}; bool valid() const { @@ -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.spill(valueId); } @@ -259,16 +275,43 @@ class TraceRecorder static char constexpr junkSymbol = '*'; public: - TraceRecorder(std::ostream& _out, TestStack::Data const& _targetArgs, Liveness const& _targetTail, size_t _targetStackSize): + TraceRecorder( + std::ostream& _out, + TestStack::Data const& _targetArgs, + Liveness const& _targetTail, + size_t _targetStackSize, + SpilledVariables const& _spillSet + ): m_out(_out), m_targetArgs(_targetArgs), m_targetTail(_targetTail), + m_spillSet(_spillSet), m_targetStackSize(_targetStackSize), m_targetTailSize( [&] { yulAssert(_targetStackSize >= m_targetArgs.size()); return _targetStackSize - m_targetArgs.size(); }() + ), + m_tailSetStr( + fmt::format( + "{{{}}}", + fmt::join( + m_targetTail | ranges::views::keys | ranges::views::transform( + [this](auto const& id) { + std::string const suffix = m_spillSet.isSpilled(id) ? "*" : ""; + return slotToString(Slot::makeValueID(id)) + suffix; + } + ), + ", " + ) + ) + ), + // Width of the phantom "tail annotation" column, shown only when the set is non-empty + // but the tail region has zero real columns (all tail-set members spilled or coinciding + // with args) + m_tailAnnotationWidth( + m_targetTailSize == 0 && !m_targetTail.empty() ? m_tailSetStr.size() + 2 : 0 ) {} @@ -335,8 +378,22 @@ class TraceRecorder bool m_truncated = false; TestStack::Data const& m_targetArgs; Liveness const& m_targetTail; + SpilledVariables const& m_spillSet; size_t const m_targetStackSize; size_t const m_targetTailSize; + std::string const m_tailSetStr; + size_t const m_tailAnnotationWidth; + + void emitTailAnnotationColumn(std::string_view _content, char const _filler, char const _junction) const + { + if (m_tailAnnotationWidth == 0) + return; + if (_content.empty()) + m_out << std::string(m_tailAnnotationWidth, _filler); + else + m_out << fmt::format("{:>{}}", _content, m_tailAnnotationWidth); + m_out << ' ' << _junction; + } void emitSeparator(size_t const _index, bool const _hasExcess, char const _junction) const { @@ -349,6 +406,7 @@ class TraceRecorder void emitHeader(bool const _hasExcess, std::vector const& _columnWidths) const { m_out << fmt::format("{:>{}}", "", operationColumnWidth) << "|"; + emitTailAnnotationColumn({}, ' ', '|'); for (std::size_t i = 0; i < _columnWidths.size(); ++i) { emitSeparator(i, _hasExcess, '|'); @@ -360,6 +418,7 @@ class TraceRecorder void emitSeparatorLine(bool const _hasExcess, std::vector const& _columnWidths) const { m_out << fmt::format("{:>{}}", "", operationColumnWidth) << '+'; + emitTailAnnotationColumn({}, '-', '+'); for (std::size_t i = 0; i < _columnWidths.size(); ++i) { emitSeparator(i, _hasExcess, '+'); @@ -371,6 +430,7 @@ class TraceRecorder void emitDataRow(TraceEntry const& _entry, bool const _hasExcess, std::vector const& _columnWidths) const { m_out << fmt::format("{:>{}}", _entry.operation, operationColumnWidth) << "|"; + emitTailAnnotationColumn({}, ' ', '|'); for (size_t i = 0; i < _entry.stackAfter.size(); ++i) { emitSeparator(i, _hasExcess, '|'); @@ -391,21 +451,14 @@ class TraceRecorder std::size_t tailWidth = 0; for (std::size_t i = 0; i < m_targetTailSize; ++i) tailWidth += _columnWidths[i]; - std::string const tailSetStr = fmt::format( - "{{{}}}", - fmt::join( - m_targetTail | ranges::views::keys | ranges::views::transform( - [](auto const& id) { return slotToString(Slot::makeValueID(id)); } - ), - ", " - ) - ); - m_out << fmt::format("{:>{}}", tailSetStr, tailWidth); - } + m_out << fmt::format("{:>{}}", m_tailSetStr, tailWidth); - // Args separator - if (!m_targetArgs.empty() && m_targetTailSize > 0) - m_out << " |"; + // Args separator + if (!m_targetArgs.empty()) + m_out << " |"; + } + else if (m_targetTailSize == 0) + emitTailAnnotationColumn(m_tailSetStr, ' ', '|'); // Print args region for (std::size_t i = 0; i < m_targetArgs.size(); ++i) @@ -475,8 +528,33 @@ 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, {}); + auto const result = StackShuffler::shuffle( + stack, + *testConfig.targetStackTop, + testConfig.targetStackTailSet, + *testConfig.targetStackSize, + &spillSet + ); + if ( + result.status != StackShufflerResult::Status::StackTooDeep + ) + break; + spillSet.spill(result.culprit.valueID()); + } + + // Final shuffle with the (possibly pre-populated) spill set, recording the trace. { - TraceRecorder trace(oss, *testConfig.targetStackTop, testConfig.targetStackTailSet, *testConfig.targetStackSize); + TraceRecorder trace(oss, *testConfig.targetStackTop, testConfig.targetStackTailSet, *testConfig.targetStackSize, spillSet); trace.record("(initial)", *testConfig.initial); TestStack stack(stackData, {.hook = [&](std::string const& op) { @@ -486,7 +564,8 @@ explicitly provided.)"; stack, *testConfig.targetStackTop, testConfig.targetStackTailSet, - *testConfig.targetStackSize + *testConfig.targetStackSize, + &spillSet ); if (shuffleResult.status == StackShufflerResult::Status::MaxIterationsReached) trace.truncate(30); @@ -506,6 +585,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.spilledValues() | ranges::views::transform( + [](auto const& id) { return slotToString(StackSlot::makeValueID(id)); } + ), + ", " + ) + ); // check stack data if (shuffleResult.status == StackShufflerResult::Status::Admissible) { @@ -514,6 +603,8 @@ explicitly provided.)"; yulAssert(stackData.size() == *testConfig.targetStackSize); for (const auto& valueID: testConfig.targetStackTailSet | ranges::views::keys) { + if (spillSet.isSpilled(valueID)) + continue; auto const findIt = ranges::find( stackData.begin(), stackData.begin() + static_cast(tailSize), diff --git a/test/libyul/ssa/stackShuffler/initial_spill_set.stack b/test/libyul/ssa/stackShuffler/initial_spill_set.stack new file mode 100644 index 000000000000..cd6d7f8eda8a --- /dev/null +++ b/test/libyul/ssa/stackShuffler/initial_spill_set.stack @@ -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)| {v1*} | v3 v4 v5 +// Status: Admissible +// Spilled: {v1} diff --git a/test/libyul/ssa/stackShuffler/spill_arg_in_liveout.stack b/test/libyul/ssa/stackShuffler/spill_arg_in_liveout.stack new file mode 100644 index 000000000000..c55beea1dd72 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_arg_in_liveout.stack @@ -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 +// Status: Admissible +// Spilled: {v1} diff --git a/test/libyul/ssa/stackShuffler/spill_disabled_default.stack b/test/libyul/ssa/stackShuffler/spill_disabled_default.stack new file mode 100644 index 000000000000..60dbc2da9fc6 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_disabled_default.stack @@ -0,0 +1,14 @@ +// Same setup as spill_arg_in_liveout but without allowSpilling: confirms the default behaviour is +// preserved and the shuffler reports StackTooDeep without retrying. +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 +allowSpilling: false +// ---- +// | 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) diff --git a/test/libyul/ssa/stackShuffler/spill_many_for_small_target.stack b/test/libyul/ssa/stackShuffler/spill_many_for_small_target.stack new file mode 100644 index 000000000000..948dd8db80ea --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_many_for_small_target.stack @@ -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*} | v1 v2 v3 v4 v5 v6 v7 v8 +// Status: Admissible +// Spilled: {v1, v2, v3, v4, v5, v6, v7, v8} diff --git a/test/libyul/ssa/stackShuffler/spill_to_pop_deep_junk.stack b/test/libyul/ssa/stackShuffler/spill_to_pop_deep_junk.stack new file mode 100644 index 000000000000..c66f09fc63b6 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_to_pop_deep_junk.stack @@ -0,0 +1,46 @@ +allowSpilling: true +initial: [JUNK, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20] +targetStackTop: [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20] +// ---- +// | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 20 +// +-------------------------------------------------------------------------------------------------------------------------------------------- +------- +// (initial)| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 | v20 +// SWAP1| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v20 | v19 +// SWAP2| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v19 v20 | v18 +// SWAP3| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v18 v19 v20 | v17 +// SWAP4| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v17 v18 v19 v20 | v16 +// SWAP5| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v16 v17 v18 v19 v20 | v15 +// SWAP6| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v15 v16 v17 v18 v19 v20 | v14 +// SWAP7| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v14 v15 v16 v17 v18 v19 v20 | v13 +// SWAP8| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v13 v14 v15 v16 v17 v18 v19 v20 | v12 +// SWAP9| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v11 +// SWAP10| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v10 +// SWAP11| * v1 v2 v3 v4 v5 v6 v7 v8 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v9 +// SWAP12| * v1 v2 v3 v4 v5 v6 v7 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v8 +// SWAP13| * v1 v2 v3 v4 v5 v6 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v7 +// SWAP14| * v1 v2 v3 v4 v5 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v6 +// SWAP15| * v1 v2 v3 v4 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v5 +// SWAP16| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v4 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 +// SWAP15| * v17 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v1 +// SWAP16| v1 v17 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 * +// SWAP14| v1 v17 * v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v2 +// SWAP15| v1 v2 * v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 +// SWAP13| v1 v2 * v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v3 +// SWAP14| v1 v2 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 * +// PUSH v4| v1 v2 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 * v4 +// SWAP14| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 * v17 +// SWAP1| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 * +// PUSH v18| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 * v18 +// SWAP1| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 * +// PUSH v19| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 * v19 +// SWAP1| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 * +// POP| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// PUSH v20| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 +// +-------------------------------------------------------------------------------------------------------------------------------------------- +------- +// (target)| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | +// Status: Admissible +// Spilled: {v4, v18, v19, v20} diff --git a/test/libyul/ssa/stackShuffler/spilled_to_pop_deep_junk.stack b/test/libyul/ssa/stackShuffler/spilled_to_pop_deep_junk.stack new file mode 100644 index 000000000000..42fe6acfb0a7 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spilled_to_pop_deep_junk.stack @@ -0,0 +1,41 @@ +allowSpilling: false +initialSpilledSet: {v1, v2, v4, v20} +initial: [JUNK, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20] +targetStackTop: [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20] +// ---- +// | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 20 +// +-------------------------------------------------------------------------------------------------------------------------------------------- +------- +// (initial)| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 | v20 +// SWAP1| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v20 | v19 +// SWAP2| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v19 v20 | v18 +// SWAP3| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v18 v19 v20 | v17 +// SWAP4| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v17 v18 v19 v20 | v16 +// SWAP5| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v16 v17 v18 v19 v20 | v15 +// SWAP6| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v15 v16 v17 v18 v19 v20 | v14 +// SWAP7| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v14 v15 v16 v17 v18 v19 v20 | v13 +// SWAP8| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v13 v14 v15 v16 v17 v18 v19 v20 | v12 +// SWAP9| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v11 +// SWAP10| * v1 v2 v3 v4 v5 v6 v7 v8 v9 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v10 +// SWAP11| * v1 v2 v3 v4 v5 v6 v7 v8 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v9 +// SWAP12| * v1 v2 v3 v4 v5 v6 v7 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v8 +// SWAP13| * v1 v2 v3 v4 v5 v6 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v7 +// SWAP14| * v1 v2 v3 v4 v5 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v6 +// SWAP15| * v1 v2 v3 v4 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v5 +// SWAP16| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | v4 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 +// POP| * v1 v2 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// SWAP16| * v1 v19 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v2 +// POP| * v1 v19 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 +// SWAP16| * v18 v19 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v1 +// POP| * v18 v19 v3 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 +// SWAP13| * v18 v19 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v3 +// SWAP14| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 +// PUSH v1| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 v1 +// POP| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 +// PUSH v1| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 v1 +// POP| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 +// PUSH v1| * v18 v3 v17 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v19 v1 +// ...| +// +-------------------------------------------------------------------------------------------------------------------------------------------- +------- +// (target)| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | +// Status: MaxIterationsReached diff --git a/test/libyul/ssa/stackShuffler/too_deep_dup_deep_tail_slot.stack b/test/libyul/ssa/stackShuffler/too_deep_dup_deep_tail_slot.stack index 79a02a199520..45fd50185b0d 100644 --- a/test/libyul/ssa/stackShuffler/too_deep_dup_deep_tail_slot.stack +++ b/test/libyul/ssa/stackShuffler/too_deep_dup_deep_tail_slot.stack @@ -9,4 +9,4 @@ targetStackSize: 19 // (initial)| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 // +------------------------------------------------------------------------------------------------------------------------------ +------- // (target)| {v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18} | v1 -// Status: StackTooDeep (culprit: v1) +// Status: StackTooDeep (culprit: v18) diff --git a/test/libyul/ssa/stackShuffler/too_deep_unreachable_slot.stack b/test/libyul/ssa/stackShuffler/too_deep_unreachable_slot.stack index 67f1238a0a23..711b33aa5a04 100644 --- a/test/libyul/ssa/stackShuffler/too_deep_unreachable_slot.stack +++ b/test/libyul/ssa/stackShuffler/too_deep_unreachable_slot.stack @@ -9,4 +9,4 @@ targetStackTop: [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v1 // (initial)| v18 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v1 // +------------------------------------------------------------------------------------------------------------------------------ // (target)| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 -// Status: StackTooDeep (culprit: v18) +// Status: StackTooDeep (culprit: v1)