From e9f2108b81f81c5b4eaeba5ec98a4f31cbcffaba Mon Sep 17 00:00:00 2001 From: clonker <1685266+clonker@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:34:45 +0200 Subject: [PATCH 1/4] SSA CFG stack abstraction can determine `canBeFreelyGenerated` based on a dynamic spill set --- libyul/backends/evm/ssa/Stack.h | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libyul/backends/evm/ssa/Stack.h b/libyul/backends/evm/ssa/Stack.h index d4342426f09d..9a057da35c49 100644 --- a/libyul/backends/evm/ssa/Stack.h +++ b/libyul/backends/evm/ssa/Stack.h @@ -25,6 +25,7 @@ #include #include +#include #include namespace solidity::yul @@ -129,6 +130,7 @@ static_assert(std::is_standard_layout_v, "Want to have a predictable static_assert(std::is_trivial_v, "Want to have no init/cpy overhead"); using StackData = std::vector; +using SpilledVariables = std::map; std::string slotToString(StackSlot const& _slot); std::string stackToString(StackData const& _stackData); @@ -273,11 +275,22 @@ class Stack return Depth{static_cast(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); } @@ -305,6 +318,7 @@ class Stack private: Data* m_data; Callbacks m_callbacks; + SpilledVariables const* m_spilledVariables = nullptr; }; } From 11ca7e2c47fd210f6cb17540752c29543cdf43f5 Mon Sep 17 00:00:00 2001 From: clonker <1685266+clonker@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:14:58 +0200 Subject: [PATCH 2/4] Adapt ssa stack shuffler to be able to deal with spill set - allNecessarySlotsReachableOrFinal takes spill set into account - target min counts / required in tail checks exclude spilled variables --- libyul/backends/evm/ssa/StackShuffler.cpp | 28 +++++++++++++++---- libyul/backends/evm/ssa/StackShuffler.h | 34 +++++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/libyul/backends/evm/ssa/StackShuffler.cpp b/libyul/backends/evm/ssa/StackShuffler.cpp index 4d5c5dbc6301..6ac366a1136c 100644 --- a/libyul/backends/evm/ssa/StackShuffler.cpp +++ b/libyul/backends/evm/ssa/StackShuffler.cpp @@ -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): @@ -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 diff --git a/libyul/backends/evm/ssa/StackShuffler.h b/libyul/backends/evm/ssa/StackShuffler.h index 14c136a142ae..e333afdba178 100644 --- a/libyul/backends/evm/ssa/StackShuffler.h +++ b/libyul/backends/evm/ssa/StackShuffler.h @@ -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; std::size_t const size; std::size_t const tailSize; boost::container::flat_map minCount; @@ -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); @@ -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"); } @@ -810,7 +834,7 @@ class StackShuffler } else { - if (_stack.isBeyondSwapRange(*depth)) + if (_stack.isBeyondSwapRange(*depth) && !_stack.canBeFreelyGenerated(_state.targetArg(offset))) return _stack.depthToOffset(*depth); } } @@ -828,7 +852,7 @@ class StackShuffler std::optional depth = _stack.findSlotDepth(slotAtOffset); // it must exist yulAssert(depth); - if (!_stack.dupReachable(*depth)) + if (!_stack.dupReachable(*depth) && !_stack.canBeFreelyGenerated(slotAtOffset)) return _stack.depthToOffset(*depth); } } From 2c9d8b2805fdaf56c6512d167018ab1a837e4187 Mon Sep 17 00:00:00 2001 From: clonker <1685266+clonker@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:15:11 +0200 Subject: [PATCH 3/4] findOptimalTargetSize pretends to do stack spilling --- libyul/backends/evm/ssa/StackUtils.cpp | 38 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/libyul/backends/evm/ssa/StackUtils.cpp b/libyul/backends/evm/ssa/StackUtils.cpp index dbc9b1541e9e..2feb6e5ede05 100644 --- a/libyul/backends/evm/ssa/StackUtils.cpp +++ b/libyul/backends/evm/ssa/StackUtils.cpp @@ -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 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, {}); + countOpsStack.setSpilledVariables(&spillSet); + result = StackShuffler::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); From 4d4f87d2297febb8649577f2b3bf2994fae79ec2 Mon Sep 17 00:00:00 2001 From: clonker <1685266+clonker@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:08:08 +0200 Subject: [PATCH 4/4] add stack spilling tests to ssa stack shuffler tests --- test/libyul/ssa/StackShufflerTest.cpp | 57 +++++++++++++++++++ .../ssa/stackShuffler/initial_spill_set.stack | 16 ++++++ .../stackShuffler/spill_arg_in_liveout.stack | 17 ++++++ .../spill_disabled_default.stack | 13 +++++ .../spill_many_for_small_target.stack | 17 ++++++ .../spill_to_pop_deep_junk.stack | 41 +++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 test/libyul/ssa/stackShuffler/initial_spill_set.stack create mode 100644 test/libyul/ssa/stackShuffler/spill_arg_in_liveout.stack create mode 100644 test/libyul/ssa/stackShuffler/spill_disabled_default.stack create mode 100644 test/libyul/ssa/stackShuffler/spill_many_for_small_target.stack create mode 100644 test/libyul/ssa/stackShuffler/spill_to_pop_deep_junk.stack diff --git a/test/libyul/ssa/StackShufflerTest.cpp b/test/libyul/ssa/StackShufflerTest.cpp index 993ba4bb97f7..a260f72f3431 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.emplace(valueId, 0u); } @@ -475,6 +491,34 @@ 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::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); @@ -482,6 +526,7 @@ explicitly provided.)"; { trace.record(op, stackData); }}); + stack.setSpilledVariables(&spillSet); shuffleResult = StackShuffler::shuffle( stack, *testConfig.targetStackTop, @@ -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) { @@ -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(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..5c1b30c834f9 --- /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)| 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..bfc4d93869d1 --- /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..c737f941d6f0 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_disabled_default.stack @@ -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. +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) 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..cbcc023f189f --- /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 +// 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..b067bf8bc673 --- /dev/null +++ b/test/libyul/ssa/stackShuffler/spill_to_pop_deep_junk.stack @@ -0,0 +1,41 @@ +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 +// SWAP15| * v1 v2 v19 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v3 +// SWAP16| * v1 v3 v19 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v2 +// PUSH v4| * v1 v3 v19 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v2 v4 +// SWAP16| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v2 v19 +// SWAP1| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v2 +// POP| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// PUSH v2| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v2 +// POP| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// PUSH v2| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v2 +// POP| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 +// PUSH v2| * v1 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v2 +// ...| +// +-------------------------------------------------------------------------------------------------------------------------------------------- +------- +// (target)| v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20 | +// Status: MaxIterationsReached +// Spilled: {v1, v2, v4, v20}