diff --git a/libyul/backends/evm/ssa/CodeTransform.cpp b/libyul/backends/evm/ssa/CodeTransform.cpp index caca86153f8b..f1292b7f13bf 100644 --- a/libyul/backends/evm/ssa/CodeTransform.cpp +++ b/libyul/backends/evm/ssa/CodeTransform.cpp @@ -185,8 +185,13 @@ void CodeTransform::operator()(SSACFG::BlockId const _blockId) // Shuffle to the block's exit layout before dispatching the exit. // This ensures the condition is on top for ConditionalJump, phi pre-images are // in the right positions for jumps, and return values are accessible for FunctionReturn. + StackData const preShuffle = m_stack.data(); auto const shuffleResult = StackShuffler::shuffle(m_stack, blockLayout->exitIn); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA codegen block {} exit shuffle", _blockId.value), + shuffleResult, + fmt::format("source stack {}, target stack {}", stackToString(preShuffle), stackToString(blockLayout->exitIn)) + ); // handle the block exit std::visit(util::GenericVisitor{ [this, &_blockId](auto const& exit) { (*this)(_blockId, exit); } }, block.exit); @@ -211,8 +216,13 @@ void CodeTransform::operator()(SSACFG::OperationId _opId, StackData const& _oper // prepare stack for operation { + StackData const preShuffle = m_stack.data(); auto const shuffleResult = StackShuffler::shuffle(m_stack, _operationInputLayout); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA codegen operation {} input shuffle", _opId.value), + shuffleResult, + fmt::format("source stack {}, target stack {}", stackToString(preShuffle), stackToString(_operationInputLayout)) + ); } // check that the assembly stack height corresponds to the stack size after shuffling @@ -400,8 +410,13 @@ void CodeTransform::prepareBlockExitStack(StackData const& _target, PhiInverse c auto const pulledBackTarget = stackPreImage(_target, _phiInverse); // shuffle to target { + StackData const preShuffle = m_stack.data(); auto const shuffleResult = StackShuffler::shuffle(m_stack, pulledBackTarget); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + "SSA codegen branch-exit shuffle", + shuffleResult, + fmt::format("source stack {}, target stack {}", stackToString(preShuffle), stackToString(pulledBackTarget)) + ); } // check that shuffling was successful assertLayoutCompatibility(m_stack.data(), pulledBackTarget); diff --git a/libyul/backends/evm/ssa/CodeTransform.h b/libyul/backends/evm/ssa/CodeTransform.h index 48d74ffd66be..b9a7caecf883 100644 --- a/libyul/backends/evm/ssa/CodeTransform.h +++ b/libyul/backends/evm/ssa/CodeTransform.h @@ -26,6 +26,8 @@ #include +#include + namespace solidity::yul { struct BuiltinContext; diff --git a/libyul/backends/evm/ssa/StackLayoutGenerator.cpp b/libyul/backends/evm/ssa/StackLayoutGenerator.cpp index ff6b896c609c..abef0567ebc8 100644 --- a/libyul/backends/evm/ssa/StackLayoutGenerator.cpp +++ b/libyul/backends/evm/ssa/StackLayoutGenerator.cpp @@ -31,6 +31,9 @@ #include #include + +#include + #include using namespace solidity::yul::ssa; @@ -198,7 +201,15 @@ void StackLayoutGenerator::defineStackIn(SSACFG::BlockId const& _blockId) {}, proposals[i].size() ); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA stack-in proposal evaluation for block {}", _blockId.value), + shuffleResult, + fmt::format( + "candidate {}, source proposal {}", + stackToString(proposals[i]), + stackToString(proposals[j]) + ) + ); cumulativeCost += stack.callbacks().opGas; } cumulativeCosts[i] = cumulativeCost; @@ -261,13 +272,23 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId) m_hasFunctionReturnLabel ); { + StackData const preShuffle = stack.data(); auto const shuffleResult = StackShuffler::shuffle( stack, requiredStackTop, opLiveOutWithoutOutputs, targetSize ); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA stack layout operation {} shuffle in block {}", operationIndex, _blockId.value), + shuffleResult, + fmt::format( + "source stack {}, target args {}, target size {}", + stackToString(preShuffle), + stackToString(requiredStackTop), + targetSize + ) + ); } blockLayout.operationIn.push_back(currentStackData); @@ -297,10 +318,20 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId) false, m_hasFunctionReturnLabel ); + StackData const preShuffle = stack.data(); auto const shuffleResult = StackShuffler::shuffle( stack, {condition}, blockLiveOut, targetSize ); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA conditional-jump shuffle in block {}", _blockId.value), + shuffleResult, + fmt::format( + "source stack {}, target args {}, target size {}", + stackToString(preShuffle), + stackToString(StackData{condition}), + targetSize + ) + ); } yulAssert(!stack.empty() && stack.top().isValueID() && stack.top().valueID() == _cJump.condition); @@ -322,8 +353,13 @@ void StackLayoutGenerator::visitBlock(SSACFG::BlockId const& _blockId) // in case there are return values, let's bring the function return label to the top StackData returnStack = _functionReturn.returnValues | ranges::views::transform(StackSlot::makeValueID) | ranges::to; returnStack.push_back(StackSlot::makeFunctionReturnLabel(m_graphID)); + StackData const preShuffle = stack.data(); auto const shuffleResult = StackShuffler::shuffle(stack, returnStack); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + fmt::format("SSA function-return shuffle in block {}", _blockId.value), + shuffleResult, + fmt::format("source stack {}, target stack {}", stackToString(preShuffle), stackToString(returnStack)) + ); blockLayout.exitIn = currentStackData; }, [&](SSACFG::BasicBlock::Jump const& _jump) { diff --git a/libyul/backends/evm/ssa/StackUtils.cpp b/libyul/backends/evm/ssa/StackUtils.cpp index dbc9b1541e9e..41d627113747 100644 --- a/libyul/backends/evm/ssa/StackUtils.cpp +++ b/libyul/backends/evm/ssa/StackUtils.cpp @@ -20,6 +20,8 @@ #include +#include + #include #include @@ -32,6 +34,45 @@ using namespace solidity::yul::ssa; +namespace +{ + +char const* stackShufflerStatusToString(StackShufflerResult::Status _status) +{ + switch (_status) + { + case StackShufflerResult::Status::Continue: + return "Continue"; + case StackShufflerResult::Status::Admissible: + return "Admissible"; + case StackShufflerResult::Status::StackTooDeep: + return "StackTooDeep"; + case StackShufflerResult::Status::MaxIterationsReached: + return "MaxIterationsReached"; + } + solAssert(false, "Unknown StackShufflerResult status."); +} + +std::string formatShuffleFailure( + std::string_view _context, + StackShufflerResult const& _shuffleResult, + std::string_view _details +) +{ + std::string message = fmt::format( + "{} failed with {}", + _context, + stackShufflerStatusToString(_shuffleResult.status) + ); + if (_shuffleResult.status == StackShufflerResult::Status::StackTooDeep) + message += fmt::format(" (culprit: {})", slotToString(_shuffleResult.culprit)); + if (!_details.empty()) + message += fmt::format(": {}", _details); + return message; +} + +} + void GasAccumulatingCallbacks::swap(StackDepth _depth) { opGas += evmasm::GasMeter::swapGas(_depth.value, cfg.evmDialect.evmVersion()); @@ -77,6 +118,21 @@ StackData solidity::yul::ssa::stackPreImage(StackData _stack, PhiInverse const& return _stack; } +void solidity::yul::ssa::requireAdmissibleShuffle( + std::string_view _context, + StackShufflerResult const& _shuffleResult, + std::string _details +) +{ + if (_shuffleResult.status == StackShufflerResult::Status::Admissible) + return; + + std::string const message = formatShuffleFailure(_context, _shuffleResult, _details); + if (_shuffleResult.status == StackShufflerResult::Status::StackTooDeep) + solThrow(langutil::StackTooDeepError, message); + solThrow(langutil::InternalCompilerError, message); +} + std::size_t solidity::yul::ssa::findOptimalTargetSize ( StackData const& _stackData, @@ -107,7 +163,22 @@ std::size_t solidity::yul::ssa::findOptimalTargetSize data = _stackData; Stack countOpsStack(data, {}); auto const shuffleResult = StackShuffler::shuffle(countOpsStack, _targetArgs, _targetLiveOut, _targetSize); - yulAssert(shuffleResult.status == StackShufflerResult::Status::Admissible); + requireAdmissibleShuffle( + "SSA stack target-size evaluation", + shuffleResult, + fmt::format( + "source stack {}, target args {}, live-out {{{}}}, target size {}", + stackToString(_stackData), + stackToString(_targetArgs), + fmt::join( + _targetLiveOut | ranges::views::keys | ranges::views::transform( + [](auto const& _value) { return slotToString(StackSlot::makeValueID(_value)); } + ), + ", " + ), + _targetSize + ) + ); yulAssert(countOpsStack.size() == _targetSize); return countOpsStack.callbacks().numOps; }; diff --git a/libyul/backends/evm/ssa/StackUtils.h b/libyul/backends/evm/ssa/StackUtils.h index 47dc6711a79a..c1e905de6467 100644 --- a/libyul/backends/evm/ssa/StackUtils.h +++ b/libyul/backends/evm/ssa/StackUtils.h @@ -22,11 +22,14 @@ #include #include +#include #include namespace solidity::yul::ssa { +struct StackShufflerResult; + class ValidationResult { public: @@ -62,6 +65,12 @@ struct GasAccumulatingCallbacks /// Transform stack data by replacing all its phi variables with their respective preimages. StackData stackPreImage(StackData _stack, PhiInverse const& _phiInverse); +void requireAdmissibleShuffle( + std::string_view _context, + StackShufflerResult const& _shuffleResult, + std::string _details = {} +); + std::size_t findOptimalTargetSize( StackData const& _stackData, StackData const& _targetArgs, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c6c8040f8536..f271918f03b2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -177,6 +177,7 @@ set(libyul_sources libyul/ssa/StackLayoutGeneratorTest.h libyul/ssa/StackShufflerTest.cpp libyul/ssa/StackShufflerTest.h + libyul/ssa/StackUtilsTest.cpp ) detect_stray_source_files("${libyul_sources}" "libyul/") diff --git a/test/libyul/ssa/StackUtilsTest.cpp b/test/libyul/ssa/StackUtilsTest.cpp new file mode 100644 index 000000000000..71a4160fd678 --- /dev/null +++ b/test/libyul/ssa/StackUtilsTest.cpp @@ -0,0 +1,115 @@ +/* + 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 +/** + * Unit tests for SSA stack utility helpers. + */ + +#include + +#include +#include +#include + +#include + +using namespace solidity::langutil; +using namespace solidity::yul::ssa; + +namespace solidity::yul::ssa::test +{ + +namespace +{ + +std::string errorMessage(util::Exception const& _exception) +{ + return _exception.comment() ? *_exception.comment() : ""; +} + +StackSlot makeVariable(unsigned _id) +{ + return StackSlot::makeValueID(SSACFG::ValueId::makeVariable(_id)); +} + +} + +BOOST_AUTO_TEST_SUITE(StackUtilsTest) + +BOOST_AUTO_TEST_CASE(requireAdmissibleShuffle_throws_stack_too_deep_error) +{ + StackShufflerResult const stackTooDeepResult{ + .status = StackShufflerResult::Status::StackTooDeep, + .culprit = makeVariable(7) + }; + BOOST_CHECK_EXCEPTION( + requireAdmissibleShuffle( + "SSA test shuffle", + stackTooDeepResult, + "source stack [v1]" + ), + langutil::StackTooDeepError, + [](langutil::StackTooDeepError const& _exception) + { + BOOST_TEST( + errorMessage(_exception) == + "SSA test shuffle failed with StackTooDeep (culprit: v7): source stack [v1]" + ); + return true; + } + ); +} + +BOOST_AUTO_TEST_CASE(requireAdmissibleShuffle_throws_internal_compiler_error_for_max_iterations) +{ + StackShufflerResult const maxIterationsResult{.status = StackShufflerResult::Status::MaxIterationsReached}; + BOOST_CHECK_EXCEPTION( + requireAdmissibleShuffle( + "SSA test shuffle", + maxIterationsResult, + "target size 17" + ), + InternalCompilerError, + [](InternalCompilerError const& _exception) + { + BOOST_TEST(errorMessage(_exception) == "SSA test shuffle failed with MaxIterationsReached: target size 17"); + return true; + } + ); +} + +BOOST_AUTO_TEST_CASE(requireAdmissibleShuffle_throws_internal_compiler_error_for_continue) +{ + StackShufflerResult const continueResult{.status = StackShufflerResult::Status::Continue}; + BOOST_CHECK_EXCEPTION( + requireAdmissibleShuffle( + "SSA test shuffle", + continueResult, + "target size 23" + ), + InternalCompilerError, + [](InternalCompilerError const& _exception) + { + BOOST_TEST(errorMessage(_exception) == "SSA test shuffle failed with Continue: target size 23"); + return true; + } + ); +} + +BOOST_AUTO_TEST_SUITE_END() + +}