diff --git a/libsolutil/CMakeLists.txt b/libsolutil/CMakeLists.txt index 535b2b90f..5065dbc30 100644 --- a/libsolutil/CMakeLists.txt +++ b/libsolutil/CMakeLists.txt @@ -24,6 +24,7 @@ set(sources Keccak256.h LazyInit.h LEB128.h + Permutations.h picosha2.h Result.h SetOnce.h diff --git a/libsolutil/CommonData.h b/libsolutil/CommonData.h index 3c8b2d80c..346e76fef 100644 --- a/libsolutil/CommonData.h +++ b/libsolutil/CommonData.h @@ -52,6 +52,21 @@ template std::vector& operator+=(std::vector& _a, U&& _ std::move(_b.begin(), _b.end(), std::back_inserter(_a)); return _a; } + +/// Concatenate the contents of a container onto a list +template std::list& operator+=(std::list& _a, U& _b) +{ + for (auto const& i: _b) + _a.push_back(T(i)); + return _a; +} +/// Concatenate the contents of a container onto a list, move variant. +template std::list& operator+=(std::list& _a, U&& _b) +{ + std::move(_b.begin(), _b.end(), std::back_inserter(_a)); + return _a; +} + /// Concatenate the contents of a container onto a multiset template std::multiset& operator+=(std::multiset& _a, U& _b) { @@ -295,6 +310,50 @@ decltype(auto) mapTuple(Callable&& _callable) return detail::MapTuple{std::forward(_callable)}; } +namespace detail +{ + +template +auto findOffset(Container&& _container, Value&& _value, int) +-> decltype(_container.find(_value) == _container.end(), std::optional()) +{ + auto it = _container.find(std::forward(_value)); + auto end = _container.end(); + if (it == end) + return std::nullopt; + return std::distance(it, end); +} +template +auto findOffset(Range&& _range, Value&& _value, void*) +-> decltype(std::find(std::begin(_range), std::end(_range), std::forward(_value)) == std::end(_range), std::optional()) +{ + auto begin = std::begin(_range); + auto end = std::end(_range); + auto it = std::find(begin, end, std::forward(_value)); + if (it == end) + return std::nullopt; + return std::distance(begin, it); +} + +} + +template +auto findOffset(Range&& _range, std::remove_reference_t const& _value) +-> decltype(detail::findOffset(std::forward(_range), _value, 0)) +{ + return detail::findOffset(std::forward(_range), _value, 0); +} + +template +std::optional findOffsetPred(Range&& _range, Pred _pred) +{ + auto begin = std::begin(_range); + auto end = std::end(_range); + auto it = std::find_if(begin, end, _pred); + if (it == end) + return std::nullopt; + return std::distance(begin, it); +} // String conversion functions, mainly to/from hex/nibble/byte representations. diff --git a/libsolutil/Permutations.h b/libsolutil/Permutations.h new file mode 100644 index 000000000..b3455ddf1 --- /dev/null +++ b/libsolutil/Permutations.h @@ -0,0 +1,228 @@ +/* + 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::util +{ + +// TODO: This is currently only used for permuteDup as special case handling, which is not the best way to do things. +// Not worth spending time reviewing this. +template +void permute(unsigned _n, GetTargetPosition _getTargetPosition, Swap _swap, Pop _pop) +{ + static_assert( + std::is_same_v, int>, + "_getTargetPosition needs to have the signature int(unsigned)" + ); + static_assert( + std::is_same_v, void>, + "_swap needs to have the signature void(unsigned)" + ); + static_assert( + std::is_same_v, void>, + "_pop needs to have the signature void()" + ); + if (_n == 0) return; + int targetPositionTop = _getTargetPosition(_n - 1); + + if (targetPositionTop < 0) + { + // The last element should not be kept. + // Pop it and recurse. + _pop(); + permute(_n - 1, _getTargetPosition, _swap, _pop); + return; + } + // TODO: exception? + // assertThrow(static_cast(targetPositionTop) < _n, langutil::InternalCompilerError, "Invalid permutation."); + if (static_cast(targetPositionTop) == _n - 1) + { + // The last element is in position. + // Search for the deepest element that is not in position. + // If there is none, we are done. Otherwise swap it up and recurse. + for (int i = 0; i < static_cast(_n - 1); ++i) + if (_getTargetPosition(static_cast(i)) != i) + { + _swap(_n - static_cast(i) - 1); + permute(_n, _getTargetPosition, _swap, _pop); + return; + } + } + else + { + // The last element is not in position. + // Move it to its position and recurse. + _swap(_n - static_cast(targetPositionTop) - 1); + permute(_n, _getTargetPosition, _swap, _pop); + } +} + +// TODO: This is now only used in StackLayoutGenerator.cpp in ``createIdealLayout`` and that usage is actually abuse, +// since it provides "invalid" target positions (it works, but it's not soundly specified). +// Hence ``createIdealLayout`` should rather be rewritten properly and it does not make much sense to review +// this in detail. +template +void permuteDup(unsigned _n, GetTargetPositions _getTargetPositions, Swap _swap, Dup _dup, Push _push, Pop _pop, bool _debug = false) +{ + static_assert( + std::is_same_v, std::set>, + "_getTargetPosition needs to have the signature std::vector(unsigned)" + ); + static_assert( + std::is_same_v, void>, + "_swap needs to have the signature void(unsigned)" + ); + static_assert( + std::is_same_v, void>, + "_dup needs to have the signature void(unsigned)" + ); + static_assert( + std::is_same_v, void>, + "_push needs to have the signature void()" + ); + static_assert( + std::is_same_v, void>, + "_pop needs to have the signature void()" + ); + if (_n == 0) return; + + if (_debug) + { + for (auto offset: ranges::views::iota(0u, _n)) + { + auto targetPositions = _getTargetPositions(offset); + std::cout << "{ "; + for (auto pos: targetPositions) + std::cout << pos << " "; + std::cout << "} "; + } + std::cout << std::endl; + } + + std::set targetPositionsTop = _getTargetPositions(_n - 1); + + if (targetPositionsTop.empty()) + { + // The last element should not be kept. + // Pop it and recurse. + _pop(); + permuteDup(_n - 1, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + } + if (targetPositionsTop.count(_n - 1)) + { + if (_debug) + std::cout << "Top position should stay" << std::endl; + // The last element should remain at the top (but potentially also be dupped). + /*if (targetPositionsTop.size() > 1) + { + std::cout << "TOP targets: { "; + for (auto i: targetPositionsTop) + std::cout << i << " "; + std::cout << "}" << std::endl; + // The last element should remain at the top and be dupped. Dup it and recurse. + _dup(1); + permuteDup(_n + 1, _getTargetPositions, _swap, _dup, _push, _pop); + return; + } + else*/ + { + if (_debug) + std::cout << "Look for deeper element to be dupped." << std::endl; + // The last element should *only* exist at the current top. + // Look for the deepest element that should still be dupped. + for (auto offset: ranges::views::iota(0u, _n)) + { + auto targetPositions = _getTargetPositions(offset); + if (targetPositions.size() > 1) + { + if (_debug) + std::cout << "DUP element " << offset << " (DUP" << (_n - offset) << ")" << std::endl; + // Dup it, adjust the target positions and recurse. + // The next recursion will move the duplicate in place. + _dup(_n - offset); + permuteDup(_n + 1, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + } + } + // There is no more dupping requested, so we can switch to the non-dupping version. + permute(_n, [&](unsigned _i) -> int { + auto const& targetPositions = _getTargetPositions(_i); + if (targetPositions.empty()) + return -1; + else + { + assertThrow(targetPositions.size() == 1, langutil::InternalCompilerError, ""); + return static_cast(*targetPositions.begin()); + } + }, _swap, _pop); + return; + } + } + else + { + // The last element should end up at *some* position that isn't its current one. + auto topTargetPos = *targetPositionsTop.begin(); + if (_debug) + std::cout << "Top target pos: " << topTargetPos << std::endl; + if (topTargetPos < _n - 1) + { + // If the element is supposed to exist anywhere deeper than the current top, swap it there and recurse. + _swap(_n - static_cast(topTargetPos) - 1); + permuteDup(_n, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + } + else + { + // If there is an element that is supposed to be dupped to the current top position. Find it, dup it and recurse. + for (auto offset: ranges::views::iota(0u, _n)) + { + auto targetPositions = _getTargetPositions(offset); + if (targetPositions.size() > 1 && targetPositions.count(static_cast(_n))) + { + _dup(static_cast(targetPositions.size() - offset)); + permuteDup(_n + 1, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + } + } + // If there is any other element that is supposed to be dupped. Find it, dup it and recurse. + for (auto offset: ranges::views::iota(0u, _n)) + { + auto targetPositions = _getTargetPositions(offset); + if (targetPositions.size() > 1) + { + _dup(static_cast(targetPositions.size() - offset)); + permuteDup(_n + 1, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + } + } + // There must be a new element requested. Request it to be pushed and recurse. + _push(); + permuteDup(_n + 1, _getTargetPositions, _swap, _dup, _push, _pop, _debug); + return; + + assertThrow(false, langutil::InternalCompilerError, "Invalid permutation."); + } + } +} + +} diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index 7b9d102e8..963d5ddf7 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -68,6 +68,9 @@ add_library(yul backends/evm/EVMMetrics.h backends/evm/NoOutputAssembly.h backends/evm/NoOutputAssembly.cpp + backends/evm/StackHelpers.h + backends/evm/StackLayoutGenerator.h + backends/evm/StackLayoutGenerator.cpp backends/evm/VariableReferenceCounter.h backends/evm/VariableReferenceCounter.cpp backends/wasm/EVMToEwasmTranslator.cpp diff --git a/libyul/backends/evm/StackHelpers.h b/libyul/backends/evm/StackHelpers.h new file mode 100644 index 000000000..a656e73dc --- /dev/null +++ b/libyul/backends/evm/StackHelpers.h @@ -0,0 +1,188 @@ +/* + 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 +#include + +namespace solidity::yul +{ + +inline std::string stackSlotToString(StackSlot const& _slot) +{ + return std::visit(util::GenericVisitor{ + [](FunctionCallReturnLabelSlot const& _ret) -> std::string { return "RET[" + _ret.call.get().functionName.name.str() + "]"; }, + [](FunctionReturnLabelSlot const&) -> std::string { return "RET"; }, + [](VariableSlot const& _var) { return _var.variable.get().name.str(); }, + [](LiteralSlot const& _lit) { return util::toCompactHexWithPrefix(_lit.value); }, + [](TemporarySlot const& _tmp) -> std::string { return "TMP[" + _tmp.call.get().functionName.name.str() + ", " + std::to_string(_tmp.index) + "]"; }, + [](JunkSlot const&) -> std::string { return "JUNK"; } + }, _slot); +} + +inline std::string stackToString(Stack const& _stack) +{ + std::string result("[ "); + for (auto const& slot: _stack) + result += stackSlotToString(slot) + ' '; + result += ']'; + return result; +} +template +std::set findAllOffsets(Range&& _range, Value&& _value) +{ + std::set result; + auto begin = std::begin(_range); + auto end = std::end(_range); + auto it = begin; + while (it != end) + { + it = std::find(it, end, std::forward(_value)); + if (it == end) + return result; + result.emplace(static_cast(std::distance(begin, it))); + ++it; + } + return result; +} + +template +void createStackLayout(Stack& _currentStack, Stack const& _targetStack, Swap _swap, Dup _dup, PushSlot _push, Pop _pop) +{ + if (_currentStack == _targetStack) + return; + + if (_currentStack.empty()) + { + while (_currentStack.size() < _targetStack.size()) + { + StackSlot newSlot = _targetStack.at(_currentStack.size()); + _push(newSlot); + _currentStack.emplace_back(newSlot); + } + yulAssert(_currentStack == _targetStack, ""); + return; + } + + auto topTargets = findAllOffsets(_targetStack, _currentStack.back()); + if (topTargets.size() < findAllOffsets(_currentStack, _currentStack.back()).size()) + { + _pop(); + _currentStack.pop_back(); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + else if (_targetStack.size() >= _currentStack.size() && _targetStack.at(_currentStack.size() - 1) == _currentStack.back()) + { + // Current top is in place. + // Dup deepest one to be dupped (TODO: choose optimal). + for (auto&& [offset, slot]: _currentStack | ranges::views::enumerate) + { + if (findAllOffsets(_currentStack, slot).size() < findAllOffsets(_targetStack, slot).size()) + { + auto leastDeepOccurrence = util::findOffset(_currentStack | ranges::views::reverse, slot); + yulAssert(leastDeepOccurrence, ""); + _dup(static_cast(*leastDeepOccurrence + 1)); + + _currentStack.emplace_back(_currentStack.at(offset)); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + // Nothing to dup. Find anything to be pushed and push it. + for (auto const& slot: _targetStack) + { + if (!util::findOffset(_currentStack, slot)) + { + _push(slot); + _currentStack.emplace_back(slot); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + // Nothing to push or dup. + // Swap the deepest one that's not in place up. + for (auto&& [offset, slot]: _currentStack | ranges::views::enumerate) + { + if (!(slot == _targetStack.at(offset)) && !(slot == _currentStack.back())) + { + _swap(static_cast(_currentStack.size() - offset - 1)); + std::swap(_currentStack.back(), _currentStack.at(offset)); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + // Nothing to push or dup and nothing out of place => done. + yulAssert(_currentStack == _targetStack, ""); + return; + } + else + { + for (unsigned deepestTopTarget: topTargets) + { + if (deepestTopTarget >= _currentStack.size()) + break; + if (!(_currentStack.at(deepestTopTarget) == _targetStack.at(deepestTopTarget))) + { + // Move top into place. + _swap(static_cast(_currentStack.size() - deepestTopTarget - 1)); + std::swap(_currentStack.back(), _currentStack.at(deepestTopTarget)); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + + // There needs to be something to dup or push. Try dupping. (TODO: suboptimal) + for (auto&& [offset, slot]: _currentStack | ranges::views::enumerate) + { + if (findAllOffsets(_currentStack, slot).size() < findAllOffsets(_targetStack, slot).size()) + { + auto leastDeepOccurrence = util::findOffset(_currentStack | ranges::views::reverse, slot); + yulAssert(leastDeepOccurrence, ""); + _dup(static_cast(*leastDeepOccurrence + 1)); + // _dup(static_cast(_currentStack.size() - offset)); + + _currentStack.emplace_back(_currentStack.at(offset)); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + // Nothing to dup. Find anything to be pushed and push it. + for (auto const& slot: _targetStack) + { + if (!util::findOffset(_currentStack, slot)) + { + _push(slot); + _currentStack.emplace_back(slot); + createStackLayout(_currentStack, _targetStack, _swap, _dup, _push, _pop); + return; + } + } + yulAssert(false, ""); + } + + yulAssert(_currentStack == _targetStack, ""); +} + +} diff --git a/libyul/backends/evm/StackLayoutGenerator.cpp b/libyul/backends/evm/StackLayoutGenerator.cpp new file mode 100644 index 000000000..bad6cfb55 --- /dev/null +++ b/libyul/backends/evm/StackLayoutGenerator.cpp @@ -0,0 +1,462 @@ +/* + 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 +/** + * Stack layout generator for Yul to EVM code generation. + */ + +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::yul; +using namespace std; + +StackLayoutGenerator::StackLayoutGenerator(StackLayout& _layout): m_layout(_layout) +{ +} + +namespace +{ +struct PreviousSlot { size_t slot; }; + +// TODO: Rewrite this as custom algorithm matching createStackLayout exactly and make it work +// for all cases, including duplicates and removals of slots that can be generated on the fly, etc. +// After that the util::permute* functions can be removed. +Stack createIdealLayout(Stack const& _post, vector>> layout) +{ + util::permuteDup(static_cast(layout.size()), [&](unsigned _i) -> set { + // For call return values the target position is known. + if (set* pos = get_if>(&layout.at(_i))) + return *pos; + // Previous arguments can stay where they are. + return {_i}; + }, [&](unsigned _i) { + std::swap(layout.back(), layout.at(layout.size() - _i - 1)); + }, [&](unsigned _i) { + auto positions = get_if>(&layout.at(layout.size() - _i)); + yulAssert(positions, ""); + if (positions->count(static_cast(layout.size()))) + { + positions->erase(static_cast(layout.size())); + layout.emplace_back(set{static_cast(layout.size())}); + } + else + { + optional duppingOffset; + for (unsigned pos: *positions) + { + if (pos != layout.size() - _i) + { + duppingOffset = pos; + break; + } + } + yulAssert(duppingOffset, ""); + positions->erase(*duppingOffset); + layout.emplace_back(set{*duppingOffset}); + } + }, [&]() { + yulAssert(false, ""); + }, [&]() { + layout.pop_back(); + }); + + // Now we can construct the ideal layout before the operation. + // "layout" has the declared variables in the desired position and + // for any PreviousSlot{x}, x yields the ideal place of the slot before the declaration. + vector> idealLayout(_post.size(), nullopt); + for (auto const& [slot, idealPosition]: ranges::zip_view(_post, layout)) + if (PreviousSlot* previousSlot = std::get_if(&idealPosition)) + idealLayout.at(previousSlot->slot) = slot; + + while (!idealLayout.empty() && !idealLayout.back()) + idealLayout.pop_back(); + + return idealLayout | ranges::views::transform([](optional s) { + yulAssert(s, ""); + return *s; + }) | ranges::to; +} +} + +Stack StackLayoutGenerator::propagateStackThroughOperation(Stack _exitStack, CFG::Operation const& _operation) +{ + Stack& stack = _exitStack; + + vector> targetPositions(_operation.output.size(), set{}); + size_t numToKeep = 0; + for (size_t idx: ranges::views::iota(0u, targetPositions.size())) + for (unsigned offset: findAllOffsets(stack, _operation.output.at(idx))) + { + targetPositions[idx].emplace(offset); + ++numToKeep; + } + + auto layout = ranges::views::iota(0u, stack.size() - numToKeep) | + ranges::views::transform([](size_t _index) { return PreviousSlot{_index}; }) | + ranges::to>>>; + // The call produces values with known target positions. + layout += targetPositions; + + stack = createIdealLayout(stack, layout); + + if (auto const* assignment = get_if(&_operation.operation)) + for (auto& stackSlot: stack) + if (auto const* varSlot = get_if(&stackSlot)) + if (util::findOffset(assignment->variables, *varSlot)) + stackSlot = JunkSlot{}; + + for (StackSlot const& input: _operation.input) + stack.emplace_back(input); + + m_layout.operationEntryLayout[&_operation] = stack; + + // TODO: We will potentially accumulate a lot of return labels here. + // Removing them naively has huge implications on both code size and runtime gas cost (both positive and negative): + // cxx20::erase_if(*m_stack, [](StackSlot const& _slot) { return holds_alternative(_slot); }); + // Consider removing them properly while accounting for the induced backwards stack shuffling. + + // Remove anything from the stack top that can be freely generated or dupped from deeper on the stack. + while (!stack.empty() && ( + canBeFreelyGenerated(stack.back()) || + util::findOffset(stack | ranges::views::drop_last(1), stack.back()) + )) + stack.pop_back(); + + // TODO: suboptimal. Should account for induced stack shuffling. + // TODO: consider if we want this kind of compression at all, resp. whether stack.size() > 12 is a good condition. + if (stack.size() > 12) + stack = stack | ranges::views::enumerate | ranges::views::filter(util::mapTuple([&](size_t _index, StackSlot const& _slot) { + // Filter out slots that can be freely generated or are already present on the stack. + return !canBeFreelyGenerated(_slot) && !util::findOffset(stack | ranges::views::take(_index), _slot); + })) | ranges::views::values | ranges::to; + return stack; +} + +Stack StackLayoutGenerator::propagateStackThroughBlock(Stack _exitStack, CFG::BasicBlock const& _block) +{ + Stack stack = std::move(_exitStack); + for (auto& operation: _block.operations | ranges::views::reverse) + stack = propagateStackThroughOperation(stack, operation); + return stack; +} + +void StackLayoutGenerator::processEntryPoint(CFG::BasicBlock const& _entry) +{ + std::list toVisit{&_entry}; + std::set visited; + + while (!toVisit.empty()) + { + // TODO: calculate backwardsJumps only once. + std::list> backwardsJumps; + while (!toVisit.empty()) + { + CFG::BasicBlock const *block = *toVisit.begin(); + toVisit.pop_front(); + + if (visited.count(block)) + continue; + + if (std::optional exitLayout = std::visit(util::GenericVisitor{ + [&](CFG::BasicBlock::MainExit const&) -> std::optional + { + visited.emplace(block); + return Stack{}; + }, + [&](CFG::BasicBlock::Jump const& _jump) -> std::optional + { + if (_jump.backwards) + { + visited.emplace(block); + backwardsJumps.emplace_back(block, _jump.target); + if (auto* info = util::valueOrNullptr(m_layout.blockInfos, _jump.target)) + return info->entryLayout; + return Stack{}; + } + if (visited.count(_jump.target)) + { + visited.emplace(block); + return m_layout.blockInfos.at(_jump.target).entryLayout; + } + toVisit.emplace_front(_jump.target); + return nullopt; + }, + [&](CFG::BasicBlock::ConditionalJump const& _conditionalJump) -> std::optional + { + bool zeroVisited = visited.count(_conditionalJump.zero); + bool nonZeroVisited = visited.count(_conditionalJump.nonZero); + if (zeroVisited && nonZeroVisited) + { + Stack stack = combineStack( + m_layout.blockInfos.at(_conditionalJump.zero).entryLayout, + m_layout.blockInfos.at(_conditionalJump.nonZero).entryLayout + ); + stack.emplace_back(_conditionalJump.condition); + visited.emplace(block); + return stack; + } + if (!zeroVisited) + toVisit.emplace_front(_conditionalJump.zero); + if (!nonZeroVisited) + toVisit.emplace_front(_conditionalJump.nonZero); + return nullopt; + }, + [&](CFG::BasicBlock::FunctionReturn const& _functionReturn) -> std::optional + { + visited.emplace(block); + yulAssert(_functionReturn.info, ""); + Stack stack = _functionReturn.info->returnVariables | ranges::views::transform([](auto const& _varSlot){ + return StackSlot{_varSlot}; + }) | ranges::to; + stack.emplace_back(FunctionReturnLabelSlot{}); + return stack; + }, + [&](CFG::BasicBlock::Terminated const&) -> std::optional + { + visited.emplace(block); + return Stack{}; + }, + }, block->exit)) + { + // We can skip the visit, if we have seen this precise exit layout already last time. + // Note: if the entire graph is revisited in the backwards jump check below, doing + // this seems to break things; not sure why. + // Note: since I don't quite understand why doing this can break things, I comment + // it out for now, since not aborting in those cases should always be safe. + // if (auto* previousInfo = util::valueOrNullptr(m_layout.blockInfos, block)) + // if (previousInfo->exitLayout == *exitLayout) + // continue; + auto& info = m_layout.blockInfos[block]; + info.exitLayout = *exitLayout; + info.entryLayout = propagateStackThroughBlock(info.exitLayout, *block); + + for (auto entry: block->entries) + toVisit.emplace_back(entry); + } + else + continue; + } + + for (auto [block, target]: backwardsJumps) + if (ranges::any_of( + m_layout.blockInfos[target].entryLayout, + [exitLayout = m_layout.blockInfos[block].exitLayout](StackSlot const& _slot) { + return !util::findOffset(exitLayout, _slot); + } + )) + { + // This block jumps backwards, but does not provide all slots required by the jump target on exit. + // Therefore we need to visit the subgraph between ``target`` and ``block`` again. + // In particular we can visit backwards starting from ``block`` and mark all entries to-be-visited- + // again until we hit ``target``. + toVisit.emplace_front(block); + util::BreadthFirstSearch{{block}}.run( + [&visited, target = target](CFG::BasicBlock const* _block, auto _addChild) { + visited.erase(_block); + if (_block == target) + return; + for (auto const* entry: _block->entries) + _addChild(entry); + } + ); + // TODO: while the above is enough, the layout of ``target`` might change in the process. + // While the shuffled layout for ``target`` will be compatible, it can be worthwhile propagating + // it further up once more. + // This would mean not stopping at _block == target above or even doing visited.clear() here, revisiting the entire graph. + // This is a tradeoff between the runtime of this process and the optimality of the result. + // Also note that while visiting the entire graph again *can* be helpful, it can also be detrimental. + // Also note that for some reason using visited.clear() is incompatible with skipping the revisit + // of already seen exit layouts above, I'm not sure yet why. + } + } + + stitchConditionalJumps(_entry); + fixStackTooDeep(_entry); +} + +Stack StackLayoutGenerator::combineStack(Stack const& _stack1, Stack const& _stack2) +{ + if (_stack1.empty()) + return _stack2; + if (_stack2.empty()) + return _stack1; + + // TODO: there is probably a better way than brute-forcing. This has n! complexity or worse, so + // we can't keep it like this. + + Stack commonPrefix; + for (auto&& [slot1, slot2]: ranges::zip_view(_stack1, _stack2)) + { + if (!(slot1 == slot2)) + break; + commonPrefix.emplace_back(slot1); + } + Stack stack1Tail = _stack1 | ranges::views::drop(commonPrefix.size()) | ranges::to; + Stack stack2Tail = _stack2 | ranges::views::drop(commonPrefix.size()) | ranges::to; + + Stack candidate; + for (auto slot: stack1Tail) + if (!util::findOffset(candidate, slot)) + candidate.emplace_back(slot); + for (auto slot: stack2Tail) + if (!util::findOffset(candidate, slot)) + candidate.emplace_back(slot); + cxx20::erase_if(candidate, [](StackSlot const& slot) { + return holds_alternative(slot) || holds_alternative(slot); + }); + + std::map sortedCandidates; + + // TODO: surprisingly this works for rather comparably large candidate size, but we should probably + // set up some limit, since this will quickly explode otherwise. + // Ideally we would then have a better fallback mechanism - although returning any naive union of both stacks + // like ``candidate`` itself may just be fine. + // if (candidate.size() > 8) + // return candidate; + + auto evaluate = [&](Stack const& _candidate) -> size_t { + size_t numOps = 0; + Stack testStack = _candidate; + auto swap = [&](unsigned _swapDepth) { ++numOps; if (_swapDepth > 16) numOps += 1000; }; + auto dup = [&](unsigned _dupDepth) { ++numOps; if (_dupDepth > 16) numOps += 1000; }; + auto push = [&](StackSlot const& _slot) { + if (!canBeFreelyGenerated(_slot)) + { + auto offsetInPrefix = util::findOffset(commonPrefix, _slot); + yulAssert(offsetInPrefix, ""); + // Effectively this is a dup. + ++numOps; + // TODO: Verify that this is correct. The idea is to penalize dupping stuff up that's too deep in + // the prefix at this point. + if (commonPrefix.size() + testStack.size() - *offsetInPrefix > 16) + numOps += 1000; + } + }; + createStackLayout(testStack, stack1Tail, swap, dup, push, [&](){} ); + testStack = _candidate; + createStackLayout(testStack, stack2Tail, swap, dup, push, [&](){}); + return numOps; + }; + + // See https://en.wikipedia.org/wiki/Heap's_algorithm + size_t n = candidate.size(); + sortedCandidates.insert(std::make_pair(evaluate(candidate), candidate)); + std::vector c(n, 0); + size_t i = 1; + while (i < n) + { + if (c[i] < i) + { + if (i & 1) + std::swap(candidate.front(), candidate[i]); + else + std::swap(candidate[c[i]], candidate[i]); + sortedCandidates.insert(std::make_pair(evaluate(candidate), candidate)); + ++c[i]; + ++i; + } + else + { + c[i] = 0; + ++i; + } + } + + return commonPrefix + sortedCandidates.begin()->second; +} + +void StackLayoutGenerator::stitchConditionalJumps(CFG::BasicBlock const& _block) +{ + util::BreadthFirstSearch breadthFirstSearch{{&_block}}; + breadthFirstSearch.run([&](CFG::BasicBlock const* _block, auto _addChild) { + auto& info = m_layout.blockInfos.at(_block); + std::visit(util::GenericVisitor{ + [&](CFG::BasicBlock::MainExit const&) {}, + [&](CFG::BasicBlock::Jump const& _jump) + { + if (!_jump.backwards) + _addChild(_jump.target); + }, + [&](CFG::BasicBlock::ConditionalJump const& _conditionalJump) + { + auto& zeroTargetInfo = m_layout.blockInfos.at(_conditionalJump.zero); + auto& nonZeroTargetInfo = m_layout.blockInfos.at(_conditionalJump.nonZero); + Stack exitLayout = info.exitLayout; + + // The last block must have produced the condition at the stack top. + yulAssert(!exitLayout.empty(), ""); + yulAssert(exitLayout.back() == _conditionalJump.condition, ""); + // The condition is consumed by the jump. + exitLayout.pop_back(); + + auto fixJumpTargetEntry = [&](Stack const& _originalEntryLayout) -> Stack { + Stack newEntryLayout = exitLayout; + // Whatever the block being jumped to does not actually require, can be marked as junk. + for (auto& slot: newEntryLayout) + if (!util::findOffset(_originalEntryLayout, slot)) + slot = JunkSlot{}; + // Make sure everything the block being jumped to requires is actually present or can be generated. + for (auto const& slot: _originalEntryLayout) + yulAssert(canBeFreelyGenerated(slot) || util::findOffset(newEntryLayout, slot), ""); + return newEntryLayout; + }; + zeroTargetInfo.entryLayout = fixJumpTargetEntry(zeroTargetInfo.entryLayout); + nonZeroTargetInfo.entryLayout = fixJumpTargetEntry(nonZeroTargetInfo.entryLayout); + _addChild(_conditionalJump.zero); + _addChild(_conditionalJump.nonZero); + }, + [&](CFG::BasicBlock::FunctionReturn const&) {}, + [&](CFG::BasicBlock::Terminated const&) { }, + }, _block->exit); + }); +} + +void StackLayoutGenerator::fixStackTooDeep(CFG::BasicBlock const&) +{ + // TODO +} + +StackLayout StackLayoutGenerator::run(CFG const& _dfg) +{ + StackLayout stackLayout; + StackLayoutGenerator stackLayoutGenerator{stackLayout}; + + stackLayoutGenerator.processEntryPoint(*_dfg.entry); + for (auto& functionInfo: _dfg.functionInfo | ranges::views::values) + stackLayoutGenerator.processEntryPoint(*functionInfo.entry); + + return stackLayout; +} diff --git a/libyul/backends/evm/StackLayoutGenerator.h b/libyul/backends/evm/StackLayoutGenerator.h new file mode 100644 index 000000000..0fcc78962 --- /dev/null +++ b/libyul/backends/evm/StackLayoutGenerator.h @@ -0,0 +1,84 @@ +/* + 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 +/** + * Stack layout generator for Yul to EVM code generation. + */ + +#pragma once + +#include + +#include + +namespace solidity::yul +{ + +struct StackLayout +{ + struct BlockInfo + { + /// Complete stack layout that is required for entering a block. + Stack entryLayout; + /// The resulting stack layout after executing the block. + Stack exitLayout; + }; + std::map blockInfos; + /// For each operation the complete stack layout that: + /// - has the slots required for the operation at the stack top. + /// - will have the operation result in a layout that makes it easy to achieve the next desired layout. + std::map operationEntryLayout; +}; + +class StackLayoutGenerator +{ +public: + static StackLayout run(CFG const& _dfg); + +private: + StackLayoutGenerator(StackLayout& _context); + + /// @returns the optimal entry stack layout, s.t. @a _operation can be applied to it and + /// the result can be transformed to @a _exitStack with minimal stack shuffling. + Stack propagateStackThroughOperation(Stack _exitStack, CFG::Operation const& _operation); + + /// @returns the desired stack layout at the entry of @a _block, assuming the layout after + /// executing the block should be @a _exitStack. + Stack propagateStackThroughBlock(Stack _exitStack, CFG::BasicBlock const& _block); + + /// Main algorithm walking the graph from entry to exit and propagating back the stack layouts to the entries. + /// Iteratively reruns itself along backwards jumps until the layout is stabilized. + void processEntryPoint(CFG::BasicBlock const& _entry); + + /// After the main algorithms, layouts at conditional jumps are merely compatible, i.e. the exit layout of the + /// jumping block is a superset of the entry layout of the target block. This function modifies the entry layouts + /// of conditional jump targets, s.t. the entry layout of target blocks match the exit layout of the jumping block + /// exactly, except that slots not required after the jump are marked as `JunkSlot`s. + void stitchConditionalJumps(CFG::BasicBlock const& _block); + + /// Calculates the ideal stack layout, s.t. both @a _stack1 and @a _stack2 can be achieved with minimal + /// stack shuffling when starting from the returned layout. + static Stack combineStack(Stack const& _stack1, Stack const& _stack2); + + /// Tries to detect stack layout transitions that are bound to cause stack too deep errors and + /// attempts to reorganize the layout to avoid those cases. + void fixStackTooDeep(CFG::BasicBlock const& _entry); + + StackLayout& m_layout; +}; + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c1b686df6..9c6a9663a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -140,6 +140,8 @@ set(libyul_sources libyul/ObjectCompilerTest.h libyul/ObjectParser.cpp libyul/Parser.cpp + libyul/StackLayoutGeneratorTest.cpp + libyul/StackLayoutGeneratorTest.h libyul/SyntaxTest.h libyul/SyntaxTest.cpp libyul/YulInterpreterTest.cpp diff --git a/test/InteractiveTests.h b/test/InteractiveTests.h index 4ffdf5a74..e27c00b84 100644 --- a/test/InteractiveTests.h +++ b/test/InteractiveTests.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,7 @@ Testsuite const g_interactiveTestsuites[] = { {"Yul Interpreter", "libyul", "yulInterpreterTests", false, false, &yul::test::YulInterpreterTest::create}, {"Yul Object Compiler", "libyul", "objectCompiler", false, false, &yul::test::ObjectCompilerTest::create}, {"Yul Control Flow Graph", "libyul", "yulControlFlowGraph", false, false, &yul::test::ControlFlowGraphTest::create}, + {"Yul Stack Layout", "libyul", "yulStackLayout", false, false, &yul::test::StackLayoutGeneratorTest::create}, {"Function Side Effects", "libyul", "functionSideEffects", false, false, &yul::test::FunctionSideEffects::create}, {"Yul Syntax", "libyul", "yulSyntaxTests", false, false, &yul::test::SyntaxTest::create}, {"EVM Code Transform", "libyul", "evmCodeTransform", false, false, &yul::test::EVMCodeTransformTest::create, {"nooptions"}}, diff --git a/test/libyul/StackLayoutGeneratorTest.cpp b/test/libyul/StackLayoutGeneratorTest.cpp new file mode 100644 index 000000000..3515a2957 --- /dev/null +++ b/test/libyul/StackLayoutGeneratorTest.cpp @@ -0,0 +1,218 @@ +/* + 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 + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace solidity; +using namespace solidity::util; +using namespace solidity::langutil; +using namespace solidity::yul; +using namespace solidity::yul::test; +using namespace solidity::frontend; +using namespace solidity::frontend::test; +using namespace std; + +StackLayoutGeneratorTest::StackLayoutGeneratorTest(string const& _filename): + TestCase(_filename) +{ + m_source = m_reader.source(); + auto dialectName = m_reader.stringSetting("dialect", "evm"); + m_dialect = &dialect(dialectName, solidity::test::CommonOptions::get().evmVersion()); + m_expectation = m_reader.simpleExpectations(); +} + +namespace +{ +static std::string stackSlotToString(StackSlot const& _slot) +{ + return std::visit(util::GenericVisitor{ + [](FunctionCallReturnLabelSlot const& _ret) -> std::string { return "RET[" + _ret.call.get().functionName.name.str() + "]"; }, + [](FunctionReturnLabelSlot const&) -> std::string { return "RET"; }, + [](VariableSlot const& _var) { return _var.variable.get().name.str(); }, + [](LiteralSlot const& _lit) { return util::toCompactHexWithPrefix(_lit.value); }, + [](TemporarySlot const& _tmp) -> std::string { return "TMP[" + _tmp.call.get().functionName.name.str() + ", " + std::to_string(_tmp.index) + "]"; }, + [](JunkSlot const&) -> std::string { return "JUNK"; } + }, _slot); +} + +static std::string stackToString(Stack const& _stack) +{ + std::string result("[ "); + for (auto const& slot: _stack) + result += stackSlotToString(slot) + ' '; + result += ']'; + return result; +} +static std::string variableSlotToString(VariableSlot const& _slot) +{ + return _slot.variable.get().name.str(); +} +} + +class StackLayoutPrinter +{ +public: + StackLayoutPrinter(std::ostream& _stream, StackLayout const& _stackLayout): + m_stream(_stream), m_stackLayout(_stackLayout) + { + } + void operator()(CFG::BasicBlock const& _block) + { + getBlockId(_block); + while (!m_blocksToPrint.empty()) + { + CFG::BasicBlock const* block = *m_blocksToPrint.begin(); + m_blocksToPrint.erase(m_blocksToPrint.begin()); + printBlock(*block); + } + + } + void operator()( + CFG::FunctionInfo const& _info + ) + { + m_stream << m_indent << "function " << _info.function.name.str() << "("; + m_stream << joinHumanReadable(_info.parameters | ranges::views::transform(variableSlotToString)); + m_stream << ")"; + if (!_info.returnVariables.empty()) + { + m_stream << " -> "; + m_stream << joinHumanReadable(_info.returnVariables | ranges::views::transform(variableSlotToString)); + } + m_stream << ":\n"; + ScopedSaveAndRestore linePrefixRestore(m_indent, m_indent + " "); + (*this)(*_info.entry); + } + +private: + void printBlock(CFG::BasicBlock const& _block) + { + m_stream << m_indent << "Block " << getBlockId(_block) << ":\n"; + ScopedSaveAndRestore linePrefixRestore(m_indent, m_indent + " "); + + m_stream << m_indent << "Entries: "; + if (_block.entries.empty()) + m_stream << "None\n"; + else + m_stream << joinHumanReadable(_block.entries | ranges::views::transform([&](auto const* _entry) { + return to_string(getBlockId(*_entry)); + })) << "\n"; + + m_stream << m_indent << "Entry Layout: " << stackToString(m_stackLayout.blockInfos.at(&_block).entryLayout) << "\n"; + + for (auto const& operation: _block.operations) + { + m_stream << m_indent; + m_stream << stackToString(m_stackLayout.operationEntryLayout.at(&operation)) << " >> "; + + std::visit(util::GenericVisitor{ + [&](CFG::FunctionCall const& _call) { + m_stream << _call.function.get().name.str(); + }, + [&](CFG::BuiltinCall const& _call) { + m_stream << _call.functionCall.get().functionName.name.str(); + + }, + [&](CFG::Assignment const& _assignment) { + m_stream << "Assignment("; + m_stream << joinHumanReadable(_assignment.variables | ranges::views::transform(variableSlotToString)); + m_stream << ")"; + } + }, operation.operation); + m_stream << "\n"; + } + m_stream << m_indent << "Exit Layout: " << stackToString(m_stackLayout.blockInfos.at(&_block).exitLayout) << "\n"; + std::visit(util::GenericVisitor{ + [&](CFG::BasicBlock::MainExit const&) + { + m_stream << m_indent << "MainExit\n"; + }, + [&](CFG::BasicBlock::Jump const& _jump) + { + m_stream << m_indent << "Jump" << (_jump.backwards ? " (backwards): " : ": ") << getBlockId(*_jump.target); + m_stream << " (Entry Layout: " << stackToString(m_stackLayout.blockInfos.at(_jump.target).entryLayout) << ")\n"; + }, + [&](CFG::BasicBlock::ConditionalJump const& _conditionalJump) + { + m_stream << m_indent << "ConditionalJump " << stackSlotToString(_conditionalJump.condition) << ":\n"; + m_stream << m_indent << " NonZero: " << getBlockId(*_conditionalJump.nonZero); + m_stream << " (Entry Layout: " << stackToString(m_stackLayout.blockInfos.at(_conditionalJump.nonZero).entryLayout) << ")\n"; + m_stream << m_indent << " Zero: " << getBlockId(*_conditionalJump.zero); + m_stream << " (Entry Layout: " << stackToString(m_stackLayout.blockInfos.at(_conditionalJump.zero).entryLayout) << ")\n"; + }, + [&](CFG::BasicBlock::FunctionReturn const& _return) + { + m_stream << m_indent << "FunctionReturn of " << _return.info->function.name.str() << "\n"; + }, + [&](CFG::BasicBlock::Terminated const&) + { + m_stream << m_indent << "Terminated\n"; + } + }, _block.exit); + } + size_t getBlockId(CFG::BasicBlock const& _block) + { + if (size_t* id = util::valueOrNullptr(m_blockIds, &_block)) + return *id; + size_t id = m_blockIds[&_block] = m_blockCount++; + m_blocksToPrint.emplace_back(&_block); + return id; + } + std::ostream& m_stream; + StackLayout const& m_stackLayout; + std::string m_indent; + std::map m_blockIds; + size_t m_blockCount = 0; + std::list m_blocksToPrint; +}; + +TestCase::TestResult StackLayoutGeneratorTest::run(ostream& _stream, string const& _linePrefix, bool const _formatted) +{ + ErrorList errors; + auto [object, analysisInfo] = parse(m_source, *m_dialect, errors); + if (!object || !analysisInfo || !Error::containsOnlyWarnings(errors)) + { + AnsiColorized(_stream, _formatted, {formatting::BOLD, formatting::RED}) << _linePrefix << "Error parsing source." << endl; + printErrors(errors); + return TestResult::FatalError; + } + + std::ostringstream output; + + std::unique_ptr cfg = ControlFlowGraphBuilder::build(*analysisInfo, *m_dialect, *object->code); + StackLayout stackLayout = StackLayoutGenerator::run(*cfg); + + StackLayoutPrinter{output, stackLayout}(*cfg->entry); + for (auto function: cfg->functions) + StackLayoutPrinter{output, stackLayout}(cfg->functionInfo.at(function)); + + m_obtainedResult = output.str(); + + return checkResult(_stream, _linePrefix, _formatted); +} diff --git a/test/libyul/StackLayoutGeneratorTest.h b/test/libyul/StackLayoutGeneratorTest.h new file mode 100644 index 000000000..9f570045f --- /dev/null +++ b/test/libyul/StackLayoutGeneratorTest.h @@ -0,0 +1,43 @@ +/* + 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 + +namespace solidity::yul +{ +struct Dialect; + +namespace test +{ + +class StackLayoutGeneratorTest: public solidity::frontend::test::TestCase +{ +public: + static std::unique_ptr create(Config const& _config) + { + return std::make_unique(_config.filename); + } + explicit StackLayoutGeneratorTest(std::string const& _filename); + TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; +private: + Dialect const* m_dialect = nullptr; +}; +} +} diff --git a/test/libyul/yulStackLayout/for.yul b/test/libyul/yulStackLayout/for.yul new file mode 100644 index 000000000..6acd4bc95 --- /dev/null +++ b/test/libyul/yulStackLayout/for.yul @@ -0,0 +1,49 @@ +{ + let x := 0x01 + let y := 0x02 + sstore(0x01, x) + for { sstore(0x02, 0x0202) } lt(x, 0x0303) { x := add(x,0x0404) } { + sstore(0x05, 0x0505) + y := sload(x) + } + sstore(0x06, 0x0506) +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// [ 0x0404 0x01 ] >> Assignment(x) +// [ 0x0404 x 0x02 ] >> Assignment(y) +// [ 0x0404 x x 0x01 ] >> sstore +// [ 0x0404 x 0x0202 0x02 ] >> sstore +// Exit Layout: [ 0x0404 x ] +// Jump: 1 (Entry Layout: [ 0x0404 0x0404 x ]) +// Block 1: +// Entries: 0, 2 +// Entry Layout: [ 0x0404 0x0404 x ] +// [ 0x0404 0x0404 x 0x0303 x ] >> lt +// Exit Layout: [ 0x0404 0x0404 x TMP[lt, 0] ] +// ConditionalJump TMP[lt, 0]: +// NonZero: 3 (Entry Layout: [ 0x0404 0x0404 x ]) +// Zero: 4 (Entry Layout: [ JUNK JUNK JUNK ]) +// Block 2: +// Entries: 3 +// Entry Layout: [ 0x0404 0x0404 x ] +// [ 0x0404 0x0404 x ] >> add +// [ 0x0404 TMP[add, 0] ] >> Assignment(x) +// Exit Layout: [ 0x0404 x ] +// Jump (backwards): 1 (Entry Layout: [ 0x0404 0x0404 x ]) +// Block 3: +// Entries: 1 +// Entry Layout: [ 0x0404 0x0404 x ] +// [ 0x0404 0x0404 x 0x0505 0x05 ] >> sstore +// [ 0x0404 0x0404 x x ] >> sload +// [ 0x0404 0x0404 x TMP[sload, 0] ] >> Assignment(y) +// Exit Layout: [ 0x0404 0x0404 x ] +// Jump: 2 (Entry Layout: [ 0x0404 0x0404 x ]) +// Block 4: +// Entries: 1 +// Entry Layout: [ JUNK JUNK JUNK ] +// [ 0x0506 0x06 ] >> sstore +// Exit Layout: [ ] +// MainExit diff --git a/test/libyul/yulStackLayout/function.yul b/test/libyul/yulStackLayout/function.yul new file mode 100644 index 000000000..53dd60738 --- /dev/null +++ b/test/libyul/yulStackLayout/function.yul @@ -0,0 +1,64 @@ +{ + function f(a, b) -> r { + let x := add(a,b) + r := sub(x,a) + } + function g() { + sstore(0x01, 0x0101) + } + function h(x) { + h(f(x, 0)) + g() + } + function i() -> v, w { + v := 0x0202 + w := 0x0303 + } + let x, y := i() + h(x) + h(y) +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// [ RET[h] RET[h] RET[i] ] >> i +// [ RET[h] RET[h] TMP[i, 0] TMP[i, 1] ] >> Assignment(x, y) +// [ RET[h] y RET[h] x ] >> h +// [ RET[h] y ] >> h +// Exit Layout: [ ] +// MainExit +// function f(a, b) -> r: +// Block 0: +// Entries: None +// Entry Layout: [ RET a b ] +// [ RET a b a ] >> add +// [ RET a TMP[add, 0] ] >> Assignment(x) +// [ RET a x ] >> sub +// [ RET TMP[sub, 0] ] >> Assignment(r) +// Exit Layout: [ r RET ] +// FunctionReturn of f +// function g(): +// Block 0: +// Entries: None +// Entry Layout: [ RET ] +// [ RET 0x0101 0x01 ] >> sstore +// Exit Layout: [ RET ] +// FunctionReturn of g +// function h(x): +// Block 0: +// Entries: None +// Entry Layout: [ RET RET[h] RET[f] 0x00 x ] +// [ RET RET[h] RET[f] 0x00 x ] >> f +// [ RET RET[h] TMP[f, 0] ] >> h +// [ RET RET[g] ] >> g +// Exit Layout: [ RET ] +// FunctionReturn of h +// function i() -> v, w: +// Block 0: +// Entries: None +// Entry Layout: [ RET ] +// [ RET 0x0202 ] >> Assignment(v) +// [ v RET 0x0303 ] >> Assignment(w) +// Exit Layout: [ v w RET ] +// FunctionReturn of i diff --git a/test/libyul/yulStackLayout/if.yul b/test/libyul/yulStackLayout/if.yul new file mode 100644 index 000000000..2a7648267 --- /dev/null +++ b/test/libyul/yulStackLayout/if.yul @@ -0,0 +1,29 @@ +{ + sstore(0x01, 0x0101) + if calldataload(0) { + sstore(0x02, 0x0202) + } + sstore(0x03, 0x003) +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// [ 0x0101 0x01 ] >> sstore +// [ 0x00 ] >> calldataload +// Exit Layout: [ TMP[calldataload, 0] ] +// ConditionalJump TMP[calldataload, 0]: +// NonZero: 1 (Entry Layout: [ ]) +// Zero: 2 (Entry Layout: [ ]) +// Block 1: +// Entries: 0 +// Entry Layout: [ ] +// [ 0x0202 0x02 ] >> sstore +// Exit Layout: [ ] +// Jump: 2 (Entry Layout: [ ]) +// Block 2: +// Entries: 0, 1 +// Entry Layout: [ ] +// [ 0x03 0x03 ] >> sstore +// Exit Layout: [ ] +// MainExit diff --git a/test/libyul/yulStackLayout/stub.yul b/test/libyul/yulStackLayout/stub.yul new file mode 100644 index 000000000..513b87c34 --- /dev/null +++ b/test/libyul/yulStackLayout/stub.yul @@ -0,0 +1,8 @@ +{ +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// Exit Layout: [ ] +// MainExit diff --git a/test/libyul/yulStackLayout/switch.yul b/test/libyul/yulStackLayout/switch.yul new file mode 100644 index 000000000..5a1a3caba --- /dev/null +++ b/test/libyul/yulStackLayout/switch.yul @@ -0,0 +1,63 @@ +{ + let x := 0x0101 + let y := 0x0202 + let z := 0x0303 + switch sload(x) + case 0 { + x := 0x42 + } + case 1 { + y := 0x42 + } + default { + sstore(z, z) + } + + sstore(0x0404, y) +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// [ 0x0101 ] >> Assignment(x) +// [ x 0x0202 ] >> Assignment(y) +// [ y x 0x0303 ] >> Assignment(z) +// [ y z x ] >> sload +// [ y z TMP[sload, 0] ] >> Assignment(GHOST[0]) +// [ y z GHOST[0] GHOST[0] 0x00 ] >> eq +// Exit Layout: [ y z GHOST[0] TMP[eq, 0] ] +// ConditionalJump TMP[eq, 0]: +// NonZero: 1 (Entry Layout: [ y JUNK JUNK ]) +// Zero: 2 (Entry Layout: [ y z GHOST[0] ]) +// Block 1: +// Entries: 0 +// Entry Layout: [ y JUNK JUNK ] +// [ y 0x42 ] >> Assignment(x) +// Exit Layout: [ y ] +// Jump: 3 (Entry Layout: [ y ]) +// Block 2: +// Entries: 0 +// Entry Layout: [ y z GHOST[0] ] +// [ y z GHOST[0] 0x01 ] >> eq +// Exit Layout: [ y z TMP[eq, 0] ] +// ConditionalJump TMP[eq, 0]: +// NonZero: 4 (Entry Layout: [ JUNK JUNK ]) +// Zero: 5 (Entry Layout: [ y z ]) +// Block 3: +// Entries: 1, 4, 5 +// Entry Layout: [ y ] +// [ y 0x0404 ] >> sstore +// Exit Layout: [ ] +// MainExit +// Block 4: +// Entries: 2 +// Entry Layout: [ JUNK JUNK ] +// [ 0x42 ] >> Assignment(y) +// Exit Layout: [ y ] +// Jump: 3 (Entry Layout: [ y ]) +// Block 5: +// Entries: 2 +// Entry Layout: [ y z ] +// [ y z z ] >> sstore +// Exit Layout: [ y ] +// Jump: 3 (Entry Layout: [ y ]) diff --git a/test/libyul/yulStackLayout/variables.yul b/test/libyul/yulStackLayout/variables.yul new file mode 100644 index 000000000..b8e31489e --- /dev/null +++ b/test/libyul/yulStackLayout/variables.yul @@ -0,0 +1,24 @@ +{ + let x := calldataload(0) + let y := calldataload(2) + + x := calldataload(3) + y := calldataload(4) + + sstore(x,y) +} +// ---- +// Block 0: +// Entries: None +// Entry Layout: [ ] +// [ 0x00 ] >> calldataload +// [ TMP[calldataload, 0] ] >> Assignment(x) +// [ 0x02 ] >> calldataload +// [ TMP[calldataload, 0] ] >> Assignment(y) +// [ 0x03 ] >> calldataload +// [ TMP[calldataload, 0] ] >> Assignment(x) +// [ x 0x04 ] >> calldataload +// [ x TMP[calldataload, 0] ] >> Assignment(y) +// [ y x ] >> sstore +// Exit Layout: [ ] +// MainExit diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 1f62b57fc..18b04afe8 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable(isoltest ../libyul/FunctionSideEffects.cpp ../libyul/ObjectCompilerTest.cpp ../libyul/SyntaxTest.cpp + ../libyul/StackLayoutGeneratorTest.cpp ../libyul/YulOptimizerTest.cpp ../libyul/YulOptimizerTestCommon.cpp ../libyul/YulInterpreterTest.cpp