From 5f291e4c1d01fe005bde97cb5cf2c5208b9922b0 Mon Sep 17 00:00:00 2001 From: r0qs Date: Fri, 9 Feb 2024 17:10:02 +0100 Subject: [PATCH] Define ShuffleOperations interface --- libyul/backends/evm/StackHelpers.h | 302 ++++------------- libyul/backends/evm/StackLayoutGenerator.cpp | 111 +----- libyul/backends/evm/StackShuffle.h | 335 +++++++++++++++++++ 3 files changed, 423 insertions(+), 325 deletions(-) create mode 100644 libyul/backends/evm/StackShuffle.h diff --git a/libyul/backends/evm/StackHelpers.h b/libyul/backends/evm/StackHelpers.h index 5e46adf9902c..e62231da7a55 100644 --- a/libyul/backends/evm/StackHelpers.h +++ b/libyul/backends/evm/StackHelpers.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include #include @@ -55,69 +56,22 @@ inline std::string stackToString(Stack const& _stack) return result; } - -// Abstraction of stack shuffling operations. Can be defined as actual concept once we switch to C++20. -// Used as an interface for the stack shuffler below. -// The shuffle operation class is expected to internally keep track of a current stack layout (the "source layout") -// that the shuffler is supposed to shuffle to a fixed target stack layout. -// The shuffler works iteratively. At each iteration it instantiates an instance of the shuffle operations and -// queries it for various information about the current source stack layout and the target layout, as described -// in the interface below. -// Based on that information the shuffler decides which is the next optimal operation to perform on the stack -// and calls the corresponding entry point in the shuffling operations (swap, pushOrDupTarget or pop). -/* -template -concept ShuffleOperationConcept = requires(ShuffleOperations ops, size_t sourceOffset, size_t targetOffset, size_t depth) { - // Returns true, iff the current slot at sourceOffset in source layout is a suitable slot at targetOffset. - { ops.isCompatible(sourceOffset, targetOffset) } -> std::convertible_to; - // Returns true, iff the slots at the two given source offsets are identical. - { ops.sourceIsSame(sourceOffset, sourceOffset) } -> std::convertible_to; - // Returns a positive integer n, if the slot at the given source offset needs n more copies. - // Returns a negative integer -n, if the slot at the given source offsets occurs n times too many. - // Returns zero if the amount of occurrences, in the current source layout, of the slot at the given source offset - // matches the desired amount of occurrences in the target. - { ops.sourceMultiplicity(sourceOffset) } -> std::convertible_to; - // Returns a positive integer n, if the slot at the given target offset needs n more copies. - // Returns a negative integer -n, if the slot at the given target offsets occurs n times too many. - // Returns zero if the amount of occurrences, in the current source layout, of the slot at the given target offset - // matches the desired amount of occurrences in the target. - { ops.targetMultiplicity(targetOffset) } -> std::convertible_to; - // Returns true, iff any slot is compatible with the given target offset. - { ops.targetIsArbitrary(targetOffset) } -> std::convertible_to; - // Returns the number of slots in the source layout. - { ops.sourceSize() } -> std::convertible_to; - // Returns the number of slots in the target layout. - { ops.targetSize() } -> std::convertible_to; - // Swaps the top most slot in the source with the slot `depth` slots below the top. - // In terms of EVM opcodes this is supposed to be a `SWAP`. - // In terms of vectors this is supposed to be `std::swap(source.at(source.size() - depth - 1, source.top))`. - { ops.swap(depth) }; - // Pops the top most slot in the source, i.e. the slot at offset ops.sourceSize() - 1. - // In terms of EVM opcodes this is `POP`. - // In terms of vectors this is `source.pop();`. - { ops.pop() }; - // Dups or pushes the slot that is supposed to end up at the given target offset. - { ops.pushOrDupTarget(targetOffset) }; -}; -*/ /// Helper class that can perform shuffling of a source stack layout to a target stack layout via /// abstracted shuffle operations. -template class Shuffler { public: - /// Executes the stack shuffling operations. Instantiates an instance of ShuffleOperations - /// in each iteration. Each iteration performs exactly one operation that modifies the stack. + /// Executes the stack shuffling operations. + /// Each iteration performs exactly one operation that modifies the stack. /// After `shuffle`, source and target have the same size and all slots in the source layout are /// compatible with the slots at the same target offset. - template - static void shuffle(Args&&... args) + static void shuffle(ShuffleOperations& _ops) { bool needsMoreShuffling = true; // The shuffling algorithm should always terminate in polynomial time, but we provide a limit // in case it does not terminate due to a bug. size_t iterationCount = 0; - while (iterationCount < 1000 && (needsMoreShuffling = shuffleStep(std::forward(args)...))) + while (iterationCount < 1000 && (needsMoreShuffling = shuffleStep(_ops))) ++iterationCount; yulAssert(!needsMoreShuffling, "Could not create stack layout after 1000 iterations."); } @@ -210,165 +164,164 @@ class Shuffler return false; } /// Performs a single stack operation, transforming the source layout closer to the target layout. - template - static bool shuffleStep(Args&&... args) + static bool shuffleStep(ShuffleOperations& _ops) { - ShuffleOperations ops{std::forward(args)...}; + _ops.updateMultiplicity(); // All source slots are final. if (ranges::all_of( - ranges::views::iota(0u, ops.sourceSize()), - [&](size_t _index) { return ops.isCompatible(_index, _index); } + ranges::views::iota(0u, _ops.sourceSize()), + [&](size_t _index) { return _ops.isCompatible(_index, _index); } )) { // Bring up all remaining target slots, if any, or terminate otherwise. - if (ops.sourceSize() < ops.targetSize()) + if (_ops.sourceSize() < _ops.targetSize()) { - if (!dupDeepSlotIfRequired(ops)) - yulAssert(bringUpTargetSlot(ops, ops.sourceSize()), ""); + if (!dupDeepSlotIfRequired(_ops)) + yulAssert(bringUpTargetSlot(_ops, _ops.sourceSize()), ""); return true; } return false; } - size_t sourceTop = ops.sourceSize() - 1; + size_t sourceTop = _ops.sourceSize() - 1; // If we no longer need the current stack top, we pop it, unless we need an arbitrary slot at this position // in the target. if ( - ops.sourceMultiplicity(sourceTop) < 0 && - !ops.targetIsArbitrary(sourceTop) + _ops.sourceMultiplicity(sourceTop) < 0 && + !_ops.targetIsArbitrary(sourceTop) ) { - ops.pop(); + _ops.pop(); return true; } - yulAssert(ops.targetSize() > 0, ""); + yulAssert(_ops.targetSize() > 0, ""); // If the top is not supposed to be exactly what is on top right now, try to find a lower position to swap it to. - if (!ops.isCompatible(sourceTop, sourceTop) || ops.targetIsArbitrary(sourceTop)) - for (size_t offset: ranges::views::iota(0u, std::min(ops.sourceSize(), ops.targetSize()))) + if (!_ops.isCompatible(sourceTop, sourceTop) || _ops.targetIsArbitrary(sourceTop)) + for (size_t offset: ranges::views::iota(0u, std::min(_ops.sourceSize(), _ops.targetSize()))) // It makes sense to swap to a lower position, if if ( - !ops.isCompatible(offset, offset) && // The lower slot is not already in position. - !ops.sourceIsSame(offset, sourceTop) && // We would not just swap identical slots. - ops.isCompatible(sourceTop, offset) // The lower position wants to have this slot. + !_ops.isCompatible(offset, offset) && // The lower slot is not already in position. + !_ops.sourceIsSame(offset, sourceTop) && // We would not just swap identical slots. + _ops.isCompatible(sourceTop, offset) // The lower position wants to have this slot. ) { // We cannot swap that deep. - if (ops.sourceSize() - offset - 1 > 16) + if (_ops.sourceSize() - offset - 1 > 16) { // If there is a reachable slot to be removed, park the current top there. for (size_t swapDepth: ranges::views::iota(1u, 17u) | ranges::views::reverse) - if (ops.sourceMultiplicity(ops.sourceSize() - 1 - swapDepth) < 0) + if (_ops.sourceMultiplicity(_ops.sourceSize() - 1 - swapDepth) < 0) { - ops.swap(swapDepth); - if (ops.targetIsArbitrary(sourceTop)) + _ops.swap(swapDepth); + if (_ops.targetIsArbitrary(sourceTop)) // Usually we keep a slot that is to-be-removed, if the current top is arbitrary. // However, since we are in a stack-too-deep situation, pop it immediately // to compress the stack (we can always push back junk in the end). - ops.pop(); + _ops.pop(); return true; } // Otherwise we rely on stack compression or stack-to-memory. } - ops.swap(ops.sourceSize() - offset - 1); + _ops.swap(_ops.sourceSize() - offset - 1); return true; } // ops.sourceSize() > ops.targetSize() cannot be true anymore, since if the source top is no longer required, // we already popped it, and if it is required, we already swapped it down to a suitable target position. - yulAssert(ops.sourceSize() <= ops.targetSize(), ""); + yulAssert(_ops.sourceSize() <= _ops.targetSize(), ""); // If a lower slot should be removed, try to bring up the slot that should end up there and bring it up. // Note that after the cases above, there will always be a target slot to duplicate in this case. - for (size_t offset: ranges::views::iota(0u, ops.sourceSize())) + for (size_t offset: ranges::views::iota(0u, _ops.sourceSize())) if ( - !ops.isCompatible(offset, offset) && // The lower slot is not already in position. - ops.sourceMultiplicity(offset) < 0 && // We have too many copies of this slot. - offset <= ops.targetSize() && // There is a target slot at this position. - !ops.targetIsArbitrary(offset) // And that target slot is not arbitrary. + !_ops.isCompatible(offset, offset) && // The lower slot is not already in position. + _ops.sourceMultiplicity(offset) < 0 && // We have too many copies of this slot. + offset <= _ops.targetSize() && // There is a target slot at this position. + !_ops.targetIsArbitrary(offset) // And that target slot is not arbitrary. ) { - if (!dupDeepSlotIfRequired(ops)) - yulAssert(bringUpTargetSlot(ops, offset), ""); + if (!dupDeepSlotIfRequired(_ops)) + yulAssert(bringUpTargetSlot(_ops, offset), ""); return true; } // At this point we want to keep all slots. - for (size_t i = 0; i < ops.sourceSize(); ++i) - yulAssert(ops.sourceMultiplicity(i) >= 0, ""); - yulAssert(ops.sourceSize() <= ops.targetSize(), ""); + for (size_t i = 0; i < _ops.sourceSize(); ++i) + yulAssert(_ops.sourceMultiplicity(i) >= 0, ""); + yulAssert(_ops.sourceSize() <= _ops.targetSize(), ""); // If the top is not in position, try to find a slot that wants to be at the top and swap it up. - if (!ops.isCompatible(sourceTop, sourceTop)) - for (size_t sourceOffset: ranges::views::iota(0u, ops.sourceSize())) + if (!_ops.isCompatible(sourceTop, sourceTop)) + for (size_t sourceOffset: ranges::views::iota(0u, _ops.sourceSize())) if ( - !ops.isCompatible(sourceOffset, sourceOffset) && - ops.isCompatible(sourceOffset, sourceTop) + !_ops.isCompatible(sourceOffset, sourceOffset) && + _ops.isCompatible(sourceOffset, sourceTop) ) { - ops.swap(ops.sourceSize() - sourceOffset - 1); + _ops.swap(_ops.sourceSize() - sourceOffset - 1); return true; } // If we still need more slots, produce a suitable one. - if (ops.sourceSize() < ops.targetSize()) + if (_ops.sourceSize() < _ops.targetSize()) { - if (!dupDeepSlotIfRequired(ops)) - yulAssert(bringUpTargetSlot(ops, ops.sourceSize()), ""); + if (!dupDeepSlotIfRequired(_ops)) + yulAssert(bringUpTargetSlot(_ops, _ops.sourceSize()), ""); return true; } // The stack has the correct size, each slot has the correct number of copies and the top is in position. - yulAssert(ops.sourceSize() == ops.targetSize(), ""); - size_t size = ops.sourceSize(); - for (size_t i = 0; i < ops.sourceSize(); ++i) - yulAssert(ops.sourceMultiplicity(i) == 0 && (ops.targetIsArbitrary(i) || ops.targetMultiplicity(i) == 0), ""); - yulAssert(ops.isCompatible(sourceTop, sourceTop), ""); + yulAssert(_ops.sourceSize() == _ops.targetSize(), ""); + size_t size = _ops.sourceSize(); + for (size_t i = 0; i < _ops.sourceSize(); ++i) + yulAssert(_ops.sourceMultiplicity(i) == 0 && (_ops.targetIsArbitrary(i) || _ops.targetMultiplicity(i) == 0), ""); + yulAssert(_ops.isCompatible(sourceTop, sourceTop), ""); auto swappableOffsets = ranges::views::iota(size > 17 ? size - 17 : 0u, size); // If we find a lower slot that is out of position, but also compatible with the top, swap that up. for (size_t offset: swappableOffsets) - if (!ops.isCompatible(offset, offset) && ops.isCompatible(sourceTop, offset)) + if (!_ops.isCompatible(offset, offset) && _ops.isCompatible(sourceTop, offset)) { - ops.swap(size - offset - 1); + _ops.swap(size - offset - 1); return true; } // Swap up any reachable slot that is still out of position. for (size_t offset: swappableOffsets) - if (!ops.isCompatible(offset, offset) && !ops.sourceIsSame(offset, sourceTop)) + if (!_ops.isCompatible(offset, offset) && !_ops.sourceIsSame(offset, sourceTop)) { - ops.swap(size - offset - 1); + _ops.swap(size - offset - 1); return true; } // We are in a stack-too-deep situation and try to reduce the stack size. // If the current top is merely kept since the target slot is arbitrary, pop it. - if (ops.targetIsArbitrary(sourceTop) && ops.sourceMultiplicity(sourceTop) <= 0) + if (_ops.targetIsArbitrary(sourceTop) && _ops.sourceMultiplicity(sourceTop) <= 0) { - ops.pop(); + _ops.pop(); return true; } // If any reachable slot is merely kept, since the target slot is arbitrary, swap it up and pop it. for (size_t offset: swappableOffsets) - if (ops.targetIsArbitrary(offset) && ops.sourceMultiplicity(offset) <= 0) + if (_ops.targetIsArbitrary(offset) && _ops.sourceMultiplicity(offset) <= 0) { - ops.swap(size - offset - 1); - ops.pop(); + _ops.swap(size - offset - 1); + _ops.pop(); return true; } // We cannot avoid a stack-too-deep error. Repeat the above without restricting to reachable slots. for (size_t offset: ranges::views::iota(0u, size)) - if (!ops.isCompatible(offset, offset) && ops.isCompatible(sourceTop, offset)) + if (!_ops.isCompatible(offset, offset) && _ops.isCompatible(sourceTop, offset)) { - ops.swap(size - offset - 1); + _ops.swap(size - offset - 1); return true; } for (size_t offset: ranges::views::iota(0u, size)) - if (!ops.isCompatible(offset, offset) && !ops.sourceIsSame(offset, sourceTop)) + if (!_ops.isCompatible(offset, offset) && !_ops.sourceIsSame(offset, sourceTop)) { - ops.swap(size - offset - 1); + _ops.swap(size - offset - 1); return true; } yulAssert(false, ""); @@ -378,50 +331,6 @@ class Shuffler } }; -class IndexingMap -{ -public: - size_t operator[](StackSlot const& _slot) - { - if (auto* p = std::get_if(&_slot)) - return getIndex(m_functionCallReturnLabelSlotIndex, *p); - if (std::holds_alternative(_slot)) - { - m_indexedSlots[1] = _slot; - return 1; - } - if (auto* p = std::get_if(&_slot)) - return getIndex(m_variableSlotIndex, *p); - if (auto* p = std::get_if(&_slot)) - return getIndex(m_literalSlotIndex, *p); - if (auto* p = std::get_if(&_slot)) - return getIndex(m_temporarySlotIndex, *p); - m_indexedSlots[0] = _slot; - return 0; - } - std::vector indexedSlots() - { - return std::move(m_indexedSlots); - } -private: - template - size_t getIndex(MapType&& _map, ElementType&& _element) - { - auto [element, newlyInserted] = _map.emplace(std::make_pair(_element, size_t(0u))); - if (newlyInserted) - { - element->second = m_indexedSlots.size(); - m_indexedSlots.emplace_back(_element); - } - return element->second; - } - std::map m_functionCallReturnLabelSlotIndex; - std::map m_variableSlotIndex; - std::map m_literalSlotIndex; - std::map m_temporarySlotIndex; - std::vector m_indexedSlots{JunkSlot{}, JunkSlot{}}; -}; - /// Transforms @a _currentStack to @a _targetStack, invoking the provided shuffling operations. /// Modifies @a _currentStack itself after each invocation of the shuffling operations. /// @a _swap is a function with signature void(unsigned) that is called when the top most slot is swapped with @@ -432,9 +341,8 @@ class IndexingMap template void createStackLayout(Stack& _currentStack, Stack const& _targetStack, Swap _swap, PushOrDup _pushOrDup, Pop _pop) { + // TODO: refactor IndexedStack std::vector indexedSlots; - using IndexedStack = std::vector; - size_t junkIndex = 0; IndexingMap indexer; auto indexTransform = ranges::views::transform([&](auto const& _slot) { return indexer[_slot]; }); IndexedStack _targetStackIndexed = _targetStack | indexTransform | ranges::to; @@ -454,87 +362,17 @@ void createStackLayout(Stack& _currentStack, Stack const& _targetStack, Swap _sw _currentStack.pop_back(); }; - - struct ShuffleOperations - { - IndexedStack& currentStack; - IndexedStack const& targetStack; - decltype(swapIndexed) swapCallback; - decltype(pushOrDupIndexed) pushOrDupCallback; - decltype(popIndexed) popCallback; - std::vector multiplicity; - size_t junkIndex = std::numeric_limits::max(); - ShuffleOperations( - IndexedStack& _currentStack, - IndexedStack const& _targetStack, - decltype(swapIndexed) _swap, - decltype(pushOrDupIndexed) _pushOrDup, - decltype(popIndexed) _pop, - size_t _numSlots, - size_t _junkIndex - ): - currentStack(_currentStack), - targetStack(_targetStack), - swapCallback(_swap), - pushOrDupCallback(_pushOrDup), - popCallback(_pop), - junkIndex(_junkIndex) - { - multiplicity.resize(_numSlots, 0); - for (auto const& slot: currentStack) - --multiplicity[slot]; - for (auto&& [offset, slot]: targetStack | ranges::views::enumerate) - if (slot == _junkIndex && offset < currentStack.size()) - ++multiplicity[currentStack.at(offset)]; - else - ++multiplicity[slot]; - } - bool isCompatible(size_t _source, size_t _target) - { - return - _source < currentStack.size() && - _target < targetStack.size() && - ( - junkIndex == targetStack.at(_target) || - currentStack.at(_source) == targetStack.at(_target) - ); - } - bool sourceIsSame(size_t _lhs, size_t _rhs) { return currentStack.at(_lhs) == currentStack.at(_rhs); } - int sourceMultiplicity(size_t _offset) { return multiplicity.at(currentStack.at(_offset)); } - int targetMultiplicity(size_t _offset) { return multiplicity.at(targetStack.at(_offset)); } - bool targetIsArbitrary(size_t offset) - { - return offset < targetStack.size() && junkIndex == targetStack.at(offset); - } - void swap(size_t _i) - { - swapCallback(static_cast(_i)); - std::swap(currentStack.at(currentStack.size() - _i - 1), currentStack.back()); - } - size_t sourceSize() { return currentStack.size(); } - size_t targetSize() { return targetStack.size(); } - void pop() - { - popCallback(); - currentStack.pop_back(); - } - void pushOrDupTarget(size_t _offset) - { - auto const& targetSlot = targetStack.at(_offset); - pushOrDupCallback(targetSlot); - currentStack.push_back(targetSlot); - } - }; - - Shuffler::shuffle( + StackShuffleOperations ops( _currentStackIndexed, _targetStackIndexed, swapIndexed, pushOrDupIndexed, popIndexed, indexedSlots.size(), - junkIndex + 0 // junkIndex ); + Shuffler shuffler{}; + shuffler.shuffle(ops); yulAssert(_currentStack.size() == _targetStack.size(), ""); for (auto&& [current, target]: ranges::zip_view(_currentStack, _targetStack)) diff --git a/libyul/backends/evm/StackLayoutGenerator.cpp b/libyul/backends/evm/StackLayoutGenerator.cpp index 03c68ed6a062..156a9cb18601 100644 --- a/libyul/backends/evm/StackLayoutGenerator.cpp +++ b/libyul/backends/evm/StackLayoutGenerator.cpp @@ -139,28 +139,24 @@ std::vector findStackTooDeep(Stack const& _s } /// @returns the ideal stack to have before executing an operation that outputs @a _operationOutput, s.t. -/// shuffling to @a _post is cheap (excluding the input of the operation itself). +/// shuffling to @a _targetStack is cheap (excluding the input of the operation itself). /// If @a _generateSlotOnTheFly returns true for a slot, this slot should not occur in the ideal stack, but /// rather be generated on the fly during shuffling. template -Stack createIdealLayout(Stack const& _operationOutput, Stack const& _post, Callable _generateSlotOnTheFly) +Stack createIdealLayout(Stack const& _operationOutput, Stack const& _targetStack, Callable _generateSlotOnTheFly) { std::vector indexedSlots; - using IndexedStack = std::vector; - size_t junkIndex = std::numeric_limits::max(); IndexingMap indexer; auto indexTransform = ranges::views::transform([&](auto const& _slot) { return indexer[_slot]; }); IndexedStack operationOutputIndexed = _operationOutput | indexTransform | ranges::to; - IndexedStack postIndexed = _post | indexTransform | ranges::to; + IndexedStack targetStackIndexed = _targetStack | indexTransform | ranges::to; indexedSlots = indexer.indexedSlots(); - struct PreviousSlot { size_t slot; }; - // Determine the number of slots that have to be on stack before executing the operation (excluding - // the inputs of the operation itself). + // the outputs of the operation itself). // That is slots that should not be generated on the fly and are not outputs of the operation. - size_t preOperationLayoutSize = _post.size(); - for (auto const& slot: _post) + size_t preOperationLayoutSize = _targetStack.size(); + for (auto const& slot: _targetStack) if (util::contains(_operationOutput, slot) || _generateSlotOnTheFly(slot)) --preOperationLayoutSize; @@ -179,95 +175,24 @@ Stack createIdealLayout(Stack const& _operationOutput, Stack const& _post, Calla return _generateSlotOnTheFly(indexedSlots.at(_slot)); }; - // Next we will shuffle the layout to the post stack using ShuffleOperations + // Next we will shuffle the layout to the target stack using ShuffleOperations // that are aware of PreviousSlot's. - struct ShuffleOperations - { - std::vector>& layout; - IndexedStack const& post; - std::set outputs; - std::vector multiplicity; - decltype(generateSlotOnTheFlyIndexed) generateSlotOnTheFly; - size_t junkIndex = std::numeric_limits::max(); - ShuffleOperations( - std::vector>& _layout, - IndexedStack const& _post, - decltype(generateSlotOnTheFlyIndexed) _generateSlotOnTheFly, - size_t _numSlots, - size_t _junkIndex - ): layout(_layout), post(_post), generateSlotOnTheFly(_generateSlotOnTheFly), junkIndex(_junkIndex) - { - multiplicity.resize(_numSlots, 0); - for (auto const& layoutSlot: layout) - if (size_t const* slot = std::get_if(&layoutSlot)) - outputs.insert(*slot); - - for (auto const& layoutSlot: layout) - if (size_t const* slot = std::get_if(&layoutSlot)) - --multiplicity[*slot]; - for (auto&& slot: post) - if (outputs.count(slot) || generateSlotOnTheFly(slot)) - ++multiplicity[slot]; - } - bool isCompatible(size_t _source, size_t _target) - { - return - _source < layout.size() && - _target < post.size() && - ( - junkIndex == post.at(_target) || - std::visit(util::GenericVisitor{ - [&](PreviousSlot const&) { - return !outputs.count(post.at(_target)) && !generateSlotOnTheFly(post.at(_target)); - }, - [&](size_t const& _s) { return _s == post.at(_target); } - }, layout.at(_source)) - ); - } - bool sourceIsSame(size_t _lhs, size_t _rhs) - { - return std::visit(util::GenericVisitor{ - [&](PreviousSlot const&, PreviousSlot const&) { return true; }, - [&](size_t const& _lhs, size_t const& _rhs) { return _lhs == _rhs; }, - [&](auto const&, auto const&) { return false; } - }, layout.at(_lhs), layout.at(_rhs)); - } - int sourceMultiplicity(size_t _offset) - { - return std::visit(util::GenericVisitor{ - [&](PreviousSlot const&) { return 0; }, - [&](size_t _s) { return multiplicity.at(_s); } - }, layout.at(_offset)); - } - int targetMultiplicity(size_t _offset) - { - if (!outputs.count(post.at(_offset)) && !generateSlotOnTheFly(post.at(_offset))) - return 0; - return multiplicity.at(post.at(_offset)); - } - bool targetIsArbitrary(size_t _offset) - { - return _offset < post.size() && junkIndex == post.at(_offset); - } - void swap(size_t _i) - { - yulAssert(!std::holds_alternative(layout.at(layout.size() - _i - 1)) || !std::holds_alternative(layout.back()), ""); - std::swap(layout.at(layout.size() - _i - 1), layout.back()); - } - size_t sourceSize() { return layout.size(); } - size_t targetSize() { return post.size(); } - void pop() { layout.pop_back(); } - void pushOrDupTarget(size_t _offset) { layout.push_back(post.at(_offset)); } - }; - Shuffler::shuffle(layout, postIndexed, generateSlotOnTheFlyIndexed, indexedSlots.size(), junkIndex); + SymbolicStackShuffleOperations ops( + layout, + targetStackIndexed, + generateSlotOnTheFlyIndexed, + indexedSlots.size() + ); + Shuffler shuffler{}; + shuffler.shuffle(ops); // Now we can construct the ideal layout before the operation. // "layout" has shuffled the PreviousSlot{x} to new places using minimal operations to move the operation // output in place. The resulting permutation of the PreviousSlot yields the ideal positions of slots - // before the operation, i.e. if PreviousSlot{2} is at a position at which _post contains VariableSlot{"tmp"}, + // before the operation, i.e. if PreviousSlot{2} is at a position at which _targetStack contains VariableSlot{"tmp"}, // then we want the variable tmp in the slot at offset 2 in the layout before the operation. - std::vector> idealLayout(postIndexed.size(), std::nullopt); - for (auto&& [slot, idealPosition]: ranges::zip_view(postIndexed, layout)) + std::vector> idealLayout(targetStackIndexed.size(), std::nullopt); + for (auto&& [slot, idealPosition]: ranges::zip_view(targetStackIndexed, layout)) if (PreviousSlot* previousSlot = std::get_if(&idealPosition)) idealLayout.at(previousSlot->slot) = indexedSlots.at(slot); diff --git a/libyul/backends/evm/StackShuffle.h b/libyul/backends/evm/StackShuffle.h new file mode 100644 index 000000000000..1526fa3a8ce0 --- /dev/null +++ b/libyul/backends/evm/StackShuffle.h @@ -0,0 +1,335 @@ + +/* + 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 +{ + +class IndexingMap +{ +public: + size_t operator[](StackSlot const& _slot) + { + if (auto* p = std::get_if(&_slot)) + return getIndex(m_functionCallReturnLabelSlotIndex, *p); + if (std::holds_alternative(_slot)) + { + m_indexedSlots[1] = _slot; + return 1; + } + if (auto* p = std::get_if(&_slot)) + return getIndex(m_variableSlotIndex, *p); + if (auto* p = std::get_if(&_slot)) + return getIndex(m_literalSlotIndex, *p); + if (auto* p = std::get_if(&_slot)) + return getIndex(m_temporarySlotIndex, *p); + m_indexedSlots[0] = _slot; + return 0; + } + std::vector indexedSlots() + { + return std::move(m_indexedSlots); + } +private: + template + size_t getIndex(MapType&& _map, ElementType&& _element) + { + auto [element, newlyInserted] = _map.emplace(std::make_pair(_element, size_t(0u))); + if (newlyInserted) + { + element->second = m_indexedSlots.size(); + m_indexedSlots.emplace_back(_element); + } + return element->second; + } + std::map m_functionCallReturnLabelSlotIndex; + std::map m_variableSlotIndex; + std::map m_literalSlotIndex; + std::map m_temporarySlotIndex; + std::vector m_indexedSlots{JunkSlot{}, JunkSlot{}}; +}; + +using IndexedStack = std::vector; + +// ShuffleOperations interface +// Abstraction of stack shuffling operations. Used as an interface for the stack shuffler. +// The shuffle operation class is expected to internally keep track of a current stack layout (the "source layout") +// that the shuffler is supposed to shuffle to a fixed target stack layout. +// The shuffler works iteratively. At each iteration it calls the shuffle operations implementation and +// queries it for various information about the current source stack layout and the target layout, as described +// in the interface below. +// Based on that information the shuffler decides which is the next optimal operation to perform on the stack +// and calls the corresponding entry point in the shuffling operations (swap, pushOrDupTarget or pop). +class ShuffleOperations { +public: + virtual ~ShuffleOperations() {} + virtual void updateMultiplicity() = 0; + // Returns true, iff the current slot at sourceOffset in source layout is a suitable slot at targetOffset. + virtual bool isCompatible(size_t _source, size_t _target) = 0; + // Returns true, iff the slots at the two given source offsets are identical. + virtual bool sourceIsSame(size_t _lhs, size_t _rhs) = 0; + // Returns a positive integer n, if the slot at the given source offset needs n more copies. + // Returns a negative integer -n, if the slot at the given source offsets occurs n times too many. + // Returns zero if the amount of occurrences, in the current source layout, of the slot at the given source offset + // matches the desired amount of occurrences in the target. + virtual int sourceMultiplicity(size_t _offset) = 0; + // Returns a positive integer n, if the slot at the given target offset needs n more copies. + // Returns a negative integer -n, if the slot at the given target offsets occurs n times too many. + // Returns zero if the amount of occurrences, in the current source layout, of the slot at the given target offset + // matches the desired amount of occurrences in the target. + virtual int targetMultiplicity(size_t _offset) = 0; + // Returns true, iff any slot is compatible with the given target offset. + virtual bool targetIsArbitrary(size_t _offset) = 0; + // Returns the number of slots in the source layout. + virtual size_t sourceSize() = 0; + // Returns the number of slots in the target layout. + virtual size_t targetSize() = 0; + // Swaps the top most slot in the source with the slot `depth` slots below the top. + // In terms of EVM opcodes this is supposed to be a `SWAP`. + // In terms of vectors this is supposed to be `std::swap(source.at(source.size() - depth - 1, source.top))`. + virtual void swap(size_t _depth) = 0; + // Pops the top most slot in the source, i.e. the slot at offset ops.sourceSize() - 1. + // In terms of EVM opcodes this is `POP`. + // In terms of vectors this is `source.pop();`. + virtual void pop() = 0; + // Dups or pushes the slot that is supposed to end up at the given target offset. + virtual void pushOrDupTarget(size_t _offset) = 0; +}; + + +struct PreviousSlot { size_t slot; }; +using SymbolicStackLayout = std::vector>; + +// Performs symbolic stack shuffling on top of the regular stack slots +class SymbolicStackShuffleOperations : public ShuffleOperations +{ +public: + SymbolicStackShuffleOperations( + SymbolicStackLayout& _stackLayout, + IndexedStack& _targetStack, + std::function _generateSlotOnTheFly, + size_t _numSlots + ) : + m_stackLayout(_stackLayout), + m_targetStack(_targetStack), + m_generateSlotOnTheFly(_generateSlotOnTheFly), + m_numSlots(_numSlots) + {} + + // TODO: get rid of multiplicity recalculation on every step and move it to IndexedStack + void updateMultiplicity() + { + std::vector multiplicity(m_numSlots, 0); + for (auto const& layoutSlot: m_stackLayout) + if (size_t const* slot = std::get_if(&layoutSlot)) + { + m_operationOutputs.insert(*slot); + --multiplicity[*slot]; + } + + for (auto&& slot: m_targetStack) + ++multiplicity[slot]; + m_multiplicity = multiplicity; + } + + bool isCompatible(size_t _source, size_t _target) + { + return + _source < m_stackLayout.size() && + _target < m_targetStack.size() && + ( + m_junkIndex == m_targetStack.at(_target) || + std::visit(util::GenericVisitor{ + [&](PreviousSlot const&) { + return !m_operationOutputs.count(m_targetStack.at(_target)) && !m_generateSlotOnTheFly(m_targetStack.at(_target)); + }, + [&](size_t const& _s) { return _s == m_targetStack.at(_target); } + }, m_stackLayout.at(_source)) + ); + } + + bool sourceIsSame(size_t _lhs, size_t _rhs) + { + return std::visit(util::GenericVisitor{ + [&](PreviousSlot const&, PreviousSlot const&) { return true; }, + [&](size_t const& _lhs, size_t const& _rhs) { return _lhs == _rhs; }, + [&](auto const&, auto const&) { return false; } + }, m_stackLayout.at(_lhs), m_stackLayout.at(_rhs)); + } + + int sourceMultiplicity(size_t _offset) + { + return std::visit(util::GenericVisitor{ + [&](PreviousSlot const&) { return 0; }, + [&](size_t _s) { return m_multiplicity.at(_s); } + }, m_stackLayout.at(_offset)); + } + + int targetMultiplicity(size_t _offset) + { + if (!m_operationOutputs.count(m_targetStack.at(_offset)) && !m_generateSlotOnTheFly(m_targetStack.at(_offset))) + return 0; + return m_multiplicity.at(m_targetStack.at(_offset)); + } + + bool targetIsArbitrary(size_t _offset) + { + return _offset < m_targetStack.size() && m_junkIndex == m_targetStack.at(_offset); + } + + void swap(size_t _i) + { + yulAssert( + !std::holds_alternative(m_stackLayout.at(m_stackLayout.size() - _i - 1)) || + !std::holds_alternative(m_stackLayout.back()), "" + ); + std::swap(m_stackLayout.at(m_stackLayout.size() - _i - 1), m_stackLayout.back()); + } + + size_t sourceSize() { return m_stackLayout.size(); } + + size_t targetSize() { return m_targetStack.size(); } + + void pop() { m_stackLayout.pop_back(); } + + void pushOrDupTarget(size_t _offset) + { + m_stackLayout.push_back(m_targetStack.at(_offset)); + } + +private: + SymbolicStackLayout& m_stackLayout; + IndexedStack& m_targetStack; + std::function m_generateSlotOnTheFly; + size_t m_numSlots; + std::set m_operationOutputs{}; + std::vector m_multiplicity; + size_t m_junkIndex = std::numeric_limits::max(); +}; + +// Performs stack shuffling over stack slots +template +class StackShuffleOperations : public ShuffleOperations +{ +public: + StackShuffleOperations( + IndexedStack& _currentStack, + IndexedStack const& _targetStack, + Swap _swap, + PushOrDup _pushOrDup, + Pop _pop, + size_t _numSlots, + size_t _junkIndex + ): + m_currentStack(_currentStack), + m_targetStack(_targetStack), + m_swap(_swap), + m_pushOrDup(_pushOrDup), + m_pop(_pop), + m_numSlots(_numSlots), + m_junkIndex(_junkIndex) + {} + + void updateMultiplicity() + { + std::vector multiplicity(m_numSlots, 0); + for (auto const& slot: m_currentStack) + --multiplicity[slot]; + for (auto&& [offset, slot]: m_targetStack | ranges::views::enumerate) + if (slot == m_junkIndex && offset < m_currentStack.size()) + ++multiplicity[m_currentStack.at(offset)]; + else + ++multiplicity[slot]; + m_multiplicity = multiplicity; + } + + bool isCompatible(size_t _source, size_t _target) + { + return + _source < m_currentStack.size() && + _target < m_targetStack.size() && + ( + m_junkIndex == m_targetStack.at(_target) || + m_currentStack.at(_source) == m_targetStack.at(_target) + ); + } + + bool sourceIsSame(size_t _lhs, size_t _rhs) + { + return m_currentStack.at(_lhs) == m_currentStack.at(_rhs); + } + + int sourceMultiplicity(size_t _offset) + { + return m_multiplicity.at(m_currentStack.at(_offset)); + } + + int targetMultiplicity(size_t _offset) + { + return m_multiplicity.at(m_targetStack.at(_offset)); + } + + bool targetIsArbitrary(size_t offset) + { + return offset < m_targetStack.size() && m_junkIndex == m_targetStack.at(offset); + } + + void swap(size_t _i) + { + m_swap(static_cast(_i)); + std::swap(m_currentStack.at(m_currentStack.size() - _i - 1), m_currentStack.back()); + } + + size_t sourceSize() { return m_currentStack.size(); } + + size_t targetSize() { return m_targetStack.size(); } + + void pop() + { + m_pop(); + m_currentStack.pop_back(); + } + + void pushOrDupTarget(size_t _offset) + { + auto const& targetSlot = m_targetStack.at(_offset); + m_pushOrDup(targetSlot); + m_currentStack.push_back(targetSlot); + } + +private: + IndexedStack& m_currentStack; + IndexedStack const& m_targetStack; + std::vector m_multiplicity; + Swap m_swap; + PushOrDup m_pushOrDup; + Pop m_pop; + size_t m_numSlots; + size_t m_junkIndex = std::numeric_limits::max(); +}; + +}