diff --git a/libsolidity/codegen/AsmCodeGen.cpp b/libsolidity/codegen/AsmCodeGen.cpp index 02ae6e661..fb39fea1a 100644 --- a/libsolidity/codegen/AsmCodeGen.cpp +++ b/libsolidity/codegen/AsmCodeGen.cpp @@ -184,18 +184,20 @@ void CodeGenerator::assemble( ) { EthAssemblyAdapter assemblyAdapter(_assembly); + shared_ptr dialect = EVMDialect::strictAssemblyForEVM(); + CodeTransform transform( + assemblyAdapter, + _analysisInfo, + _parsedData, + *dialect, + _optimize, + false, + _identifierAccess, + _useNamedLabelsForFunctions + ); try { - CodeTransform( - assemblyAdapter, - _analysisInfo, - _parsedData, - *EVMDialect::strictAssemblyForEVM(), - _optimize, - false, - _identifierAccess, - _useNamedLabelsForFunctions - )(_parsedData); + transform(_parsedData); } catch (StackTooDeepError const& _e) { @@ -205,4 +207,5 @@ void CodeGenerator::assemble( (_e.comment() ? ": " + *_e.comment() : ".") )); } + solAssert(transform.stackErrors().empty(), "Stack errors present but not thrown."); } diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index edae65552..13cd0c37f 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -13,6 +13,8 @@ add_library(yul AsmScope.h AsmScopeFiller.cpp AsmScopeFiller.h + CompilabilityChecker.cpp + CompilabilityChecker.h Dialect.cpp Dialect.h Exceptions.h diff --git a/libyul/CompilabilityChecker.cpp b/libyul/CompilabilityChecker.cpp new file mode 100644 index 000000000..1a3977efd --- /dev/null +++ b/libyul/CompilabilityChecker.cpp @@ -0,0 +1,64 @@ +/*( + 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 . +*/ +/** + * Component that checks whether all variables are reachable on the stack. + */ + +#include + +#include +#include + +#include +#include + +#include + +using namespace std; +using namespace yul; +using namespace dev; +using namespace dev::solidity; + +std::map CompilabilityChecker::run(std::shared_ptr _dialect, Block const& _ast) +{ + if (_dialect->flavour == AsmFlavour::Yul) + return {}; + + solAssert(_dialect->flavour == AsmFlavour::Strict, ""); + + EVMDialect const& evmDialect = dynamic_cast(*_dialect); + + bool optimize = true; + yul::AsmAnalysisInfo analysisInfo = + yul::AsmAnalyzer::analyzeStrictAssertCorrect(_dialect, EVMVersion(), _ast); + NoOutputAssembly assembly; + CodeTransform transform(assembly, analysisInfo, _ast, evmDialect, optimize); + try + { + transform(_ast); + } + catch (StackTooDeepError const&) + { + solAssert(!transform.stackErrors().empty(), "Got stack too deep exception that was not stored."); + } + + std::map functions; + for (StackTooDeepError const& error: transform.stackErrors()) + functions[error.functionName] = max(error.depth, functions[error.functionName]); + + return functions; +} diff --git a/libyul/CompilabilityChecker.h b/libyul/CompilabilityChecker.h new file mode 100644 index 000000000..80c91f73a --- /dev/null +++ b/libyul/CompilabilityChecker.h @@ -0,0 +1,45 @@ +/* + 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 . +*/ +/** + * Component that checks whether all variables are reachable on the stack. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace yul +{ + +/** + * Component that checks whether all variables are reachable on the stack and + * returns a mapping from function name to the largest stack difference found + * in that function (no entry present if that function is compilable). + * This only works properly if the outermost block is compilable and + * functions are not nested. Otherwise, it might miss reporting some functions. + */ +class CompilabilityChecker +{ +public: + static std::map run(std::shared_ptr _dialect, Block const& _ast); +}; + +} diff --git a/libyul/backends/evm/EVMCodeTransform.cpp b/libyul/backends/evm/EVMCodeTransform.cpp index d36540d46..7b90912d2 100644 --- a/libyul/backends/evm/EVMCodeTransform.cpp +++ b/libyul/backends/evm/EVMCodeTransform.cpp @@ -227,6 +227,18 @@ void CodeTransform::operator()(VariableDeclaration const& _varDecl) checkStackHeight(&_varDecl); } +void CodeTransform::stackError(StackTooDeepError _error, int _targetStackHeight) +{ + m_assembly.appendInstruction(solidity::Instruction::INVALID); + // Correct the stack. + while (m_assembly.stackHeight() > _targetStackHeight) + m_assembly.appendInstruction(solidity::Instruction::POP); + while (m_assembly.stackHeight() < _targetStackHeight) + m_assembly.appendConstant(u256(0)); + // Store error. + m_stackErrors.emplace_back(std::move(_error)); +} + void CodeTransform::operator()(Assignment const& _assignment) { int height = m_assembly.stackHeight(); @@ -513,18 +525,32 @@ void CodeTransform::operator()(FunctionDefinition const& _function) m_assembly.appendConstant(u256(0)); } - CodeTransform( - m_assembly, - m_info, - _function.body, - m_allowStackOpt, - m_dialect, - m_evm15, - m_identifierAccess, - m_useNamedLabelsForFunctions, - localStackAdjustment, - m_context - )(_function.body); + try + { + CodeTransform( + m_assembly, + m_info, + _function.body, + m_allowStackOpt, + m_dialect, + m_evm15, + m_identifierAccess, + m_useNamedLabelsForFunctions, + localStackAdjustment, + m_context + )(_function.body); + } + catch (StackTooDeepError const& _error) + { + // This exception will be re-thrown after the end of the surrounding block. + // It enables us to see which functions compiled successfully and which did not. + // Even if we emit actual code, add an illegal instruction to make sure that tests + // will catch it. + StackTooDeepError error(_error); + if (error.functionName.empty()) + error.functionName = _function.name; + stackError(error, height); + } { // The stack layout here is: @@ -544,28 +570,34 @@ void CodeTransform::operator()(FunctionDefinition const& _function) stackLayout.push_back(i); // Move return values down, but keep order. if (stackLayout.size() > 17) - BOOST_THROW_EXCEPTION(StackTooDeepError() << errinfo_comment( + { + StackTooDeepError error(_function.name, YulString{}, stackLayout.size() - 17); + error << errinfo_comment( "The function " + _function.name.str() + " has " + to_string(stackLayout.size() - 17) + " parameters or return variables too many to fit the stack size." - )); - while (!stackLayout.empty() && stackLayout.back() != int(stackLayout.size() - 1)) - if (stackLayout.back() < 0) - { - m_assembly.appendInstruction(solidity::Instruction::POP); - stackLayout.pop_back(); - } - else - { - m_assembly.appendInstruction(swapInstruction(stackLayout.size() - stackLayout.back() - 1)); - swap(stackLayout[stackLayout.back()], stackLayout.back()); - } - for (int i = 0; size_t(i) < stackLayout.size(); ++i) - solAssert(i == stackLayout[i], "Error reshuffling stack."); + ); + stackError(error, m_assembly.stackHeight() - _function.parameters.size()); + } + else + { + while (!stackLayout.empty() && stackLayout.back() != int(stackLayout.size() - 1)) + if (stackLayout.back() < 0) + { + m_assembly.appendInstruction(solidity::Instruction::POP); + stackLayout.pop_back(); + } + else + { + m_assembly.appendInstruction(swapInstruction(stackLayout.size() - stackLayout.back() - 1)); + swap(stackLayout[stackLayout.back()], stackLayout.back()); + } + for (int i = 0; size_t(i) < stackLayout.size(); ++i) + solAssert(i == stackLayout[i], "Error reshuffling stack."); + } } - if (m_evm15) m_assembly.appendReturnsub(_function.returnVariables.size(), stackHeightBefore); else @@ -623,6 +655,9 @@ void CodeTransform::operator()(Block const& _block) finalizeBlock(_block, blockStartStackHeight); m_scope = originalScope; + + if (!m_stackErrors.empty()) + BOOST_THROW_EXCEPTION(m_stackErrors.front()); } AbstractAssembly::LabelID CodeTransform::labelFromIdentifier(Identifier const& _identifier) @@ -741,7 +776,8 @@ int CodeTransform::variableHeightDiff(Scope::Variable const& _var, YulString _va solAssert(heightDiff > (_forSwap ? 1 : 0), "Negative stack difference for variable."); int limit = _forSwap ? 17 : 16; if (heightDiff > limit) - BOOST_THROW_EXCEPTION(StackTooDeepError() << errinfo_comment( + // throw exception with variable name and height diff + BOOST_THROW_EXCEPTION(StackTooDeepError(_varName, heightDiff - limit) << errinfo_comment( "Variable " + _varName.str() + " is " + diff --git a/libyul/backends/evm/EVMCodeTransform.h b/libyul/backends/evm/EVMCodeTransform.h index 511fa73d7..e5f083b8a 100644 --- a/libyul/backends/evm/EVMCodeTransform.h +++ b/libyul/backends/evm/EVMCodeTransform.h @@ -40,7 +40,16 @@ namespace yul struct AsmAnalysisInfo; class EVMAssembly; -struct StackTooDeepError: virtual YulException {}; +struct StackTooDeepError: virtual YulException +{ + StackTooDeepError(YulString _variable, int _depth): variable(_variable), depth(_depth) {} + StackTooDeepError(YulString _functionName, YulString _variable, int _depth): + functionName(_functionName), variable(_variable), depth(_depth) + {} + YulString functionName; + YulString variable; + int depth; +}; struct CodeTransformContext { @@ -115,6 +124,8 @@ public: { } + std::vector const& stackErrors() const { return m_stackErrors; } + protected: using Context = CodeTransformContext; @@ -184,6 +195,10 @@ private: void checkStackHeight(void const* _astElement) const; + /// Stores the stack error in the list of errors, appends an invalid opcode + /// and corrects the stack height to the target stack height. + void stackError(StackTooDeepError _error, int _targetStackSize); + AbstractAssembly& m_assembly; AsmAnalysisInfo& m_info; Scope* m_scope = nullptr; @@ -204,6 +219,8 @@ private: /// statement level in the scope where the variable was defined. std::set m_variablesScheduledForDeletion; std::set m_unusedStackSlots; + + std::vector m_stackErrors; }; } diff --git a/libyul/backends/evm/EVMObjectCompiler.cpp b/libyul/backends/evm/EVMObjectCompiler.cpp index dfeb7bb3a..20e822e47 100644 --- a/libyul/backends/evm/EVMObjectCompiler.cpp +++ b/libyul/backends/evm/EVMObjectCompiler.cpp @@ -62,5 +62,7 @@ void EVMObjectCompiler::run(Object& _object, bool _optimize) yulAssert(_object.code, "No code."); // We do not catch and re-throw the stack too deep exception here because it is a YulException, // which should be native to this part of the code. - CodeTransform{m_assembly, *_object.analysisInfo, *_object.code, m_dialect, _optimize, m_evm15}(*_object.code); + CodeTransform transform{m_assembly, *_object.analysisInfo, *_object.code, m_dialect, _optimize, m_evm15}; + transform(*_object.code); + yulAssert(transform.stackErrors().empty(), "Stack errors present but not thrown."); } diff --git a/test/libyul/CompilabilityChecker.cpp b/test/libyul/CompilabilityChecker.cpp new file mode 100644 index 000000000..1429762a6 --- /dev/null +++ b/test/libyul/CompilabilityChecker.cpp @@ -0,0 +1,196 @@ +/* + 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 . +*/ +/** + * Unit tests for the compilability checker. + */ + +#include + +#include +#include + +#include + + +using namespace std; + +namespace yul +{ +namespace test +{ + +namespace +{ +string check(string const& _input) +{ + shared_ptr ast = yul::test::parse(_input, false).first; + BOOST_REQUIRE(ast); + map functions = CompilabilityChecker::run(EVMDialect::strictAssemblyForEVM(), *ast); + string out; + for (auto const& function: functions) + out += function.first.str() + ": " + to_string(function.second) + " "; + return out; +} +} + +BOOST_AUTO_TEST_SUITE(CompilabilityChecker) + +BOOST_AUTO_TEST_CASE(smoke_test) +{ + string out = check("{}"); + BOOST_CHECK_EQUAL(out, ""); +} + +BOOST_AUTO_TEST_CASE(simple_function) +{ + string out = check("{ function f(a, b) -> x, y { x := a y := b } }"); + BOOST_CHECK_EQUAL(out, ""); +} + +BOOST_AUTO_TEST_CASE(many_variables_few_uses) +{ + string out = check(R"({ + function f(a, b) -> x, y { + let r1 := 0 + let r2 := 0 + let r3 := 0 + let r4 := 0 + let r5 := 0 + let r6 := 0 + let r7 := 0 + let r8 := 0 + let r9 := 0 + let r10 := 0 + let r11 := 0 + let r12 := 0 + let r13 := 0 + let r14 := 0 + let r15 := 0 + let r16 := 0 + let r17 := 0 + let r18 := 0 + x := add(add(add(add(add(add(add(add(add(x, r9), r8), r7), r6), r5), r4), r3), r2), r1) + } + })"); + BOOST_CHECK_EQUAL(out, "f: 4 "); +} + +BOOST_AUTO_TEST_CASE(many_variables_many_uses) +{ + string out = check(R"({ + function f(a, b) -> x, y { + let r1 := 0 + let r2 := 0 + let r3 := 0 + let r4 := 0 + let r5 := 0 + let r6 := 0 + let r7 := 0 + let r8 := 0 + let r9 := 0 + let r10 := 0 + let r11 := 0 + let r12 := 0 + let r13 := 0 + let r14 := 0 + let r15 := 0 + let r16 := 0 + let r17 := 0 + let r18 := 0 + x := add(add(add(add(add(add(add(add(add(add(add(add(x, r12), r11), r10), r9), r8), r7), r6), r5), r4), r3), r2), r1) + } + })"); + BOOST_CHECK_EQUAL(out, "f: 10 "); +} + +BOOST_AUTO_TEST_CASE(many_return_variables) +{ + string out = check(R"({ + function f(a, b) -> r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19 { + } + })"); + BOOST_CHECK_EQUAL(out, "f: 5 "); +} + +BOOST_AUTO_TEST_CASE(multiple_functions) +{ + string out = check(R"({ + function f(a, b) -> r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19 { + } + function g(r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19) -> x, y { + } + function h(x) { + let r1 := 0 + let r2 := 0 + let r3 := 0 + let r4 := 0 + let r5 := 0 + let r6 := 0 + let r7 := 0 + let r8 := 0 + let r9 := 0 + let r10 := 0 + let r11 := 0 + let r12 := 0 + let r13 := 0 + let r14 := 0 + let r15 := 0 + let r16 := 0 + let r17 := 0 + let r18 := 0 + x := add(add(add(add(add(add(add(add(add(add(add(add(x, r12), r11), r10), r9), r8), r7), r6), r5), r4), r3), r2), r1) + } + })"); + BOOST_CHECK_EQUAL(out, "h: 9 g: 5 f: 5 "); +} + +BOOST_AUTO_TEST_CASE(nested) +{ + string out = check(R"({ + function h(x) { + let r1 := 0 + let r2 := 0 + let r3 := 0 + let r4 := 0 + let r5 := 0 + let r6 := 0 + let r7 := 0 + let r8 := 0 + let r9 := 0 + let r10 := 0 + let r11 := 0 + let r12 := 0 + let r13 := 0 + let r14 := 0 + let r15 := 0 + let r16 := 0 + let r17 := 0 + let r18 := 0 + function f(a, b) -> t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19 { + function g(s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15, s16, s17, s18, s19) -> w, v { + } + } + x := add(add(add(add(add(add(add(add(add(add(add(add(x, r12), r11), r10), r9), r8), r7), r6), r5), r4), r3), r2), r1) + } + })"); + BOOST_CHECK_EQUAL(out, "h: 9 "); +} + +BOOST_AUTO_TEST_SUITE_END() + +} +}