diff --git a/Changelog.md b/Changelog.md index 863590ed091b..d5a4e6aa02e7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -41,6 +41,7 @@ Compiler Features: * General: Replace the current greedy stack shuffler in the experimental SSA CFG pipeline with a planning one. * SMTChecker: Emit a deprecation warning for the BMC engine. * SMTChecker: Support `block.slotnum`. +* Yul: Add the `@ast-id-instance` debug info annotation, which distinguishes Yul scopes cloned from the same Solidity definition. * Yul Optimizer: `LoopInvariantCodeMotion` can now move expressions depending on function parameters out of loops. * Yul Optimizer: `UnusedStoreEliminator` can now recognize redundant memory and storage operations whose start offset or length is a function parameter. * Yul Optimizer: Remove the ineffective elimination of unused `returndatacopy()` operations in simple cases from UnusedStoreEliminator. diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index e85920824a80..e13cb145c54e 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -392,7 +392,8 @@ Input Description // The snippet is quoted and follows the corresponding `@src` annotation. // Depends on `location`; selecting `snippet` without it is an error. // - `ast-id`: Annotations of the form `@ast-id ` over elements that can be mapped back to a definition in the original Solidity file. - // `` is a node ID in the Solidity AST ('ast' output). + // `` is a node ID in the Solidity AST ('ast' output). A Yul scope cloned from the same definition + // additionally carries `@ast-id-instance ` to distinguish it from the original. // - `ethdebug`: Ethdebug annotations (experimental). Depends on `ast-id`; selecting // `ethdebug` without `ast-id` is an error. Requesting an ethdebug output does not // change this selection; without `ethdebug` in it the `evm.bytecode.ethdebug` and diff --git a/liblangutil/DebugData.h b/liblangutil/DebugData.h index 70259bd039f3..fd7a2166815e 100644 --- a/liblangutil/DebugData.h +++ b/liblangutil/DebugData.h @@ -21,10 +21,13 @@ #include #include #include +#include namespace solidity::langutil { +struct SemanticDebugScope; + struct DebugData { typedef typename std::shared_ptr ConstPtr; @@ -32,23 +35,31 @@ struct DebugData explicit DebugData( langutil::SourceLocation _nativeLocation = {}, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + std::optional _astIDInstance = {}, + std::shared_ptr _semanticDebugScope = {} ): nativeLocation(std::move(_nativeLocation)), originLocation(std::move(_originLocation)), - astID(_astID) + astID(_astID), + astIDInstance(_astIDInstance), + semanticDebugScope(std::move(_semanticDebugScope)) {} static DebugData::ConstPtr create( langutil::SourceLocation _nativeLocation, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + std::optional _astIDInstance = {}, + std::shared_ptr _semanticDebugScope = {} ) { return std::make_shared( std::move(_nativeLocation), std::move(_originLocation), - _astID + _astID, + _astIDInstance, + std::move(_semanticDebugScope) ); } @@ -58,6 +69,16 @@ struct DebugData return emptyDebugData; } + /// The locations of @a _debugData without its source-language identity: + /// for nodes the compiler generates rather than derives from a source node, + /// which must not inherit the AST ID, instance or semantic scope. + static DebugData::ConstPtr locationsOnly(DebugData::ConstPtr const& _debugData) + { + if (!_debugData) + return create(); + return create(_debugData->nativeLocation, _debugData->originLocation); + } + /// Location in the Yul code. langutil::SourceLocation nativeLocation; /// Location in the original source that the Yul code was produced from. @@ -65,6 +86,14 @@ struct DebugData langutil::SourceLocation originLocation; /// ID in the (Solidity) source AST. std::optional astID; + /// Discriminator of the generated scope instance among those sharing + /// @a astID, carried in Yul text by the `@ast-id-instance` annotation. + /// Unset or zero for code that has not been cloned. + std::optional astIDInstance; + /// Semantic scope record attached to this node. It has no Yul comment + /// form and travels in the ethdebug sidecar instead + /// (see liblangutil/SemanticDebugData.h). + std::shared_ptr semanticDebugScope; }; } // namespace solidity::langutil diff --git a/libsolidity/codegen/ir/IRGenerator.cpp b/libsolidity/codegen/ir/IRGenerator.cpp index bce47a671d1a..279608cc3e77 100644 --- a/libsolidity/codegen/ir/IRGenerator.cpp +++ b/libsolidity/codegen/ir/IRGenerator.cpp @@ -123,7 +123,7 @@ std::string IRGenerator::generate( Whiskers t(R"(/// ethdebug: enabled /// @use-src object "" { - code { + code { @@ -137,7 +137,7 @@ std::string IRGenerator::generate( } /// @use-src object "" { - code { + code { @@ -158,6 +158,13 @@ std::string IRGenerator::generate( m_context.registerImmutableVariable(*var); t("isEthdebugEnabled", m_context.debugInfoSelection().ethdebug); + // The semantic debug info of contract-level variables attaches to the + // top-level Yul block of each object, so the block carries the contract's + // AST ID whenever that info is transported. + t( + "contractASTIDComment", + m_context.debugInfoSelection().ethdebug ? "/** @ast-id " + std::to_string(_contract.id()) + " */ " : "" + ); t("CreationObject", IRNames::creationObject(_contract)); t("sourceLocationCommentCreation", dispenseLocationComment(_contract)); t("library", _contract.isLibrary()); diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 06854360d300..5552c0ab5d35 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -810,7 +810,10 @@ void CompilerStack::link() } } -YulStack CompilerStack::loadGeneratedIR(std::string const& _ir) const +YulStack CompilerStack::loadGeneratedIR( + std::string const& _ir, + SemanticDebugDataTable const* _semanticDebugData +) const { YulStack stack( m_evmVersion, @@ -831,6 +834,8 @@ YulStack CompilerStack::loadGeneratedIR(std::string const& _ir) const true // _withErrorIds ) + "\n" ); + if (_semanticDebugData) + stack.attachSemanticDebugData(*_semanticDebugData); return stack; } @@ -970,6 +975,12 @@ std::optional const& CompilerStack::yulIR(std::string const& _contr return contract(_contractName).yulIR; } +std::optional const& CompilerStack::yulSemanticDebugData(std::string const& _contractName) const +{ + solAssert(m_stackState == CompilationSuccessful, "Compilation was not successful."); + return contract(_contractName).yulSemanticDebugData; +} + std::optional CompilerStack::yulIRAst(std::string const& _contractName) const { solAssert(m_stackState == CompilationSuccessful, "Compilation was not successful."); @@ -1590,7 +1601,20 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti ); yulAssert(compiledContract.yulIR); - YulStack stack = loadGeneratedIR(*compiledContract.yulIR); + if (m_debugInfoSelection.ethdebug) + compiledContract.yulSemanticDebugData = ethdebug::semanticDebugDataTable(_contract, sourceIndices()); + else + compiledContract.yulSemanticDebugData = std::nullopt; + + YulStack stack = loadGeneratedIR( + *compiledContract.yulIR, + compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr + ); + // The side table pairs with the unoptimized IR: attaching checks every + // pointer against it, and the table is kept as attached. The optimized IR, + // whose variable names differ, is not a supported pairing yet. + if (compiledContract.yulSemanticDebugData) + compiledContract.yulSemanticDebugData = stack.semanticDebugData(); if (!_unoptimizedOnly) { stack.optimize(m_viaSSACFG); @@ -1612,7 +1636,10 @@ void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract) return; // Re-parse the Yul IR in EVM dialect - YulStack stack = loadGeneratedIR(*compiledContract.yulIROptimized); + YulStack stack = loadGeneratedIR( + *compiledContract.yulIROptimized, + compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr + ); std::string deployedName = IRNames::deployedObject(_contract); solAssert(!deployedName.empty(), ""); diff --git a/libsolidity/interface/CompilerStack.h b/libsolidity/interface/CompilerStack.h index 0a8bd18d0adc..369fdd1d0a10 100644 --- a/libsolidity/interface/CompilerStack.h +++ b/libsolidity/interface/CompilerStack.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -325,6 +326,9 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac /// @returns the IR representation of a contract. std::optional const& yulIR(std::string const& _contractName) const; + /// @returns the semantic debug data sidecar associated with the contract's Yul IR. + std::optional const& yulSemanticDebugData(std::string const& _contractName) const; + /// @returns the IR representation of a contract AST in format. std::optional yulIRAst(std::string const& _contractName) const; @@ -460,6 +464,7 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac evmasm::LinkerObject runtimeObject; ///< Runtime object. std::optional yulIR; ///< Yul IR code straight from the code generator. std::optional yulIROptimized; ///< Reparsed and possibly optimized Yul IR code. + std::optional yulSemanticDebugData; util::LazyInit metadata; ///< The metadata json that will be hashed into the chain. util::LazyInit abi; util::LazyInit storageLayout; @@ -538,7 +543,10 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac /// Parses and analyzes specified Yul source and returns the YulStack that can be used to manipulate it. /// Assumes that the IR was generated from sources loaded currently into CompilerStack, which /// means that it is error-free and uses the same settings. - yul::YulStack loadGeneratedIR(std::string const& _ir) const; + yul::YulStack loadGeneratedIR( + std::string const& _ir, + langutil::SemanticDebugDataTable const* _semanticDebugData = nullptr + ) const; /// @returns the contract object for the given @a _contractName. /// Can only be called after state is CompilationSuccessful. diff --git a/libyul/AsmParser.cpp b/libyul/AsmParser.cpp index a6949d7f8bdb..ef78c890c7a8 100644 --- a/libyul/AsmParser.cpp +++ b/libyul/AsmParser.cpp @@ -58,6 +58,18 @@ std::optional toInt(std::string const& _value) } } +std::optional toInt64(std::string const& _value) +{ + try + { + return stoll(_value); + } + catch (std::out_of_range const&) + { + return std::nullopt; + } +} + } langutil::DebugData::ConstPtr Parser::createDebugData() const @@ -69,7 +81,12 @@ langutil::DebugData::ConstPtr Parser::createDebugData() const case UseSourceLocationFrom::LocationOverride: return DebugData::create(m_locationOverride, m_locationOverride); case UseSourceLocationFrom::Comments: - return DebugData::create(ParserBase::currentLocation(), m_locationFromComment, m_astIDFromComment); + return DebugData::create( + ParserBase::currentLocation(), + m_locationFromComment, + m_astIDFromComment, + m_astIDInstanceFromComment + ); } solAssert(false, ""); } @@ -156,6 +173,7 @@ void Parser::fetchDebugDataFromComment() if (commentLiteral.empty()) { m_astIDFromComment = std::nullopt; + m_astIDInstanceFromComment = std::nullopt; return; } static std::regex const tagRegex = std::regex( @@ -167,7 +185,8 @@ void Parser::fetchDebugDataFromComment() langutil::SourceLocation originLocation = m_locationFromComment; // Empty for each new node. - std::optional astID; + std::optional astID; + std::optional astIDInstance; while (regex_search(commentLiteral.cbegin(), commentLiteral.cend(), match, tagRegex)) { @@ -188,6 +207,13 @@ void Parser::fetchDebugDataFromComment() else break; } + else if (match[1] == "@ast-id-instance") + { + if (auto parseResult = parseASTIDInstanceComment(commentLiteral, m_scanner->currentCommentLocation())) + tie(commentLiteral, astIDInstance) = *parseResult; + else + break; + } else // Ignore unrecognized tags. continue; @@ -195,6 +221,9 @@ void Parser::fetchDebugDataFromComment() m_locationFromComment = originLocation; m_astIDFromComment = astID; + // An instance without an accompanying @ast-id on the same node is + // malformed and ignored. + m_astIDInstanceFromComment = astID ? astIDInstance : std::nullopt; } std::optional> Parser::parseSrcComment( @@ -297,36 +326,46 @@ std::optional> Parser::parseSrcComme return {{tail, SourceLocation{}}}; } -std::optional>> Parser::parseASTIDComment( +std::optional>> Parser::parseASTIDComment( std::string_view _arguments, langutil::SourceLocation const& _commentLocation ) +{ + return parseIntegerArgumentComment(_arguments, _commentLocation, "@ast-id", 1749_error); +} + +std::optional>> Parser::parseASTIDInstanceComment( + std::string_view _arguments, + langutil::SourceLocation const& _commentLocation +) +{ + return parseIntegerArgumentComment(_arguments, _commentLocation, "@ast-id-instance", 9913_error); +} + +std::optional>> Parser::parseIntegerArgumentComment( + std::string_view _arguments, + langutil::SourceLocation const& _commentLocation, + std::string_view _tag, + langutil::ErrorId _errorID +) { static std::regex const argRegex = std::regex( R"~~(^(\d+)(?:\s|$))~~", std::regex_constants::ECMAScript | std::regex_constants::optimize ); std::match_results match; - std::optional astID; - bool matched = regex_search(_arguments.cbegin(), _arguments.cend(), match, argRegex); - std::string_view tail = _arguments; - if (matched) + if (!regex_search(_arguments.cbegin(), _arguments.cend(), match, argRegex)) { - solAssert(match.size() == 2, ""); - tail = _arguments.substr(static_cast(match.position() + match.length())); - - astID = toInt(match[1].str()); + m_errorReporter.syntaxError(_errorID, _commentLocation, "Invalid argument for " + std::string(_tag) + "."); + return std::nullopt; } + solAssert(match.size() == 2, ""); - if (!matched || !astID || *astID < 0 || static_cast(*astID) != *astID) - { - m_errorReporter.syntaxError(1749_error, _commentLocation, "Invalid argument for @ast-id."); - astID = std::nullopt; - } - if (matched) - return {{_arguments, astID}}; - else - return std::nullopt; + // AST IDs and instances are 64-bit; anything larger is not an ID of this compiler. + std::optional const value = toInt64(match[1].str()); + if (!value) + m_errorReporter.syntaxError(_errorID, _commentLocation, "Invalid argument for " + std::string(_tag) + "."); + return {{_arguments, value}}; } Block Parser::parseBlock() diff --git a/libyul/AsmParser.h b/libyul/AsmParser.h index 75c1cba873f9..9bd7ff1355b9 100644 --- a/libyul/AsmParser.h +++ b/libyul/AsmParser.h @@ -23,6 +23,7 @@ #pragma once +#include #include #include #include @@ -112,11 +113,25 @@ class Parser: public langutil::ParserBase langutil::SourceLocation const& _commentLocation ); - std::optional>> parseASTIDComment( + std::optional>> parseASTIDComment( std::string_view _arguments, langutil::SourceLocation const& _commentLocation ); + std::optional>> parseASTIDInstanceComment( + std::string_view _arguments, + langutil::SourceLocation const& _commentLocation + ); + + /// Parses the non-negative 64-bit integer argument of the `@ast-id` and + /// `@ast-id-instance` annotations. + std::optional>> parseIntegerArgumentComment( + std::string_view _arguments, + langutil::SourceLocation const& _commentLocation, + std::string_view _tag, + langutil::ErrorId _errorID + ); + /// Creates a DebugData object with the correct source location set. langutil::DebugData::ConstPtr createDebugData() const; @@ -161,6 +176,7 @@ class Parser: public langutil::ParserBase langutil::SourceLocation m_locationOverride; langutil::SourceLocation m_locationFromComment; std::optional m_astIDFromComment; + std::optional m_astIDInstanceFromComment; UseSourceLocationFrom m_useSourceLocationFrom = UseSourceLocationFrom::Scanner; ForLoopComponent m_currentForLoopComponent = ForLoopComponent::None; bool m_insideFunction = false; diff --git a/libyul/AsmPrinter.cpp b/libyul/AsmPrinter.cpp index a3f7b4ca2b23..0b38979084f4 100644 --- a/libyul/AsmPrinter.cpp +++ b/libyul/AsmPrinter.cpp @@ -313,7 +313,13 @@ std::string AsmPrinter::formatDebugData(langutil::DebugData::ConstPtr const& _de std::vector items; if (auto id = _debugData->astID) if (m_debugInfoSelection.astID) + { items.emplace_back("@ast-id " + std::to_string(*id)); + // The instance discriminator of a cloned scope travels next to the + // origin; uncloned code keeps the plain form. + if (_debugData->astIDInstance && *_debugData->astIDInstance != 0) + items.emplace_back("@ast-id-instance " + std::to_string(*_debugData->astIDInstance)); + } if ( m_lastLocation != _debugData->originLocation && diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index a6c313eb60ce..2a3a4e28d354 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -40,6 +40,8 @@ add_library(yul Scope.h ScopeFiller.cpp ScopeFiller.h + SemanticDebugDataTransfer.cpp + SemanticDebugDataTransfer.h Utilities.cpp Utilities.h YulName.h diff --git a/libyul/SemanticDebugDataTransfer.cpp b/libyul/SemanticDebugDataTransfer.cpp new file mode 100644 index 000000000000..9cc9145137e8 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -0,0 +1,264 @@ +/* + 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 +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::yul; +using namespace solidity::langutil; + +namespace +{ + +template +constexpr bool isVariant = false; +template +constexpr bool isVariant> = true; + +template +constexpr bool isUniquePtr = false; +template +constexpr bool isUniquePtr> = true; + +template +constexpr bool isVector = false; +template +constexpr bool isVector> = true; + +/// Visits the debug data of every node of a Yul AST, including the names in +/// declarations, parameter and return variable lists, which ASTWalker does not +/// visit. Constness is a template parameter so that the same traversal reads +/// a const AST and rewrites a mutable one. +template +class DebugDataVisitor +{ +public: + explicit DebugDataVisitor(Visitor _visitor): + m_visitor(std::move(_visitor)) + {} + + template + void operator()(BlockT& _block) { visitNode(_block); } + +private: + template + void visitNode(T& _node) + { + using Node = std::remove_const_t; + if constexpr (isVariant) + std::visit([&](auto& _alternative) { visitNode(_alternative); }, _node); + else if constexpr (isUniquePtr) + { + if (_node) + visitNode(*_node); + } + else if constexpr (isVector) + { + for (auto& element: _node) + visitNode(element); + } + else + { + if constexpr (requires { _node.debugData; }) + m_visitor(_node.debugData); + visitChildren(_node); + } + } + + template + void visitChildren(T& _node) + { + if constexpr (requires { _node.statements; }) + visitNode(_node.statements); + if constexpr (requires { _node.expression; }) + visitNode(_node.expression); + if constexpr (requires { _node.value; }) + visitNode(_node.value); + if constexpr (requires { _node.variables; }) + visitNode(_node.variables); + if constexpr (requires { _node.variableNames; }) + visitNode(_node.variableNames); + if constexpr (requires { _node.parameters; }) + visitNode(_node.parameters); + if constexpr (requires { _node.returnVariables; }) + visitNode(_node.returnVariables); + if constexpr (requires { _node.body; }) + visitNode(_node.body); + if constexpr (requires { _node.condition; }) + visitNode(_node.condition); + if constexpr (requires { _node.cases; }) + visitNode(_node.cases); + if constexpr (requires { _node.pre; }) + visitNode(_node.pre); + if constexpr (requires { _node.post; }) + visitNode(_node.post); + if constexpr (requires { _node.functionName; }) + visitNode(_node.functionName); + if constexpr (requires { _node.arguments; }) + visitNode(_node.arguments); + } + + Visitor m_visitor; +}; + +ScopeID keyOf(langutil::DebugData const& _debugData) +{ + return {*_debugData.astID, _debugData.astIDInstance.value_or(0)}; +} + +std::set declaredVariableNames(Block const& _root) +{ + std::set names; + for (auto const& name: NameCollector(_root, NameCollector::OnlyVariables).names()) + names.insert(name.str()); + return names; +} + +/// Whether every name that @a _pointer reads and nothing binds is a variable +/// declared in the current Yul code. +bool pointerDependenciesPresent( + ethdebug::schema::Pointer const& _pointer, + std::set const& _yulNames, + SemanticDebugDataTable const& _table +) +{ + for (std::string const& name: ethdebug::schema::freeNames(_pointer, _table.pointerTemplates())) + if (!_yulNames.contains(name)) + return false; + return true; +} + +/// Marking a value unavailable is always sound, while keeping a pointer that +/// no longer describes the value is not: a materialized variable whose pointer +/// reads a Yul name that no longer exists becomes optimized-out, and so does +/// an update that would rebind it to such a pointer. +SemanticDebugScope::ConstPtr withLostPointersInvalidated( + SemanticDebugScope::ConstPtr const& _scope, + std::set const& _yulNames, + SemanticDebugDataTable const& _table +) +{ + if (!_scope) + return nullptr; + + SemanticDebugScope result = *_scope; + bool changed = false; + for (SemanticDebugVariable& variable: result.variableDefinitions) + if ( + variable.phase == SemanticDebugVariablePhase::Materialized && + variable.pointer && + !pointerDependenciesPresent(*variable.pointer, _yulNames, _table) + ) + { + variable.phase = SemanticDebugVariablePhase::OptimizedOut; + variable.pointer = std::nullopt; + changed = true; + } + for (SemanticDebugVariableUpdate& update: result.variableUpdates) + if (update.pointer && !pointerDependenciesPresent(*update.pointer, _yulNames, _table)) + { + update.phase = SemanticDebugVariablePhase::OptimizedOut; + update.pointer = std::nullopt; + changed = true; + } + + if (!changed) + return _scope; + return std::make_shared(std::move(result)); +} + +void applySemanticDebugDataToCode(Object& _object, SemanticDebugDataTable& _table) +{ + std::set const yulNames = declaredVariableNames(_object.code()->root()); + // Records are checked once per object and key, then attached to every + // node carrying that key and written back to the table. + std::map attached; + DebugDataVisitor rewriter{[&](langutil::DebugData::ConstPtr& _debugData) { + if (!_debugData || !_debugData->astID) + return; + + ScopeID const key = keyOf(*_debugData); + auto scope = attached.find(key); + if (scope == attached.end()) + { + SemanticDebugScope::ConstPtr record = _table.find(key); + if (!record) + return; + scope = attached.emplace(key, withLostPointersInvalidated(record, yulNames, _table)).first; + } + + _debugData = langutil::DebugData::create( + _debugData->nativeLocation, + _debugData->originLocation, + _debugData->astID, + _debugData->astIDInstance, + scope->second + ); + }}; + // The AST is owned mutably by the object; this is the one place that + // rewrites its debug data in place. + rewriter(const_cast(_object.code()->root())); + + for (auto const& [key, scope]: attached) + _table.set(key, scope); +} + +} + +void yul::collectSemanticDebugData(Object const& _object, SemanticDebugDataTable& _table) +{ + if (_object.hasCode()) + { + DebugDataVisitor collector{[&](langutil::DebugData::ConstPtr const& _debugData) { + if (_debugData && _debugData->astID && _debugData->semanticDebugScope) + _table.set(keyOf(*_debugData), _debugData->semanticDebugScope); + }}; + collector(_object.code()->root()); + } + + for (auto const& subNode: _object.subObjects) + if (auto const* subObject = dynamic_cast(subNode.get())) + collectSemanticDebugData(*subObject, _table); +} + +void yul::applySemanticDebugData(Object& _object, SemanticDebugDataTable& _table) +{ + if (_object.hasCode()) + applySemanticDebugDataToCode(_object, _table); + + for (auto const& subNode: _object.subObjects) + if (auto* subObject = dynamic_cast(subNode.get())) + applySemanticDebugData(*subObject, _table); +} diff --git a/libyul/SemanticDebugDataTransfer.h b/libyul/SemanticDebugDataTransfer.h new file mode 100644 index 000000000000..5fa0b5acf2e0 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.h @@ -0,0 +1,48 @@ +/* + 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 +/** + * Moves semantic debug info between the (AST ID, instance)-keyed side table + * and the DebugData of Yul AST nodes. + */ + +#pragma once + +#include + +namespace solidity::yul +{ + +class Object; + +/// Collects the scope records attached to the Yul AST nodes of @a _object and +/// its sub-objects into @a _table, keyed by (AST ID, instance). A record +/// already in the table for the same key is replaced; records with no +/// attached node are left untouched. +void collectSemanticDebugData(Object const& _object, langutil::SemanticDebugDataTable& _table); + +/// Attaches the scope records of @a _table to every Yul AST node of +/// @a _object and its sub-objects whose (AST ID, instance) has a record. +/// Pointers are checked per object: a materialized variable whose pointer +/// reads a generated Yul local that does not exist in that object's code is +/// attached there as optimized-out without a pointer, even if the local exists +/// in a sibling object. @a _table receives the records as attached, so it +/// describes the same state as the AST; where an object attaches a record that +/// a sibling object also attaches, the sibling visited last wins. +void applySemanticDebugData(Object& _object, langutil::SemanticDebugDataTable& _table); + +} diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index e468b3fa25d3..e806828f5d63 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -202,6 +203,11 @@ void YulStack::reparse() // NOTE: it is important for the source printed here to exactly match what the compiler will // eventually output to the user. In particular, debug info must be exactly the same. // Otherwise source locations will be off. + // Semantic debug info has no comment form in the printed Yul. Collect it from the AST into the + // side table before printing and reattach it by (AST ID, instance) after the reparse; records + // no Yul node carries survive in the table. + if (m_debugInfoSelection.ethdebug) + collectSemanticDebugData(*m_parserResult, m_semanticDebugData); std::string source = print(); YulStack cleanStack( @@ -221,6 +227,8 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); + if (m_debugInfoSelection.ethdebug && !m_semanticDebugData.empty()) + applySemanticDebugData(*m_parserResult, m_semanticDebugData); // NOTE: We keep the char stream, and errors, even though they no longer match the object, // because it's the original source that matters to the user. Optimized code may have different @@ -386,6 +394,20 @@ std::shared_ptr YulStack::parserResult() const return m_parserResult; } +void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) +{ + yulAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); + yulAssert(m_parserResult, ""); + yulAssert(m_debugInfoSelection.ethdebug, "Semantic debug info was supplied without requesting ethdebug."); + + m_semanticDebugData.merge(_table); + // Applying rather than merely reattaching re-checks every pointer against the current code: + // when the table is attached to Yul reloaded from text, a variable whose pointer reads a Yul + // local that no longer exists must become optimized-out instead of keeping a stale pointer. + if (!m_semanticDebugData.empty()) + applySemanticDebugData(*m_parserResult, m_semanticDebugData); +} + Dialect const& YulStack::dialect() const { yulAssert(m_stackState >= AnalysisSuccessful); diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 3ae7e9fc466d..1843cce210f8 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -35,6 +35,8 @@ #include +#include + #include #include @@ -97,7 +99,12 @@ class YulStack: public langutil::CharStreamProvider m_soliditySourceProvider(_soliditySourceProvider), m_errorReporter(m_errors), m_objectOptimizer(_objectOptimizer ? std::move(_objectOptimizer) : std::make_shared()) - {} + { + yulAssert( + !m_debugInfoSelection.ethdebug || m_debugInfoSelection.astID, + "Ethdebug semantic data requires AST ID debug information." + ); + } /// @returns the char stream used during parsing langutil::CharStream const& charStream(std::string const& _sourceName) const override; @@ -144,6 +151,14 @@ class YulStack: public langutil::CharStreamProvider /// Return the parsed and analyzed object. std::shared_ptr parserResult() const; + /// Merges @a _table into the retained side table and attaches its scope + /// records to the current Yul AST, checking every pointer against the + /// current code (see applySemanticDebugData). + void attachSemanticDebugData(langutil::SemanticDebugDataTable const& _table); + /// The side table as attached to the current Yul AST: the resource tables + /// and every scope record, whether or not a Yul node carries it. + langutil::SemanticDebugDataTable const& semanticDebugData() const { return m_semanticDebugData; } + Dialect const& dialect() const; langutil::DebugInfoSelection debugInfoSelection() const { return m_debugInfoSelection; } @@ -177,6 +192,9 @@ class YulStack: public langutil::CharStreamProvider State m_stackState = Empty; std::shared_ptr m_parserResult; + /// Semantic debug info keyed by (AST ID, instance). Retained across reparses + /// so that scope records no Yul node carries survive the Yul text boundary. + langutil::SemanticDebugDataTable m_semanticDebugData; langutil::ErrorList m_errors; langutil::ErrorReporter m_errorReporter; diff --git a/libyul/optimiser/FunctionGrouper.cpp b/libyul/optimiser/FunctionGrouper.cpp index 5f649d1891e8..586f10bc4c51 100644 --- a/libyul/optimiser/FunctionGrouper.cpp +++ b/libyul/optimiser/FunctionGrouper.cpp @@ -34,7 +34,9 @@ void FunctionGrouper::operator()(Block& _block) return; std::vector reordered; - reordered.emplace_back(Block{_block.debugData, {}}); + // The grouping block is generated: it keeps the locations but not the + // source-node identity of the block it groups. + reordered.emplace_back(Block{langutil::DebugData::locationsOnly(_block.debugData), {}}); for (auto&& statement: _block.statements) { diff --git a/libyul/optimiser/FunctionHoister.cpp b/libyul/optimiser/FunctionHoister.cpp index 356056a99abd..b87d19026708 100644 --- a/libyul/optimiser/FunctionHoister.cpp +++ b/libyul/optimiser/FunctionHoister.cpp @@ -43,7 +43,7 @@ void FunctionHoister::operator()(Block& _block) if (std::holds_alternative(statement)) { m_functions.emplace_back(std::move(statement)); - statement = Block{_block.debugData, {}}; + statement = Block{langutil::DebugData::locationsOnly(_block.debugData), {}}; } } removeEmptyBlocks(_block); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9fbc55426d88..102d13470bfa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,6 +154,7 @@ set(libyul_sources libyul/ControlFlowGraphTest.h libyul/ControlFlowSideEffectsTest.cpp libyul/ControlFlowSideEffectsTest.h + libyul/DebugData.cpp libyul/EVMCodeTransformTest.cpp libyul/EVMCodeTransformTest.h libyul/EVMDialectCompatibility.cpp diff --git a/test/cmdlineTests/standard_metadata_experimental/output.json b/test/cmdlineTests/standard_metadata_experimental/output.json index 755a59486808..98929312468e 100644 --- a/test/cmdlineTests/standard_metadata_experimental/output.json +++ b/test/cmdlineTests/standard_metadata_experimental/output.json @@ -5,7 +5,7 @@ "ir": "/// ethdebug: enabled /// @use-src 0:\"C\" object \"C_2\" { - code { + code /** @ast-id 2 */ { /// @src 0:56:69 mstore(64, memoryguard(128)) if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() } @@ -36,7 +36,7 @@ object \"C_2\" { } /// @use-src 0:\"C\" object \"C_2_deployed\" { - code { + code /** @ast-id 2 */ { /// @src 0:56:69 mstore(64, memoryguard(128)) diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json index ea3e07cbc99f..5b87dfeeccaf 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json @@ -13,7 +13,7 @@ "ir": "/// ethdebug: enabled /// @use-src 0:\"a.sol\" object \"A1_14\" { - code { + code /** @ast-id 14 */ { /// @src 0:58:123 mstore(64, memoryguard(128)) if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() } @@ -44,7 +44,7 @@ object \"A1_14\" { } /// @use-src 0:\"a.sol\" object \"A1_14_deployed\" { - code { + code /** @ast-id 14 */ { /// @src 0:58:123 mstore(64, memoryguard(128)) @@ -183,7 +183,8 @@ object \"A1_14\" { "irOptimized": "/// ethdebug: enabled /// @use-src 0:\"a.sol\" object \"A1_14\" { - code { + code /// @ast-id 14 + { { /// @src 0:58:123 mstore(64, memoryguard(0x80)) @@ -202,7 +203,8 @@ object \"A1_14\" { } /// @use-src 0:\"a.sol\" object \"A1_14_deployed\" { - code { + code /// @ast-id 14 + { { /// @src 0:58:123 mstore(64, memoryguard(0x80)) @@ -308,7 +310,7 @@ object \"A1_14\" { "ir": "/// ethdebug: enabled /// @use-src 0:\"a.sol\" object \"A2_27\" { - code { + code /** @ast-id 27 */ { /// @src 0:124:189 mstore(64, memoryguard(128)) if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() } @@ -339,7 +341,7 @@ object \"A2_27\" { } /// @use-src 0:\"a.sol\" object \"A2_27_deployed\" { - code { + code /** @ast-id 27 */ { /// @src 0:124:189 mstore(64, memoryguard(128)) @@ -478,7 +480,8 @@ object \"A2_27\" { "irOptimized": "/// ethdebug: enabled /// @use-src 0:\"a.sol\" object \"A2_27\" { - code { + code /// @ast-id 27 + { { /// @src 0:124:189 mstore(64, memoryguard(0x80)) @@ -497,7 +500,8 @@ object \"A2_27\" { } /// @use-src 0:\"a.sol\" object \"A2_27_deployed\" { - code { + code /// @ast-id 27 + { { /// @src 0:124:189 mstore(64, memoryguard(0x80)) @@ -605,7 +609,7 @@ object \"A2_27\" { "ir": "/// ethdebug: enabled /// @use-src 1:\"b.sol\" object \"A1_42\" { - code { + code /** @ast-id 42 */ { /// @src 1:58:123 mstore(64, memoryguard(128)) if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() } @@ -636,7 +640,7 @@ object \"A1_42\" { } /// @use-src 1:\"b.sol\" object \"A1_42_deployed\" { - code { + code /** @ast-id 42 */ { /// @src 1:58:123 mstore(64, memoryguard(128)) @@ -775,7 +779,8 @@ object \"A1_42\" { "irOptimized": "/// ethdebug: enabled /// @use-src 1:\"b.sol\" object \"A1_42\" { - code { + code /// @ast-id 42 + { { /// @src 1:58:123 mstore(64, memoryguard(0x80)) @@ -794,7 +799,8 @@ object \"A1_42\" { } /// @use-src 1:\"b.sol\" object \"A1_42_deployed\" { - code { + code /// @ast-id 42 + { { /// @src 1:58:123 mstore(64, memoryguard(0x80)) @@ -900,7 +906,7 @@ object \"A1_42\" { "ir": "/// ethdebug: enabled /// @use-src 1:\"b.sol\" object \"B2_55\" { - code { + code /** @ast-id 55 */ { /// @src 1:124:189 mstore(64, memoryguard(128)) if callvalue() { revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb() } @@ -931,7 +937,7 @@ object \"B2_55\" { } /// @use-src 1:\"b.sol\" object \"B2_55_deployed\" { - code { + code /** @ast-id 55 */ { /// @src 1:124:189 mstore(64, memoryguard(128)) @@ -1070,7 +1076,8 @@ object \"B2_55\" { "irOptimized": "/// ethdebug: enabled /// @use-src 1:\"b.sol\" object \"B2_55\" { - code { + code /// @ast-id 55 + { { /// @src 1:124:189 mstore(64, memoryguard(0x80)) @@ -1089,7 +1096,8 @@ object \"B2_55\" { } /// @use-src 1:\"b.sol\" object \"B2_55_deployed\" { - code { + code /// @ast-id 55 + { { /// @src 1:124:189 mstore(64, memoryguard(0x80)) diff --git a/test/libsolidity/EthdebugTest.cpp b/test/libsolidity/EthdebugTest.cpp index 6ae88cac95ca..3d84929e76e9 100644 --- a/test/libsolidity/EthdebugTest.cpp +++ b/test/libsolidity/EthdebugTest.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -147,10 +146,12 @@ std::optional EthdebugTest::fetchOutput( return creation["contract"]; } if (_outputName == "semantic") - return langutil::semanticDebugDataToJson(ethdebug::semanticDebugDataTable( - compiler().contractDefinition(*resolved), - compiler().sourceIndices() - )); + { + auto const& semanticDebugData = compiler().yulSemanticDebugData(*resolved); + if (!semanticDebugData) + return std::nullopt; + return langutil::semanticDebugDataToJson(*semanticDebugData); + } } return std::nullopt; } diff --git a/test/libsolidity/EthdebugTest.h b/test/libsolidity/EthdebugTest.h index 8bac1be707e1..6d6ab6e692c9 100644 --- a/test/libsolidity/EthdebugTest.h +++ b/test/libsolidity/EthdebugTest.h @@ -40,8 +40,8 @@ namespace solidity::frontend::test /// Scope keys exposed to expectations: /// - Globals: `.resources`, `.compilation`. /// - Per contract: `Contract.creation`, `Contract.runtime`, `Contract.contract`, -/// `Contract.semantic` (the internal semantic debug info side table, in the -/// shape of its sidecar). +/// `Contract.semantic` (the internal semantic debug info side table as +/// attached to the Yul IR, in the format of its sidecar). /// - Source-qualified per contract (when needed to disambiguate same-named /// contracts in different sources): `source.sol:Contract.creation`, etc. class EthdebugTest: public JSONExpectationTest diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp new file mode 100644 index 000000000000..06cf9f048c84 --- /dev/null +++ b/test/libyul/DebugData.cpp @@ -0,0 +1,636 @@ +/* + 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 the transfer of semantic debug info between the side table + * and Yul AST nodes across reparsing. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::langutil; + +namespace solidity::yul::test +{ + +namespace +{ + +FunctionDefinition* findFunctionDefinition(Block& _block) +{ + for (Statement& statement: _block.statements) + if (auto* functionDefinition = std::get_if(&statement)) + return functionDefinition; + return nullptr; +} + +FunctionDefinition const* findFunctionDefinition(Block const& _block) +{ + for (Statement const& statement: _block.statements) + if (auto const* functionDefinition = std::get_if(&statement)) + return functionDefinition; + return nullptr; +} + +namespace schema = solidity::langutil::ethdebug::schema; +using Pointer = schema::Pointer; +using Expression = schema::Pointer::Expression; + +Expression yulLocal(std::string _name) +{ + return {Pointer::YulLocal{std::move(_name)}}; +} + +Expression variable(std::string _name) +{ + return {Pointer::Variable{std::move(_name)}}; +} + +Expression literal(uint64_t _value) +{ + return {Pointer::Literal{schema::data::Unsigned{_value}}}; +} + +Expression sum(Pointer::Operands _operands) +{ + return {Pointer::Arithmetic{Pointer::Arithmetic::Operator::Sum, std::move(_operands)}}; +} + +Pointer region(Pointer::Location _location, std::string _name, Expression _slot) +{ + return {Pointer::Region{std::move(_name), _location, std::move(_slot)}}; +} + +/// A template without parameters describing @a _body. +Pointer::Template closedTemplate(Pointer _body) +{ + return Pointer::Template{{}, std::make_shared(std::move(_body))}; +} + +/// A stack region reading the Yul local @a _name, as the producer emits it. +Pointer stackPointer(std::string _name) +{ + return region(Pointer::Location::Stack, _name, yulLocal(_name)); +} + +/// Settings that reparse without optimizing, so that only the transfer is tested. +OptimiserSettings noOptimization() +{ + OptimiserSettings settings = OptimiserSettings::none(); + settings.yulOptimiserSteps = ""; + settings.yulOptimiserCleanupSteps = ""; + return settings; +} + +SemanticDebugDataTable stackVariableTable(int64_t _astID, std::string _name, std::string _slot) +{ + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = stackPointer(std::move(_slot)); + + SemanticDebugScope data; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(ScopeID{_astID, 0}, std::make_shared(std::move(data))); + return table; +} + +void checkStackVariableSurvived(FunctionDefinition const& _function) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugScope); + SemanticDebugScope const& data = *_function.debugData->semanticDebugScope; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_CHECK(variable.phase == SemanticDebugVariablePhase::Materialized); + BOOST_REQUIRE(variable.pointer); +} + +void checkStackVariableOptimizedOut(FunctionDefinition const& _function) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugScope); + SemanticDebugScope const& data = *_function.debugData->semanticDebugScope; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_CHECK(variable.phase == SemanticDebugVariablePhase::OptimizedOut); + BOOST_CHECK(!variable.pointer); +} + +} + +BOOST_AUTO_TEST_SUITE(YulDebugDataTest) + +BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + auto const object = yulStack.parserResult(); + auto& root = const_cast(object->code()->root()); + auto* funDef = findFunctionDefinition(root); + BOOST_REQUIRE(funDef); + BOOST_REQUIRE(funDef->debugData); + BOOST_REQUIRE(funDef->debugData->astID); + BOOST_REQUIRE_EQUAL(*funDef->debugData->astID, 23); + + SemanticDebugScope data; + auto semanticDebugData = std::make_shared(std::move(data)); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + funDef->debugData->astIDInstance, + semanticDebugData + ); + + yulStack.optimize(); + + auto const& reparsedRoot = yulStack.parserResult()->code()->root(); + auto const* reparsedFunDef = findFunctionDefinition(reparsedRoot); + BOOST_REQUIRE(reparsedFunDef); + BOOST_REQUIRE(reparsedFunDef->debugData); + BOOST_REQUIRE(reparsedFunDef->debugData->astID); + BOOST_CHECK_EQUAL(*reparsedFunDef->debugData->astID, 23); + BOOST_REQUIRE(reparsedFunDef->debugData->semanticDebugScope); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugScope == semanticDebugData); +} + +BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + auto const object = yulStack.parserResult(); + auto& root = const_cast(object->code()->root()); + auto* funDef = findFunctionDefinition(root); + BOOST_REQUIRE(funDef); + BOOST_REQUIRE(funDef->debugData); + BOOST_REQUIRE(funDef->debugData->astID); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = stackPointer("missing_slot"); + + SemanticDebugScope data; + data.variableDefinitions.emplace_back(std::move(variable)); + auto semanticDebugData = std::make_shared(std::move(data)); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + funDef->debugData->astIDInstance, + semanticDebugData + ); + + yulStack.optimize(); + + auto const& reparsedRoot = yulStack.parserResult()->code()->root(); + auto const* reparsedFunDef = findFunctionDefinition(reparsedRoot); + BOOST_REQUIRE(reparsedFunDef); + BOOST_REQUIRE(reparsedFunDef->debugData); + BOOST_REQUIRE(reparsedFunDef->debugData->semanticDebugScope); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugScope != semanticDebugData); + + SemanticDebugScope const& reparsedDebugData = *reparsedFunDef->debugData->semanticDebugScope; + BOOST_REQUIRE_EQUAL(reparsedDebugData.variableDefinitions.size(), 1); + SemanticDebugVariable const& reparsedVariable = reparsedDebugData.variableDefinitions.front(); + BOOST_CHECK(reparsedVariable.phase == SemanticDebugVariablePhase::OptimizedOut); + BOOST_CHECK(!reparsedVariable.pointer); +} + +BOOST_AUTO_TEST_CASE(attach_marks_missing_stack_locations_optimized_out) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + // No optimization or reparse involved: attaching to IR whose stack slots are + // already gone must mark the variable OptimizedOut right away. + yulStack.attachSemanticDebugData(stackVariableTable(23, "value", "missing_slot")); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableOptimizedOut(*funDef); +} + +BOOST_AUTO_TEST_CASE(bound_pointer_variables_do_not_affect_survival) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + })")); + + // The pointer references var_x (a Yul variable that exists), "aux" (bound by + // a scope definition) and "item" (bound as a list index). Only var_x is a + // free Yul dependency, so the stack location must survive. + Pointer element = region( + Pointer::Location::Stack, + "element", + sum({variable("var_x"), variable("aux"), variable("item")}) + ); + Pointer pointer{Pointer::Scope{ + {{"aux", literal(1)}}, + std::make_shared(Pointer{Pointer::List{literal(2), "item", std::make_shared(std::move(element))}}) + }}; + + SemanticDebugVariable variable; + variable.identifier = "x"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = std::move(pointer); + + SemanticDebugScope data; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(ScopeID{23, 0}, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableSurvived(*funDef); +} + +BOOST_AUTO_TEST_CASE(free_pointer_variables_in_expressions_require_yul_names) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + })")); + + // The slot expression references a Yul variable that does not exist even + // though it is buried inside arithmetic; the location must be dropped. + Pointer pointer = region( + Pointer::Location::Stack, + "element", + sum({yulLocal("var_x"), yulLocal("var_missing")}) + ); + + SemanticDebugVariable variable; + variable.identifier = "x"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = std::move(pointer); + + SemanticDebugScope data; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(ScopeID{23, 0}, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableOptimizedOut(*funDef); +} + +BOOST_AUTO_TEST_CASE(stack_location_survival_is_scoped_per_object) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + object "a" { + code { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + } + /// @use-src 0:"source" + object "a_deployed" { + code { + /** @ast-id 23 */ + function f() { + pop(1) + } + f() + } + } + })")); + + yulStack.attachSemanticDebugData(stackVariableTable(23, "x", "var_x")); + + auto const object = yulStack.parserResult(); + auto const* creationFunDef = findFunctionDefinition(object->code()->root()); + BOOST_REQUIRE(creationFunDef); + checkStackVariableSurvived(*creationFunDef); + + BOOST_REQUIRE_EQUAL(object->subObjects.size(), 1); + auto const* deployedObject = dynamic_cast(object->subObjects.front().get()); + BOOST_REQUIRE(deployedObject); + auto const* deployedFunDef = findFunctionDefinition(deployedObject->code()->root()); + BOOST_REQUIRE(deployedFunDef); + // The slot only survives in the creation code, so the deployed code must not + // report a stale stack location for it. + checkStackVariableOptimizedOut(*deployedFunDef); +} + +BOOST_AUTO_TEST_CASE(template_references_resolve_through_the_table) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + // The variable's pointer is a reference to a storage template in the + // table. Storage templates read no Yul names, so the reference survives + // even though the function declares no variables at all. + SemanticDebugVariable variable; + variable.identifier = "stored"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = Pointer{Pointer::TemplateReference{"storage_1_2"}}; + + SemanticDebugScope data; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.setPointerTemplate("storage_1_2", closedTemplate(region(Pointer::Location::Storage, "stored", literal(2)))); + table.set(ScopeID{23, 0}, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableSurvived(*funDef); + BOOST_REQUIRE(yulStack.semanticDebugData().findPointerTemplate("storage_1_2")); + + // A reference whose template reads a missing Yul name is invalidated + // like a direct pointer would be. + SemanticDebugDataTable stackTable; + SemanticDebugVariable referencing; + referencing.identifier = "value"; + referencing.phase = SemanticDebugVariablePhase::Materialized; + referencing.pointer = Pointer{Pointer::TemplateReference{"stack-template"}}; + SemanticDebugScope referencingScope; + referencingScope.variableDefinitions.emplace_back(std::move(referencing)); + stackTable.setPointerTemplate("stack-template", closedTemplate(stackPointer("missing_slot"))); + stackTable.set(ScopeID{23, 0}, std::make_shared(std::move(referencingScope))); + yulStack.attachSemanticDebugData(stackTable); + checkStackVariableOptimizedOut(*funDef); +} + +BOOST_AUTO_TEST_CASE(ast_id_instance_annotation_round_trips) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + // A cloned scope carries its instance next to the origin. An instance + // without an accompanying @ast-id on the same node is ignored. + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 @ast-id-instance 2 */ + function f() { + let var_x := 1 + pop(var_x) + } + /** @ast-id-instance 5 */ + function g() {} + f() + g() + })")); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + BOOST_REQUIRE(funDef->debugData->astID); + BOOST_CHECK_EQUAL(*funDef->debugData->astID, 23); + BOOST_REQUIRE(funDef->debugData->astIDInstance); + BOOST_CHECK_EQUAL(*funDef->debugData->astIDInstance, 2); + FunctionDefinition const* ignored = nullptr; + for (Statement const& statement: yulStack.parserResult()->code()->root().statements) + if (auto const* definition = std::get_if(&statement)) + if (definition->name.str() == "g") + ignored = definition; + BOOST_REQUIRE(ignored); + BOOST_CHECK(!ignored->debugData->astID); + BOOST_CHECK(!ignored->debugData->astIDInstance); + + // The table keys the record by (astID, instance); instance 0 does not match. + SemanticDebugDataTable table; + SemanticDebugScope clonedScope; + SemanticDebugVariable variable; + variable.identifier = "x"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = stackPointer("var_x"); + clonedScope.variableDefinitions.emplace_back(std::move(variable)); + table.set({23, 2}, std::make_shared(std::move(clonedScope))); + table.set({23, 0}, std::make_shared()); + yulStack.attachSemanticDebugData(table); + checkStackVariableSurvived(*funDef); + + // The annotation survives printing and reparsing, and so does the record. + std::string const printed = yulStack.print(); + BOOST_CHECK(printed.find("@ast-id 23 @ast-id-instance 2") != std::string::npos); + yulStack.optimize(); + auto const* reparsedFunDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(reparsedFunDef); + BOOST_REQUIRE(reparsedFunDef->debugData->astIDInstance); + BOOST_CHECK_EQUAL(*reparsedFunDef->debugData->astIDInstance, 2); + checkStackVariableSurvived(*reparsedFunDef); +} + +BOOST_AUTO_TEST_CASE(contract_scope_attaches_to_the_top_level_block) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + // Contract-level variables have no Yul node of their own; their record + // attaches to the object's top-level block, which carries the contract's + // AST ID the way the IR generator emits it. + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + object "C" { + code /** @ast-id 7 */ { + sstore(0, 1) + } + })")); + + SemanticDebugVariable stored; + stored.identifier = "stored"; + stored.phase = SemanticDebugVariablePhase::Materialized; + stored.pointer = Pointer{Pointer::TemplateReference{"storage_7_3"}}; + SemanticDebugScope contractScope; + contractScope.variableDefinitions.emplace_back(std::move(stored)); + SemanticDebugDataTable table; + table.setPointerTemplate("storage_7_3", closedTemplate(region(Pointer::Location::Storage, "stored", literal(0)))); + table.set(ScopeID{7, 0}, std::make_shared(std::move(contractScope))); + yulStack.attachSemanticDebugData(table); + + Block const& root = yulStack.parserResult()->code()->root(); + BOOST_REQUIRE(root.debugData); + BOOST_REQUIRE(root.debugData->astID); + BOOST_CHECK_EQUAL(*root.debugData->astID, 7); + BOOST_REQUIRE(root.debugData->semanticDebugScope); + BOOST_REQUIRE_EQUAL(root.debugData->semanticDebugScope->variableDefinitions.size(), 1); + BOOST_CHECK(root.debugData->semanticDebugScope->variableDefinitions.front().phase == SemanticDebugVariablePhase::Materialized); + + // The block's annotation is printed before the block, so the attachment + // survives a reparse. + std::string const printed = yulStack.print(); + size_t const code = printed.find("code"); + size_t const annotation = printed.find("@ast-id 7"); + BOOST_REQUIRE(code != std::string::npos && annotation != std::string::npos); + BOOST_CHECK(code < annotation && annotation < printed.find('{', code)); + yulStack.optimize(); + Block const& reparsedRoot = yulStack.parserResult()->code()->root(); + BOOST_REQUIRE(reparsedRoot.debugData && reparsedRoot.debugData->semanticDebugScope); + BOOST_REQUIRE_EQUAL(reparsedRoot.debugData->semanticDebugScope->variableDefinitions.size(), 1); +} + +BOOST_AUTO_TEST_CASE(attach_invalidates_variable_updates_reading_missing_locals) +{ + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + noOptimization(), + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f(var_x) { + pop(var_x) + } + })")); + + SemanticDebugVariableUpdate survivingUpdate; + survivingUpdate.variableASTID = 5; + survivingUpdate.phase = SemanticDebugVariablePhase::Materialized; + survivingUpdate.pointer = stackPointer("var_x"); + SemanticDebugVariableUpdate lostUpdate; + lostUpdate.variableASTID = 6; + lostUpdate.phase = SemanticDebugVariablePhase::Materialized; + lostUpdate.pointer = stackPointer("missing_slot"); + + SemanticDebugScope scope; + scope.variableUpdates.emplace_back(std::move(survivingUpdate)); + scope.variableUpdates.emplace_back(std::move(lostUpdate)); + SemanticDebugDataTable table; + table.set(ScopeID{23, 0}, std::make_shared(std::move(scope))); + yulStack.attachSemanticDebugData(table); + + // Updates are checked like definitions: the one reading a local that no + // longer exists becomes optimized-out, both on the node and in the table. + auto checkUpdates = [](SemanticDebugScope const& _scope) { + BOOST_REQUIRE_EQUAL(_scope.variableUpdates.size(), 2); + BOOST_CHECK(_scope.variableUpdates[0].phase == SemanticDebugVariablePhase::Materialized); + BOOST_CHECK(_scope.variableUpdates[0].pointer); + BOOST_CHECK(_scope.variableUpdates[1].phase == SemanticDebugVariablePhase::OptimizedOut); + BOOST_CHECK(!_scope.variableUpdates[1].pointer); + }; + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef && funDef->debugData && funDef->debugData->semanticDebugScope); + checkUpdates(*funDef->debugData->semanticDebugScope); + auto const tableScope = yulStack.semanticDebugData().find(ScopeID{23, 0}); + BOOST_REQUIRE(tableScope); + checkUpdates(*tableScope); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::yul::test diff --git a/test/libyul/Parser.cpp b/test/libyul/Parser.cpp index 64f0c1600e55..45149349abf4 100644 --- a/test/libyul/Parser.cpp +++ b/test/libyul/Parser.cpp @@ -934,6 +934,56 @@ BOOST_AUTO_TEST_CASE(astid_invalid) CHECK_LOCATION(result->root().debugData->originLocation, "", -1, -1); } +BOOST_AUTO_TEST_CASE(astid_instance) +{ + ErrorList errorList; + ErrorReporter reporter(errorList); + auto const sourceText = R"( + /// @ast-id 5 @ast-id-instance 3 + {} + )"; + auto const& dialect = EVMDialect::strictAssemblyForEVM(solidity::test::CommonOptions::get().evmVersion()); + std::shared_ptr result = parse(sourceText, dialect, reporter); + BOOST_REQUIRE(!!result); + BOOST_REQUIRE(errorList.empty()); + BOOST_REQUIRE(result->root().debugData->astID); + BOOST_CHECK_EQUAL(*result->root().debugData->astID, 5); + BOOST_REQUIRE(result->root().debugData->astIDInstance); + BOOST_CHECK_EQUAL(*result->root().debugData->astIDInstance, 3); +} + +BOOST_AUTO_TEST_CASE(astid_instance_without_astid_is_ignored) +{ + ErrorList errorList; + ErrorReporter reporter(errorList); + auto const sourceText = R"( + /// @ast-id-instance 3 + {} + )"; + auto const& dialect = EVMDialect::strictAssemblyForEVM(solidity::test::CommonOptions::get().evmVersion()); + std::shared_ptr result = parse(sourceText, dialect, reporter); + BOOST_REQUIRE(!!result); + BOOST_REQUIRE(errorList.empty()); + BOOST_CHECK(!result->root().debugData->astID); + BOOST_CHECK(!result->root().debugData->astIDInstance); +} + +BOOST_AUTO_TEST_CASE(astid_instance_invalid) +{ + ErrorList errorList; + ErrorReporter reporter(errorList); + auto const sourceText = R"( + /// @ast-id 5 @ast-id-instance abc + {} + )"; + auto const& dialect = EVMDialect::strictAssemblyForEVM(solidity::test::CommonOptions::get().evmVersion()); + std::shared_ptr result = parse(sourceText, dialect, reporter); + BOOST_REQUIRE(!!result); + BOOST_REQUIRE(errorList.size() == 1); + BOOST_TEST(errorList[0]->type() == Error::Type::SyntaxError); + BOOST_TEST(errorList[0]->errorId() == 9913_error); +} + BOOST_AUTO_TEST_CASE(astid_too_large) { ErrorList errorList; diff --git a/test/libyul/yulSyntaxTests/ast_id_instance_invalid_argument.yul b/test/libyul/yulSyntaxTests/ast_id_instance_invalid_argument.yul new file mode 100644 index 000000000000..0659a2204e5a --- /dev/null +++ b/test/libyul/yulSyntaxTests/ast_id_instance_invalid_argument.yul @@ -0,0 +1,10 @@ +/// @use-src 0:"input.sol" +{ + /// @ast-id 5 @ast-id-instance abc + let x := 1 + /// @ast-id-instance 3 + let y := 2 + sstore(x, y) +} +// ---- +// SyntaxError 9913: (33-67): Invalid argument for @ast-id-instance.