From 664ca545d1c03d272424079a530e1f0d652c4f9e Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 5 Oct 2026 08:25:17 +0200 Subject: [PATCH] ethdebug: Attach semantic debug info to Yul nodes and survive the text round trip Attach the scope records of the side table to Yul AST nodes through DebugData, keyed by (AST ID, instance), and keep them across the Yul -> text -> Yul reparse the compiler performs after optimization: records are collected from the AST into the retained table before printing and reattached by their @ast-id annotations after parsing. Attaching re-checks every pointer against the current code: a materialized variable (or update) whose pointer reads a generated Yul local that no longer exists becomes optimized-out instead of keeping a stale pointer. The check is scoped per object. Scope instances travel in the new @ast-id-instance annotation next to @ast-id, so passes that clone code can give each copy its own records; code generation only produces instance 0. Contract-level scopes attach to the top-level block of each object, which now carries the contract's AST ID. Blocks generated by the function grouper and hoister keep locations only, so they do not inherit another node's identity. CompilerStack stores the attached table on the contract; it pairs with the unoptimized IR, whose generated locals it references. The golden tests now show Contract.semantic as attached. --- Changelog.md | 1 + docs/using-the-compiler.rst | 3 +- liblangutil/DebugData.h | 37 +- libsolidity/codegen/ir/IRGenerator.cpp | 11 +- libsolidity/interface/CompilerStack.cpp | 33 +- libsolidity/interface/CompilerStack.h | 10 +- libyul/AsmParser.cpp | 79 ++- libyul/AsmParser.h | 18 +- libyul/AsmPrinter.cpp | 6 + libyul/CMakeLists.txt | 2 + libyul/SemanticDebugDataTransfer.cpp | 264 ++++++++ libyul/SemanticDebugDataTransfer.h | 48 ++ libyul/YulStack.cpp | 22 + libyul/YulStack.h | 20 +- libyul/optimiser/FunctionGrouper.cpp | 4 +- libyul/optimiser/FunctionHoister.cpp | 2 +- test/CMakeLists.txt | 1 + .../output.json | 4 +- .../output.json | 40 +- test/libsolidity/EthdebugTest.cpp | 11 +- test/libsolidity/EthdebugTest.h | 4 +- test/libyul/DebugData.cpp | 636 ++++++++++++++++++ test/libyul/Parser.cpp | 50 ++ .../ast_id_instance_invalid_argument.yul | 10 + 24 files changed, 1256 insertions(+), 60 deletions(-) create mode 100644 libyul/SemanticDebugDataTransfer.cpp create mode 100644 libyul/SemanticDebugDataTransfer.h create mode 100644 test/libyul/DebugData.cpp create mode 100644 test/libyul/yulSyntaxTests/ast_id_instance_invalid_argument.yul 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.