From f42fbdcf0579e4b8de6815696e023263c93a0660 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 26 May 2026 09:37:17 +0200 Subject: [PATCH 01/47] ethdebug: Add semantic debug metadata carrier --- liblangutil/CMakeLists.txt | 1 + liblangutil/DebugData.h | 16 ++++++-- liblangutil/SemanticDebugData.h | 67 ++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/liblangutil/DebugData.cpp | 69 +++++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 liblangutil/SemanticDebugData.h create mode 100644 test/liblangutil/DebugData.cpp diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 054d01d91881..fd81c8006ff6 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -17,6 +17,7 @@ set(sources Scanner.cpp Scanner.h CharStreamProvider.h + SemanticDebugData.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/DebugData.h b/liblangutil/DebugData.h index 70259bd039f3..8d1cdf940729 100644 --- a/liblangutil/DebugData.h +++ b/liblangutil/DebugData.h @@ -18,9 +18,11 @@ #pragma once +#include #include #include #include +#include namespace solidity::langutil { @@ -32,23 +34,27 @@ struct DebugData explicit DebugData( langutil::SourceLocation _nativeLocation = {}, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + SemanticDebugData::ConstPtr _semanticDebugData = {} ): nativeLocation(std::move(_nativeLocation)), originLocation(std::move(_originLocation)), - astID(_astID) + astID(_astID), + semanticDebugData(std::move(_semanticDebugData)) {} static DebugData::ConstPtr create( langutil::SourceLocation _nativeLocation, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + SemanticDebugData::ConstPtr _semanticDebugData = {} ) { return std::make_shared( std::move(_nativeLocation), std::move(_originLocation), - _astID + _astID, + std::move(_semanticDebugData) ); } @@ -65,6 +71,8 @@ struct DebugData langutil::SourceLocation originLocation; /// ID in the (Solidity) source AST. std::optional astID; + /// Extended semantic debug data that cannot be represented in Yul comments. + SemanticDebugData::ConstPtr semanticDebugData; }; } // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h new file mode 100644 index 000000000000..2d1e4431bc3f --- /dev/null +++ b/liblangutil/SemanticDebugData.h @@ -0,0 +1,67 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +struct SemanticDebugVariableLocation +{ + enum class Kind + { + Stack, + Storage, + TransientStorage, + Memory, + Calldata, + Immutable, + Constant, + OptimizedOut + }; + + Kind kind = Kind::OptimizedOut; + std::optional pointerID; +}; + +struct SemanticDebugVariable +{ + std::string name; + std::optional declarationAstID; + std::optional declarationLocation; + std::optional typeID; + std::optional location; +}; + +struct SemanticDebugData +{ + using ConstPtr = std::shared_ptr; + + std::optional lexicalScopeID; + std::vector variableDefinitions; +}; + +} // namespace solidity::langutil diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b14bccfe95a4..e0ca687263ba 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,7 @@ detect_stray_source_files("${libevmasm_sources}" "libevmasm/") set(liblangutil_sources liblangutil/CharStream.cpp + liblangutil/DebugData.cpp liblangutil/Scanner.cpp liblangutil/SourceLocation.cpp ) diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp new file mode 100644 index 000000000000..8f4915cb190e --- /dev/null +++ b/test/liblangutil/DebugData.cpp @@ -0,0 +1,69 @@ +/* + 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 + +namespace solidity::langutil::test +{ + +BOOST_AUTO_TEST_SUITE(DebugDataTest) + +BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) +{ + auto semanticDebugData = std::make_shared(SemanticDebugData{ + .lexicalScopeID = 17, + .variableDefinitions = {{ + .name = "value", + .declarationAstID = 23, + .declarationLocation = SourceLocation{1, 6, std::make_shared("input.sol")}, + .typeID = "type:uint256", + .location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "pointer:value" + } + }} + }); + + auto debugData = DebugData::create( + SourceLocation{}, + SourceLocation{}, + 23, + semanticDebugData + ); + + BOOST_REQUIRE(debugData->semanticDebugData); + BOOST_REQUIRE(debugData->semanticDebugData->lexicalScopeID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->lexicalScopeID, 17); + BOOST_REQUIRE_EQUAL(debugData->semanticDebugData->variableDefinitions.size(), 1); + BOOST_CHECK_EQUAL(debugData->semanticDebugData->variableDefinitions.front().name, "value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().typeID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().typeID, "type:uint256"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location->pointerID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().location->pointerID, "pointer:value"); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::langutil::test From 2eb9591953a050510d3ae492578440f5ed7ba4c0 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 26 May 2026 09:41:40 +0200 Subject: [PATCH 02/47] ethdebug: Add semantic debug AST ID side table --- liblangutil/CMakeLists.txt | 1 + liblangutil/SemanticDebugDataTable.h | 57 ++++++++++++++++++++++++++++ test/liblangutil/DebugData.cpp | 18 +++++++++ 3 files changed, 76 insertions(+) create mode 100644 liblangutil/SemanticDebugDataTable.h diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index fd81c8006ff6..38eda841ffe3 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -18,6 +18,7 @@ set(sources Scanner.h CharStreamProvider.h SemanticDebugData.h + SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h new file mode 100644 index 000000000000..c49aa95e6096 --- /dev/null +++ b/liblangutil/SemanticDebugDataTable.h @@ -0,0 +1,57 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include +#include +#include +#include + +namespace solidity::langutil +{ + +class SemanticDebugDataTable +{ +public: + void set(int64_t _astID, SemanticDebugData::ConstPtr _debugData) + { + m_byASTID[_astID] = std::move(_debugData); + } + + SemanticDebugData::ConstPtr find(std::optional _astID) const + { + if (!_astID) + return nullptr; + + auto const it = m_byASTID.find(*_astID); + return it == m_byASTID.end() ? nullptr : it->second; + } + + bool empty() const + { + return m_byASTID.empty(); + } + +private: + std::map m_byASTID; +}; + +} // namespace solidity::langutil diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 8f4915cb190e..da8b13b1c927 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -17,6 +17,7 @@ // SPDX-License-Identifier: GPL-3.0 #include +#include #include @@ -64,6 +65,23 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().location->pointerID, "pointer:value"); } +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) +{ + auto semanticDebugData = std::make_shared(SemanticDebugData{ + .lexicalScopeID = 17 + }); + + SemanticDebugDataTable table; + BOOST_CHECK(table.empty()); + + table.set(23, semanticDebugData); + + BOOST_CHECK(!table.empty()); + BOOST_CHECK(table.find(23) == semanticDebugData); + BOOST_CHECK(!table.find(24)); + BOOST_CHECK(!table.find(std::nullopt)); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::langutil::test From db60f8581583a2fecbbba19ce35568761060d615 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 26 May 2026 09:56:34 +0200 Subject: [PATCH 03/47] ethdebug: Preserve semantic debug data across Yul reparse --- libyul/YulStack.cpp | 355 ++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/libyul/DebugData.cpp | 115 ++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 test/libyul/DebugData.cpp diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index 525f0d243f54..01b5415c402c 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -28,11 +29,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include @@ -45,6 +48,354 @@ using namespace solidity::yul; using namespace solidity::langutil; using namespace solidity::util; +namespace +{ + +void collectSemanticDebugData(SemanticDebugDataTable& _table, langutil::DebugData::ConstPtr const& _debugData) +{ + if (_debugData && _debugData->astID && _debugData->semanticDebugData) + _table.set(*_debugData->astID, _debugData->semanticDebugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, NameWithDebugData const& _name) +{ + collectSemanticDebugData(_table, _name.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Literal const& _literal); +void collectSemanticDebugData(SemanticDebugDataTable& _table, Identifier const& _identifier); +void collectSemanticDebugData(SemanticDebugDataTable& _table, BuiltinName const& _builtin); +void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionName const& _functionName); +void collectSemanticDebugData(SemanticDebugDataTable& _table, Expression const& _expression); +void collectSemanticDebugData(SemanticDebugDataTable& _table, Case const& _case); +void collectSemanticDebugData(SemanticDebugDataTable& _table, Statement const& _statement); +void collectSemanticDebugData(SemanticDebugDataTable& _table, Block const& _block); + +template +void collectSemanticDebugData(SemanticDebugDataTable& _table, std::vector const& _nodes) +{ + for (auto const& node: _nodes) + collectSemanticDebugData(_table, node); +} + +template +void collectSemanticDebugData(SemanticDebugDataTable& _table, std::unique_ptr const& _node) +{ + if (_node) + collectSemanticDebugData(_table, *_node); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Literal const& _literal) +{ + collectSemanticDebugData(_table, _literal.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Identifier const& _identifier) +{ + collectSemanticDebugData(_table, _identifier.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, BuiltinName const& _builtin) +{ + collectSemanticDebugData(_table, _builtin.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionName const& _functionName) +{ + std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _functionName); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionCall const& _call) +{ + collectSemanticDebugData(_table, _call.debugData); + collectSemanticDebugData(_table, _call.functionName); + collectSemanticDebugData(_table, _call.arguments); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Expression const& _expression) +{ + std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _expression); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, ExpressionStatement const& _statement) +{ + collectSemanticDebugData(_table, _statement.debugData); + collectSemanticDebugData(_table, _statement.expression); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Assignment const& _assignment) +{ + collectSemanticDebugData(_table, _assignment.debugData); + collectSemanticDebugData(_table, _assignment.variableNames); + collectSemanticDebugData(_table, _assignment.value); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, VariableDeclaration const& _varDecl) +{ + collectSemanticDebugData(_table, _varDecl.debugData); + collectSemanticDebugData(_table, _varDecl.variables); + collectSemanticDebugData(_table, _varDecl.value); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionDefinition const& _function) +{ + collectSemanticDebugData(_table, _function.debugData); + collectSemanticDebugData(_table, _function.parameters); + collectSemanticDebugData(_table, _function.returnVariables); + collectSemanticDebugData(_table, _function.body); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, If const& _if) +{ + collectSemanticDebugData(_table, _if.debugData); + collectSemanticDebugData(_table, _if.condition); + collectSemanticDebugData(_table, _if.body); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Case const& _case) +{ + collectSemanticDebugData(_table, _case.debugData); + collectSemanticDebugData(_table, _case.value); + collectSemanticDebugData(_table, _case.body); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Switch const& _switch) +{ + collectSemanticDebugData(_table, _switch.debugData); + collectSemanticDebugData(_table, _switch.expression); + collectSemanticDebugData(_table, _switch.cases); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, ForLoop const& _forLoop) +{ + collectSemanticDebugData(_table, _forLoop.debugData); + collectSemanticDebugData(_table, _forLoop.pre); + collectSemanticDebugData(_table, _forLoop.condition); + collectSemanticDebugData(_table, _forLoop.post); + collectSemanticDebugData(_table, _forLoop.body); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Break const& _break) +{ + collectSemanticDebugData(_table, _break.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Continue const& _continue) +{ + collectSemanticDebugData(_table, _continue.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Leave const& _leave) +{ + collectSemanticDebugData(_table, _leave.debugData); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Statement const& _statement) +{ + std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _statement); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Block const& _block) +{ + collectSemanticDebugData(_table, _block.debugData); + collectSemanticDebugData(_table, _block.statements); +} + +void collectSemanticDebugData(SemanticDebugDataTable& _table, Object const& _object) +{ + if (_object.hasCode()) + collectSemanticDebugData(_table, _object.code()->root()); + + for (auto const& subNode: _object.subObjects) + if (auto const* subObject = dynamic_cast(subNode.get())) + collectSemanticDebugData(_table, *subObject); +} + +langutil::DebugData::ConstPtr reattachSemanticDebugData( + langutil::DebugData::ConstPtr const& _debugData, + SemanticDebugDataTable const& _table +) +{ + if (!_debugData) + return nullptr; + + auto semanticDebugData = _table.find(_debugData->astID); + if (!semanticDebugData) + return _debugData; + + return langutil::DebugData::create( + _debugData->nativeLocation, + _debugData->originLocation, + _debugData->astID, + std::move(semanticDebugData) + ); +} + +void reattachSemanticDebugData(langutil::DebugData::ConstPtr& _debugData, SemanticDebugDataTable const& _table) +{ + _debugData = reattachSemanticDebugData(static_cast(_debugData), _table); +} + +void reattachSemanticDebugData(NameWithDebugData& _name, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_name.debugData, _table); +} + +void reattachSemanticDebugData(Literal& _literal, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(Identifier& _identifier, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(BuiltinName& _builtin, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(FunctionName& _functionName, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(Expression& _expression, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(Case& _case, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(Statement& _statement, SemanticDebugDataTable const& _table); +void reattachSemanticDebugData(Block& _block, SemanticDebugDataTable const& _table); + +template +void reattachSemanticDebugData(std::vector& _nodes, SemanticDebugDataTable const& _table) +{ + for (auto& node: _nodes) + reattachSemanticDebugData(node, _table); +} + +template +void reattachSemanticDebugData(std::unique_ptr& _node, SemanticDebugDataTable const& _table) +{ + if (_node) + reattachSemanticDebugData(*_node, _table); +} + +void reattachSemanticDebugData(Literal& _literal, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_literal.debugData, _table); +} + +void reattachSemanticDebugData(Identifier& _identifier, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_identifier.debugData, _table); +} + +void reattachSemanticDebugData(BuiltinName& _builtin, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_builtin.debugData, _table); +} + +void reattachSemanticDebugData(FunctionName& _functionName, SemanticDebugDataTable const& _table) +{ + std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _functionName); +} + +void reattachSemanticDebugData(FunctionCall& _call, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_call.debugData, _table); + reattachSemanticDebugData(_call.functionName, _table); + reattachSemanticDebugData(_call.arguments, _table); +} + +void reattachSemanticDebugData(Expression& _expression, SemanticDebugDataTable const& _table) +{ + std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _expression); +} + +void reattachSemanticDebugData(ExpressionStatement& _statement, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_statement.debugData, _table); + reattachSemanticDebugData(_statement.expression, _table); +} + +void reattachSemanticDebugData(Assignment& _assignment, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_assignment.debugData, _table); + reattachSemanticDebugData(_assignment.variableNames, _table); + reattachSemanticDebugData(_assignment.value, _table); +} + +void reattachSemanticDebugData(VariableDeclaration& _varDecl, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_varDecl.debugData, _table); + reattachSemanticDebugData(_varDecl.variables, _table); + reattachSemanticDebugData(_varDecl.value, _table); +} + +void reattachSemanticDebugData(FunctionDefinition& _function, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_function.debugData, _table); + reattachSemanticDebugData(_function.parameters, _table); + reattachSemanticDebugData(_function.returnVariables, _table); + reattachSemanticDebugData(_function.body, _table); +} + +void reattachSemanticDebugData(If& _if, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_if.debugData, _table); + reattachSemanticDebugData(_if.condition, _table); + reattachSemanticDebugData(_if.body, _table); +} + +void reattachSemanticDebugData(Case& _case, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_case.debugData, _table); + reattachSemanticDebugData(_case.value, _table); + reattachSemanticDebugData(_case.body, _table); +} + +void reattachSemanticDebugData(Switch& _switch, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_switch.debugData, _table); + reattachSemanticDebugData(_switch.expression, _table); + reattachSemanticDebugData(_switch.cases, _table); +} + +void reattachSemanticDebugData(ForLoop& _forLoop, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_forLoop.debugData, _table); + reattachSemanticDebugData(_forLoop.pre, _table); + reattachSemanticDebugData(_forLoop.condition, _table); + reattachSemanticDebugData(_forLoop.post, _table); + reattachSemanticDebugData(_forLoop.body, _table); +} + +void reattachSemanticDebugData(Break& _break, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_break.debugData, _table); +} + +void reattachSemanticDebugData(Continue& _continue, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_continue.debugData, _table); +} + +void reattachSemanticDebugData(Leave& _leave, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_leave.debugData, _table); +} + +void reattachSemanticDebugData(Statement& _statement, SemanticDebugDataTable const& _table) +{ + std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _statement); +} + +void reattachSemanticDebugData(Block& _block, SemanticDebugDataTable const& _table) +{ + reattachSemanticDebugData(_block.debugData, _table); + reattachSemanticDebugData(_block.statements, _table); +} + +void reattachSemanticDebugData(Object& _object, SemanticDebugDataTable const& _table) +{ + if (_object.hasCode()) + { + Block root = ASTCopier{}.translate(_object.code()->root()); + reattachSemanticDebugData(root, _table); + _object.setCode(std::make_shared(*_object.dialect(), std::move(root)), _object.analysisInfo); + } + + for (auto& subNode: _object.subObjects) + if (auto* subObject = dynamic_cast(subNode.get())) + reattachSemanticDebugData(*subObject, _table); +} + +} + CharStream const& YulStack::charStream(std::string const& _sourceName) const { yulAssert(m_charStream, ""); @@ -205,6 +556,8 @@ 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. + SemanticDebugDataTable semanticDebugData; + collectSemanticDebugData(semanticDebugData, *m_parserResult); std::string source = print(); YulStack cleanStack( @@ -224,6 +577,8 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); + if (!semanticDebugData.empty()) + reattachSemanticDebugData(*m_parserResult, 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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e0ca687263ba..2ca204140cf7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -153,6 +153,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/libyul/DebugData.cpp b/test/libyul/DebugData.cpp new file mode 100644 index 000000000000..55bb24804483 --- /dev/null +++ b/test/libyul/DebugData.cpp @@ -0,0 +1,115 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERMERCHANTABILITY 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 + +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; +} + +} + +BOOST_AUTO_TEST_SUITE(YulDebugDataTest) + +BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + solidity::test::CommonOptions::get().eofVersion(), + optimiserSettings, + 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); + + auto semanticDebugData = std::make_shared(SemanticDebugData{ + .lexicalScopeID = 17 + }); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + 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->semanticDebugData); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugData == semanticDebugData); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::yul::test From 98aa61563f5f0b4b3555ac51f4944447fb3aaecb Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 26 May 2026 10:42:19 +0200 Subject: [PATCH 04/47] ethdebug: Attach semantic debug data for function variables --- libsolidity/CMakeLists.txt | 2 + .../codegen/ir/SemanticDebugDataBuilder.cpp | 108 ++++++++++++++++++ .../codegen/ir/SemanticDebugDataBuilder.h | 30 +++++ libsolidity/interface/CompilerStack.cpp | 23 +++- libsolidity/interface/CompilerStack.h | 7 +- libyul/YulStack.cpp | 15 ++- libyul/YulStack.h | 3 + test/CMakeLists.txt | 1 + test/libsolidity/SemanticDebugData.cpp | 108 ++++++++++++++++++ 9 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp create mode 100644 libsolidity/codegen/ir/SemanticDebugDataBuilder.h create mode 100644 test/libsolidity/SemanticDebugData.cpp diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 49bdb3d0932a..14e2d10ca2bb 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -101,6 +101,8 @@ set(sources codegen/ir/IRLValue.h codegen/ir/IRVariable.cpp codegen/ir/IRVariable.h + codegen/ir/SemanticDebugDataBuilder.cpp + codegen/ir/SemanticDebugDataBuilder.h formal/ArraySlicePredicate.cpp formal/ArraySlicePredicate.h formal/BMC.cpp diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp new file mode 100644 index 000000000000..809a440ed2dc --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -0,0 +1,108 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::langutil; + +namespace +{ + +std::optional stackLocation(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + return SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = IRVariable(_variable).commaSeparatedList() + }; +} + +std::optional typeID(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + return _variable.annotation().type->identifier(); +} + +SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) +{ + return { + .name = _variable.name(), + .declarationAstID = _variable.id(), + .declarationLocation = _variable.location(), + .typeID = typeID(_variable), + .location = stackLocation(_variable) + }; +} + +void appendVariables( + std::vector& _variables, + std::vector> const& _declarations +) +{ + for (ASTPointer const& declaration: _declarations) + if (!declaration->name().empty()) + _variables.emplace_back(semanticVariable(*declaration)); +} + +template +void addCallable(SemanticDebugDataTable& _table, Callable const& _callable) +{ + std::vector variables; + appendVariables(variables, _callable.parameters()); + appendVariables(variables, _callable.returnParameters()); + + if (variables.empty()) + return; + + _table.set(_callable.id(), std::make_shared(SemanticDebugData{ + .lexicalScopeID = _callable.id(), + .variableDefinitions = std::move(variables) + })); +} + +} + +SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable(ContractDefinition const& _contract) +{ + SemanticDebugDataTable table; + + for (FunctionDefinition const* function: _contract.definedFunctions()) + addCallable(table, *function); + + for (ModifierDefinition const* modifier: _contract.functionModifiers()) + addCallable(table, *modifier); + + return table; +} diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.h b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h new file mode 100644 index 000000000000..59b12fe9518a --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h @@ -0,0 +1,30 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +namespace solidity::frontend +{ + +class ContractDefinition; + +langutil::SemanticDebugDataTable buildSemanticDebugDataTable(ContractDefinition const& _contract); + +} // namespace solidity::frontend diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b2b6f95625ae..10391da91d0a 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -59,6 +59,7 @@ #include #include +#include #include #include @@ -802,7 +803,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, @@ -823,6 +827,8 @@ YulStack CompilerStack::loadGeneratedIR(std::string const& _ir) const true // _withErrorIds ) + "\n" ); + if (_semanticDebugData) + stack.attachSemanticDebugData(*_semanticDebugData); return stack; } @@ -1561,7 +1567,15 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti ); yulAssert(compiledContract.yulIR); - YulStack stack = loadGeneratedIR(*compiledContract.yulIR); + if (m_debugInfoSelection.astID || m_debugInfoSelection.ethdebug) + compiledContract.yulSemanticDebugData = buildSemanticDebugDataTable(_contract); + else + compiledContract.yulSemanticDebugData = std::nullopt; + + YulStack stack = loadGeneratedIR( + *compiledContract.yulIR, + compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr + ); if (!_unoptimizedOnly) { stack.optimize(m_viaSSACFG); @@ -1583,7 +1597,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 d6deb69dea1e..9e7b80dad72a 100644 --- a/libsolidity/interface/CompilerStack.h +++ b/libsolidity/interface/CompilerStack.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -462,6 +463,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; @@ -540,7 +542,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/YulStack.cpp b/libyul/YulStack.cpp index 01b5415c402c..ed719642795d 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -383,11 +382,7 @@ void reattachSemanticDebugData(Block& _block, SemanticDebugDataTable const& _tab void reattachSemanticDebugData(Object& _object, SemanticDebugDataTable const& _table) { if (_object.hasCode()) - { - Block root = ASTCopier{}.translate(_object.code()->root()); - reattachSemanticDebugData(root, _table); - _object.setCode(std::make_shared(*_object.dialect(), std::move(root)), _object.analysisInfo); - } + reattachSemanticDebugData(const_cast(_object.code()->root()), _table); for (auto& subNode: _object.subObjects) if (auto* subObject = dynamic_cast(subNode.get())) @@ -789,6 +784,14 @@ 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, ""); + if (!_table.empty()) + reattachSemanticDebugData(*m_parserResult, _table); +} + Dialect const& YulStack::dialect() const { yulAssert(m_stackState >= AnalysisSuccessful); diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 2e8f8ed30e73..128af3715c47 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -48,6 +48,7 @@ class Assembly; namespace solidity::langutil { class Scanner; +class SemanticDebugDataTable; } namespace solidity::yul @@ -147,6 +148,8 @@ class YulStack: public langutil::CharStreamProvider /// Return the parsed and analyzed object. std::shared_ptr parserResult() const; + void attachSemanticDebugData(langutil::SemanticDebugDataTable const& _table); + Dialect const& dialect() const; langutil::DebugInfoSelection debugInfoSelection() const { return m_debugInfoSelection; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2ca204140cf7..7dcb8afbfd12 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -101,6 +101,7 @@ set(libsolidity_sources libsolidity/OptimizedIRCachingTest.h libsolidity/SemanticTest.cpp libsolidity/SemanticTest.h + libsolidity/SemanticDebugData.cpp libsolidity/SemVerMatcher.cpp libsolidity/SMTCheckerTest.cpp libsolidity/SMTCheckerTest.h diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp new file mode 100644 index 000000000000..eb428c394a92 --- /dev/null +++ b/test/libsolidity/SemanticDebugData.cpp @@ -0,0 +1,108 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::frontend::test; +using namespace solidity::langutil; + +namespace +{ + +std::string stackPointer(VariableDeclaration const& _variable) +{ + return "var_" + _variable.name() + "_" + std::to_string(_variable.id()); +} + +} + +class SemanticDebugDataFixture: public AnalysisFramework +{ +}; + +BOOST_FIXTURE_TEST_SUITE(SemanticDebugDataTest, SemanticDebugDataFixture) + +BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + function f(uint256 value) public pure returns (uint256 result) { + return value; + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + FunctionDefinition const* function = nullptr; + for (FunctionDefinition const* candidate: contract->definedFunctions()) + if (candidate->name() == "f") + { + function = candidate; + break; + } + BOOST_REQUIRE(function); + BOOST_REQUIRE_EQUAL(function->parameters().size(), 1); + BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); + + VariableDeclaration const& parameter = *function->parameters().front(); + VariableDeclaration const& returnVariable = *function->returnParameters().front(); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(function->id()); + BOOST_REQUIRE(data); + BOOST_REQUIRE(data->lexicalScopeID); + BOOST_CHECK_EQUAL(*data->lexicalScopeID, function->id()); + BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 2); + + SemanticDebugVariable const& parameterDebugData = data->variableDefinitions.at(0); + BOOST_CHECK_EQUAL(parameterDebugData.name, "value"); + BOOST_REQUIRE(parameterDebugData.declarationAstID); + BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, parameter.id()); + BOOST_REQUIRE(parameterDebugData.declarationLocation); + BOOST_REQUIRE(parameterDebugData.typeID); + BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(parameterDebugData.location); + BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(parameterDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(parameter)); + + SemanticDebugVariable const& returnDebugData = data->variableDefinitions.at(1); + BOOST_CHECK_EQUAL(returnDebugData.name, "result"); + BOOST_REQUIRE(returnDebugData.declarationAstID); + BOOST_CHECK_EQUAL(*returnDebugData.declarationAstID, returnVariable.id()); + BOOST_REQUIRE(returnDebugData.typeID); + BOOST_CHECK_EQUAL(*returnDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(returnDebugData.location); + BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(returnDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(returnVariable)); +} + +BOOST_AUTO_TEST_SUITE_END() From 5ba220a654941f20247ee3dcc1f60dc67f2d23f8 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 26 May 2026 10:53:46 +0200 Subject: [PATCH 05/47] docs: Document ETHDebug internal metadata --- docs/index.rst | 1 + docs/internals/ethdebug_internal_metadata.rst | 160 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 docs/internals/ethdebug_internal_metadata.rst diff --git a/docs/index.rst b/docs/index.rst index 77d06cd6b31c..9e4b70041510 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,6 +149,7 @@ Contents internals/layout_in_calldata.rst internals/variable_cleanup.rst internals/source_mappings.rst + internals/ethdebug_internal_metadata.rst internals/optimizer.rst metadata.rst abi-spec.rst diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst new file mode 100644 index 000000000000..322d913981de --- /dev/null +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -0,0 +1,160 @@ +.. index:: ethdebug, debug info, metadata + +************************** +ETHDebug Internal Metadata +************************** + +.. warning:: + + ETHDebug support is experimental. The internal representation described here is + intended for compiler development and may change before the public ETHDebug + output is stabilized. + +The compiler can emit debug information in the +`ethdebug format `_. The JSON outputs are +validated against the upstream schemas, but the compiler does not construct that +JSON directly during the Solidity-to-Yul lowering. This internal representation +is the compiler-side carrier for semantic information that needs to survive the +Yul pipeline before it can be lowered into public ETHDebug type and pointer +entities. + +This page describes the internal representation used for semantic debug metadata. +It complements :doc:`source_mappings`, which describe source ranges and bytecode +instruction mapping. + +Overview +======== + +The internal ETHDebug metadata flow is: + +1. Solidity analysis assigns stable AST IDs and type information to declarations. +2. The IR generator prints AST ID comments into generated Yul when AST ID debug + info is enabled. +3. The compiler builds a side table keyed by Solidity AST ID. +4. Generated Yul is parsed and analyzed into a ``YulStack``. +5. Semantic metadata is attached to Yul ``DebugData`` objects by matching the + AST ID in each Yul node's debug data. +6. If the Yul optimizer reparses optimized IR, semantic metadata is collected + before reparse and reattached afterward by AST ID. + +The AST ID is the join key. It allows semantic information from the Solidity AST +to survive the text-based Yul print/parse boundary. + +Core Structures +=============== + +``langutil::DebugData`` is the common debug payload carried by Yul AST nodes. +For ETHDebug, it contains: + +* the native Yul source location, +* the original Solidity source location, +* the optional Solidity AST ID, +* optional semantic debug metadata. + +The semantic payload is represented by ``langutil::SemanticDebugData``. It +currently contains: + +* ``lexicalScopeID``: the Solidity AST ID of the lexical scope represented by + this metadata, +* ``variableDefinitions``: the variables introduced in that scope. + +Each ``SemanticDebugVariable`` contains: + +* ``name``: Solidity source-level name, +* ``declarationAstID``: AST ID of the Solidity declaration, +* ``declarationLocation``: Solidity source location of the declaration, +* ``typeID``: compiler-internal Solidity type identifier, +* ``location``: initial internal variable location. + +The variable location is represented by ``SemanticDebugVariableLocation``. The +location kind can describe stack, storage, transient storage, memory, calldata, +immutable, constant, or optimized-out values. The current first implementation +populates stack locations for function parameters and named return variables. + +Side Table +========== + +``langutil::SemanticDebugDataTable`` maps Solidity AST IDs to +``SemanticDebugData`` instances. + +This table is intentionally separate from the Yul AST because generated IR is +still passed through textual Yul at multiple points. The table lets the compiler +reattach semantic metadata whenever a Yul AST is reconstructed from text. + +The table is used in two places: + +* ``CompilerStack`` builds the table from the Solidity contract and attaches it + after generated IR is parsed into a ``YulStack``. +* ``YulStack::reparse()`` collects semantic metadata before printing and parsing + optimized IR, then reattaches it to the new Yul AST by AST ID. + +Current Producer +================ + +The current Solidity-side producer is +``frontend::buildSemanticDebugDataTable(ContractDefinition const&)``. + +It records named function parameters, named modifier parameters, and named +function return variables. +For each variable it stores: + +* the declaration name, +* the declaration AST ID, +* the declaration source location, +* the compiler type identifier, for example ``t_uint256``, +* the initial Yul stack slot name used by IR generation. + +The stack pointer is based on ``IRVariable`` and therefore matches the names used +by generated IR, such as ``var_value_42`` for a Solidity variable named +``value`` with AST ID ``42``. Multi-slot variables use the comma-separated stack +slot list produced by ``IRVariable``. + +Current Scope +============= + +This is not yet the complete ETHDebug variable model. The current scope is a +minimal internal carrier and first real producer: + +* function parameters, +* modifier parameters, +* named function return variables, +* initial stack locations. + +Local variables, state variables, storage pointers, memory pointers, calldata +pointers, optimizer location updates, and the final mapping to public ETHDebug +type and pointer schema objects are still future work. + +Type and Pointer Mapping +======================== + +The current ``typeID`` is the compiler's existing internal type identifier. This +is useful as a stable compiler-side key, but it is not the final ETHDebug type +schema representation. + +Similarly, the current stack ``pointerID`` is an internal pointer into generated +Yul stack slots. It is sufficient to connect Solidity declarations with their +initial IR variables, but it still needs to be lowered into the public ETHDebug +pointer model. + +Future work should define: + +* Solidity type to ETHDebug type mapping, +* Solidity/Yul variable location to ETHDebug pointer mapping, +* optimizer rules for updating, splitting, merging, or removing variable + locations, +* schema-validated emission of the resulting type and pointer entities. + +Testing +======== + +The internal metadata plumbing is covered by focused tests: + +* ``DebugDataTest`` checks that ``DebugData`` can carry semantic metadata and + that the AST-ID side table resolves it. +* ``YulDebugDataTest`` checks that semantic metadata survives Yul reparse by AST + ID. +* ``SemanticDebugDataTest`` checks that Solidity function variables produce + semantic metadata with declaration IDs, type IDs, and initial stack locations. + +These tests deliberately target the internal model. Schema validation tests cover +the public ETHDebug JSON output separately. From 673e29200600ec56f3965d4d4a784b0a400eeb90 Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 5 Jun 2026 17:37:14 +0200 Subject: [PATCH 06/47] test: Cover semantic debug data across Yul reparse --- test/libsolidity/SemanticDebugData.cpp | 195 +++++++++++++++++++++---- test/libyul/DebugData.cpp | 1 - 2 files changed, 164 insertions(+), 32 deletions(-) diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index eb428c394a92..ce9d1aabd297 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -17,11 +17,17 @@ // SPDX-License-Identifier: GPL-3.0 #include +#include #include #include +#include +#include #include +#include +#include +#include #include @@ -40,10 +46,120 @@ std::string stackPointer(VariableDeclaration const& _variable) return "var_" + _variable.name() + "_" + std::to_string(_variable.id()); } +FunctionDefinition const* findFunction(ContractDefinition const& _contract, std::string_view _name) +{ + for (FunctionDefinition const* candidate: _contract.definedFunctions()) + if (candidate->name() == _name) + return candidate; + return nullptr; +} + +SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) +{ + if ( + _debugData && + _debugData->astID && + *_debugData->astID == _astID && + _debugData->semanticDebugData + ) + return _debugData->semanticDebugData; + + return nullptr; +} + +SemanticDebugData::ConstPtr findSemanticDebugData(yul::Block const& _block, int64_t _astID); + +SemanticDebugData::ConstPtr findSemanticDebugData(yul::FunctionDefinition const& _function, int64_t _astID) +{ + if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_function.debugData, _astID)) + return result; + return findSemanticDebugData(_function.body, _astID); +} + +SemanticDebugData::ConstPtr findSemanticDebugData(yul::Statement const& _statement, int64_t _astID) +{ + return std::visit([&](auto const& _node) -> SemanticDebugData::ConstPtr { + if constexpr (std::is_same_v, yul::FunctionDefinition>) + return findSemanticDebugData(_node, _astID); + else if constexpr (std::is_same_v, yul::Block>) + return findSemanticDebugData(_node, _astID); + else + return findSemanticDebugData(_node.debugData, _astID); + }, _statement); +} + +SemanticDebugData::ConstPtr findSemanticDebugData(yul::Block const& _block, int64_t _astID) +{ + if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_block.debugData, _astID)) + return result; + for (yul::Statement const& statement: _block.statements) + if (SemanticDebugData::ConstPtr result = findSemanticDebugData(statement, _astID)) + return result; + return nullptr; +} + +SemanticDebugData::ConstPtr findSemanticDebugData(yul::Object const& _object, int64_t _astID) +{ + if (_object.hasCode()) + if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_object.code()->root(), _astID)) + return result; + + for (std::shared_ptr const& subObject: _object.subObjects) + if (auto const* object = dynamic_cast(subObject.get())) + if (SemanticDebugData::ConstPtr result = findSemanticDebugData(*object, _astID)) + return result; + + return nullptr; +} + +void checkFunctionVariableDebugData( + SemanticDebugData const& _data, + FunctionDefinition const& _function, + VariableDeclaration const& _parameter, + VariableDeclaration const& _returnVariable +) +{ + BOOST_REQUIRE(_data.lexicalScopeID); + BOOST_CHECK_EQUAL(*_data.lexicalScopeID, _function.id()); + BOOST_REQUIRE_EQUAL(_data.variableDefinitions.size(), 2); + + SemanticDebugVariable const& parameterDebugData = _data.variableDefinitions.at(0); + BOOST_CHECK_EQUAL(parameterDebugData.name, "value"); + BOOST_REQUIRE(parameterDebugData.declarationAstID); + BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, _parameter.id()); + BOOST_REQUIRE(parameterDebugData.declarationLocation); + BOOST_REQUIRE(parameterDebugData.typeID); + BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(parameterDebugData.location); + BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(parameterDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(_parameter)); + + SemanticDebugVariable const& returnDebugData = _data.variableDefinitions.at(1); + BOOST_CHECK_EQUAL(returnDebugData.name, "result"); + BOOST_REQUIRE(returnDebugData.declarationAstID); + BOOST_CHECK_EQUAL(*returnDebugData.declarationAstID, _returnVariable.id()); + BOOST_REQUIRE(returnDebugData.typeID); + BOOST_CHECK_EQUAL(*returnDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(returnDebugData.location); + BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(returnDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(_returnVariable)); +} + } class SemanticDebugDataFixture: public AnalysisFramework { + void setupCompiler(CompilerStack& _compiler) override + { + AnalysisFramework::setupCompiler(_compiler); + _compiler.setViaIR(true); + _compiler.setOptimiserSettings(OptimiserSettings::none()); + DebugInfoSelection selection = DebugInfoSelection::Default(); + selection.enable("ast-id"); + _compiler.selectDebugInfo(selection); + } }; BOOST_FIXTURE_TEST_SUITE(SemanticDebugDataTest, SemanticDebugDataFixture) @@ -60,13 +176,7 @@ BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); BOOST_REQUIRE(contract); - FunctionDefinition const* function = nullptr; - for (FunctionDefinition const* candidate: contract->definedFunctions()) - if (candidate->name() == "f") - { - function = candidate; - break; - } + FunctionDefinition const* function = findFunction(*contract, "f"); BOOST_REQUIRE(function); BOOST_REQUIRE_EQUAL(function->parameters().size(), 1); BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); @@ -77,32 +187,55 @@ BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); SemanticDebugData::ConstPtr data = table.find(function->id()); BOOST_REQUIRE(data); - BOOST_REQUIRE(data->lexicalScopeID); - BOOST_CHECK_EQUAL(*data->lexicalScopeID, function->id()); - BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 2); + checkFunctionVariableDebugData(*data, *function, parameter, returnVariable); +} - SemanticDebugVariable const& parameterDebugData = data->variableDefinitions.at(0); - BOOST_CHECK_EQUAL(parameterDebugData.name, "value"); - BOOST_REQUIRE(parameterDebugData.declarationAstID); - BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, parameter.id()); - BOOST_REQUIRE(parameterDebugData.declarationLocation); - BOOST_REQUIRE(parameterDebugData.typeID); - BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); - BOOST_REQUIRE(parameterDebugData.location); - BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(parameterDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(parameter)); +BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + function f(uint256 value) public pure returns (uint256 result) { + return value; + } + } + )", PipelineStage::Compilation)); - SemanticDebugVariable const& returnDebugData = data->variableDefinitions.at(1); - BOOST_CHECK_EQUAL(returnDebugData.name, "result"); - BOOST_REQUIRE(returnDebugData.declarationAstID); - BOOST_CHECK_EQUAL(*returnDebugData.declarationAstID, returnVariable.id()); - BOOST_REQUIRE(returnDebugData.typeID); - BOOST_CHECK_EQUAL(*returnDebugData.typeID, "t_uint256"); - BOOST_REQUIRE(returnDebugData.location); - BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(returnDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(returnVariable)); + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + FunctionDefinition const* function = findFunction(*contract, "f"); + BOOST_REQUIRE(function); + BOOST_REQUIRE_EQUAL(function->parameters().size(), 1); + BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); + + VariableDeclaration const& parameter = *function->parameters().front(); + VariableDeclaration const& returnVariable = *function->returnParameters().front(); + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + + std::optional const& yulIR = compiler().yulIR("C"); + BOOST_REQUIRE(yulIR); + + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + yul::YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All(), + &compiler() + ); + BOOST_REQUIRE(yulStack.parseAndAnalyze("", *yulIR)); + + yulStack.attachSemanticDebugData(table); + SemanticDebugData::ConstPtr attachedData = findSemanticDebugData(*yulStack.parserResult(), function->id()); + BOOST_REQUIRE(attachedData); + checkFunctionVariableDebugData(*attachedData, *function, parameter, returnVariable); + + yulStack.optimize(); + + SemanticDebugData::ConstPtr reparsedData = findSemanticDebugData(*yulStack.parserResult(), function->id()); + BOOST_REQUIRE(reparsedData); + BOOST_CHECK(reparsedData == attachedData); + checkFunctionVariableDebugData(*reparsedData, *function, parameter, returnVariable); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index 55bb24804483..b390e6b64293 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -67,7 +67,6 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) YulStack yulStack( solidity::test::CommonOptions::get().evmVersion(), - solidity::test::CommonOptions::get().eofVersion(), optimiserSettings, DebugInfoSelection::All() ); From 81ab972607ade8bbfb4b2696e137f98c7f97946e Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 5 Jun 2026 17:45:25 +0200 Subject: [PATCH 07/47] docs: improve ethdebug internal metadata --- docs/internals/ethdebug_internal_metadata.rst | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 322d913981de..4310686601c6 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -27,18 +27,23 @@ Overview The internal ETHDebug metadata flow is: -1. Solidity analysis assigns stable AST IDs and type information to declarations. +1. Solidity analysis assigns AST IDs and type information to declarations. AST + IDs are stable within a single compilation and are the internal join key used + by this metadata pipeline. 2. The IR generator prints AST ID comments into generated Yul when AST ID debug - info is enabled. + info is enabled. Semantic metadata reattachment relies on those comments + being present at Yul parse/reparse boundaries. 3. The compiler builds a side table keyed by Solidity AST ID. 4. Generated Yul is parsed and analyzed into a ``YulStack``. -5. Semantic metadata is attached to Yul ``DebugData`` objects by matching the - AST ID in each Yul node's debug data. +5. Semantic metadata is attached to Yul ``DebugData`` objects when the AST ID + in the Yul node's debug data has an entry in the side table. 6. If the Yul optimizer reparses optimized IR, semantic metadata is collected before reparse and reattached afterward by AST ID. The AST ID is the join key. It allows semantic information from the Solidity AST to survive the text-based Yul print/parse boundary. +The side table itself is not serialized; the serialized part that crosses the +Yul text boundary is the AST ID comment in the Yul source. Core Structures =============== @@ -80,6 +85,12 @@ Side Table This table is intentionally separate from the Yul AST because generated IR is still passed through textual Yul at multiple points. The table lets the compiler reattach semantic metadata whenever a Yul AST is reconstructed from text. +It is an in-memory compiler data structure, not a public output format. + +The current table entries are keyed by lexical-scope AST IDs, such as function +or modifier AST IDs. Variable declaration AST IDs are stored inside +``SemanticDebugVariable`` records as declaration identities; they are not +top-level keys in the table. The table is used in two places: @@ -123,6 +134,8 @@ minimal internal carrier and first real producer: Local variables, state variables, storage pointers, memory pointers, calldata pointers, optimizer location updates, and the final mapping to public ETHDebug type and pointer schema objects are still future work. +The current implementation does not yet add variable, type, or pointer entries +to the public ETHDebug JSON output. Type and Pointer Mapping ======================== @@ -134,7 +147,7 @@ schema representation. Similarly, the current stack ``pointerID`` is an internal pointer into generated Yul stack slots. It is sufficient to connect Solidity declarations with their initial IR variables, but it still needs to be lowered into the public ETHDebug -pointer model. +pointer model. It is not a runtime stack depth. Future work should define: @@ -155,6 +168,8 @@ The internal metadata plumbing is covered by focused tests: ID. * ``SemanticDebugDataTest`` checks that Solidity function variables produce semantic metadata with declaration IDs, type IDs, and initial stack locations. + It also checks that this function variable metadata can be attached to + generated Yul and survives the Yul reparse path. These tests deliberately target the internal model. Schema validation tests cover the public ETHDebug JSON output separately. From 042f5845cacb17bd6ba871ea3cf1816b24d12611 Mon Sep 17 00:00:00 2001 From: djole Date: Sun, 7 Jun 2026 11:11:13 +0200 Subject: [PATCH 08/47] ethdebug: Map semantic variables to type descriptors --- docs/internals/ethdebug_internal_metadata.rst | 21 ++- liblangutil/SemanticDebugData.h | 41 ++++++ .../codegen/ir/SemanticDebugDataBuilder.cpp | 135 ++++++++++++++++++ test/liblangutil/DebugData.cpp | 16 +++ test/libsolidity/SemanticDebugData.cpp | 115 +++++++++++++++ 5 files changed, 325 insertions(+), 3 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 4310686601c6..aa12f0af0672 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -69,6 +69,8 @@ Each ``SemanticDebugVariable`` contains: * ``declarationAstID``: AST ID of the Solidity declaration, * ``declarationLocation``: Solidity source location of the declaration, * ``typeID``: compiler-internal Solidity type identifier, +* ``ethdebugType``: ETHDebug-oriented type descriptor derived from the Solidity + type, * ``location``: initial internal variable location. The variable location is represented by ``SemanticDebugVariableLocation``. The @@ -113,6 +115,7 @@ For each variable it stores: * the declaration AST ID, * the declaration source location, * the compiler type identifier, for example ``t_uint256``, +* an ETHDebug-oriented type descriptor, for example ``uint`` with ``bits = 256``, * the initial Yul stack slot name used by IR generation. The stack pointer is based on ``IRVariable`` and therefore matches the names used @@ -129,6 +132,8 @@ minimal internal carrier and first real producer: * function parameters, * modifier parameters, * named function return variables, +* ETHDebug-oriented type descriptors for elementary scalar types and basic + complex type categories, * initial stack locations. Local variables, state variables, storage pointers, memory pointers, calldata @@ -144,6 +149,15 @@ The current ``typeID`` is the compiler's existing internal type identifier. This is useful as a stable compiler-side key, but it is not the final ETHDebug type schema representation. +The current ``ethdebugType`` descriptor is the first bridge from Solidity types +to the public ETHDebug type vocabulary. It records whether the type is +elementary, complex, or unknown, plus the ETHDebug kind. The first mapping covers +``uint``/``int`` bit widths, ``fixed``/``ufixed`` bit widths and decimal places, +``bool``, fixed and dynamic ``bytes``, ``string``, ``address`` payable-ness, +contracts, enums, aliases, tuples, arrays, mappings, and structs. Complex +descriptors do not yet recursively contain member, key, value, or element type +wrappers. + Similarly, the current stack ``pointerID`` is an internal pointer into generated Yul stack slots. It is sufficient to connect Solidity declarations with their initial IR variables, but it still needs to be lowered into the public ETHDebug @@ -167,9 +181,10 @@ The internal metadata plumbing is covered by focused tests: * ``YulDebugDataTest`` checks that semantic metadata survives Yul reparse by AST ID. * ``SemanticDebugDataTest`` checks that Solidity function variables produce - semantic metadata with declaration IDs, type IDs, and initial stack locations. - It also checks that this function variable metadata can be attached to - generated Yul and survives the Yul reparse path. + semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type + descriptors, and initial stack locations. It also checks that this function + variable metadata can be attached to generated Yul and survives the Yul reparse + path. These tests deliberately target the internal model. Schema validation tests cover the public ETHDebug JSON output separately. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 2d1e4431bc3f..7c8593375afc 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -47,12 +47,53 @@ struct SemanticDebugVariableLocation std::optional pointerID; }; +struct SemanticDebugType +{ + enum class Class + { + Elementary, + Complex, + Unknown + }; + + enum class Kind + { + Uint, + Int, + Ufixed, + Fixed, + Bool, + Bytes, + String, + Address, + Contract, + Enum, + Alias, + Tuple, + Array, + Mapping, + Struct, + Function, + Unknown + }; + + Class typeClass = Class::Unknown; + Kind kind = Kind::Unknown; + std::optional bits; + std::optional places; + std::optional bytes; + std::optional dataLocation; + std::optional payable; + std::optional dynamic; +}; + struct SemanticDebugVariable { std::string name; std::optional declarationAstID; std::optional declarationLocation; std::optional typeID; + std::optional ethdebugType; std::optional location; }; diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 809a440ed2dc..0a9f5a0baf4d 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -24,6 +24,8 @@ #include +#include + #include #include #include @@ -36,6 +38,130 @@ using namespace solidity::langutil; namespace { +std::string dataLocationName(DataLocation _location) +{ + switch (_location) + { + case DataLocation::Storage: + return "storage"; + case DataLocation::Transient: + return "transient"; + case DataLocation::CallData: + return "calldata"; + case DataLocation::Memory: + return "memory"; + } + solAssert(false, "Invalid data location."); + return ""; +} + +SemanticDebugType semanticType(Type const& _type) +{ + SemanticDebugType result; + + switch (_type.category()) + { + case Type::Category::Address: + { + auto const& addressType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Address; + result.payable = addressType.stateMutability() == StateMutability::Payable; + break; + } + case Type::Category::Integer: + { + auto const& integerType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = integerType.isSigned() ? SemanticDebugType::Kind::Int : SemanticDebugType::Kind::Uint; + result.bits = integerType.numBits(); + break; + } + case Type::Category::FixedPoint: + { + auto const& fixedPointType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = fixedPointType.isSigned() ? SemanticDebugType::Kind::Fixed : SemanticDebugType::Kind::Ufixed; + result.bits = fixedPointType.numBits(); + result.places = fixedPointType.fractionalDigits(); + break; + } + case Type::Category::Bool: + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Bool; + break; + case Type::Category::FixedBytes: + { + auto const& bytesType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Bytes; + result.bytes = bytesType.numBytes(); + break; + } + case Type::Category::Array: + { + auto const& arrayType = dynamic_cast(_type); + result.typeClass = arrayType.isByteArrayOrString() ? + SemanticDebugType::Class::Elementary : + SemanticDebugType::Class::Complex; + result.kind = arrayType.isString() ? + SemanticDebugType::Kind::String : + (arrayType.isByteArray() ? SemanticDebugType::Kind::Bytes : SemanticDebugType::Kind::Array); + result.dataLocation = dataLocationName(arrayType.location()); + result.dynamic = arrayType.isDynamicallySized(); + break; + } + case Type::Category::Contract: + { + auto const& contractType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Contract; + result.payable = contractType.isPayable(); + break; + } + case Type::Category::Struct: + { + auto const& structType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Struct; + result.dataLocation = dataLocationName(structType.location()); + break; + } + case Type::Category::Enum: + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Enum; + break; + case Type::Category::UserDefinedValueType: + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Alias; + break; + case Type::Category::Tuple: + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Tuple; + break; + case Type::Category::Mapping: + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Mapping; + result.dataLocation = "storage"; + break; + case Type::Category::Function: + result.typeClass = SemanticDebugType::Class::Unknown; + result.kind = SemanticDebugType::Kind::Function; + break; + case Type::Category::RationalNumber: + case Type::Category::StringLiteral: + case Type::Category::ArraySlice: + case Type::Category::TypeType: + case Type::Category::Modifier: + case Type::Category::Magic: + case Type::Category::Module: + case Type::Category::InaccessibleDynamic: + break; + } + + return result; +} + std::optional stackLocation(VariableDeclaration const& _variable) { if (!_variable.annotation().type) @@ -55,6 +181,14 @@ std::optional typeID(VariableDeclaration const& _variable) return _variable.annotation().type->identifier(); } +std::optional ethdebugType(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + return semanticType(*_variable.annotation().type); +} + SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) { return { @@ -62,6 +196,7 @@ SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) .declarationAstID = _variable.id(), .declarationLocation = _variable.location(), .typeID = typeID(_variable), + .ethdebugType = ethdebugType(_variable), .location = stackLocation(_variable) }; } diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index da8b13b1c927..c4e3b2e06a97 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -22,6 +22,7 @@ #include #include +#include #include namespace solidity::langutil::test @@ -38,6 +39,16 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) .declarationAstID = 23, .declarationLocation = SourceLocation{1, 6, std::make_shared("input.sol")}, .typeID = "type:uint256", + .ethdebugType = SemanticDebugType{ + .typeClass = SemanticDebugType::Class::Elementary, + .kind = SemanticDebugType::Kind::Uint, + .bits = 256, + .places = std::nullopt, + .bytes = std::nullopt, + .dataLocation = std::nullopt, + .payable = std::nullopt, + .dynamic = std::nullopt + }, .location = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = "pointer:value" @@ -59,6 +70,11 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_CHECK_EQUAL(debugData->semanticDebugData->variableDefinitions.front().name, "value"); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().typeID); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().typeID, "type:uint256"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits, 256); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location); BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location->pointerID); diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index ce9d1aabd297..5990fc4c76f5 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -54,6 +54,15 @@ FunctionDefinition const* findFunction(ContractDefinition const& _contract, std: return nullptr; } +SemanticDebugVariable const* findVariableByName(SemanticDebugData const& _data, std::string_view _name) +{ + for (SemanticDebugVariable const& variable: _data.variableDefinitions) + if (variable.name == _name) + return &variable; + + return nullptr; +} + SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) { if ( @@ -130,6 +139,11 @@ void checkFunctionVariableDebugData( BOOST_REQUIRE(parameterDebugData.declarationLocation); BOOST_REQUIRE(parameterDebugData.typeID); BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(parameterDebugData.ethdebugType); + BOOST_CHECK(parameterDebugData.ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(parameterDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(parameterDebugData.ethdebugType->bits); + BOOST_CHECK_EQUAL(*parameterDebugData.ethdebugType->bits, 256); BOOST_REQUIRE(parameterDebugData.location); BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(parameterDebugData.location->pointerID); @@ -141,6 +155,11 @@ void checkFunctionVariableDebugData( BOOST_CHECK_EQUAL(*returnDebugData.declarationAstID, _returnVariable.id()); BOOST_REQUIRE(returnDebugData.typeID); BOOST_CHECK_EQUAL(*returnDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(returnDebugData.ethdebugType); + BOOST_CHECK(returnDebugData.ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(returnDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(returnDebugData.ethdebugType->bits); + BOOST_CHECK_EQUAL(*returnDebugData.ethdebugType->bits, 256); BOOST_REQUIRE(returnDebugData.location); BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(returnDebugData.location->pointerID); @@ -190,6 +209,102 @@ BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) checkFunctionVariableDebugData(*data, *function, parameter, returnVariable); } +BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_type_descriptors) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + function f( + uint256 amount, + int128 signedAmount, + bool enabled, + bytes16 tag, + bytes memory payload, + string memory label, + uint256[] memory values, + address payable recipient + ) public pure returns (bool ok) { + ok = enabled; + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + FunctionDefinition const* function = findFunction(*contract, "f"); + BOOST_REQUIRE(function); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(function->id()); + BOOST_REQUIRE(data); + BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 9); + + SemanticDebugVariable const* amount = findVariableByName(*data, "amount"); + BOOST_REQUIRE(amount); + BOOST_REQUIRE(amount->ethdebugType); + BOOST_CHECK(amount->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(amount->ethdebugType->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(amount->ethdebugType->bits); + BOOST_CHECK_EQUAL(*amount->ethdebugType->bits, 256); + + SemanticDebugVariable const* signedAmount = findVariableByName(*data, "signedAmount"); + BOOST_REQUIRE(signedAmount); + BOOST_REQUIRE(signedAmount->ethdebugType); + BOOST_CHECK(signedAmount->ethdebugType->kind == SemanticDebugType::Kind::Int); + BOOST_REQUIRE(signedAmount->ethdebugType->bits); + BOOST_CHECK_EQUAL(*signedAmount->ethdebugType->bits, 128); + + SemanticDebugVariable const* enabled = findVariableByName(*data, "enabled"); + BOOST_REQUIRE(enabled); + BOOST_REQUIRE(enabled->ethdebugType); + BOOST_CHECK(enabled->ethdebugType->kind == SemanticDebugType::Kind::Bool); + + SemanticDebugVariable const* tag = findVariableByName(*data, "tag"); + BOOST_REQUIRE(tag); + BOOST_REQUIRE(tag->ethdebugType); + BOOST_CHECK(tag->ethdebugType->kind == SemanticDebugType::Kind::Bytes); + BOOST_REQUIRE(tag->ethdebugType->bytes); + BOOST_CHECK_EQUAL(*tag->ethdebugType->bytes, 16); + + SemanticDebugVariable const* payload = findVariableByName(*data, "payload"); + BOOST_REQUIRE(payload); + BOOST_REQUIRE(payload->ethdebugType); + BOOST_CHECK(payload->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(payload->ethdebugType->kind == SemanticDebugType::Kind::Bytes); + BOOST_REQUIRE(payload->ethdebugType->dataLocation); + BOOST_CHECK_EQUAL(*payload->ethdebugType->dataLocation, "memory"); + BOOST_REQUIRE(payload->ethdebugType->dynamic); + BOOST_CHECK(*payload->ethdebugType->dynamic); + + SemanticDebugVariable const* label = findVariableByName(*data, "label"); + BOOST_REQUIRE(label); + BOOST_REQUIRE(label->ethdebugType); + BOOST_CHECK(label->ethdebugType->kind == SemanticDebugType::Kind::String); + BOOST_REQUIRE(label->ethdebugType->dataLocation); + BOOST_CHECK_EQUAL(*label->ethdebugType->dataLocation, "memory"); + + SemanticDebugVariable const* values = findVariableByName(*data, "values"); + BOOST_REQUIRE(values); + BOOST_REQUIRE(values->ethdebugType); + BOOST_CHECK(values->ethdebugType->typeClass == SemanticDebugType::Class::Complex); + BOOST_CHECK(values->ethdebugType->kind == SemanticDebugType::Kind::Array); + BOOST_REQUIRE(values->ethdebugType->dataLocation); + BOOST_CHECK_EQUAL(*values->ethdebugType->dataLocation, "memory"); + BOOST_REQUIRE(values->ethdebugType->dynamic); + BOOST_CHECK(*values->ethdebugType->dynamic); + + SemanticDebugVariable const* recipient = findVariableByName(*data, "recipient"); + BOOST_REQUIRE(recipient); + BOOST_REQUIRE(recipient->ethdebugType); + BOOST_CHECK(recipient->ethdebugType->kind == SemanticDebugType::Kind::Address); + BOOST_REQUIRE(recipient->ethdebugType->payable); + BOOST_CHECK(*recipient->ethdebugType->payable); + + SemanticDebugVariable const* ok = findVariableByName(*data, "ok"); + BOOST_REQUIRE(ok); + BOOST_REQUIRE(ok->ethdebugType); + BOOST_CHECK(ok->ethdebugType->kind == SemanticDebugType::Kind::Bool); +} + BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) { BOOST_REQUIRE(runFramework(R"( From 17ac6adf3ddbf3dd632b741d74fd200b003326fe Mon Sep 17 00:00:00 2001 From: djole Date: Sun, 7 Jun 2026 11:17:29 +0200 Subject: [PATCH 09/47] ethdebug: Map semantic variables to pointer descriptors --- docs/internals/ethdebug_internal_metadata.rst | 33 ++++--- liblangutil/SemanticDebugData.h | 31 ++++++ .../codegen/ir/SemanticDebugDataBuilder.cpp | 33 ++++++- test/liblangutil/DebugData.cpp | 17 +++- test/libsolidity/SemanticDebugData.cpp | 94 +++++++++++++++++++ 5 files changed, 194 insertions(+), 14 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index aa12f0af0672..691bf0de8d74 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -71,7 +71,9 @@ Each ``SemanticDebugVariable`` contains: * ``typeID``: compiler-internal Solidity type identifier, * ``ethdebugType``: ETHDebug-oriented type descriptor derived from the Solidity type, -* ``location``: initial internal variable location. +* ``location``: initial internal variable location, +* ``ethdebugPointer``: ETHDebug-oriented pointer descriptor derived from the + initial internal location. The variable location is represented by ``SemanticDebugVariableLocation``. The location kind can describe stack, storage, transient storage, memory, calldata, @@ -116,12 +118,16 @@ For each variable it stores: * the declaration source location, * the compiler type identifier, for example ``t_uint256``, * an ETHDebug-oriented type descriptor, for example ``uint`` with ``bits = 256``, -* the initial Yul stack slot name used by IR generation. +* the initial Yul stack slot name used by IR generation, +* an ETHDebug-oriented stack pointer descriptor for the initial Yul stack slot or + slots. The stack pointer is based on ``IRVariable`` and therefore matches the names used by generated IR, such as ``var_value_42`` for a Solidity variable named -``value`` with AST ID ``42``. Multi-slot variables use the comma-separated stack -slot list produced by ``IRVariable``. +``value`` with AST ID ``42``. Multi-slot variables use the stack slot list +produced by ``IRVariable``. In the ETHDebug-oriented pointer descriptor, one-slot +variables become stack region pointers and multi-slot variables become groups of +stack region pointers. Current Scope ============= @@ -134,7 +140,7 @@ minimal internal carrier and first real producer: * named function return variables, * ETHDebug-oriented type descriptors for elementary scalar types and basic complex type categories, -* initial stack locations. +* initial stack locations and ETHDebug-oriented stack pointer descriptors. Local variables, state variables, storage pointers, memory pointers, calldata pointers, optimizer location updates, and the final mapping to public ETHDebug @@ -159,14 +165,17 @@ descriptors do not yet recursively contain member, key, value, or element type wrappers. Similarly, the current stack ``pointerID`` is an internal pointer into generated -Yul stack slots. It is sufficient to connect Solidity declarations with their -initial IR variables, but it still needs to be lowered into the public ETHDebug -pointer model. It is not a runtime stack depth. +Yul stack slots. The current ``ethdebugPointer`` descriptor is the first bridge +from these internal locations to the public ETHDebug pointer vocabulary. It +records stack region pointers for one-slot variables and pointer groups for +multi-slot variables. The stack slot expression is still the generated Yul +variable name, not a runtime stack depth. Future work should define: * Solidity type to ETHDebug type mapping, -* Solidity/Yul variable location to ETHDebug pointer mapping, +* storage, memory, calldata, transient, immutable, constant, and optimized-out + variable location to ETHDebug pointer mapping, * optimizer rules for updating, splitting, merging, or removing variable locations, * schema-validated emission of the resulting type and pointer entities. @@ -182,9 +191,9 @@ The internal metadata plumbing is covered by focused tests: ID. * ``SemanticDebugDataTest`` checks that Solidity function variables produce semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type - descriptors, and initial stack locations. It also checks that this function - variable metadata can be attached to generated Yul and survives the Yul reparse - path. + descriptors, initial stack locations, and ETHDebug-oriented pointer + descriptors. It also checks that this function variable metadata can be + attached to generated Yul and survives the Yul reparse path. These tests deliberately target the internal model. Schema validation tests cover the public ETHDebug JSON output separately. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 7c8593375afc..3de3afd81454 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -87,6 +87,36 @@ struct SemanticDebugType std::optional dynamic; }; +struct SemanticDebugPointer +{ + enum class Class + { + Region, + Group, + Unknown + }; + + enum class Location + { + Stack, + Storage, + Transient, + Memory, + Calldata, + Returndata, + Code, + Unknown + }; + + Class pointerClass = Class::Unknown; + std::optional location; + std::optional name; + std::optional slot; + std::optional offset; + std::optional length; + std::vector group; +}; + struct SemanticDebugVariable { std::string name; @@ -95,6 +125,7 @@ struct SemanticDebugVariable std::optional typeID; std::optional ethdebugType; std::optional location; + std::optional ethdebugPointer; }; struct SemanticDebugData diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 0a9f5a0baf4d..4ebd638e3917 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -162,6 +162,16 @@ SemanticDebugType semanticType(Type const& _type) return result; } +SemanticDebugPointer stackRegionPointer(std::string _name, std::string _slot) +{ + SemanticDebugPointer result; + result.pointerClass = SemanticDebugPointer::Class::Region; + result.location = SemanticDebugPointer::Location::Stack; + result.name = std::move(_name); + result.slot = std::move(_slot); + return result; +} + std::optional stackLocation(VariableDeclaration const& _variable) { if (!_variable.annotation().type) @@ -173,6 +183,26 @@ std::optional stackLocation(VariableDeclaration c }; } +std::optional stackPointer(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + std::vector stackSlots = IRVariable(_variable).stackSlots(); + if (stackSlots.empty()) + return std::nullopt; + + if (stackSlots.size() == 1) + return stackRegionPointer(_variable.name(), stackSlots.front()); + + SemanticDebugPointer result; + result.pointerClass = SemanticDebugPointer::Class::Group; + result.name = _variable.name(); + for (std::string& stackSlot: stackSlots) + result.group.emplace_back(stackRegionPointer(stackSlot, stackSlot)); + return result; +} + std::optional typeID(VariableDeclaration const& _variable) { if (!_variable.annotation().type) @@ -197,7 +227,8 @@ SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) .declarationLocation = _variable.location(), .typeID = typeID(_variable), .ethdebugType = ethdebugType(_variable), - .location = stackLocation(_variable) + .location = stackLocation(_variable), + .ethdebugPointer = stackPointer(_variable) }; } diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index c4e3b2e06a97..f1dd4432a9a0 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -32,6 +32,12 @@ BOOST_AUTO_TEST_SUITE(DebugDataTest) BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) { + SemanticDebugPointer ethdebugPointer; + ethdebugPointer.pointerClass = SemanticDebugPointer::Class::Region; + ethdebugPointer.location = SemanticDebugPointer::Location::Stack; + ethdebugPointer.name = "value"; + ethdebugPointer.slot = "pointer:value"; + auto semanticDebugData = std::make_shared(SemanticDebugData{ .lexicalScopeID = 17, .variableDefinitions = {{ @@ -52,7 +58,8 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) .location = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = "pointer:value" - } + }, + .ethdebugPointer = ethdebugPointer }} }); @@ -79,6 +86,14 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location->pointerID); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().location->pointerID, "pointer:value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->location); + BOOST_CHECK(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->location == SemanticDebugPointer::Location::Stack); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name, "value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot, "pointer:value"); } BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index 5990fc4c76f5..62de98c90fa4 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -32,6 +33,8 @@ #include #include +#include +#include using namespace solidity; using namespace solidity::frontend; @@ -63,6 +66,49 @@ SemanticDebugVariable const* findVariableByName(SemanticDebugData const& _data, return nullptr; } +std::vector stackSlots(VariableDeclaration const& _variable) +{ + return IRVariable(_variable).stackSlots(); +} + +void checkStackPointer( + SemanticDebugPointer const& _pointer, + std::string_view _name, + std::vector const& _stackSlots +) +{ + BOOST_REQUIRE(!_stackSlots.empty()); + + if (_stackSlots.size() == 1) + { + BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(_pointer.location); + BOOST_CHECK(*_pointer.location == SemanticDebugPointer::Location::Stack); + BOOST_REQUIRE(_pointer.name); + BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); + BOOST_REQUIRE(_pointer.slot); + BOOST_CHECK_EQUAL(*_pointer.slot, _stackSlots.front()); + BOOST_CHECK(_pointer.group.empty()); + return; + } + + BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE(_pointer.name); + BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); + BOOST_REQUIRE_EQUAL(_pointer.group.size(), _stackSlots.size()); + for (size_t slotIndex = 0; slotIndex < _stackSlots.size(); ++slotIndex) + { + SemanticDebugPointer const& stackPointer = _pointer.group.at(slotIndex); + BOOST_CHECK(stackPointer.pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(stackPointer.location); + BOOST_CHECK(*stackPointer.location == SemanticDebugPointer::Location::Stack); + BOOST_REQUIRE(stackPointer.name); + BOOST_CHECK_EQUAL(*stackPointer.name, _stackSlots.at(slotIndex)); + BOOST_REQUIRE(stackPointer.slot); + BOOST_CHECK_EQUAL(*stackPointer.slot, _stackSlots.at(slotIndex)); + } +} + SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) { if ( @@ -148,6 +194,8 @@ void checkFunctionVariableDebugData( BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(parameterDebugData.location->pointerID); BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(_parameter)); + BOOST_REQUIRE(parameterDebugData.ethdebugPointer); + checkStackPointer(*parameterDebugData.ethdebugPointer, "value", stackSlots(_parameter)); SemanticDebugVariable const& returnDebugData = _data.variableDefinitions.at(1); BOOST_CHECK_EQUAL(returnDebugData.name, "result"); @@ -164,6 +212,8 @@ void checkFunctionVariableDebugData( BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); BOOST_REQUIRE(returnDebugData.location->pointerID); BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(_returnVariable)); + BOOST_REQUIRE(returnDebugData.ethdebugPointer); + checkStackPointer(*returnDebugData.ethdebugPointer, "result", stackSlots(_returnVariable)); } } @@ -305,6 +355,50 @@ BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_type_descriptors) BOOST_CHECK(ok->ethdebugType->kind == SemanticDebugType::Kind::Bool); } +BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_pointer_descriptors) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + function f(uint256 value, bytes calldata payload) external pure returns (uint256 result) { + result = value + payload.length; + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + FunctionDefinition const* function = findFunction(*contract, "f"); + BOOST_REQUIRE(function); + BOOST_REQUIRE_EQUAL(function->parameters().size(), 2); + BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); + + VariableDeclaration const& value = *function->parameters().at(0); + VariableDeclaration const& payload = *function->parameters().at(1); + VariableDeclaration const& result = *function->returnParameters().front(); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(function->id()); + BOOST_REQUIRE(data); + BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 3); + + SemanticDebugVariable const* valueDebugData = findVariableByName(*data, "value"); + BOOST_REQUIRE(valueDebugData); + BOOST_REQUIRE(valueDebugData->ethdebugPointer); + checkStackPointer(*valueDebugData->ethdebugPointer, "value", stackSlots(value)); + + std::vector payloadStackSlots = stackSlots(payload); + BOOST_REQUIRE_GT(payloadStackSlots.size(), 1); + SemanticDebugVariable const* payloadDebugData = findVariableByName(*data, "payload"); + BOOST_REQUIRE(payloadDebugData); + BOOST_REQUIRE(payloadDebugData->ethdebugPointer); + checkStackPointer(*payloadDebugData->ethdebugPointer, "payload", payloadStackSlots); + + SemanticDebugVariable const* resultDebugData = findVariableByName(*data, "result"); + BOOST_REQUIRE(resultDebugData); + BOOST_REQUIRE(resultDebugData->ethdebugPointer); + checkStackPointer(*resultDebugData->ethdebugPointer, "result", stackSlots(result)); +} + BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) { BOOST_REQUIRE(runFramework(R"( From e4e8a2d3960536f8a2504ea5c175d640f3338daa Mon Sep 17 00:00:00 2001 From: djole Date: Sun, 7 Jun 2026 11:38:14 +0200 Subject: [PATCH 10/47] ethdebug: Add optimizer debug location update rule --- docs/internals/ethdebug_internal_metadata.rst | 24 +++- liblangutil/SemanticDebugDataTable.h | 5 + libyul/YulStack.cpp | 136 +++++++++++++++++- test/libsolidity/SemanticDebugData.cpp | 36 +++-- test/libyul/DebugData.cpp | 75 ++++++++++ 5 files changed, 259 insertions(+), 17 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 691bf0de8d74..8a2d1a9e1a6a 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -180,6 +180,28 @@ Future work should define: locations, * schema-validated emission of the resulting type and pointer entities. +Optimizer Update Rules +====================== + +The first implemented optimizer rule is conservative and runs across the Yul +text reparse boundary: + +* semantic metadata is collected before reparse and reattached afterward by AST + ID, +* surviving Yul variable names are collected from the reparsed Yul AST, +* stack-backed semantic variables keep their locations if all referenced stack + slots still exist, +* stack-backed semantic variables become ``OptimizedOut`` if any referenced + stack slot no longer exists. + +This avoids reporting stale stack locations after an optimizer pass removes the +generated Yul variables that originally held a Solidity value. + +The current rule does not yet infer new locations. If an optimizer pass renames, +splits, merges, inlines, or rematerializes a value, the compiler must eventually +provide an explicit debug-location update. Until those mappings exist, losing the +old stack slot is treated as ``OptimizedOut`` rather than guessed. + Testing ======== @@ -188,7 +210,7 @@ The internal metadata plumbing is covered by focused tests: * ``DebugDataTest`` checks that ``DebugData`` can carry semantic metadata and that the AST-ID side table resolves it. * ``YulDebugDataTest`` checks that semantic metadata survives Yul reparse by AST - ID. + ID and that missing stack locations are marked ``OptimizedOut``. * ``SemanticDebugDataTest`` checks that Solidity function variables produce semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type descriptors, initial stack locations, and ETHDebug-oriented pointer diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h index c49aa95e6096..9084e4f8b3a5 100644 --- a/liblangutil/SemanticDebugDataTable.h +++ b/liblangutil/SemanticDebugDataTable.h @@ -50,6 +50,11 @@ class SemanticDebugDataTable return m_byASTID.empty(); } + std::map const& entries() const + { + return m_byASTID; + } + private: std::map m_byASTID; }; diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index ed719642795d..1d46c823f5c1 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,9 @@ #include +#include #include +#include using namespace solidity; using namespace solidity::frontend; @@ -50,6 +53,56 @@ using namespace solidity::util; namespace { +class YulNameCollector: public ASTWalker +{ +public: + using ASTWalker::operator(); + + void operator()(Identifier const& _identifier) override + { + names.insert(_identifier.name.str()); + } + + void operator()(VariableDeclaration const& _varDecl) override + { + insertNames(_varDecl.variables); + ASTWalker::operator()(_varDecl); + } + + void operator()(FunctionDefinition const& _function) override + { + insertNames(_function.parameters); + insertNames(_function.returnVariables); + ASTWalker::operator()(_function); + } + + std::set names; + +private: + void insertNames(NameWithDebugDataList const& _names) + { + for (NameWithDebugData const& name: _names) + names.insert(name.name.str()); + } +}; + +void collectYulNames(YulNameCollector& _collector, Object const& _object) +{ + if (_object.hasCode()) + _collector(_object.code()->root()); + + for (auto const& subNode: _object.subObjects) + if (auto const* subObject = dynamic_cast(subNode.get())) + collectYulNames(_collector, *subObject); +} + +std::set collectYulNames(Object const& _object) +{ + YulNameCollector collector; + collectYulNames(collector, _object); + return std::move(collector.names); +} + void collectSemanticDebugData(SemanticDebugDataTable& _table, langutil::DebugData::ConstPtr const& _debugData) { if (_debugData && _debugData->astID && _debugData->semanticDebugData) @@ -200,6 +253,81 @@ void collectSemanticDebugData(SemanticDebugDataTable& _table, Block const& _bloc collectSemanticDebugData(_table, _block.statements); } +bool stackPointerSurvives(SemanticDebugPointer const& _pointer, std::set const& _yulNames) +{ + if (_pointer.pointerClass == SemanticDebugPointer::Class::Region) + { + if (!_pointer.location || *_pointer.location != SemanticDebugPointer::Location::Stack || !_pointer.slot) + return true; + return _yulNames.count(*_pointer.slot) != 0; + } + + if (_pointer.pointerClass == SemanticDebugPointer::Class::Group) + return std::all_of( + _pointer.group.begin(), + _pointer.group.end(), + [&](SemanticDebugPointer const& _part) { return stackPointerSurvives(_part, _yulNames); } + ); + + return true; +} + +bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) +{ + if ( + !_variable.location || + _variable.location->kind != SemanticDebugVariableLocation::Kind::Stack || + !_variable.ethdebugPointer + ) + return true; + + return stackPointerSurvives(*_variable.ethdebugPointer, _yulNames); +} + +SemanticDebugVariable optimizedOutVariable(SemanticDebugVariable _variable) +{ + _variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::OptimizedOut, + .pointerID = std::nullopt + }; + _variable.ethdebugPointer = std::nullopt; + return _variable; +} + +SemanticDebugData::ConstPtr updateSemanticDebugDataLocations( + SemanticDebugData::ConstPtr const& _debugData, + std::set const& _yulNames +) +{ + if (!_debugData) + return nullptr; + + SemanticDebugData result = *_debugData; + bool changed = false; + for (SemanticDebugVariable& variable: result.variableDefinitions) + if (!stackLocationSurvives(variable, _yulNames)) + { + variable = optimizedOutVariable(std::move(variable)); + changed = true; + } + + if (!changed) + return _debugData; + + return std::make_shared(std::move(result)); +} + +SemanticDebugDataTable updateSemanticDebugDataLocations( + SemanticDebugDataTable const& _table, + std::set const& _yulNames +) +{ + SemanticDebugDataTable result; + for (auto const& [astID, debugData]: _table.entries()) + result.set(astID, updateSemanticDebugDataLocations(debugData, _yulNames)); + return result; +} + void collectSemanticDebugData(SemanticDebugDataTable& _table, Object const& _object) { if (_object.hasCode()) @@ -573,7 +701,13 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); if (!semanticDebugData.empty()) - reattachSemanticDebugData(*m_parserResult, semanticDebugData); + { + SemanticDebugDataTable updatedSemanticDebugData = updateSemanticDebugDataLocations( + semanticDebugData, + collectYulNames(*m_parserResult) + ); + reattachSemanticDebugData(*m_parserResult, updatedSemanticDebugData); + } // 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 diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index 62de98c90fa4..b236749f8291 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -171,7 +171,8 @@ void checkFunctionVariableDebugData( SemanticDebugData const& _data, FunctionDefinition const& _function, VariableDeclaration const& _parameter, - VariableDeclaration const& _returnVariable + VariableDeclaration const& _returnVariable, + bool _requireInitialStackLocations = true ) { BOOST_REQUIRE(_data.lexicalScopeID); @@ -190,12 +191,15 @@ void checkFunctionVariableDebugData( BOOST_CHECK(parameterDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); BOOST_REQUIRE(parameterDebugData.ethdebugType->bits); BOOST_CHECK_EQUAL(*parameterDebugData.ethdebugType->bits, 256); - BOOST_REQUIRE(parameterDebugData.location); - BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(parameterDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(_parameter)); - BOOST_REQUIRE(parameterDebugData.ethdebugPointer); - checkStackPointer(*parameterDebugData.ethdebugPointer, "value", stackSlots(_parameter)); + if (_requireInitialStackLocations) + { + BOOST_REQUIRE(parameterDebugData.location); + BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(parameterDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(_parameter)); + BOOST_REQUIRE(parameterDebugData.ethdebugPointer); + checkStackPointer(*parameterDebugData.ethdebugPointer, "value", stackSlots(_parameter)); + } SemanticDebugVariable const& returnDebugData = _data.variableDefinitions.at(1); BOOST_CHECK_EQUAL(returnDebugData.name, "result"); @@ -208,12 +212,15 @@ void checkFunctionVariableDebugData( BOOST_CHECK(returnDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); BOOST_REQUIRE(returnDebugData.ethdebugType->bits); BOOST_CHECK_EQUAL(*returnDebugData.ethdebugType->bits, 256); - BOOST_REQUIRE(returnDebugData.location); - BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(returnDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(_returnVariable)); - BOOST_REQUIRE(returnDebugData.ethdebugPointer); - checkStackPointer(*returnDebugData.ethdebugPointer, "result", stackSlots(_returnVariable)); + if (_requireInitialStackLocations) + { + BOOST_REQUIRE(returnDebugData.location); + BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(returnDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(_returnVariable)); + BOOST_REQUIRE(returnDebugData.ethdebugPointer); + checkStackPointer(*returnDebugData.ethdebugPointer, "result", stackSlots(_returnVariable)); + } } } @@ -443,8 +450,7 @@ BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) SemanticDebugData::ConstPtr reparsedData = findSemanticDebugData(*yulStack.parserResult(), function->id()); BOOST_REQUIRE(reparsedData); - BOOST_CHECK(reparsedData == attachedData); - checkFunctionVariableDebugData(*reparsedData, *function, parameter, returnVariable); + checkFunctionVariableDebugData(*reparsedData, *function, parameter, returnVariable, false); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index b390e6b64293..d4f9e5615c6b 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -28,6 +28,7 @@ #include #include +#include using namespace solidity; using namespace solidity::frontend; @@ -55,6 +56,16 @@ FunctionDefinition const* findFunctionDefinition(Block const& _block) return nullptr; } +SemanticDebugPointer stackPointer(std::string _name) +{ + SemanticDebugPointer pointer; + pointer.pointerClass = SemanticDebugPointer::Class::Region; + pointer.location = SemanticDebugPointer::Location::Stack; + pointer.name = _name; + pointer.slot = std::move(_name); + return pointer; +} + } BOOST_AUTO_TEST_SUITE(YulDebugDataTest) @@ -109,6 +120,70 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) BOOST_CHECK(reparsedFunDef->debugData->semanticDebugData == semanticDebugData); } +BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + 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.name = "value"; + variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "missing_slot" + }; + variable.ethdebugPointer = stackPointer("missing_slot"); + + auto semanticDebugData = std::make_shared(SemanticDebugData{ + .lexicalScopeID = 17, + .variableDefinitions = {std::move(variable)} + }); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + 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->semanticDebugData); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugData != semanticDebugData); + + SemanticDebugData const& reparsedDebugData = *reparsedFunDef->debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(reparsedDebugData.variableDefinitions.size(), 1); + SemanticDebugVariable const& reparsedVariable = reparsedDebugData.variableDefinitions.front(); + BOOST_REQUIRE(reparsedVariable.location); + BOOST_CHECK(reparsedVariable.location->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!reparsedVariable.location->pointerID); + BOOST_CHECK(!reparsedVariable.ethdebugPointer); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::yul::test From c7179bbd4bf18c1c51dc5f88a0c7171a14285491 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 9 Jun 2026 15:53:02 +0200 Subject: [PATCH 11/47] ethdebug: Export semantic type resources --- docs/internals/ethdebug_internal_metadata.rst | 16 ++-- libevmasm/Ethdebug.cpp | 11 ++- libevmasm/Ethdebug.h | 7 +- libsolidity/interface/CompilerStack.cpp | 94 ++++++++++++++++++- .../test_ethdebug_schema_conformity.py | 7 +- .../ethdebugTests/basic_contract.sol | 2 + 6 files changed, 124 insertions(+), 13 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 8a2d1a9e1a6a..78cecc551386 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -143,10 +143,10 @@ minimal internal carrier and first real producer: * initial stack locations and ETHDebug-oriented stack pointer descriptors. Local variables, state variables, storage pointers, memory pointers, calldata -pointers, optimizer location updates, and the final mapping to public ETHDebug -type and pointer schema objects are still future work. -The current implementation does not yet add variable, type, or pointer entries -to the public ETHDebug JSON output. +pointers, and detailed optimizer location updates are still future work. +The current implementation exports schema-valid elementary type entries to +``ethdebug.resources.types``. It does not yet add variable contexts or pointer +entries to the public ETHDebug JSON output. Type and Pointer Mapping ======================== @@ -171,14 +171,18 @@ records stack region pointers for one-slot variables and pointer groups for multi-slot variables. The stack slot expression is still the generated Yul variable name, not a runtime stack depth. +The public resource exporter currently emits schema-valid elementary type +descriptors to ``ethdebug.resources.types``, keyed by compiler type ID, for +example ``t_uint256``. + Future work should define: -* Solidity type to ETHDebug type mapping, * storage, memory, calldata, transient, immutable, constant, and optimized-out variable location to ETHDebug pointer mapping, * optimizer rules for updating, splitting, merging, or removing variable locations, -* schema-validated emission of the resulting type and pointer entities. +* schema-validated emission of pointer entities and per-instruction variable + contexts. Optimizer Update Rules ====================== diff --git a/libevmasm/Ethdebug.cpp b/libevmasm/Ethdebug.cpp index 0b341d7e8df2..374297beb430 100644 --- a/libevmasm/Ethdebug.cpp +++ b/libevmasm/Ethdebug.cpp @@ -191,12 +191,17 @@ Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly cons }; } -Json ethdebug::resources(std::vector const& _sources, std::string_view _version) +Json ethdebug::resources( + std::vector const& _sources, + std::string_view _version, + Json _types, + Json _pointers +) { schema::info::Resources result; result.compilation = materialCompilation(_sources, _version); - result.types = Json::object(); - result.pointers = Json::object(); + result.types = std::move(_types); + result.pointers = std::move(_pointers); return result; } diff --git a/libevmasm/Ethdebug.h b/libevmasm/Ethdebug.h index dc67a9ffc812..60ed02c426f7 100644 --- a/libevmasm/Ethdebug.h +++ b/libevmasm/Ethdebug.h @@ -38,7 +38,12 @@ struct Source Json program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject); // returns ethdebug/format/info/resources -Json resources(std::vector const& _sources, std::string_view _version); +Json resources( + std::vector const& _sources, + std::string_view _version, + Json _types = Json::object(), + Json _pointers = Json::object() +); // returns the 'compilation' object from ethdebug/format/info/resources Json compilation(std::vector const& _sources, std::string_view _version); diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 10391da91d0a..785bf4023daf 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -103,6 +103,85 @@ using solidity::util::errinfo_comment; static int g_compilerStackCounts = 0; +static std::optional ethdebugType(langutil::SemanticDebugType const& _type) +{ + if (_type.typeClass != langutil::SemanticDebugType::Class::Elementary) + return std::nullopt; + + Json result = Json::object(); + switch (_type.kind) + { + case langutil::SemanticDebugType::Kind::Uint: + if (!_type.bits) + return std::nullopt; + result["kind"] = "uint"; + result["bits"] = *_type.bits; + break; + case langutil::SemanticDebugType::Kind::Int: + if (!_type.bits) + return std::nullopt; + result["kind"] = "int"; + result["bits"] = *_type.bits; + break; + case langutil::SemanticDebugType::Kind::Ufixed: + if (!_type.bits || !_type.places) + return std::nullopt; + result["kind"] = "ufixed"; + result["bits"] = *_type.bits; + result["places"] = *_type.places; + break; + case langutil::SemanticDebugType::Kind::Fixed: + if (!_type.bits || !_type.places) + return std::nullopt; + result["kind"] = "fixed"; + result["bits"] = *_type.bits; + result["places"] = *_type.places; + break; + case langutil::SemanticDebugType::Kind::Bool: + result["kind"] = "bool"; + break; + case langutil::SemanticDebugType::Kind::Bytes: + result["kind"] = "bytes"; + if (_type.bytes) + result["size"] = *_type.bytes; + break; + case langutil::SemanticDebugType::Kind::String: + result["kind"] = "string"; + break; + case langutil::SemanticDebugType::Kind::Address: + result["kind"] = "address"; + if (_type.payable) + result["payable"] = *_type.payable; + break; + case langutil::SemanticDebugType::Kind::Contract: + result["kind"] = "contract"; + break; + default: + return std::nullopt; + } + + return result; +} + +static void collectEthdebugTypes(Json& _types, langutil::SemanticDebugDataTable const& _semanticDebugData) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + if (!variable.typeID || !variable.ethdebugType || _types.contains(*variable.typeID)) + continue; + + if (std::optional type = ethdebugType(*variable.ethdebugType)) + _types[*variable.typeID] = std::move(*type); + } + } +} + CompilerStack::CompilerStack(ReadCallback::Callback _readFile): m_readFile{std::move(_readFile)}, m_objectOptimizer(std::make_shared()), @@ -1167,7 +1246,20 @@ Json CompilerStack::interfaceSymbols(std::string const& _contractName) const Json CompilerStack::ethdebug() const { solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); - return evmasm::ethdebug::resources(ethdebugSources(), VersionString); + Json types = Json::object(); + for (auto const& contractEntry: m_contracts) + { + Contract const& compiledContract = contractEntry.second; + if (!compiledContract.contract) + continue; + + if (compiledContract.yulSemanticDebugData) + collectEthdebugTypes(types, *compiledContract.yulSemanticDebugData); + else + collectEthdebugTypes(types, buildSemanticDebugDataTable(*compiledContract.contract)); + } + + return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types)); } Json CompilerStack::ethdebugCompilation() const diff --git a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py index bc176072a0fe..f9cbbb702c6f 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -125,8 +125,11 @@ def test_resources_include_standard_json_source_contents(standard_json_input, so assert ethdebug_sources[source_name]["language"] == "Solidity" -def test_resources_include_empty_type_and_pointer_tables(solc_output): - assert solc_output["ethdebug"]["resources"]["types"] == {} +def test_resources_include_type_and_pointer_tables(solc_output): + assert solc_output["ethdebug"]["resources"]["types"]["t_uint256"] == { + "kind": "uint", + "bits": 256, + } assert solc_output["ethdebug"]["resources"]["pointers"] == {} diff --git a/test/libsolidity/ethdebugTests/basic_contract.sol b/test/libsolidity/ethdebugTests/basic_contract.sol index 7b5fd3174258..0e067c1d94ab 100644 --- a/test/libsolidity/ethdebugTests/basic_contract.sol +++ b/test/libsolidity/ethdebugTests/basic_contract.sol @@ -19,6 +19,8 @@ contract C { // .resources.compilation.sources | length: 1 // .resources.compilation.sources[0].id: 0 // .resources.compilation.sources[0].language: Solidity +// .resources.types.t_uint256.kind: uint +// .resources.types.t_uint256.bits: 256 // // C.contract.name: C // C.creation.environment: create From 06a7aa64918872bb2cfae36a27c69935074ec3d1 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 9 Jun 2026 16:08:40 +0200 Subject: [PATCH 12/47] ethdebug: Export storage pointer resources --- docs/internals/ethdebug_internal_metadata.rst | 59 +++++++----- .../codegen/ir/SemanticDebugDataBuilder.cpp | 89 ++++++++++++++++++- libsolidity/interface/CompilerStack.cpp | 67 +++++++++++++- test/ethdebugSchemaTests/sources/a.sol | 3 + .../test_ethdebug_schema_conformity.py | 18 +++- test/libsolidity/SemanticDebugData.cpp | 89 +++++++++++++++++++ .../ethdebugTests/basic_contract.sol | 1 + 7 files changed, 296 insertions(+), 30 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 78cecc551386..b0a9b7730f51 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -77,8 +77,9 @@ Each ``SemanticDebugVariable`` contains: The variable location is represented by ``SemanticDebugVariableLocation``. The location kind can describe stack, storage, transient storage, memory, calldata, -immutable, constant, or optimized-out values. The current first implementation -populates stack locations for function parameters and named return variables. +immutable, constant, or optimized-out values. The current implementation +populates stack locations for function parameters and named return variables, +and storage locations for named state variables. Side Table ========== @@ -109,8 +110,8 @@ Current Producer The current Solidity-side producer is ``frontend::buildSemanticDebugDataTable(ContractDefinition const&)``. -It records named function parameters, named modifier parameters, and named -function return variables. +It records named state variables, named function parameters, named modifier +parameters, and named function return variables. For each variable it stores: * the declaration name, @@ -118,9 +119,8 @@ For each variable it stores: * the declaration source location, * the compiler type identifier, for example ``t_uint256``, * an ETHDebug-oriented type descriptor, for example ``uint`` with ``bits = 256``, -* the initial Yul stack slot name used by IR generation, -* an ETHDebug-oriented stack pointer descriptor for the initial Yul stack slot or - slots. +* the initial location, +* an ETHDebug-oriented pointer descriptor for that initial location. The stack pointer is based on ``IRVariable`` and therefore matches the names used by generated IR, such as ``var_value_42`` for a Solidity variable named @@ -129,6 +129,12 @@ produced by ``IRVariable``. In the ETHDebug-oriented pointer descriptor, one-slo variables become stack region pointers and multi-slot variables become groups of stack region pointers. +The storage pointer is based on the compiler's existing storage layout +calculation. Named state variables use contract-scope semantic metadata keyed by +the contract AST ID. Each storage variable records a storage region pointer with +the base slot, and for packed variables, the byte offset and byte length within +the slot. + Current Scope ============= @@ -138,15 +144,18 @@ minimal internal carrier and first real producer: * function parameters, * modifier parameters, * named function return variables, +* named state variables, * ETHDebug-oriented type descriptors for elementary scalar types and basic complex type categories, -* initial stack locations and ETHDebug-oriented stack pointer descriptors. +* initial stack locations and ETHDebug-oriented stack pointer descriptors, +* initial storage locations and ETHDebug-oriented storage pointer descriptors. -Local variables, state variables, storage pointers, memory pointers, calldata -pointers, and detailed optimizer location updates are still future work. -The current implementation exports schema-valid elementary type entries to -``ethdebug.resources.types``. It does not yet add variable contexts or pointer -entries to the public ETHDebug JSON output. +Local variables, memory pointers, calldata pointers, transient storage pointers, +immutable and constant values, and detailed optimizer location updates are still +future work. The current implementation exports schema-valid elementary type +entries to ``ethdebug.resources.types`` and schema-valid storage pointer +templates to ``ethdebug.resources.pointers``. It does not yet add per-instruction +variable contexts to the public ETHDebug JSON output. Type and Pointer Mapping ======================== @@ -169,20 +178,24 @@ Yul stack slots. The current ``ethdebugPointer`` descriptor is the first bridge from these internal locations to the public ETHDebug pointer vocabulary. It records stack region pointers for one-slot variables and pointer groups for multi-slot variables. The stack slot expression is still the generated Yul -variable name, not a runtime stack depth. +variable name, not a runtime stack depth, so stack pointer descriptors remain +internal for now. The public resource exporter currently emits schema-valid elementary type descriptors to ``ethdebug.resources.types``, keyed by compiler type ID, for -example ``t_uint256``. +example ``t_uint256``. It also emits storage pointer templates to +``ethdebug.resources.pointers`` for named state variables. Storage pointer keys +are compiler-generated pointer IDs, and the template body contains the storage +slot plus optional byte offset and length for packed values. Future work should define: -* storage, memory, calldata, transient, immutable, constant, and optimized-out - variable location to ETHDebug pointer mapping, +* memory, calldata, transient, immutable, constant, and optimized-out variable + location to ETHDebug pointer mapping, +* complete recursive storage pointer mapping for structured values, * optimizer rules for updating, splitting, merging, or removing variable locations, -* schema-validated emission of pointer entities and per-instruction variable - contexts. +* schema-validated emission of per-instruction variable contexts. Optimizer Update Rules ====================== @@ -218,8 +231,10 @@ The internal metadata plumbing is covered by focused tests: * ``SemanticDebugDataTest`` checks that Solidity function variables produce semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type descriptors, initial stack locations, and ETHDebug-oriented pointer - descriptors. It also checks that this function variable metadata can be - attached to generated Yul and survives the Yul reparse path. + descriptors. It also checks state-variable storage pointer descriptors and + verifies that function variable metadata can be attached to generated Yul and + survives the Yul reparse path. These tests deliberately target the internal model. Schema validation tests cover -the public ETHDebug JSON output separately. +the public ETHDebug JSON output separately, including the exported storage +pointer templates. diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 4ebd638e3917..e44dbcb745c0 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -172,6 +173,16 @@ SemanticDebugPointer stackRegionPointer(std::string _name, std::string _slot) return result; } +std::string pointerExpression(u256 const& _value) +{ + return toCompactHexWithPrefix(_value); +} + +std::string storagePointerID(ContractDefinition const& _contract, VariableDeclaration const& _variable) +{ + return "storage_" + std::to_string(_contract.id()) + "_" + std::to_string(_variable.id()); +} + std::optional stackLocation(VariableDeclaration const& _variable) { if (!_variable.annotation().type) @@ -203,6 +214,32 @@ std::optional stackPointer(VariableDeclaration const& _var return result; } +SemanticDebugVariableLocation storageLocation(ContractDefinition const& _contract, VariableDeclaration const& _variable) +{ + return { + .kind = SemanticDebugVariableLocation::Kind::Storage, + .pointerID = storagePointerID(_contract, _variable) + }; +} + +SemanticDebugPointer storagePointer(VariableDeclaration const& _variable, u256 const& _slot, unsigned _offset) +{ + solAssert(_variable.annotation().type, "Storage variable type expected."); + Type const& type = *_variable.annotation().type; + u256 const byteLength = u256(type.storageBytes()) * type.storageSize(); + + SemanticDebugPointer result; + result.pointerClass = SemanticDebugPointer::Class::Region; + result.location = SemanticDebugPointer::Location::Storage; + result.name = _variable.name(); + result.slot = pointerExpression(_slot); + if (_offset != 0) + result.offset = pointerExpression(u256(_offset)); + if (_offset != 0 || byteLength != 32) + result.length = pointerExpression(byteLength); + return result; +} + std::optional typeID(VariableDeclaration const& _variable) { if (!_variable.annotation().type) @@ -219,7 +256,7 @@ std::optional ethdebugType(VariableDeclaration const& _variab return semanticType(*_variable.annotation().type); } -SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) +SemanticDebugVariable baseSemanticVariable(VariableDeclaration const& _variable) { return { .name = _variable.name(), @@ -227,11 +264,32 @@ SemanticDebugVariable semanticVariable(VariableDeclaration const& _variable) .declarationLocation = _variable.location(), .typeID = typeID(_variable), .ethdebugType = ethdebugType(_variable), - .location = stackLocation(_variable), - .ethdebugPointer = stackPointer(_variable) + .location = std::nullopt, + .ethdebugPointer = std::nullopt }; } +SemanticDebugVariable stackSemanticVariable(VariableDeclaration const& _variable) +{ + SemanticDebugVariable result = baseSemanticVariable(_variable); + result.location = stackLocation(_variable); + result.ethdebugPointer = stackPointer(_variable); + return result; +} + +SemanticDebugVariable storageSemanticVariable( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset +) +{ + SemanticDebugVariable result = baseSemanticVariable(_variable); + result.location = storageLocation(_contract, _variable); + result.ethdebugPointer = storagePointer(_variable, _slot, _offset); + return result; +} + void appendVariables( std::vector& _variables, std::vector> const& _declarations @@ -239,7 +297,7 @@ void appendVariables( { for (ASTPointer const& declaration: _declarations) if (!declaration->name().empty()) - _variables.emplace_back(semanticVariable(*declaration)); + _variables.emplace_back(stackSemanticVariable(*declaration)); } template @@ -258,12 +316,35 @@ void addCallable(SemanticDebugDataTable& _table, Callable const& _callable) })); } +void addStorageVariables(SemanticDebugDataTable& _table, ContractDefinition const& _contract) +{ + auto const* typeType = dynamic_cast(_contract.type()); + solAssert(typeType, "Contract TypeType expected."); + auto const* contractType = dynamic_cast(typeType->actualType()); + solAssert(contractType, "Contract type expected."); + + std::vector variables; + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(DataLocation::Storage)) + if (!variable->name().empty()) + variables.emplace_back(storageSemanticVariable(_contract, *variable, slot, offset)); + + if (variables.empty()) + return; + + _table.set(_contract.id(), std::make_shared(SemanticDebugData{ + .lexicalScopeID = _contract.id(), + .variableDefinitions = std::move(variables) + })); +} + } SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable(ContractDefinition const& _contract) { SemanticDebugDataTable table; + addStorageVariables(table, _contract); + for (FunctionDefinition const* function: _contract.definedFunctions()) addCallable(table, *function); diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 785bf4023daf..b23f31024ed3 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -182,6 +182,66 @@ static void collectEthdebugTypes(Json& _types, langutil::SemanticDebugDataTable } } +static std::optional ethdebugStoragePointer(langutil::SemanticDebugPointer const& _pointer) +{ + if ( + _pointer.pointerClass != langutil::SemanticDebugPointer::Class::Region || + !_pointer.location || + *_pointer.location != langutil::SemanticDebugPointer::Location::Storage || + !_pointer.slot + ) + return std::nullopt; + + Json result = Json::object(); + if (_pointer.name) + result["name"] = *_pointer.name; + result["location"] = "storage"; + result["slot"] = *_pointer.slot; + if (_pointer.offset) + result["offset"] = *_pointer.offset; + if (_pointer.length) + result["length"] = *_pointer.length; + return result; +} + +static void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable const& _semanticDebugData) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + if ( + !variable.location || + variable.location->kind != langutil::SemanticDebugVariableLocation::Kind::Storage || + !variable.location->pointerID || + !variable.ethdebugPointer || + _pointers.contains(*variable.location->pointerID) + ) + continue; + + if (std::optional pointer = ethdebugStoragePointer(*variable.ethdebugPointer)) + _pointers[*variable.location->pointerID] = Json{ + {"expect", Json::array()}, + {"for", std::move(*pointer)} + }; + } + } +} + +static void collectEthdebugResources( + Json& _types, + Json& _pointers, + langutil::SemanticDebugDataTable const& _semanticDebugData +) +{ + collectEthdebugTypes(_types, _semanticDebugData); + collectEthdebugPointers(_pointers, _semanticDebugData); +} + CompilerStack::CompilerStack(ReadCallback::Callback _readFile): m_readFile{std::move(_readFile)}, m_objectOptimizer(std::make_shared()), @@ -1247,6 +1307,7 @@ Json CompilerStack::ethdebug() const { solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); Json types = Json::object(); + Json pointers = Json::object(); for (auto const& contractEntry: m_contracts) { Contract const& compiledContract = contractEntry.second; @@ -1254,12 +1315,12 @@ Json CompilerStack::ethdebug() const continue; if (compiledContract.yulSemanticDebugData) - collectEthdebugTypes(types, *compiledContract.yulSemanticDebugData); + collectEthdebugResources(types, pointers, *compiledContract.yulSemanticDebugData); else - collectEthdebugTypes(types, buildSemanticDebugDataTable(*compiledContract.contract)); + collectEthdebugResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract)); } - return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types)); + return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types), std::move(pointers)); } Json CompilerStack::ethdebugCompilation() const diff --git a/test/ethdebugSchemaTests/sources/a.sol b/test/ethdebugSchemaTests/sources/a.sol index feada97f0246..07736b72d8f9 100644 --- a/test/ethdebugSchemaTests/sources/a.sol +++ b/test/ethdebugSchemaTests/sources/a.sol @@ -2,6 +2,9 @@ pragma solidity >=0.0; contract A1 { + uint128 stored; + bool enabled; + function a(uint x) public pure { assert(x > 0); } diff --git a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py index f9cbbb702c6f..769f2de6a036 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -130,7 +130,23 @@ def test_resources_include_type_and_pointer_tables(solc_output): "kind": "uint", "bits": 256, } - assert solc_output["ethdebug"]["resources"]["pointers"] == {} + + pointers = solc_output["ethdebug"]["resources"]["pointers"] + assert all(pointer["expect"] == [] for pointer in pointers.values()) + pointer_targets = [pointer["for"] for pointer in pointers.values()] + assert { + "name": "stored", + "location": "storage", + "slot": "0x00", + "length": "0x10", + } in pointer_targets + assert { + "name": "enabled", + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } in pointer_targets def test_resources_and_compilation_share_compilation(solc_output): diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index b236749f8291..64eb75d8096c 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -34,6 +34,7 @@ #include #include +#include #include using namespace solidity; @@ -49,6 +50,12 @@ std::string stackPointer(VariableDeclaration const& _variable) return "var_" + _variable.name() + "_" + std::to_string(_variable.id()); } +std::string storagePointer(ContractDefinition const& _contract, SemanticDebugVariable const& _variable) +{ + BOOST_REQUIRE(_variable.declarationAstID); + return "storage_" + std::to_string(_contract.id()) + "_" + std::to_string(*_variable.declarationAstID); +} + FunctionDefinition const* findFunction(ContractDefinition const& _contract, std::string_view _name) { for (FunctionDefinition const* candidate: _contract.definedFunctions()) @@ -109,6 +116,46 @@ void checkStackPointer( } } +void checkStoragePointer( + SemanticDebugVariable const& _variable, + ContractDefinition const& _contract, + std::string_view _slot, + std::optional _offset, + std::optional _length +) +{ + BOOST_REQUIRE(_variable.location); + BOOST_CHECK(_variable.location->kind == SemanticDebugVariableLocation::Kind::Storage); + BOOST_REQUIRE(_variable.location->pointerID); + BOOST_CHECK_EQUAL(*_variable.location->pointerID, storagePointer(_contract, _variable)); + + BOOST_REQUIRE(_variable.ethdebugPointer); + SemanticDebugPointer const& pointer = *_variable.ethdebugPointer; + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(pointer.location); + BOOST_CHECK(*pointer.location == SemanticDebugPointer::Location::Storage); + BOOST_REQUIRE(pointer.name); + BOOST_CHECK_EQUAL(*pointer.name, _variable.name); + BOOST_REQUIRE(pointer.slot); + BOOST_CHECK_EQUAL(*pointer.slot, std::string(_slot)); + + if (_offset) + { + BOOST_REQUIRE(pointer.offset); + BOOST_CHECK_EQUAL(*pointer.offset, std::string(*_offset)); + } + else + BOOST_CHECK(!pointer.offset); + + if (_length) + { + BOOST_REQUIRE(pointer.length); + BOOST_CHECK_EQUAL(*pointer.length, std::string(*_length)); + } + else + BOOST_CHECK(!pointer.length); +} + SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) { if ( @@ -406,6 +453,48 @@ BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_pointer_descriptors) checkStackPointer(*resultDebugData->ethdebugPointer, "result", stackSlots(result)); } +BOOST_AUTO_TEST_CASE(state_variables_include_storage_pointer_descriptors) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + uint128 packedA; + bool packedB; + uint256 wide; + uint256 constant ignoredConstant = 1; + uint256 immutable ignoredImmutable; + + constructor() { + ignoredImmutable = 2; + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + BOOST_REQUIRE(data->lexicalScopeID); + BOOST_CHECK_EQUAL(*data->lexicalScopeID, contract->id()); + BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 3); + + SemanticDebugVariable const* packedA = findVariableByName(*data, "packedA"); + BOOST_REQUIRE(packedA); + checkStoragePointer(*packedA, *contract, "0x00", std::nullopt, "0x10"); + + SemanticDebugVariable const* packedB = findVariableByName(*data, "packedB"); + BOOST_REQUIRE(packedB); + checkStoragePointer(*packedB, *contract, "0x00", "0x10", "0x01"); + + SemanticDebugVariable const* wide = findVariableByName(*data, "wide"); + BOOST_REQUIRE(wide); + checkStoragePointer(*wide, *contract, "0x01", std::nullopt, std::nullopt); + + BOOST_CHECK(!findVariableByName(*data, "ignoredConstant")); + BOOST_CHECK(!findVariableByName(*data, "ignoredImmutable")); +} + BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) { BOOST_REQUIRE(runFramework(R"( diff --git a/test/libsolidity/ethdebugTests/basic_contract.sol b/test/libsolidity/ethdebugTests/basic_contract.sol index 0e067c1d94ab..0099cc6cedc3 100644 --- a/test/libsolidity/ethdebugTests/basic_contract.sol +++ b/test/libsolidity/ethdebugTests/basic_contract.sol @@ -21,6 +21,7 @@ contract C { // .resources.compilation.sources[0].language: Solidity // .resources.types.t_uint256.kind: uint // .resources.types.t_uint256.bits: 256 +// .resources.pointers | length: 1 // // C.contract.name: C // C.creation.environment: create From 43fbf6a71203acd0469fa4d6545fbe3c4341f2e5 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 10 Jun 2026 13:56:46 +0200 Subject: [PATCH 13/47] ethdebug: Default initialize semantic variable list --- liblangutil/SemanticDebugData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 3de3afd81454..87927c05a191 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -133,7 +133,7 @@ struct SemanticDebugData using ConstPtr = std::shared_ptr; std::optional lexicalScopeID; - std::vector variableDefinitions; + std::vector variableDefinitions = {}; }; } // namespace solidity::langutil From c978e4734c4444cc4b7ab4b91679cc65c33dbd57 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 10 Jun 2026 14:20:46 +0200 Subject: [PATCH 14/47] ethdebug: Fix semantic metadata for modifiers --- .../codegen/ir/SemanticDebugDataBuilder.cpp | 14 ++++- libsolidity/interface/CompilerStack.cpp | 2 +- test/libsolidity/SemanticDebugData.cpp | 51 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index e44dbcb745c0..c30a01460a6e 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -300,12 +300,22 @@ void appendVariables( _variables.emplace_back(stackSemanticVariable(*declaration)); } +void appendCallableVariables(std::vector& _variables, FunctionDefinition const& _function) +{ + appendVariables(_variables, _function.parameters()); + appendVariables(_variables, _function.returnParameters()); +} + +void appendCallableVariables(std::vector& _variables, ModifierDefinition const& _modifier) +{ + appendVariables(_variables, _modifier.parameters()); +} + template void addCallable(SemanticDebugDataTable& _table, Callable const& _callable) { std::vector variables; - appendVariables(variables, _callable.parameters()); - appendVariables(variables, _callable.returnParameters()); + appendCallableVariables(variables, _callable); if (variables.empty()) return; diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b23f31024ed3..e8344c2475ee 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -1720,7 +1720,7 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti ); yulAssert(compiledContract.yulIR); - if (m_debugInfoSelection.astID || m_debugInfoSelection.ethdebug) + if (m_debugInfoSelection.ethdebug) compiledContract.yulSemanticDebugData = buildSemanticDebugDataTable(_contract); else compiledContract.yulSemanticDebugData = std::nullopt; diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index 64eb75d8096c..67ef0ca94a6a 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -64,6 +64,14 @@ FunctionDefinition const* findFunction(ContractDefinition const& _contract, std: return nullptr; } +ModifierDefinition const* findModifier(ContractDefinition const& _contract, std::string_view _name) +{ + for (ModifierDefinition const* candidate: _contract.functionModifiers()) + if (candidate->name() == _name) + return candidate; + return nullptr; +} + SemanticDebugVariable const* findVariableByName(SemanticDebugData const& _data, std::string_view _name) { for (SemanticDebugVariable const& variable: _data.variableDefinitions) @@ -313,6 +321,49 @@ BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) checkFunctionVariableDebugData(*data, *function, parameter, returnVariable); } +BOOST_AUTO_TEST_CASE(modifier_parameters_have_semantic_metadata) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + modifier guarded(uint256 guard) { + _; + } + + function f(uint256 value) public guarded(value) returns (uint256 result) { + return value; + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + ModifierDefinition const* modifier = findModifier(*contract, "guarded"); + BOOST_REQUIRE(modifier); + BOOST_REQUIRE_EQUAL(modifier->parameters().size(), 1); + + VariableDeclaration const& parameter = *modifier->parameters().front(); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(modifier->id()); + BOOST_REQUIRE(data); + BOOST_REQUIRE(data->lexicalScopeID); + BOOST_CHECK_EQUAL(*data->lexicalScopeID, modifier->id()); + BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 1); + + SemanticDebugVariable const& parameterDebugData = data->variableDefinitions.front(); + BOOST_CHECK_EQUAL(parameterDebugData.name, "guard"); + BOOST_REQUIRE(parameterDebugData.declarationAstID); + BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, parameter.id()); + BOOST_REQUIRE(parameterDebugData.typeID); + BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); + BOOST_REQUIRE(parameterDebugData.location); + BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(parameterDebugData.location->pointerID); + BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(parameter)); + BOOST_REQUIRE(parameterDebugData.ethdebugPointer); + checkStackPointer(*parameterDebugData.ethdebugPointer, "guard", stackSlots(parameter)); +} + BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_type_descriptors) { BOOST_REQUIRE(runFramework(R"( From 05fdbb3b7ab09d221fd58bbf498c2289c280c891 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 10 Jun 2026 17:55:05 +0200 Subject: [PATCH 15/47] yul: Avoid MSVC leave keyword in debug data visitors --- libyul/YulStack.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index 1d46c823f5c1..3b87cae14ad5 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -237,9 +237,9 @@ void collectSemanticDebugData(SemanticDebugDataTable& _table, Continue const& _c collectSemanticDebugData(_table, _continue.debugData); } -void collectSemanticDebugData(SemanticDebugDataTable& _table, Leave const& _leave) +void collectSemanticDebugData(SemanticDebugDataTable& _table, Leave const& leave_) { - collectSemanticDebugData(_table, _leave.debugData); + collectSemanticDebugData(_table, leave_.debugData); } void collectSemanticDebugData(SemanticDebugDataTable& _table, Statement const& _statement) @@ -491,9 +491,9 @@ void reattachSemanticDebugData(Continue& _continue, SemanticDebugDataTable const reattachSemanticDebugData(_continue.debugData, _table); } -void reattachSemanticDebugData(Leave& _leave, SemanticDebugDataTable const& _table) +void reattachSemanticDebugData(Leave& leave_, SemanticDebugDataTable const& _table) { - reattachSemanticDebugData(_leave.debugData, _table); + reattachSemanticDebugData(leave_.debugData, _table); } void reattachSemanticDebugData(Statement& _statement, SemanticDebugDataTable const& _table) From 596ce3d076a112835b8920df23ea2ed3adda4afa Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 10 Jun 2026 21:12:34 +0200 Subject: [PATCH 16/47] ethdebug: Harden semantic debug metadata propagation --- docs/internals/ethdebug_internal_metadata.rst | 38 +- .../codegen/ir/SemanticDebugDataBuilder.cpp | 24 +- libsolidity/interface/CompilerStack.cpp | 4 + libyul/CMakeLists.txt | 2 + libyul/SemanticDebugDataTransfer.cpp | 283 ++++++++++ libyul/SemanticDebugDataTransfer.h | 44 ++ libyul/YulStack.cpp | 503 +----------------- libyul/YulStack.h | 7 +- .../output.json | 12 +- test/libsolidity/SemanticDebugData.cpp | 103 ++++ test/libyul/DebugData.cpp | 127 +++++ 11 files changed, 642 insertions(+), 505 deletions(-) create mode 100644 libyul/SemanticDebugDataTransfer.cpp create mode 100644 libyul/SemanticDebugDataTransfer.h diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index b0a9b7730f51..5a9f305b6559 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -32,7 +32,9 @@ The internal ETHDebug metadata flow is: by this metadata pipeline. 2. The IR generator prints AST ID comments into generated Yul when AST ID debug info is enabled. Semantic metadata reattachment relies on those comments - being present at Yul parse/reparse boundaries. + being present at Yul parse/reparse boundaries. Because of that, selecting + ``ethdebug`` debug info in ``CompilerStack`` implicitly enables ``ast-id`` + debug info as well. 3. The compiler builds a side table keyed by Solidity AST ID. 4. Generated Yul is parsed and analyzed into a ``YulStack``. 5. Semantic metadata is attached to Yul ``DebugData`` objects when the AST ID @@ -100,9 +102,18 @@ top-level keys in the table. The table is used in two places: * ``CompilerStack`` builds the table from the Solidity contract and attaches it - after generated IR is parsed into a ``YulStack``. -* ``YulStack::reparse()`` collects semantic metadata before printing and parsing - optimized IR, then reattaches it to the new Yul AST by AST ID. + after generated IR is parsed into a ``YulStack``. This happens both for the + freshly generated IR and when the optimized IR is reloaded from text for EVM + code generation. +* ``YulStack`` retains the attached table as a member. ``reparse()`` merges + metadata collected from the current Yul AST into the retained table before + printing, then reattaches it to the new Yul AST by AST ID. Retaining the table + also preserves entries that are not attached to any Yul node, such as the + contract-scope storage metadata, which has no corresponding ``@ast-id`` + comment in generated Yul. + +The transfer between the table and Yul ASTs is implemented in +``libyul/SemanticDebugDataTransfer.h``. Current Producer ================ @@ -111,7 +122,11 @@ The current Solidity-side producer is ``frontend::buildSemanticDebugDataTable(ContractDefinition const&)``. It records named state variables, named function parameters, named modifier -parameters, and named function return variables. +parameters, and named function return variables. Functions and modifiers are +collected from all linearized base contracts, because inherited definitions are +compiled into the most derived contract's IR with their original AST IDs. Free +functions from the contract's source unit and all recursively referenced source +units are collected as well. For each variable it stores: * the declaration name, @@ -200,16 +215,19 @@ Future work should define: Optimizer Update Rules ====================== -The first implemented optimizer rule is conservative and runs across the Yul -text reparse boundary: +The first implemented optimizer rule is conservative and runs whenever semantic +metadata is attached to a Yul AST, including across the Yul text reparse +boundary and when optimized IR is reloaded from text for EVM code generation: * semantic metadata is collected before reparse and reattached afterward by AST ID, -* surviving Yul variable names are collected from the reparsed Yul AST, +* surviving Yul variable names are collected separately for each object in the + Yul object tree, so a variable that only survives in the creation code is + still marked ``OptimizedOut`` in the deployed code, and vice versa, * stack-backed semantic variables keep their locations if all referenced stack - slots still exist, + slots still exist in the object the metadata is attached to, * stack-backed semantic variables become ``OptimizedOut`` if any referenced - stack slot no longer exists. + stack slot no longer exists there. This avoids reporting stale stack locations after an optimizer pass removes the generated Yul variables that originally held a Solidity value. diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index c30a01460a6e..6ed07ace4005 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -355,11 +356,26 @@ SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable(ContractD addStorageVariables(table, _contract); - for (FunctionDefinition const* function: _contract.definedFunctions()) - addCallable(table, *function); + // Inherited functions and modifiers are compiled into the most derived contract's IR + // with the AST IDs of their original definitions, so all linearized base contracts + // must contribute entries. + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + { + for (FunctionDefinition const* function: contract->definedFunctions()) + addCallable(table, *function); + + for (ModifierDefinition const* modifier: contract->functionModifiers()) + addCallable(table, *modifier); + } - for (ModifierDefinition const* modifier: _contract.functionModifiers()) - addCallable(table, *modifier); + // Free functions reachable through imports are compiled into the contract's IR as well. + SourceUnit const& sourceUnit = _contract.sourceUnit(); + std::set sourceUnits = sourceUnit.referencedSourceUnits(true); + sourceUnits.insert(&sourceUnit); + for (SourceUnit const* unit: sourceUnits) + for (ASTPointer const& node: unit->nodes()) + if (auto const* freeFunction = dynamic_cast(node.get())) + addCallable(table, *freeFunction); return table; } diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index e8344c2475ee..96184b314b12 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -436,6 +436,10 @@ void CompilerStack::selectDebugInfo(DebugInfoSelection _debugInfoSelection) { solAssert(m_stackState < CompilationSuccessful, "Must select debug info components before compilation."); m_debugInfoSelection = _debugInfoSelection; + // Semantic debug metadata is reattached across the Yul text boundary using @ast-id + // comments as the join key. Without them the metadata would be silently lost. + if (m_debugInfoSelection.ethdebug) + m_debugInfoSelection.astID = true; } void CompilerStack::addSMTLib2Response(h256 const& _hash, std::string const& _response) diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index fbfadcde163e..50f95c469a84 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..d2f885b73879 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -0,0 +1,283 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::yul; +using namespace solidity::langutil; + +namespace +{ + +/// Applies a rewriting function to every debug data pointer in a Yul AST, +/// including the ones on names in declarations, parameter and return variable lists. +class DebugDataRewriter +{ +public: + using Rewriter = std::function; + + explicit DebugDataRewriter(Rewriter _rewriter): m_rewriter(std::move(_rewriter)) {} + + void operator()(Block& _block) { visitNode(_block); } + +private: + template + void visitNode(std::vector& _nodes) + { + for (T& node: _nodes) + visitNode(node); + } + + template + void visitNode(std::unique_ptr& _node) + { + if (_node) + visitNode(*_node); + } + + template + void visitNode(std::variant& _node) + { + std::visit([this](auto& node) { this->visitNode(node); }, _node); + } + + template + void visitNode(NodeType& _node) + { + _node.debugData = m_rewriter(_node.debugData); + + if constexpr (std::is_same_v) + visitNode(_node.statements); + else if constexpr (std::is_same_v) + { + visitNode(_node.parameters); + visitNode(_node.returnVariables); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.variables); + visitNode(_node.value); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.variableNames); + visitNode(_node.value); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.functionName); + visitNode(_node.arguments); + } + else if constexpr (std::is_same_v) + visitNode(_node.expression); + else if constexpr (std::is_same_v) + { + visitNode(_node.condition); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.expression); + visitNode(_node.cases); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.value); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.pre); + visitNode(_node.condition); + visitNode(_node.post); + visitNode(_node.body); + } + // NameWithDebugData, Identifier, Literal, BuiltinName, Break, Continue and + // Leave carry nothing but debug data. + } + + Rewriter m_rewriter; +}; + +/// The Yul AST is exposed as const to discourage structural modification. Rewriting +/// debug data in place changes neither the AST structure nor any names, so analysis +/// info keyed by node addresses remains valid. Replacing the AST with a modified +/// copy instead would invalidate AsmAnalysisInfo. This is the single sanctioned +/// mutation point; do not const_cast the AST anywhere else. +Block& mutableCodeRoot(Object& _object) +{ + return const_cast(_object.code()->root()); +} + +void collectSemanticDebugDataFromRoot(Block& _root, SemanticDebugDataTable& _table) +{ + DebugDataRewriter collector{[&](langutil::DebugData::ConstPtr const& _debugData) { + if (_debugData && _debugData->astID && _debugData->semanticDebugData) + _table.set(*_debugData->astID, _debugData->semanticDebugData); + return _debugData; + }}; + collector(_root); +} + +std::set declaredVariableNames(Block const& _root) +{ + std::set names; + for (auto const& name: NameCollector(_root, NameCollector::OnlyVariables).names()) + names.insert(name.str()); + return names; +} + +bool stackPointerSurvives(SemanticDebugPointer const& _pointer, std::set const& _yulNames) +{ + if (_pointer.pointerClass == SemanticDebugPointer::Class::Region) + { + if (!_pointer.location || *_pointer.location != SemanticDebugPointer::Location::Stack || !_pointer.slot) + return true; + return _yulNames.count(*_pointer.slot) != 0; + } + + if (_pointer.pointerClass == SemanticDebugPointer::Class::Group) + return std::all_of( + _pointer.group.begin(), + _pointer.group.end(), + [&](SemanticDebugPointer const& _part) { return stackPointerSurvives(_part, _yulNames); } + ); + + return true; +} + +bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) +{ + if ( + !_variable.location || + _variable.location->kind != SemanticDebugVariableLocation::Kind::Stack || + !_variable.ethdebugPointer + ) + return true; + + return stackPointerSurvives(*_variable.ethdebugPointer, _yulNames); +} + +SemanticDebugVariable optimizedOutVariable(SemanticDebugVariable _variable) +{ + _variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::OptimizedOut, + .pointerID = std::nullopt + }; + _variable.ethdebugPointer = std::nullopt; + return _variable; +} + +SemanticDebugData::ConstPtr updateSemanticDebugDataLocations( + SemanticDebugData::ConstPtr const& _debugData, + std::set const& _yulNames +) +{ + if (!_debugData) + return nullptr; + + SemanticDebugData result = *_debugData; + bool changed = false; + for (SemanticDebugVariable& variable: result.variableDefinitions) + if (!stackLocationSurvives(variable, _yulNames)) + { + variable = optimizedOutVariable(std::move(variable)); + changed = true; + } + + if (!changed) + return _debugData; + + return std::make_shared(std::move(result)); +} + +SemanticDebugDataTable updateSemanticDebugDataLocations( + SemanticDebugDataTable const& _table, + std::set const& _yulNames +) +{ + SemanticDebugDataTable result; + for (auto const& [astID, debugData]: _table.entries()) + result.set(astID, updateSemanticDebugDataLocations(debugData, _yulNames)); + return result; +} + +void applySemanticDebugDataToCode(Object& _object, SemanticDebugDataTable const& _table) +{ + SemanticDebugDataTable updated = updateSemanticDebugDataLocations( + _table, + declaredVariableNames(_object.code()->root()) + ); + DebugDataRewriter rewriter{[&](langutil::DebugData::ConstPtr const& _debugData) -> langutil::DebugData::ConstPtr { + if (!_debugData) + return _debugData; + + SemanticDebugData::ConstPtr semanticDebugData = updated.find(_debugData->astID); + if (!semanticDebugData) + return _debugData; + + return langutil::DebugData::create( + _debugData->nativeLocation, + _debugData->originLocation, + _debugData->astID, + std::move(semanticDebugData) + ); + }}; + rewriter(mutableCodeRoot(_object)); +} + +} + +void yul::collectSemanticDebugData(Object const& _object, SemanticDebugDataTable& _table) +{ + if (_object.hasCode()) + // Collection only reads debug data; the rewriter returns each pointer unchanged. + collectSemanticDebugDataFromRoot(mutableCodeRoot(const_cast(_object)), _table); + + for (auto const& subNode: _object.subObjects) + if (auto const* subObject = dynamic_cast(subNode.get())) + collectSemanticDebugData(*subObject, _table); +} + +void yul::applySemanticDebugData(Object& _object, SemanticDebugDataTable const& _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..274e82fd3327 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.h @@ -0,0 +1,44 @@ +/* + 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 +/** + * Transfers semantic debug metadata between an AST-ID-keyed side table and Yul ASTs. + */ + +#pragma once + +#include + +namespace solidity::yul +{ + +class Object; + +/// Collects semantic debug metadata attached to the Yul AST nodes of @a _object and +/// all its sub-objects into @a _table, keyed by Solidity AST ID. +/// Existing entries in @a _table are overwritten when the AST carries newer metadata +/// for the same AST ID; entries without a corresponding Yul node are left untouched. +void collectSemanticDebugData(Object const& _object, langutil::SemanticDebugDataTable& _table); + +/// Attaches semantic debug metadata from @a _table to all Yul AST nodes of @a _object +/// and its sub-objects whose AST ID has an entry in the table. +/// Stack locations are validated separately against each object's code: a variable +/// whose referenced Yul stack slots no longer exist in that object is attached as +/// OptimizedOut there, even if the slots still exist in a sibling object. +void applySemanticDebugData(Object& _object, langutil::SemanticDebugDataTable const& _table); + +} diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index 3b87cae14ad5..4f5633b8fd75 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -18,7 +18,6 @@ #include -#include #include #include #include @@ -30,19 +29,16 @@ #include #include #include -#include #include +#include #include #include #include -#include #include #include -#include #include -#include using namespace solidity; using namespace solidity::frontend; @@ -50,475 +46,6 @@ using namespace solidity::yul; using namespace solidity::langutil; using namespace solidity::util; -namespace -{ - -class YulNameCollector: public ASTWalker -{ -public: - using ASTWalker::operator(); - - void operator()(Identifier const& _identifier) override - { - names.insert(_identifier.name.str()); - } - - void operator()(VariableDeclaration const& _varDecl) override - { - insertNames(_varDecl.variables); - ASTWalker::operator()(_varDecl); - } - - void operator()(FunctionDefinition const& _function) override - { - insertNames(_function.parameters); - insertNames(_function.returnVariables); - ASTWalker::operator()(_function); - } - - std::set names; - -private: - void insertNames(NameWithDebugDataList const& _names) - { - for (NameWithDebugData const& name: _names) - names.insert(name.name.str()); - } -}; - -void collectYulNames(YulNameCollector& _collector, Object const& _object) -{ - if (_object.hasCode()) - _collector(_object.code()->root()); - - for (auto const& subNode: _object.subObjects) - if (auto const* subObject = dynamic_cast(subNode.get())) - collectYulNames(_collector, *subObject); -} - -std::set collectYulNames(Object const& _object) -{ - YulNameCollector collector; - collectYulNames(collector, _object); - return std::move(collector.names); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, langutil::DebugData::ConstPtr const& _debugData) -{ - if (_debugData && _debugData->astID && _debugData->semanticDebugData) - _table.set(*_debugData->astID, _debugData->semanticDebugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, NameWithDebugData const& _name) -{ - collectSemanticDebugData(_table, _name.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Literal const& _literal); -void collectSemanticDebugData(SemanticDebugDataTable& _table, Identifier const& _identifier); -void collectSemanticDebugData(SemanticDebugDataTable& _table, BuiltinName const& _builtin); -void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionName const& _functionName); -void collectSemanticDebugData(SemanticDebugDataTable& _table, Expression const& _expression); -void collectSemanticDebugData(SemanticDebugDataTable& _table, Case const& _case); -void collectSemanticDebugData(SemanticDebugDataTable& _table, Statement const& _statement); -void collectSemanticDebugData(SemanticDebugDataTable& _table, Block const& _block); - -template -void collectSemanticDebugData(SemanticDebugDataTable& _table, std::vector const& _nodes) -{ - for (auto const& node: _nodes) - collectSemanticDebugData(_table, node); -} - -template -void collectSemanticDebugData(SemanticDebugDataTable& _table, std::unique_ptr const& _node) -{ - if (_node) - collectSemanticDebugData(_table, *_node); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Literal const& _literal) -{ - collectSemanticDebugData(_table, _literal.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Identifier const& _identifier) -{ - collectSemanticDebugData(_table, _identifier.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, BuiltinName const& _builtin) -{ - collectSemanticDebugData(_table, _builtin.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionName const& _functionName) -{ - std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _functionName); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionCall const& _call) -{ - collectSemanticDebugData(_table, _call.debugData); - collectSemanticDebugData(_table, _call.functionName); - collectSemanticDebugData(_table, _call.arguments); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Expression const& _expression) -{ - std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _expression); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, ExpressionStatement const& _statement) -{ - collectSemanticDebugData(_table, _statement.debugData); - collectSemanticDebugData(_table, _statement.expression); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Assignment const& _assignment) -{ - collectSemanticDebugData(_table, _assignment.debugData); - collectSemanticDebugData(_table, _assignment.variableNames); - collectSemanticDebugData(_table, _assignment.value); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, VariableDeclaration const& _varDecl) -{ - collectSemanticDebugData(_table, _varDecl.debugData); - collectSemanticDebugData(_table, _varDecl.variables); - collectSemanticDebugData(_table, _varDecl.value); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, FunctionDefinition const& _function) -{ - collectSemanticDebugData(_table, _function.debugData); - collectSemanticDebugData(_table, _function.parameters); - collectSemanticDebugData(_table, _function.returnVariables); - collectSemanticDebugData(_table, _function.body); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, If const& _if) -{ - collectSemanticDebugData(_table, _if.debugData); - collectSemanticDebugData(_table, _if.condition); - collectSemanticDebugData(_table, _if.body); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Case const& _case) -{ - collectSemanticDebugData(_table, _case.debugData); - collectSemanticDebugData(_table, _case.value); - collectSemanticDebugData(_table, _case.body); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Switch const& _switch) -{ - collectSemanticDebugData(_table, _switch.debugData); - collectSemanticDebugData(_table, _switch.expression); - collectSemanticDebugData(_table, _switch.cases); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, ForLoop const& _forLoop) -{ - collectSemanticDebugData(_table, _forLoop.debugData); - collectSemanticDebugData(_table, _forLoop.pre); - collectSemanticDebugData(_table, _forLoop.condition); - collectSemanticDebugData(_table, _forLoop.post); - collectSemanticDebugData(_table, _forLoop.body); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Break const& _break) -{ - collectSemanticDebugData(_table, _break.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Continue const& _continue) -{ - collectSemanticDebugData(_table, _continue.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Leave const& leave_) -{ - collectSemanticDebugData(_table, leave_.debugData); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Statement const& _statement) -{ - std::visit([&](auto const& node) { collectSemanticDebugData(_table, node); }, _statement); -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Block const& _block) -{ - collectSemanticDebugData(_table, _block.debugData); - collectSemanticDebugData(_table, _block.statements); -} - -bool stackPointerSurvives(SemanticDebugPointer const& _pointer, std::set const& _yulNames) -{ - if (_pointer.pointerClass == SemanticDebugPointer::Class::Region) - { - if (!_pointer.location || *_pointer.location != SemanticDebugPointer::Location::Stack || !_pointer.slot) - return true; - return _yulNames.count(*_pointer.slot) != 0; - } - - if (_pointer.pointerClass == SemanticDebugPointer::Class::Group) - return std::all_of( - _pointer.group.begin(), - _pointer.group.end(), - [&](SemanticDebugPointer const& _part) { return stackPointerSurvives(_part, _yulNames); } - ); - - return true; -} - -bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) -{ - if ( - !_variable.location || - _variable.location->kind != SemanticDebugVariableLocation::Kind::Stack || - !_variable.ethdebugPointer - ) - return true; - - return stackPointerSurvives(*_variable.ethdebugPointer, _yulNames); -} - -SemanticDebugVariable optimizedOutVariable(SemanticDebugVariable _variable) -{ - _variable.location = SemanticDebugVariableLocation{ - .kind = SemanticDebugVariableLocation::Kind::OptimizedOut, - .pointerID = std::nullopt - }; - _variable.ethdebugPointer = std::nullopt; - return _variable; -} - -SemanticDebugData::ConstPtr updateSemanticDebugDataLocations( - SemanticDebugData::ConstPtr const& _debugData, - std::set const& _yulNames -) -{ - if (!_debugData) - return nullptr; - - SemanticDebugData result = *_debugData; - bool changed = false; - for (SemanticDebugVariable& variable: result.variableDefinitions) - if (!stackLocationSurvives(variable, _yulNames)) - { - variable = optimizedOutVariable(std::move(variable)); - changed = true; - } - - if (!changed) - return _debugData; - - return std::make_shared(std::move(result)); -} - -SemanticDebugDataTable updateSemanticDebugDataLocations( - SemanticDebugDataTable const& _table, - std::set const& _yulNames -) -{ - SemanticDebugDataTable result; - for (auto const& [astID, debugData]: _table.entries()) - result.set(astID, updateSemanticDebugDataLocations(debugData, _yulNames)); - return result; -} - -void collectSemanticDebugData(SemanticDebugDataTable& _table, Object const& _object) -{ - if (_object.hasCode()) - collectSemanticDebugData(_table, _object.code()->root()); - - for (auto const& subNode: _object.subObjects) - if (auto const* subObject = dynamic_cast(subNode.get())) - collectSemanticDebugData(_table, *subObject); -} - -langutil::DebugData::ConstPtr reattachSemanticDebugData( - langutil::DebugData::ConstPtr const& _debugData, - SemanticDebugDataTable const& _table -) -{ - if (!_debugData) - return nullptr; - - auto semanticDebugData = _table.find(_debugData->astID); - if (!semanticDebugData) - return _debugData; - - return langutil::DebugData::create( - _debugData->nativeLocation, - _debugData->originLocation, - _debugData->astID, - std::move(semanticDebugData) - ); -} - -void reattachSemanticDebugData(langutil::DebugData::ConstPtr& _debugData, SemanticDebugDataTable const& _table) -{ - _debugData = reattachSemanticDebugData(static_cast(_debugData), _table); -} - -void reattachSemanticDebugData(NameWithDebugData& _name, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_name.debugData, _table); -} - -void reattachSemanticDebugData(Literal& _literal, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(Identifier& _identifier, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(BuiltinName& _builtin, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(FunctionName& _functionName, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(Expression& _expression, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(Case& _case, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(Statement& _statement, SemanticDebugDataTable const& _table); -void reattachSemanticDebugData(Block& _block, SemanticDebugDataTable const& _table); - -template -void reattachSemanticDebugData(std::vector& _nodes, SemanticDebugDataTable const& _table) -{ - for (auto& node: _nodes) - reattachSemanticDebugData(node, _table); -} - -template -void reattachSemanticDebugData(std::unique_ptr& _node, SemanticDebugDataTable const& _table) -{ - if (_node) - reattachSemanticDebugData(*_node, _table); -} - -void reattachSemanticDebugData(Literal& _literal, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_literal.debugData, _table); -} - -void reattachSemanticDebugData(Identifier& _identifier, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_identifier.debugData, _table); -} - -void reattachSemanticDebugData(BuiltinName& _builtin, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_builtin.debugData, _table); -} - -void reattachSemanticDebugData(FunctionName& _functionName, SemanticDebugDataTable const& _table) -{ - std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _functionName); -} - -void reattachSemanticDebugData(FunctionCall& _call, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_call.debugData, _table); - reattachSemanticDebugData(_call.functionName, _table); - reattachSemanticDebugData(_call.arguments, _table); -} - -void reattachSemanticDebugData(Expression& _expression, SemanticDebugDataTable const& _table) -{ - std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _expression); -} - -void reattachSemanticDebugData(ExpressionStatement& _statement, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_statement.debugData, _table); - reattachSemanticDebugData(_statement.expression, _table); -} - -void reattachSemanticDebugData(Assignment& _assignment, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_assignment.debugData, _table); - reattachSemanticDebugData(_assignment.variableNames, _table); - reattachSemanticDebugData(_assignment.value, _table); -} - -void reattachSemanticDebugData(VariableDeclaration& _varDecl, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_varDecl.debugData, _table); - reattachSemanticDebugData(_varDecl.variables, _table); - reattachSemanticDebugData(_varDecl.value, _table); -} - -void reattachSemanticDebugData(FunctionDefinition& _function, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_function.debugData, _table); - reattachSemanticDebugData(_function.parameters, _table); - reattachSemanticDebugData(_function.returnVariables, _table); - reattachSemanticDebugData(_function.body, _table); -} - -void reattachSemanticDebugData(If& _if, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_if.debugData, _table); - reattachSemanticDebugData(_if.condition, _table); - reattachSemanticDebugData(_if.body, _table); -} - -void reattachSemanticDebugData(Case& _case, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_case.debugData, _table); - reattachSemanticDebugData(_case.value, _table); - reattachSemanticDebugData(_case.body, _table); -} - -void reattachSemanticDebugData(Switch& _switch, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_switch.debugData, _table); - reattachSemanticDebugData(_switch.expression, _table); - reattachSemanticDebugData(_switch.cases, _table); -} - -void reattachSemanticDebugData(ForLoop& _forLoop, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_forLoop.debugData, _table); - reattachSemanticDebugData(_forLoop.pre, _table); - reattachSemanticDebugData(_forLoop.condition, _table); - reattachSemanticDebugData(_forLoop.post, _table); - reattachSemanticDebugData(_forLoop.body, _table); -} - -void reattachSemanticDebugData(Break& _break, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_break.debugData, _table); -} - -void reattachSemanticDebugData(Continue& _continue, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_continue.debugData, _table); -} - -void reattachSemanticDebugData(Leave& leave_, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(leave_.debugData, _table); -} - -void reattachSemanticDebugData(Statement& _statement, SemanticDebugDataTable const& _table) -{ - std::visit([&](auto& node) { reattachSemanticDebugData(node, _table); }, _statement); -} - -void reattachSemanticDebugData(Block& _block, SemanticDebugDataTable const& _table) -{ - reattachSemanticDebugData(_block.debugData, _table); - reattachSemanticDebugData(_block.statements, _table); -} - -void reattachSemanticDebugData(Object& _object, SemanticDebugDataTable const& _table) -{ - if (_object.hasCode()) - reattachSemanticDebugData(const_cast(_object.code()->root()), _table); - - for (auto& subNode: _object.subObjects) - if (auto* subObject = dynamic_cast(subNode.get())) - reattachSemanticDebugData(*subObject, _table); -} - -} - CharStream const& YulStack::charStream(std::string const& _sourceName) const { yulAssert(m_charStream, ""); @@ -679,8 +206,10 @@ 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. - SemanticDebugDataTable semanticDebugData; - collectSemanticDebugData(semanticDebugData, *m_parserResult); + // Semantic debug metadata cannot be represented in the printed source. It is merged into the + // retained side table here and reattached by AST ID after the reparse. Entries that are not + // attached to any Yul node (e.g. contract-scope storage metadata) survive in the table itself. + collectSemanticDebugData(*m_parserResult, m_semanticDebugData); std::string source = print(); YulStack cleanStack( @@ -700,14 +229,8 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); - if (!semanticDebugData.empty()) - { - SemanticDebugDataTable updatedSemanticDebugData = updateSemanticDebugDataLocations( - semanticDebugData, - collectYulNames(*m_parserResult) - ); - reattachSemanticDebugData(*m_parserResult, updatedSemanticDebugData); - } + if (!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 @@ -922,8 +445,16 @@ void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) { yulAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); yulAssert(m_parserResult, ""); - if (!_table.empty()) - reattachSemanticDebugData(*m_parserResult, _table); + + for (auto const& [astID, debugData]: _table.entries()) + m_semanticDebugData.set(astID, debugData); + + // Applying instead of blindly reattaching validates stack locations against the + // current Yul code. This matters when the table is attached to already optimized + // IR that has been reloaded from text: variables whose Yul stack slots no longer + // exist must be marked OptimizedOut instead of keeping stale locations. + if (!m_semanticDebugData.empty()) + applySemanticDebugData(*m_parserResult, m_semanticDebugData); } Dialect const& YulStack::dialect() const diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 128af3715c47..752fa9ea70d3 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -35,6 +35,8 @@ #include +#include + #include #include @@ -48,7 +50,6 @@ class Assembly; namespace solidity::langutil { class Scanner; -class SemanticDebugDataTable; } namespace solidity::yul @@ -183,6 +184,10 @@ class YulStack: public langutil::CharStreamProvider State m_stackState = Empty; std::shared_ptr m_parserResult; + /// Semantic debug metadata keyed by Solidity AST ID. Retained across reparses so + /// that entries not attached to any Yul node (e.g. contract-scope storage + /// metadata) are not lost at the Yul text boundary. + langutil::SemanticDebugDataTable m_semanticDebugData; langutil::ErrorList m_errors; langutil::ErrorReporter m_errorReporter; diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json index 8f198f1f3adb..ea3e07cbc99f 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json @@ -155,6 +155,7 @@ object \"A1_14\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 13 /// @src 0:72:121 function fun_a_13(var_x_3) { @@ -276,7 +277,7 @@ object \"A1_14\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 0:72:121 + /// @ast-id 13 @src 0:72:121 function fun_a(var_x) { /// @src 0:112:113 @@ -449,6 +450,7 @@ object \"A2_27\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 26 /// @src 0:138:187 function fun_a_26(var_x_16) { @@ -570,7 +572,7 @@ object \"A2_27\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 0:138:187 + /// @ast-id 26 @src 0:138:187 function fun_a(var_x) { /// @src 0:178:179 @@ -745,6 +747,7 @@ object \"A1_42\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 41 /// @src 1:72:121 function fun_b_41(var_x_31) { @@ -866,7 +869,7 @@ object \"A1_42\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 1:72:121 + /// @ast-id 41 @src 1:72:121 function fun_b(var_x) { /// @src 1:112:113 @@ -1039,6 +1042,7 @@ object \"B2_55\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 54 /// @src 1:138:187 function fun_b_54(var_x_44) { @@ -1160,7 +1164,7 @@ object \"B2_55\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 1:138:187 + /// @ast-id 54 @src 1:138:187 function fun_b(var_x) { /// @src 1:178:179 diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index 67ef0ca94a6a..b03e978505c9 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -293,6 +293,19 @@ class SemanticDebugDataFixture: public AnalysisFramework } }; +class EthdebugOnlySemanticDebugDataFixture: public AnalysisFramework +{ + void setupCompiler(CompilerStack& _compiler) override + { + AnalysisFramework::setupCompiler(_compiler); + _compiler.setViaIR(true); + _compiler.setOptimiserSettings(OptimiserSettings::none()); + DebugInfoSelection selection = DebugInfoSelection::None(); + selection.enable("ethdebug"); + _compiler.selectDebugInfo(selection); + } +}; + BOOST_FIXTURE_TEST_SUITE(SemanticDebugDataTest, SemanticDebugDataFixture) BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) @@ -546,6 +559,96 @@ BOOST_AUTO_TEST_CASE(state_variables_include_storage_pointer_descriptors) BOOST_CHECK(!findVariableByName(*data, "ignoredImmutable")); } +BOOST_AUTO_TEST_CASE(inherited_function_variables_have_semantic_metadata) +{ + BOOST_REQUIRE(runFramework(R"( + contract Base { + modifier baseGuarded(uint256 guard) { + _; + } + + function inherited(uint256 value) public pure returns (uint256 result) { + return value; + } + } + + contract Derived is Base {} + )", PipelineStage::Analysis)); + + ContractDefinition const* base = retrieveContractByName(compiler().ast(""), "Base"); + BOOST_REQUIRE(base); + ContractDefinition const* derived = retrieveContractByName(compiler().ast(""), "Derived"); + BOOST_REQUIRE(derived); + FunctionDefinition const* function = findFunction(*base, "inherited"); + BOOST_REQUIRE(function); + ModifierDefinition const* modifier = findModifier(*base, "baseGuarded"); + BOOST_REQUIRE(modifier); + + // The derived contract's IR contains the inherited definitions with the base + // contract's AST IDs, so the derived contract's table must cover them. + SemanticDebugDataTable table = buildSemanticDebugDataTable(*derived); + SemanticDebugData::ConstPtr data = table.find(function->id()); + BOOST_REQUIRE(data); + checkFunctionVariableDebugData( + *data, + *function, + *function->parameters().front(), + *function->returnParameters().front() + ); + BOOST_CHECK(table.find(modifier->id())); +} + +BOOST_AUTO_TEST_CASE(free_functions_have_semantic_metadata) +{ + BOOST_REQUIRE(runFramework(R"( + function freeHelper(uint256 value) pure returns (uint256 result) { + return value + 1; + } + + contract C { + function f(uint256 value) public pure returns (uint256 result) { + return freeHelper(value); + } + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + FunctionDefinition const* freeFunction = nullptr; + for (ASTPointer const& node: compiler().ast("").nodes()) + if (auto const* candidate = dynamic_cast(node.get())) + freeFunction = candidate; + BOOST_REQUIRE(freeFunction); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(freeFunction->id()); + BOOST_REQUIRE(data); + checkFunctionVariableDebugData( + *data, + *freeFunction, + *freeFunction->parameters().front(), + *freeFunction->returnParameters().front() + ); +} + +BOOST_FIXTURE_TEST_CASE(ethdebug_debug_info_implies_ast_id, EthdebugOnlySemanticDebugDataFixture) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + function f(uint256 value) public pure returns (uint256 result) { + return value; + } + } + )", PipelineStage::Compilation)); + + // The AST ID comments are the join key for semantic metadata across the Yul text + // boundary. Selecting only ethdebug debug info must still produce them. + std::optional const& yulIR = compiler().yulIR("C"); + BOOST_REQUIRE(yulIR); + BOOST_CHECK(yulIR->find("@ast-id") != std::string::npos); +} + BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) { BOOST_REQUIRE(runFramework(R"( diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index d4f9e5615c6b..192d3465f220 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -20,8 +20,10 @@ #include #include +#include #include #include +#include #include #include @@ -66,6 +68,51 @@ SemanticDebugPointer stackPointer(std::string _name) return pointer; } +SemanticDebugDataTable stackVariableTable(int64_t _astID, std::string _name, std::string _slot) +{ + SemanticDebugVariable variable; + variable.name = std::move(_name); + variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = _slot + }; + variable.ethdebugPointer = stackPointer(std::move(_slot)); + + SemanticDebugDataTable table; + table.set(_astID, std::make_shared(SemanticDebugData{ + .lexicalScopeID = _astID, + .variableDefinitions = {std::move(variable)} + })); + return table; +} + +void checkStackVariableSurvived(FunctionDefinition const& _function, std::string const& _slot) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugData); + SemanticDebugData const& data = *_function.debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_REQUIRE(variable.location); + BOOST_CHECK(variable.location->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(variable.location->pointerID); + BOOST_CHECK_EQUAL(*variable.location->pointerID, _slot); + BOOST_REQUIRE(variable.ethdebugPointer); +} + +void checkStackVariableOptimizedOut(FunctionDefinition const& _function) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugData); + SemanticDebugData const& data = *_function.debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_REQUIRE(variable.location); + BOOST_CHECK(variable.location->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!variable.location->pointerID); + BOOST_CHECK(!variable.ethdebugPointer); +} + } BOOST_AUTO_TEST_SUITE(YulDebugDataTest) @@ -184,6 +231,86 @@ BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) BOOST_CHECK(!reparsedVariable.ethdebugPointer); } +BOOST_AUTO_TEST_CASE(attach_marks_missing_stack_locations_optimized_out) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + 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(stack_location_survival_is_scoped_per_object) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + 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, "var_x"); + + 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_SUITE_END() } // namespace solidity::yul::test From 53a8f2cce566e303a11faacd005a87d3260fe2f7 Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 11 Jun 2026 09:05:23 +0200 Subject: [PATCH 17/47] test: Avoid nested designated initializers that crash MSVC --- test/liblangutil/DebugData.cpp | 54 +++++++++++++++++----------------- test/libyul/DebugData.cpp | 23 ++++++++------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index f1dd4432a9a0..40028549d446 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -38,30 +38,30 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) ethdebugPointer.name = "value"; ethdebugPointer.slot = "pointer:value"; - auto semanticDebugData = std::make_shared(SemanticDebugData{ - .lexicalScopeID = 17, - .variableDefinitions = {{ - .name = "value", - .declarationAstID = 23, - .declarationLocation = SourceLocation{1, 6, std::make_shared("input.sol")}, - .typeID = "type:uint256", - .ethdebugType = SemanticDebugType{ - .typeClass = SemanticDebugType::Class::Elementary, - .kind = SemanticDebugType::Kind::Uint, - .bits = 256, - .places = std::nullopt, - .bytes = std::nullopt, - .dataLocation = std::nullopt, - .payable = std::nullopt, - .dynamic = std::nullopt - }, - .location = SemanticDebugVariableLocation{ - .kind = SemanticDebugVariableLocation::Kind::Stack, - .pointerID = "pointer:value" - }, - .ethdebugPointer = ethdebugPointer - }} - }); + // NOTE: Built imperatively instead of with nested designated initializers, + // which crash MSVC with an internal compiler error. + SemanticDebugType ethdebugType; + ethdebugType.typeClass = SemanticDebugType::Class::Elementary; + ethdebugType.kind = SemanticDebugType::Kind::Uint; + ethdebugType.bits = 256; + + SemanticDebugVariableLocation location; + location.kind = SemanticDebugVariableLocation::Kind::Stack; + location.pointerID = "pointer:value"; + + SemanticDebugVariable variable; + variable.name = "value"; + variable.declarationAstID = 23; + variable.declarationLocation = SourceLocation{1, 6, std::make_shared("input.sol")}; + variable.typeID = "type:uint256"; + variable.ethdebugType = ethdebugType; + variable.location = location; + variable.ethdebugPointer = ethdebugPointer; + + SemanticDebugData data; + data.lexicalScopeID = 17; + data.variableDefinitions.emplace_back(std::move(variable)); + auto semanticDebugData = std::make_shared(std::move(data)); auto debugData = DebugData::create( SourceLocation{}, @@ -98,9 +98,9 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) { - auto semanticDebugData = std::make_shared(SemanticDebugData{ - .lexicalScopeID = 17 - }); + SemanticDebugData data; + data.lexicalScopeID = 17; + auto semanticDebugData = std::make_shared(std::move(data)); SemanticDebugDataTable table; BOOST_CHECK(table.empty()); diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index 192d3465f220..27df5462dfe2 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -78,11 +78,12 @@ SemanticDebugDataTable stackVariableTable(int64_t _astID, std::string _name, std }; variable.ethdebugPointer = stackPointer(std::move(_slot)); + SemanticDebugData data; + data.lexicalScopeID = _astID; + data.variableDefinitions.emplace_back(std::move(variable)); + SemanticDebugDataTable table; - table.set(_astID, std::make_shared(SemanticDebugData{ - .lexicalScopeID = _astID, - .variableDefinitions = {std::move(variable)} - })); + table.set(_astID, std::make_shared(std::move(data))); return table; } @@ -145,9 +146,9 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) BOOST_REQUIRE(funDef->debugData->astID); BOOST_REQUIRE_EQUAL(*funDef->debugData->astID, 23); - auto semanticDebugData = std::make_shared(SemanticDebugData{ - .lexicalScopeID = 17 - }); + SemanticDebugData data; + data.lexicalScopeID = 17; + auto semanticDebugData = std::make_shared(std::move(data)); funDef->debugData = DebugData::create( funDef->debugData->nativeLocation, funDef->debugData->originLocation, @@ -202,10 +203,10 @@ BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) }; variable.ethdebugPointer = stackPointer("missing_slot"); - auto semanticDebugData = std::make_shared(SemanticDebugData{ - .lexicalScopeID = 17, - .variableDefinitions = {std::move(variable)} - }); + SemanticDebugData data; + data.lexicalScopeID = 17; + 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, From d76fb19d6dcf81a00a7a6e6b8ec909f461d1ae75 Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 25 Jun 2026 12:26:13 +0200 Subject: [PATCH 18/47] ethdebug: Emit program-level state variable context --- libevmasm/Ethdebug.cpp | 4 +- libevmasm/Ethdebug.h | 11 +- libevmasm/EthdebugSchema.cpp | 8 +- libevmasm/EthdebugSchema.h | 6 +- libsolidity/interface/CompilerStack.cpp | 103 +++++++++++++++++- .../test_ethdebug_schema_conformity.py | 29 +++++ 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/libevmasm/Ethdebug.cpp b/libevmasm/Ethdebug.cpp index 374297beb430..35992dbac1bf 100644 --- a/libevmasm/Ethdebug.cpp +++ b/libevmasm/Ethdebug.cpp @@ -171,7 +171,7 @@ schema::materials::Compilation materialCompilation(std::vector const& _s } // anonymous namespace -Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject) +Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject, std::optional _programContext) { return schema::Program{ .compilation = std::nullopt, @@ -186,7 +186,7 @@ Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly cons } }, .environment = _assembly.isCreation() ? schema::Program::Environment::CREATE : schema::Program::Environment::CALL, - .context = std::nullopt, + .context = std::move(_programContext), .instructions = programInstructions(_assembly, _linkerObject, _sourceID) }; } diff --git a/libevmasm/Ethdebug.h b/libevmasm/Ethdebug.h index 60ed02c426f7..3af9c463e6b8 100644 --- a/libevmasm/Ethdebug.h +++ b/libevmasm/Ethdebug.h @@ -21,8 +21,11 @@ #include #include +#include #include +#include + namespace solidity::evmasm::ethdebug { @@ -35,7 +38,13 @@ struct Source }; // returns ethdebug/format/program. -Json program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject); +Json program( + std::string_view _name, + unsigned _sourceID, + Assembly const& _assembly, + LinkerObject const& _linkerObject, + std::optional _programContext = std::nullopt +); // returns ethdebug/format/info/resources Json resources( diff --git a/libevmasm/EthdebugSchema.cpp b/libevmasm/EthdebugSchema.cpp index d67878449b14..188bb883863b 100644 --- a/libevmasm/EthdebugSchema.cpp +++ b/libevmasm/EthdebugSchema.cpp @@ -102,7 +102,9 @@ void schema::program::to_json(Json& _json, Context::Variable const& _contextVari { auto const numProperties = _contextVariable.identifier.has_value() + - _contextVariable.declaration.has_value(); + _contextVariable.declaration.has_value() + + _contextVariable.type.has_value() + + _contextVariable.pointer.has_value(); solRequire(numProperties >= 1, EthdebugException, "Context variable has no properties."); if (_contextVariable.identifier) { @@ -111,6 +113,10 @@ void schema::program::to_json(Json& _json, Context::Variable const& _contextVari } if (_contextVariable.declaration) _json["declaration"] = *_contextVariable.declaration; + if (_contextVariable.type) + _json["type"] = *_contextVariable.type; + if (_contextVariable.pointer) + _json["pointer"] = *_contextVariable.pointer; } void schema::program::to_json(Json& _json, Context const& _context) diff --git a/libevmasm/EthdebugSchema.h b/libevmasm/EthdebugSchema.h index 57f21a550f51..d0f4fe31d2e9 100644 --- a/libevmasm/EthdebugSchema.h +++ b/libevmasm/EthdebugSchema.h @@ -123,8 +123,10 @@ struct Context { std::optional identifier; std::optional declaration; - // TODO: type - // TODO: pointer according to ethdebug/format/spec/pointer + // ethdebug/format/type/specifier: a full type representation or an { "id": ... } reference. + std::optional type; + // ethdebug/format/pointer: a region or collection describing where the value lives. + std::optional pointer; }; std::optional code; diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 96184b314b12..156f2ab5fd2f 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -80,6 +80,7 @@ #include #include +#include #include @@ -242,6 +243,91 @@ static void collectEthdebugResources( collectEthdebugPointers(_pointers, _semanticDebugData); } +static evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( + langutil::SourceLocation const& _location, + unsigned _sourceID +) +{ + namespace schema = evmasm::ethdebug::schema; + schema::materials::Reference reference; + reference.id = schema::materials::ID{static_cast(_sourceID)}; + reference.type = std::nullopt; + + schema::materials::SourceRange::Range range{ + .length = schema::data::Unsigned{_location.end - _location.start}, + .offset = schema::data::Unsigned{_location.start} + }; + + schema::materials::SourceRange sourceRange; + sourceRange.source = std::move(reference); + sourceRange.range = range; + return sourceRange; +} + +/// Builds the program-level ethdebug context: the contract's named state +/// variables, each with its declaration range, ethdebug type, and storage +/// pointer. Returns nullopt when there are no such variables. +static std::optional buildEthdebugProgramContext( + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const& _sourceIndices +) +{ + namespace schema = evmasm::ethdebug::schema; + std::vector variables; + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + // Program-level context currently carries named state variables, + // i.e. variables resolved to a storage location. + if ( + !variable.location || + variable.location->kind != langutil::SemanticDebugVariableLocation::Kind::Storage + ) + continue; + + schema::program::Context::Variable contextVariable; + contextVariable.identifier = variable.name; + + if ( + variable.declarationLocation && + variable.declarationLocation->hasText() && + variable.declarationLocation->sourceName && + _sourceIndices.count(*variable.declarationLocation->sourceName) + ) + contextVariable.declaration = ethdebugDeclarationRange( + *variable.declarationLocation, + _sourceIndices.at(*variable.declarationLocation->sourceName) + ); + + if (variable.ethdebugType) + contextVariable.type = ethdebugType(*variable.ethdebugType); + + if (variable.ethdebugPointer) + if (std::optional pointer = ethdebugStoragePointer(*variable.ethdebugPointer)) + { + // The context variable's identifier already names it; the bare + // pointer region must not carry an extra "name" property. + pointer->erase("name"); + contextVariable.pointer = std::move(*pointer); + } + + variables.emplace_back(std::move(contextVariable)); + } + } + + if (variables.empty()) + return std::nullopt; + + schema::program::Context context; + context.variables = std::move(variables); + return context; +} + CompilerStack::CompilerStack(ReadCallback::Callback _readFile): m_readFile{std::move(_readFile)}, m_objectOptimizer(std::make_shared()), @@ -1367,8 +1453,21 @@ Json CompilerStack::ethdebug(Contract const& _contract, bool _runtime) const if (!assembly) return {}; - solAssert(sourceIndices().contains(_contract.contract->sourceUnitName())); - return evmasm::ethdebug::program(_contract.contract->name(), sourceIndices()[_contract.contract->sourceUnitName()], *assembly, object); + std::map const sourceIndexMap = sourceIndices(); + solAssert(sourceIndexMap.contains(_contract.contract->sourceUnitName())); + + std::optional programContext = + _contract.yulSemanticDebugData + ? buildEthdebugProgramContext(*_contract.yulSemanticDebugData, sourceIndexMap) + : buildEthdebugProgramContext(buildSemanticDebugDataTable(*_contract.contract), sourceIndexMap); + + return evmasm::ethdebug::program( + _contract.contract->name(), + sourceIndexMap.at(_contract.contract->sourceUnitName()), + *assembly, + object, + std::move(programContext) + ); } bytes CompilerStack::cborMetadata(std::string const& _contractName, bool _forIR) const diff --git a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py index 769f2de6a036..cf5f3a6afd8b 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -104,6 +104,35 @@ def test_program_sanity(output_selection, environment, solc_output): assert all(instruction["operation"]["mnemonic"] for instruction in instructions) +def test_program_context_includes_state_variables(solc_output): + programs = { + (source_name, contract_name): program + for source_name, contract_name, program + in ethdebug_programs(solc_output, "evm.deployedBytecode.ethdebug") + } + + variables = { + variable["identifier"]: variable + for variable in programs[("a.sol", "A1")]["context"]["variables"] + } + assert variables["stored"]["type"] == {"kind": "uint", "bits": 128} + assert variables["stored"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "length": "0x10", + } + assert variables["enabled"]["type"] == {"kind": "bool"} + assert variables["enabled"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } + + # Contracts without state variables emit no program-level context. + assert "context" not in programs[("a.sol", "A2")] + + def test_resources_match_standard_json_sources(solc_output): standard_json_sources = {source_name: source["id"] for source_name, source in solc_output["sources"].items()} ethdebug_sources = { From 4bec7edcfd98f9832bf7d5892e76381316048d6a Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 2 Jul 2026 01:36:12 +0200 Subject: [PATCH 19/47] ethdebug: Support recursive type and pointer descriptors --- docs/internals/ethdebug_internal_metadata.rst | 214 +++++-- liblangutil/SemanticDebugData.h | 380 ++++++++++- .../codegen/ir/SemanticDebugDataBuilder.cpp | 543 ++++++++++++++-- libsolidity/interface/CompilerStack.cpp | 605 ++++++++++++++++-- libyul/SemanticDebugDataTransfer.cpp | 71 +- test/ethdebugSchemaTests/sources/a.sol | 3 + .../test_ethdebug_schema_conformity.py | 64 +- test/liblangutil/DebugData.cpp | 129 +++- test/libsolidity/SemanticDebugData.cpp | 493 +++++++++++++- test/libyul/DebugData.cpp | 123 +++- 10 files changed, 2409 insertions(+), 216 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 5a9f305b6559..a31faa036170 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -81,7 +81,72 @@ The variable location is represented by ``SemanticDebugVariableLocation``. The location kind can describe stack, storage, transient storage, memory, calldata, immutable, constant, or optimized-out values. The current implementation populates stack locations for function parameters and named return variables, -and storage locations for named state variables. +and storage locations for named state variables in persistent and transient +storage. + +Type Descriptors +---------------- + +``SemanticDebugType`` mirrors the ethdebug type vocabulary. Elementary kinds +carry their payload directly: bit width for ``uint``/``int``, bit width and +decimal places for ``fixed``/``ufixed``, byte size for static ``bytes``, +payability for addresses, payability and library/interface flags for contracts, +and the member name list for enums. + +Composed types are recursive. Each ``SemanticDebugType`` holds a list of +``SemanticDebugTypeComponent`` entries, where each component records: + +* ``role``: how the component is composed (array element, mapping key or value, + struct member or tuple element, function parameter or return, alias + underlying type, contract providing an external function), +* ``name``: the member or element name, if any, +* ``referenceID``: the stable compiler type identifier of the composed type, + usable as an ``{"id": ...}`` reference into the exported type resources, +* ``type``: the inline recursive representation. + +The inline representation of a component is cut (left null) when the composed +type is already being described further up the recursion path. This terminates +recursive types, e.g. structs that contain themselves through arrays or +mappings; the ``referenceID`` still identifies the type. + +Statically sized arrays record their element ``count``. User defined types +(contracts, enums, structs, aliases, functions) record the definition name and +source location. Function types record whether they follow internal or external +call semantics. + +Pointer Descriptors +------------------- + +``SemanticDebugPointer`` mirrors the ethdebug pointer schema. A pointer is +either a single region or one of the collection forms: + +* ``Region``: a data range in one location (stack, storage, transient storage, + memory, calldata, returndata, code). Word-oriented locations address by + ``slot`` with optional byte ``offset`` and ``length``; byte-oriented locations + address by ``offset`` and ``length``. +* ``Group``: an ordered composition of sub-pointers. +* ``List``: a dynamically sized repetition. ``count`` is an expression, and the + index is bound to ``indexName`` inside the repeated element pointer. +* ``Conditional``: chooses between ``thenPointer`` and the optional + ``elsePointer`` based on the non-zero-ness of ``condition``. +* ``Scope``: binds ordered auxiliary ``definitions`` (name/expression pairs) + inside a target pointer. Later definitions may reference earlier ones. +* ``TemplateReference``: refers to a pointer template defined elsewhere and can + rename the regions it produces. + +Slots, offsets, lengths, counts, conditions and scope definitions are +``SemanticDebugPointerExpression`` trees covering the ethdebug expression +grammar: literals, the ``$wordsize`` constant, variable references, region +lookups (``.slot``/``.offset``/``.length``), region reads (``$read``), +arithmetic (``$sum``, ``$difference``, ``$product``, ``$quotient``, +``$remainder``), hashing (``$keccak256``), concatenation (``$concat``) and +resizing (``$sized``/``$wordsized``). + +A root pointer additionally lists ``expectedParameters``: template variables +that must be bound externally before the pointer can be evaluated. Mapping keys +are the canonical example — the key is not stored anywhere, so the debugger +must provide it. Pointers with expected parameters are exported as pointer +templates, not as closed program-context pointers. Side Table ========== @@ -142,72 +207,96 @@ by generated IR, such as ``var_value_42`` for a Solidity variable named ``value`` with AST ID ``42``. Multi-slot variables use the stack slot list produced by ``IRVariable``. In the ETHDebug-oriented pointer descriptor, one-slot variables become stack region pointers and multi-slot variables become groups of -stack region pointers. +stack region pointers. The stack slot is a symbolic variable expression holding +the generated Yul name; actual stack depths are only known after code +generation. The storage pointer is based on the compiler's existing storage layout calculation. Named state variables use contract-scope semantic metadata keyed by -the contract AST ID. Each storage variable records a storage region pointer with -the base slot, and for packed variables, the byte offset and byte length within -the slot. +the contract AST ID. The pointer construction is recursive over the variable's +type, with the base slot threaded through as an expression: + +* Value types become a storage region with the base slot, and for packed + variables, the byte offset and byte length within the slot. +* Mappings register the key as an expected template parameter and describe the + value at ``$keccak256($wordsized(key), $wordsized(slot))``. Value-type keys + are padded to a word; ``bytes`` and ``string`` keys hash their raw bytes. + Nested mappings chain the hashes and expect one parameter per key. +* Dynamically sized arrays become a group of the length region at the base slot + and a list of element pointers starting at ``$keccak256($wordsized(slot))``, + with the element count read from the length region. +* Statically sized arrays become a list with a literal count. Elements narrower + than a word derive their slot and byte offset from the index (packed + elements); wider elements advance in whole slots, recursing into the element + type. +* ``bytes`` and ``string`` values become the canonical short/long conditional: + the doubled length lives in the last byte of the base slot; short values keep + their data in place while long values store ``2 * length + 1`` in the base + slot and their data at ``$keccak256($wordsized(slot))``. +* Structs become a group of member pointers using the struct storage layout, + recursing into each member type. Recursive structs and pathologically deep + compositions fall back to a region covering the struct's slots. +* Transient storage variables use the same construction with transient regions. Current Scope ============= -This is not yet the complete ETHDebug variable model. The current scope is a -minimal internal carrier and first real producer: +This is not yet the complete ETHDebug variable model. The current scope covers: * function parameters, * modifier parameters, * named function return variables, -* named state variables, -* ETHDebug-oriented type descriptors for elementary scalar types and basic - complex type categories, +* named state variables in persistent and transient storage, +* recursive ETHDebug-oriented type descriptors for all elementary and composed + Solidity type categories, * initial stack locations and ETHDebug-oriented stack pointer descriptors, -* initial storage locations and ETHDebug-oriented storage pointer descriptors. +* initial storage locations and recursive ETHDebug-oriented storage pointer + descriptors, including mappings, arrays, ``bytes``/``string`` and structs. -Local variables, memory pointers, calldata pointers, transient storage pointers, -immutable and constant values, and detailed optimizer location updates are still -future work. The current implementation exports schema-valid elementary type -entries to ``ethdebug.resources.types`` and schema-valid storage pointer -templates to ``ethdebug.resources.pointers``. It does not yet add per-instruction -variable contexts to the public ETHDebug JSON output. +Local variables, memory pointers, calldata pointers, immutable and constant +values, and detailed optimizer location updates are still future work. The +current implementation exports schema-valid type entries (elementary and +composed) to ``ethdebug.resources.types`` and schema-valid pointer templates to +``ethdebug.resources.pointers``. It does not yet add per-instruction variable +contexts to the public ETHDebug JSON output. Type and Pointer Mapping ======================== -The current ``typeID`` is the compiler's existing internal type identifier. This -is useful as a stable compiler-side key, but it is not the final ETHDebug type -schema representation. - -The current ``ethdebugType`` descriptor is the first bridge from Solidity types -to the public ETHDebug type vocabulary. It records whether the type is -elementary, complex, or unknown, plus the ETHDebug kind. The first mapping covers -``uint``/``int`` bit widths, ``fixed``/``ufixed`` bit widths and decimal places, -``bool``, fixed and dynamic ``bytes``, ``string``, ``address`` payable-ness, -contracts, enums, aliases, tuples, arrays, mappings, and structs. Complex -descriptors do not yet recursively contain member, key, value, or element type -wrappers. - -Similarly, the current stack ``pointerID`` is an internal pointer into generated -Yul stack slots. The current ``ethdebugPointer`` descriptor is the first bridge -from these internal locations to the public ETHDebug pointer vocabulary. It -records stack region pointers for one-slot variables and pointer groups for -multi-slot variables. The stack slot expression is still the generated Yul -variable name, not a runtime stack depth, so stack pointer descriptors remain -internal for now. - -The public resource exporter currently emits schema-valid elementary type -descriptors to ``ethdebug.resources.types``, keyed by compiler type ID, for -example ``t_uint256``. It also emits storage pointer templates to -``ethdebug.resources.pointers`` for named state variables. Storage pointer keys -are compiler-generated pointer IDs, and the template body contains the storage -slot plus optional byte offset and length for packed values. +The ``typeID`` is the compiler's existing internal type identifier, for example +``t_uint256``. It doubles as the key of the exported type resources and as the +``referenceID`` used by type components, so composed types can reference each +other by ID. + +The ``ethdebugType`` descriptor maps Solidity types to the public ETHDebug type +vocabulary, including recursive composition: array element types, mapping key +and value types, struct members, tuple elements, alias underlying types and +function parameter/return types are carried as components with reference IDs +and inline representations (see `Type Descriptors`_). + +The stack ``pointerID`` is an internal pointer into generated Yul stack slots. +Stack pointer descriptors use symbolic variable expressions holding the +generated Yul variable names, not runtime stack depths, so they remain internal +for now. + +The public resource exporter emits schema-valid type descriptors to +``ethdebug.resources.types``, keyed by compiler type ID. Composed resource +entries reference their component types with ``{"id": ...}`` into the same +table; every referenced component is registered as well. It also emits pointer +templates to ``ethdebug.resources.pointers`` for named state variables, keyed +by compiler-generated pointer IDs. The template's ``expect`` list carries the +pointer's expected parameters (mapping keys); the template body contains the +full recursive pointer. In the program-level context, types are inlined and +only closed pointers (without expected parameters) are attached to variables. + +Scope definitions inside exported pointers are order-sensitive, but JSON object +members are not ordered; the exporter therefore emits one nested +``define``/``in`` scope per definition so that ordering is structural. Future work should define: -* memory, calldata, transient, immutable, constant, and optimized-out variable - location to ETHDebug pointer mapping, -* complete recursive storage pointer mapping for structured values, +* memory, calldata, immutable, constant, and optimized-out variable location to + ETHDebug pointer mapping, * optimizer rules for updating, splitting, merging, or removing variable locations, * schema-validated emission of per-instruction variable contexts. @@ -224,10 +313,16 @@ boundary and when optimized IR is reloaded from text for EVM code generation: * surviving Yul variable names are collected separately for each object in the Yul object tree, so a variable that only survives in the creation code is still marked ``OptimizedOut`` in the deployed code, and vice versa, -* stack-backed semantic variables keep their locations if all referenced stack - slots still exist in the object the metadata is attached to, -* stack-backed semantic variables become ``OptimizedOut`` if any referenced - stack slot no longer exists there. +* stack-backed semantic variables keep their locations if all free variables of + their pointer expressions still exist as Yul variables in the object the + metadata is attached to, +* stack-backed semantic variables become ``OptimizedOut`` if any free variable + no longer exists there. + +A pointer variable is free if it is not bound within the pointer itself. Scope +definitions, list index names and template parameters bind identifiers; +references to named regions live in a separate namespace and are never treated +as Yul variable dependencies. This avoids reporting stale stack locations after an optimizer pass removes the generated Yul variables that originally held a Solidity value. @@ -242,16 +337,21 @@ Testing The internal metadata plumbing is covered by focused tests: -* ``DebugDataTest`` checks that ``DebugData`` can carry semantic metadata and - that the AST-ID side table resolves it. +* ``DebugDataTest`` checks that ``DebugData`` can carry semantic metadata, that + the AST-ID side table resolves it, and that pointer expressions, pointer + collections and recursive type components compose as designed. * ``YulDebugDataTest`` checks that semantic metadata survives Yul reparse by AST - ID and that missing stack locations are marked ``OptimizedOut``. + ID, that missing stack locations are marked ``OptimizedOut``, and that the + free-variable analysis distinguishes Yul dependencies from identifiers bound + within the pointer (scope definitions, list indices, template parameters). * ``SemanticDebugDataTest`` checks that Solidity function variables produce semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type descriptors, initial stack locations, and ETHDebug-oriented pointer - descriptors. It also checks state-variable storage pointer descriptors and - verifies that function variable metadata can be attached to generated Yul and - survives the Yul reparse path. + descriptors. It covers the recursive state-variable constructions — packed + values, mappings (including nested ones), dynamic and static arrays, + ``string`` storage, structs and recursive struct fallbacks — as well as enum, + alias and contract type descriptors. It also verifies that function variable + metadata can be attached to generated Yul and survives the Yul reparse path. These tests deliberately target the internal model. Schema validation tests cover the public ETHDebug JSON output separately, including the exported storage diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 87927c05a191..00073d0979d1 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace solidity::langutil @@ -47,6 +48,236 @@ struct SemanticDebugVariableLocation std::optional pointerID; }; +/// Node in an ethdebug/format/pointer/expression tree. Expressions evaluate to +/// unsigned values and may reference named regions and externally bound +/// variables (scope definitions, list indices or template parameters). +struct SemanticDebugPointerExpression +{ + enum class Kind + { + Unknown, + /// Literal unsigned value. @a value holds the canonical `0x`-prefixed hex form. + Literal, + /// The EVM word size in bytes (`$wordsize`). + WordSize, + /// Reference to a variable bound by a scope definition, a list index name or + /// a template parameter. In internal stack pointers this is also used for + /// generated Yul variable names that stand in for not-yet-known stack depths. + Variable, + /// `{".slot": }` — the slot defined for the referenced region. + LookupSlot, + /// `{".offset": }` — the offset defined for the referenced region. + LookupOffset, + /// `{".length": }` — the length defined for the referenced region. + LookupLength, + /// `{"$read": }` — the raw machine-state bytes in the referenced region. + Read, + /// `{"$sum": [...]}` over any number of operands. + Sum, + /// `{"$product": [...]}` over any number of operands. + Product, + /// `{"$difference": [a, b]}` — clamped at zero. + Difference, + /// `{"$quotient": [a, b]}` — integer division. + Quotient, + /// `{"$remainder": [a, b]}` — modular remainder. + Remainder, + /// `{"$keccak256": [...]}` — hash of the tightly packed operand bytes. + Keccak256, + /// `{"$concat": [...]}` — byte concatenation of the operands. + Concat, + /// `{"$sized": x}` when @a value holds the decimal byte width N, + /// `{"$wordsized": x}` when @a value is unset. + Resize + }; + + Kind kind = Kind::Unknown; + /// Payload interpreted according to @a kind: literal value, variable identifier, + /// referenced region name (or `$this`) or resize width. + std::optional value; + /// Sub-expressions for arithmetic, hashing, concatenation and resize kinds. + std::vector operands; + + static SemanticDebugPointerExpression literal(std::string _value) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Literal; + result.value = std::move(_value); + return result; + } + + static SemanticDebugPointerExpression wordSize() + { + SemanticDebugPointerExpression result; + result.kind = Kind::WordSize; + return result; + } + + static SemanticDebugPointerExpression variable(std::string _identifier) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Variable; + result.value = std::move(_identifier); + return result; + } + + static SemanticDebugPointerExpression lookupSlot(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupSlot; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression lookupOffset(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupOffset; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression lookupLength(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupLength; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression read(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Read; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression sum(std::vector _operands) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Sum; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression product(std::vector _operands) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Product; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression difference( + SemanticDebugPointerExpression _minuend, + SemanticDebugPointerExpression _subtrahend + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Difference; + result.operands.emplace_back(std::move(_minuend)); + result.operands.emplace_back(std::move(_subtrahend)); + return result; + } + + static SemanticDebugPointerExpression quotient( + SemanticDebugPointerExpression _dividend, + SemanticDebugPointerExpression _divisor + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Quotient; + result.operands.emplace_back(std::move(_dividend)); + result.operands.emplace_back(std::move(_divisor)); + return result; + } + + static SemanticDebugPointerExpression remainder( + SemanticDebugPointerExpression _dividend, + SemanticDebugPointerExpression _divisor + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Remainder; + result.operands.emplace_back(std::move(_dividend)); + result.operands.emplace_back(std::move(_divisor)); + return result; + } + + static SemanticDebugPointerExpression keccak256(std::vector _operands) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Keccak256; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression concat(std::vector _operands) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Concat; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression wordSized(SemanticDebugPointerExpression _operand) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Resize; + result.operands.emplace_back(std::move(_operand)); + return result; + } + + static SemanticDebugPointerExpression sized(unsigned _bytes, SemanticDebugPointerExpression _operand) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Resize; + result.value = std::to_string(_bytes); + result.operands.emplace_back(std::move(_operand)); + return result; + } +}; + +struct SemanticDebugType; + +/// Reference to a composed type: a stable compiler type identifier usable as an +/// `{"id": ...}` reference into the type resources table and/or an inline +/// representation. The inline representation is absent when it would recurse +/// into a type that is currently being described (recursive structs). +/// Mirrors the ethdebug/format/type wrapper and specifier schemas. +struct SemanticDebugTypeComponent +{ + enum class Role + { + /// Array element type. + Element, + /// Mapping key type. + Key, + /// Mapping value type. + Value, + /// Struct member or tuple element type. + Member, + /// Function parameter type. + Parameter, + /// Function return type. + Return, + /// Underlying type of a user defined value type. + Underlying, + /// Contract type providing an external function. + Contract + }; + + Role role = Role::Member; + /// Member, element or parameter name, if any. + std::optional name; + /// Stable compiler type identifier, e.g. `t_uint256`. + std::optional referenceID; + /// Inline type representation. Null when cut to break a recursive type cycle, + /// in which case @a referenceID identifies the type. + std::shared_ptr type; +}; + struct SemanticDebugType { enum class Class @@ -79,20 +310,61 @@ struct SemanticDebugType Class typeClass = Class::Unknown; Kind kind = Kind::Unknown; + + // Elementary payloads. std::optional bits; std::optional places; std::optional bytes; - std::optional dataLocation; std::optional payable; + std::optional isLibrary; + std::optional isInterface; + /// Names of the enum members, in declaration order. + std::vector enumValues = {}; + + // Complex payloads. + /// Fixed element count of a statically sized array, as canonical + /// `0x`-prefixed hex. Unset for dynamically sized arrays. + std::optional count; + /// Function types: true for externally callable functions, false for internal + /// ones. Function types with unknown visibility leave this unset. + std::optional externalFunction; + /// Composed types: array element, mapping key/value, struct members, tuple + /// elements, alias underlying type, function parameters and returns. + std::vector components = {}; + + /// Source definition of user defined types (contract, enum, struct, alias, function). + std::optional definitionName; + std::optional definitionLocation; + + // Internal annotations that have no direct ethdebug type schema equivalent. + std::optional dataLocation; std::optional dynamic; }; +/// Internal representation of an ethdebug/format/pointer: a single region of EVM +/// data or a structured collection of sub-pointers. Which fields are meaningful +/// depends on @a pointerClass. struct SemanticDebugPointer { enum class Class { + /// A single addressed range of data. Uses @a location, @a name, @a slot, + /// @a offset and @a length. Region, + /// An ordered composition of sub-pointers in @a group. Group, + /// A dynamically sized repetition: @a count elements, the index bound to + /// @a indexName inside @a listElement. + List, + /// A pointer chosen by the non-zero-ness of @a condition: @a thenPointer, + /// otherwise the optional @a elsePointer. + Conditional, + /// A pointer with auxiliary variables: ordered @a definitions bound inside + /// @a scopeTarget. Later definitions may reference earlier ones. + Scope, + /// A reference to a pointer template defined elsewhere: @a templateName, + /// with produced region names optionally renamed through @a yields. + TemplateReference, Unknown }; @@ -109,12 +381,110 @@ struct SemanticDebugPointer }; Class pointerClass = Class::Unknown; + + /// Template parameters (ethdebug pointer template `expect` list) that must be + /// bound externally before this pointer can be evaluated, e.g. mapping keys. + /// Only meaningful on a root pointer. + std::vector expectedParameters = {}; + + // Class::Region std::optional location; std::optional name; - std::optional slot; - std::optional offset; - std::optional length; - std::vector group; + /// Word-oriented locations (stack, storage, transient) address by slot. + std::optional slot; + /// Byte offset: within the slot for word-oriented locations, absolute for + /// byte-oriented ones (memory, calldata, returndata, code). + std::optional offset; + /// Byte length of the region. + std::optional length; + + // Class::Group + std::vector group = {}; + + // Class::List + std::optional count; + std::optional indexName; + std::shared_ptr listElement; + + // Class::Conditional + std::optional condition; + std::shared_ptr thenPointer; + std::shared_ptr elsePointer; + + // Class::Scope + std::vector> definitions = {}; + std::shared_ptr scopeTarget; + + // Class::TemplateReference + std::optional templateName; + std::vector> yields = {}; + + static SemanticDebugPointer region( + Location _location, + std::optional _name, + std::optional _slot, + std::optional _offset = std::nullopt, + std::optional _length = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Region; + result.location = _location; + result.name = std::move(_name); + result.slot = std::move(_slot); + result.offset = std::move(_offset); + result.length = std::move(_length); + return result; + } + + static SemanticDebugPointer makeGroup(std::vector _members) + { + SemanticDebugPointer result; + result.pointerClass = Class::Group; + result.group = std::move(_members); + return result; + } + + static SemanticDebugPointer list( + SemanticDebugPointerExpression _count, + std::string _indexName, + SemanticDebugPointer _element + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::List; + result.count = std::move(_count); + result.indexName = std::move(_indexName); + result.listElement = std::make_shared(std::move(_element)); + return result; + } + + static SemanticDebugPointer conditional( + SemanticDebugPointerExpression _condition, + SemanticDebugPointer _then, + std::optional _else = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Conditional; + result.condition = std::move(_condition); + result.thenPointer = std::make_shared(std::move(_then)); + if (_else) + result.elsePointer = std::make_shared(std::move(*_else)); + return result; + } + + static SemanticDebugPointer scope( + std::vector> _definitions, + SemanticDebugPointer _target + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Scope; + result.definitions = std::move(_definitions); + result.scopeTarget = std::make_shared(std::move(_target)); + return result; + } }; struct SemanticDebugVariable diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 6ed07ace4005..e58562639688 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include using namespace solidity; @@ -40,6 +41,8 @@ using namespace solidity::langutil; namespace { +using PointerExpression = SemanticDebugPointerExpression; + std::string dataLocationName(DataLocation _location) { switch (_location) @@ -57,9 +60,39 @@ std::string dataLocationName(DataLocation _location) return ""; } -SemanticDebugType semanticType(Type const& _type) +SemanticDebugType semanticType(Type const& _type, std::set& _typesOnPath); + +/// Wraps a composed type. The inline representation is cut when the composed +/// type is already being described further up the recursion path, leaving only +/// the reference ID; this terminates recursive types such as structs that +/// contain themselves through arrays or mappings. +SemanticDebugTypeComponent typeComponent( + SemanticDebugTypeComponent::Role _role, + std::optional _name, + Type const& _type, + std::set& _typesOnPath +) +{ + SemanticDebugTypeComponent result; + result.role = _role; + result.name = std::move(_name); + result.referenceID = _type.identifier(); + if (!_typesOnPath.count(_type.identifier())) + result.type = std::make_shared(semanticType(_type, _typesOnPath)); + return result; +} + +void setDefinition(SemanticDebugType& _result, Declaration const& _declaration) +{ + if (!_declaration.name().empty()) + _result.definitionName = _declaration.name(); + _result.definitionLocation = _declaration.location(); +} + +SemanticDebugType semanticType(Type const& _type, std::set& _typesOnPath) { SemanticDebugType result; + bool const insertedOnPath = _typesOnPath.insert(_type.identifier()).second; switch (_type.category()) { @@ -103,14 +136,24 @@ SemanticDebugType semanticType(Type const& _type) case Type::Category::Array: { auto const& arrayType = dynamic_cast(_type); - result.typeClass = arrayType.isByteArrayOrString() ? - SemanticDebugType::Class::Elementary : - SemanticDebugType::Class::Complex; - result.kind = arrayType.isString() ? - SemanticDebugType::Kind::String : - (arrayType.isByteArray() ? SemanticDebugType::Kind::Bytes : SemanticDebugType::Kind::Array); result.dataLocation = dataLocationName(arrayType.location()); result.dynamic = arrayType.isDynamicallySized(); + if (arrayType.isByteArrayOrString()) + { + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = arrayType.isString() ? SemanticDebugType::Kind::String : SemanticDebugType::Kind::Bytes; + break; + } + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Array; + if (!arrayType.isDynamicallySized()) + result.count = toCompactHexWithPrefix(arrayType.length()); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Element, + std::nullopt, + *arrayType.baseType(), + _typesOnPath + )); break; } case Type::Category::Contract: @@ -119,6 +162,11 @@ SemanticDebugType semanticType(Type const& _type) result.typeClass = SemanticDebugType::Class::Elementary; result.kind = SemanticDebugType::Kind::Contract; result.payable = contractType.isPayable(); + if (contractType.contractDefinition().isLibrary()) + result.isLibrary = true; + if (contractType.contractDefinition().isInterface()) + result.isInterface = true; + setDefinition(result, contractType.contractDefinition()); break; } case Type::Category::Struct: @@ -127,29 +175,105 @@ SemanticDebugType semanticType(Type const& _type) result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Struct; result.dataLocation = dataLocationName(structType.location()); + setDefinition(result, structType.structDefinition()); + for (ASTPointer const& member: structType.structDefinition().members()) + if (member->annotation().type) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Member, + member->name(), + *member->annotation().type, + _typesOnPath + )); break; } case Type::Category::Enum: + { + auto const& enumType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Elementary; result.kind = SemanticDebugType::Kind::Enum; + setDefinition(result, enumType.enumDefinition()); + for (ASTPointer const& member: enumType.enumDefinition().members()) + result.enumValues.emplace_back(member->name()); break; + } case Type::Category::UserDefinedValueType: + { + auto const& aliasType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Alias; + setDefinition(result, aliasType.definition()); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Underlying, + std::nullopt, + aliasType.underlyingType(), + _typesOnPath + )); break; + } case Type::Category::Tuple: + { + auto const& tupleType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Tuple; + for (Type const* component: tupleType.components()) + if (component) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Member, + std::nullopt, + *component, + _typesOnPath + )); break; + } case Type::Category::Mapping: + { + auto const& mappingType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Mapping; result.dataLocation = "storage"; + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Key, + std::nullopt, + *mappingType.keyType(), + _typesOnPath + )); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Value, + std::nullopt, + *mappingType.valueType(), + _typesOnPath + )); break; + } case Type::Category::Function: - result.typeClass = SemanticDebugType::Class::Unknown; + { + auto const& functionType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Function; + if (functionType.kind() == FunctionType::Kind::Internal) + result.externalFunction = false; + else if (functionType.kind() == FunctionType::Kind::External) + result.externalFunction = true; + if (functionType.hasDeclaration()) + setDefinition(result, functionType.declaration()); + for (Type const* parameterType: functionType.parameterTypes()) + if (parameterType) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Parameter, + std::nullopt, + *parameterType, + _typesOnPath + )); + for (Type const* returnType: functionType.returnParameterTypes()) + if (returnType) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Return, + std::nullopt, + *returnType, + _typesOnPath + )); break; + } case Type::Category::RationalNumber: case Type::Category::StringLiteral: case Type::Category::ArraySlice: @@ -161,22 +285,36 @@ SemanticDebugType semanticType(Type const& _type) break; } + if (insertedOnPath) + _typesOnPath.erase(_type.identifier()); return result; } -SemanticDebugPointer stackRegionPointer(std::string _name, std::string _slot) +SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _yulVariable) { - SemanticDebugPointer result; - result.pointerClass = SemanticDebugPointer::Class::Region; - result.location = SemanticDebugPointer::Location::Stack; - result.name = std::move(_name); - result.slot = std::move(_slot); - return result; + // Yul variable names stand in for stack depths that are only known after + // code generation, so the slot is a symbolic variable expression. + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + std::move(_name), + PointerExpression::variable(_yulVariable) + ); +} + +PointerExpression literalExpression(u256 const& _value) +{ + return PointerExpression::literal(toCompactHexWithPrefix(_value)); } -std::string pointerExpression(u256 const& _value) +/// @returns @a _base advanced by @a _slots storage slots, folding the addition +/// into the literal when possible to keep emitted pointers readable. +PointerExpression advanceSlots(PointerExpression _base, u256 const& _slots) { - return toCompactHexWithPrefix(_value); + if (_slots == 0) + return _base; + if (_base.kind == PointerExpression::Kind::Literal && _base.value) + return literalExpression(u256(*_base.value) + _slots); + return PointerExpression::sum({std::move(_base), literalExpression(_slots)}); } std::string storagePointerID(ContractDefinition const& _contract, VariableDeclaration const& _variable) @@ -210,34 +348,344 @@ std::optional stackPointer(VariableDeclaration const& _var SemanticDebugPointer result; result.pointerClass = SemanticDebugPointer::Class::Group; result.name = _variable.name(); - for (std::string& stackSlot: stackSlots) + for (std::string const& stackSlot: stackSlots) result.group.emplace_back(stackRegionPointer(stackSlot, stackSlot)); return result; } -SemanticDebugVariableLocation storageLocation(ContractDefinition const& _contract, VariableDeclaration const& _variable) +/// Builds ethdebug-oriented pointer descriptors for state variables. One builder +/// instance describes one root pointer; mapping keys encountered anywhere in the +/// pointer become template parameters collected in @a expectedParameters. +class StoragePointerBuilder { - return { - .kind = SemanticDebugVariableLocation::Kind::Storage, - .pointerID = storagePointerID(_contract, _variable) - }; +public: + explicit StoragePointerBuilder(SemanticDebugPointer::Location _dataLocation): + m_dataLocation(_dataLocation) + {} + + SemanticDebugPointer build( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + if (auto const* mappingType = dynamic_cast(&_type)) + return buildMapping(*mappingType, std::move(_slot), _name); + + if (auto const* arrayType = dynamic_cast(&_type)) + { + if (arrayType->isByteArrayOrString()) + return buildBytesOrString(std::move(_slot), _name); + if (arrayType->isDynamicallySized()) + return buildDynamicArray(*arrayType, std::move(_slot), _name); + return buildStaticArray(*arrayType, std::move(_slot), _name); + } + + if (auto const* structType = dynamic_cast(&_type)) + return buildStruct(*structType, std::move(_slot), _name); + + return wholeRegion(_type, std::move(_slot), std::move(_offset), _name); + } + + std::vector takeExpectedParameters() + { + return std::move(m_expectedParameters); + } + +private: + /// A single region covering the value as laid out from its base slot. Used + /// for value types and as the fallback for compositions that are not (or + /// cannot be) decomposed further. + SemanticDebugPointer wholeRegion( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + u256 const byteLength = u256(_type.storageBytes()) * _type.storageSize(); + std::optional length; + if (_offset.has_value() || byteLength != 32) + length = literalExpression(byteLength); + return SemanticDebugPointer::region( + m_dataLocation, + _name, + std::move(_slot), + std::move(_offset), + std::move(length) + ); + } + + /// The mapping value lives at `keccak256(pad(key) . slot)`. The key is not + /// stored anywhere; it becomes a template parameter the debugger must bind. + SemanticDebugPointer buildMapping( + MappingType const& _mappingType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const keyParameter = m_expectedParameters.empty() + ? "key" + : "key" + std::to_string(m_expectedParameters.size()); + m_expectedParameters.emplace_back(keyParameter); + + PointerExpression keyExpression = PointerExpression::variable(keyParameter); + // Value-type keys are hashed as full words; bytes and string keys are + // hashed as their raw bytes. + if (_mappingType.keyType()->isValueType()) + keyExpression = PointerExpression::wordSized(std::move(keyExpression)); + + PointerExpression valueSlot = PointerExpression::keccak256({ + std::move(keyExpression), + PointerExpression::wordSized(std::move(_slot)) + }); + return build(*_mappingType.valueType(), std::move(valueSlot), std::nullopt, _name); + } + + /// Dynamic arrays store their element count in the base slot and their data + /// starting at `keccak256(slot)`. + SemanticDebugPointer buildDynamicArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const lengthName = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthRegion = SemanticDebugPointer::region(m_dataLocation, lengthName, _slot); + SemanticDebugPointer elements = SemanticDebugPointer::scope( + {{dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})}}, + elementList( + _arrayType, + PointerExpression::variable(dataVariable), + PointerExpression::read(lengthName), + _name + ) + ); + + std::vector members; + members.emplace_back(std::move(lengthRegion)); + members.emplace_back(std::move(elements)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStaticArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + return elementList(_arrayType, std::move(_slot), literalExpression(_arrayType.length()), _name); + } + + /// A list of element pointers laid out from @a _dataStart. Value-type + /// elements narrower than a word are packed multiple to a slot; everything + /// else advances in whole slots. + SemanticDebugPointer elementList( + ArrayType const& _arrayType, + PointerExpression _dataStart, + PointerExpression _count, + std::string const& _name + ) + { + Type const& elementType = *_arrayType.baseType(); + std::string const indexName = _name + "-index"; + std::string const elementName = _name + "-item"; + PointerExpression index = PointerExpression::variable(indexName); + + SemanticDebugPointer element; + if (elementType.storageBytes() < 32) + { + solAssert(elementType.isValueType(), "Only value types can be packed."); + u256 const elementBytes = elementType.storageBytes(); + u256 const elementsPerSlot = 32 / elementBytes; + element = SemanticDebugPointer::region( + m_dataLocation, + elementName, + PointerExpression::sum({ + std::move(_dataStart), + PointerExpression::quotient(index, literalExpression(elementsPerSlot)) + }), + PointerExpression::product({ + PointerExpression::remainder(index, literalExpression(elementsPerSlot)), + literalExpression(elementBytes) + }), + literalExpression(elementBytes) + ); + } + else + { + u256 const slotsPerElement = elementType.storageSize(); + PointerExpression stride = slotsPerElement == 1 + ? index + : PointerExpression::product({index, literalExpression(slotsPerElement)}); + PointerExpression elementSlot = PointerExpression::sum({std::move(_dataStart), std::move(stride)}); + element = build(elementType, std::move(elementSlot), std::nullopt, elementName); + } + + return SemanticDebugPointer::list(std::move(_count), indexName, std::move(element)); + } + + /// `bytes` and `string` use the compact encoding: short values keep their + /// data in the base slot with the doubled length in the last byte; long + /// values keep `2 * length + 1` in the base slot and their data starting at + /// `keccak256(slot)`. + SemanticDebugPointer buildBytesOrString(PointerExpression _slot, std::string const& _name) + { + std::string const lengthFlagName = _name + "-length-flag"; + std::string const longLengthName = _name + "-long-length"; + std::string const lengthVariable = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthFlagRegion = SemanticDebugPointer::region( + m_dataLocation, + lengthFlagName, + _slot, + PointerExpression::difference(PointerExpression::wordSize(), literalExpression(1)), + literalExpression(1) + ); + + SemanticDebugPointer shortValue = SemanticDebugPointer::scope( + {{lengthVariable, PointerExpression::quotient(PointerExpression::read(lengthFlagName), literalExpression(2))}}, + SemanticDebugPointer::region( + m_dataLocation, + _name, + _slot, + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + + SemanticDebugPointer longLengthRegion = SemanticDebugPointer::region(m_dataLocation, longLengthName, _slot); + SemanticDebugPointer longData = SemanticDebugPointer::scope( + { + { + lengthVariable, + PointerExpression::quotient( + PointerExpression::difference(PointerExpression::read(longLengthName), literalExpression(1)), + literalExpression(2) + ) + }, + {dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})} + }, + SemanticDebugPointer::region( + m_dataLocation, + _name, + PointerExpression::variable(dataVariable), + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + std::vector longMembers; + longMembers.emplace_back(std::move(longLengthRegion)); + longMembers.emplace_back(std::move(longData)); + + // The flag byte is even (2 * length) for short values and odd + // (2 * length + 1) for long ones, so `(flag + 1) % 2` selects short. + SemanticDebugPointer value = SemanticDebugPointer::conditional( + PointerExpression::remainder( + PointerExpression::sum({PointerExpression::read(lengthFlagName), literalExpression(1)}), + literalExpression(2) + ), + std::move(shortValue), + SemanticDebugPointer::makeGroup(std::move(longMembers)) + ); + + std::vector members; + members.emplace_back(std::move(lengthFlagRegion)); + members.emplace_back(std::move(value)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStruct( + StructType const& _structType, + PointerExpression _slot, + std::string const& _name + ) + { + // Recursive structs and pathological nesting fall back to a region + // covering the struct's slots. + if (m_depth >= maxCompositionDepth || m_structsOnPath.count(_structType.identifier())) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + + m_structsOnPath.insert(_structType.identifier()); + ++m_depth; + + std::vector members; + for (ASTPointer const& member: _structType.structDefinition().members()) + { + if (!member->annotation().type) + continue; + auto const& [slotOffset, byteOffset] = _structType.storageOffsetsOfMember(member->name()); + std::optional offset; + if (byteOffset != 0) + offset = literalExpression(byteOffset); + members.emplace_back(build( + *member->annotation().type, + advanceSlots(_slot, slotOffset), + std::move(offset), + _name + "-" + member->name() + )); + } + + --m_depth; + m_structsOnPath.erase(_structType.identifier()); + + if (members.empty()) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + static constexpr unsigned maxCompositionDepth = 16; + + SemanticDebugPointer::Location m_dataLocation; + std::vector m_expectedParameters; + std::set m_structsOnPath; + unsigned m_depth = 0; +}; + +SemanticDebugVariableLocation dataLocationKind(SemanticDebugPointer::Location _location) +{ + SemanticDebugVariableLocation result; + result.kind = _location == SemanticDebugPointer::Location::Transient + ? SemanticDebugVariableLocation::Kind::TransientStorage + : SemanticDebugVariableLocation::Kind::Storage; + return result; +} + +SemanticDebugVariableLocation storageLocation( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + SemanticDebugPointer::Location _dataLocation +) +{ + SemanticDebugVariableLocation result = dataLocationKind(_dataLocation); + result.pointerID = storagePointerID(_contract, _variable); + return result; } -SemanticDebugPointer storagePointer(VariableDeclaration const& _variable, u256 const& _slot, unsigned _offset) +SemanticDebugPointer storagePointer( + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _dataLocation +) { solAssert(_variable.annotation().type, "Storage variable type expected."); - Type const& type = *_variable.annotation().type; - u256 const byteLength = u256(type.storageBytes()) * type.storageSize(); - SemanticDebugPointer result; - result.pointerClass = SemanticDebugPointer::Class::Region; - result.location = SemanticDebugPointer::Location::Storage; - result.name = _variable.name(); - result.slot = pointerExpression(_slot); + StoragePointerBuilder builder{_dataLocation}; + std::optional offset; if (_offset != 0) - result.offset = pointerExpression(u256(_offset)); - if (_offset != 0 || byteLength != 32) - result.length = pointerExpression(byteLength); + offset = literalExpression(_offset); + SemanticDebugPointer result = builder.build( + *_variable.annotation().type, + literalExpression(_slot), + std::move(offset), + _variable.name() + ); + result.expectedParameters = builder.takeExpectedParameters(); return result; } @@ -254,7 +702,8 @@ std::optional ethdebugType(VariableDeclaration const& _variab if (!_variable.annotation().type) return std::nullopt; - return semanticType(*_variable.annotation().type); + std::set typesOnPath; + return semanticType(*_variable.annotation().type, typesOnPath); } SemanticDebugVariable baseSemanticVariable(VariableDeclaration const& _variable) @@ -282,12 +731,13 @@ SemanticDebugVariable storageSemanticVariable( ContractDefinition const& _contract, VariableDeclaration const& _variable, u256 const& _slot, - unsigned _offset + unsigned _offset, + SemanticDebugPointer::Location _dataLocation ) { SemanticDebugVariable result = baseSemanticVariable(_variable); - result.location = storageLocation(_contract, _variable); - result.ethdebugPointer = storagePointer(_variable, _slot, _offset); + result.location = storageLocation(_contract, _variable, _dataLocation); + result.ethdebugPointer = storagePointer(_variable, _slot, _offset, _dataLocation); return result; } @@ -337,7 +787,22 @@ void addStorageVariables(SemanticDebugDataTable& _table, ContractDefinition cons std::vector variables; for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(DataLocation::Storage)) if (!variable->name().empty()) - variables.emplace_back(storageSemanticVariable(_contract, *variable, slot, offset)); + variables.emplace_back(storageSemanticVariable( + _contract, + *variable, + slot, + offset, + SemanticDebugPointer::Location::Storage + )); + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(DataLocation::Transient)) + if (!variable->name().empty()) + variables.emplace_back(storageSemanticVariable( + _contract, + *variable, + slot, + offset, + SemanticDebugPointer::Location::Transient + )); if (variables.empty()) return; diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 156f2ab5fd2f..fb60cff0f089 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -104,67 +104,546 @@ using solidity::util::errinfo_comment; static int g_compilerStackCounts = 0; -static std::optional ethdebugType(langutil::SemanticDebugType const& _type) +static evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( + langutil::SourceLocation const& _location, + unsigned _sourceID +); + +/// Serializes @a _location as an ethdebug source range when it points into a +/// known source unit. +static std::optional ethdebugSourceRange( + langutil::SourceLocation const& _location, + std::map const* _sourceIndices +) { - if (_type.typeClass != langutil::SemanticDebugType::Class::Elementary) + if ( + !_sourceIndices || + !_location.hasText() || + !_location.sourceName || + !_sourceIndices->count(*_location.sourceName) + ) return std::nullopt; + return Json(ethdebugDeclarationRange(_location, _sourceIndices->at(*_location.sourceName))); +} - Json result = Json::object(); - switch (_type.kind) +/// Lowers an internal pointer expression to the ethdebug/format/pointer/expression +/// JSON grammar. Returns nullopt for malformed expressions. +static std::optional ethdebugPointerExpression(langutil::SemanticDebugPointerExpression const& _expression) +{ + using Kind = langutil::SemanticDebugPointerExpression::Kind; + + auto loweredOperands = [&]() -> std::optional { + Json operands = Json::array(); + for (langutil::SemanticDebugPointerExpression const& operand: _expression.operands) + if (std::optional lowered = ethdebugPointerExpression(operand)) + operands.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return operands; + }; + + auto arithmetic = [&](std::string const& _operation, std::optional _arity) -> std::optional { + if (_arity && _expression.operands.size() != *_arity) + return std::nullopt; + std::optional operands = loweredOperands(); + if (!operands) + return std::nullopt; + return Json{{_operation, std::move(*operands)}}; + }; + + switch (_expression.kind) { - case langutil::SemanticDebugType::Kind::Uint: - if (!_type.bits) + case Kind::Literal: + case Kind::Variable: + if (!_expression.value) return std::nullopt; - result["kind"] = "uint"; - result["bits"] = *_type.bits; + return Json(*_expression.value); + case Kind::WordSize: + return Json("$wordsize"); + case Kind::LookupSlot: + case Kind::LookupOffset: + case Kind::LookupLength: + { + if (!_expression.value) + return std::nullopt; + std::string const property = + _expression.kind == Kind::LookupSlot ? ".slot" : + _expression.kind == Kind::LookupOffset ? ".offset" : ".length"; + return Json{{property, *_expression.value}}; + } + case Kind::Read: + if (!_expression.value) + return std::nullopt; + return Json{{"$read", *_expression.value}}; + case Kind::Sum: + return arithmetic("$sum", std::nullopt); + case Kind::Product: + return arithmetic("$product", std::nullopt); + case Kind::Difference: + return arithmetic("$difference", 2); + case Kind::Quotient: + return arithmetic("$quotient", 2); + case Kind::Remainder: + return arithmetic("$remainder", 2); + case Kind::Keccak256: + return arithmetic("$keccak256", std::nullopt); + case Kind::Concat: + return arithmetic("$concat", std::nullopt); + case Kind::Resize: + { + if (_expression.operands.size() != 1) + return std::nullopt; + std::optional operand = ethdebugPointerExpression(_expression.operands.front()); + if (!operand) + return std::nullopt; + if (_expression.value) + return Json{{"$sized" + *_expression.value, std::move(*operand)}}; + return Json{{"$wordsized", std::move(*operand)}}; + } + case Kind::Unknown: break; - case langutil::SemanticDebugType::Kind::Int: - if (!_type.bits) + } + return std::nullopt; +} + +/// Lowers an internal pointer descriptor to an ethdebug/format/pointer. Returns +/// nullopt whenever any part cannot be represented; a partial pointer would +/// mislead a consuming debugger. +static std::optional ethdebugPointer(langutil::SemanticDebugPointer const& _pointer) +{ + using Class = langutil::SemanticDebugPointer::Class; + using Location = langutil::SemanticDebugPointer::Location; + + auto loweredExpression = [](std::optional const& _expression) + -> std::optional + { + if (!_expression) return std::nullopt; - result["kind"] = "int"; - result["bits"] = *_type.bits; + return ethdebugPointerExpression(*_expression); + }; + + switch (_pointer.pointerClass) + { + case Class::Region: + { + if (!_pointer.location) + return std::nullopt; + + std::string locationName; + // Stack, storage and transient storage address word-sized slots; the + // byte-oriented locations address byte ranges via offset and length. + bool wordOriented = false; + switch (*_pointer.location) + { + case Location::Stack: + locationName = "stack"; + wordOriented = true; + break; + case Location::Storage: + locationName = "storage"; + wordOriented = true; + break; + case Location::Transient: + locationName = "transient"; + wordOriented = true; + break; + case Location::Memory: + locationName = "memory"; + break; + case Location::Calldata: + locationName = "calldata"; + break; + case Location::Returndata: + locationName = "returndata"; + break; + case Location::Code: + locationName = "code"; + break; + case Location::Unknown: + return std::nullopt; + } + + Json result = Json::object(); + if (_pointer.name) + result["name"] = *_pointer.name; + result["location"] = locationName; + + if (wordOriented) + { + std::optional slot = loweredExpression(_pointer.slot); + if (!slot) + return std::nullopt; + result["slot"] = std::move(*slot); + } + else if (!_pointer.offset || !_pointer.length) + return std::nullopt; + + if (_pointer.offset) + { + std::optional offset = loweredExpression(_pointer.offset); + if (!offset) + return std::nullopt; + result["offset"] = std::move(*offset); + } + if (_pointer.length) + { + std::optional length = loweredExpression(_pointer.length); + if (!length) + return std::nullopt; + result["length"] = std::move(*length); + } + return result; + } + case Class::Group: + { + if (_pointer.group.empty()) + return std::nullopt; + Json members = Json::array(); + for (langutil::SemanticDebugPointer const& member: _pointer.group) + if (std::optional lowered = ethdebugPointer(member)) + members.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return Json{{"group", std::move(members)}}; + } + case Class::List: + { + if (!_pointer.indexName || !_pointer.listElement) + return std::nullopt; + std::optional count = loweredExpression(_pointer.count); + std::optional element = ethdebugPointer(*_pointer.listElement); + if (!count || !element) + return std::nullopt; + return Json{{"list", Json{ + {"count", std::move(*count)}, + {"each", *_pointer.indexName}, + {"is", std::move(*element)} + }}}; + } + case Class::Conditional: + { + if (!_pointer.thenPointer) + return std::nullopt; + std::optional condition = loweredExpression(_pointer.condition); + std::optional thenPointer = ethdebugPointer(*_pointer.thenPointer); + if (!condition || !thenPointer) + return std::nullopt; + Json result{{"if", std::move(*condition)}, {"then", std::move(*thenPointer)}}; + if (_pointer.elsePointer) + { + std::optional elsePointer = ethdebugPointer(*_pointer.elsePointer); + if (!elsePointer) + return std::nullopt; + result["else"] = std::move(*elsePointer); + } + return result; + } + case Class::Scope: + { + if (_pointer.definitions.empty() || !_pointer.scopeTarget) + return std::nullopt; + std::optional inner = ethdebugPointer(*_pointer.scopeTarget); + if (!inner) + return std::nullopt; + // Scope definitions are ordered, but JSON object members are not, so + // each definition becomes its own nested scope: ordering by structure. + for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) + { + std::optional value = ethdebugPointerExpression(definition->second); + if (!value) + return std::nullopt; + inner = Json{ + {"define", Json{{definition->first, std::move(*value)}}}, + {"in", std::move(*inner)} + }; + } + return inner; + } + case Class::TemplateReference: + { + if (!_pointer.templateName) + return std::nullopt; + Json result{{"template", *_pointer.templateName}}; + if (!_pointer.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _pointer.yields) + yields[producedName] = newName; + result["yields"] = std::move(yields); + } + return result; + } + case Class::Unknown: break; - case langutil::SemanticDebugType::Kind::Ufixed: - if (!_type.bits || !_type.places) + } + return std::nullopt; +} + +static std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +); + +/// Lowers a composed type to an ethdebug type wrapper: `{"type": ...}` with an +/// optional `name`. With @a _referenceComponents the type is referenced by ID +/// into the type resources; otherwise it is inlined, falling back to an ID +/// reference where inlining is impossible (recursive types). +static std::optional ethdebugTypeWrapper( + langutil::SemanticDebugTypeComponent const& _component, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + Json wrapper = Json::object(); + if (_component.name) + wrapper["name"] = *_component.name; + + if ((_referenceComponents || !_component.type) && _component.referenceID) + { + wrapper["type"] = Json{{"id", *_component.referenceID}}; + return wrapper; + } + if (!_component.type) + return std::nullopt; + + if (std::optional inlined = ethdebugType(*_component.type, _referenceComponents, _sourceIndices)) + wrapper["type"] = std::move(*inlined); + else if (_component.referenceID) + wrapper["type"] = Json{{"id", *_component.referenceID}}; + else + return std::nullopt; + return wrapper; +} + +static std::optional ethdebugTypeDefinition( + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + Json definition = Json::object(); + if (_type.definitionName) + definition["name"] = *_type.definitionName; + if (_type.definitionLocation) + if (std::optional location = ethdebugSourceRange(*_type.definitionLocation, _sourceIndices)) + definition["location"] = std::move(*location); + if (definition.empty()) + return std::nullopt; + return definition; +} + +/// Lowers an internal type descriptor to an ethdebug/format/type. Composed types +/// are referenced by ID or inlined according to @a _referenceComponents; see +/// ethdebugTypeWrapper. Returns nullopt for types the ethdebug vocabulary cannot +/// express yet. +static std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + using TypeKind = langutil::SemanticDebugType::Kind; + using Role = langutil::SemanticDebugTypeComponent::Role; + + auto componentsWithRole = [&](Role _role) { + std::vector components; + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + components.emplace_back(&component); + return components; + }; + + auto singleWrapper = [&](Role _role) -> std::optional { + std::vector components = componentsWithRole(_role); + if (components.size() != 1) + return std::nullopt; + return ethdebugTypeWrapper(*components.front(), _referenceComponents, _sourceIndices); + }; + + auto wrapperArray = [&](Role _role) -> std::optional { + Json wrappers = Json::array(); + for (langutil::SemanticDebugTypeComponent const* component: componentsWithRole(_role)) + if (std::optional wrapper = ethdebugTypeWrapper(*component, _referenceComponents, _sourceIndices)) + wrappers.emplace_back(std::move(*wrapper)); + else + return std::nullopt; + return wrappers; + }; + + auto wrappedTuple = [&](Role _role) -> std::optional { + std::optional wrappers = wrapperArray(_role); + if (!wrappers) + return std::nullopt; + return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; + }; + + Json result = Json::object(); + auto attachDefinition = [&]() { + if (std::optional definition = ethdebugTypeDefinition(_type, _sourceIndices)) + result["definition"] = std::move(*definition); + }; + + switch (_type.kind) + { + case TypeKind::Uint: + case TypeKind::Int: + if (!_type.bits) return std::nullopt; - result["kind"] = "ufixed"; + result["kind"] = _type.kind == TypeKind::Uint ? "uint" : "int"; result["bits"] = *_type.bits; - result["places"] = *_type.places; break; - case langutil::SemanticDebugType::Kind::Fixed: + case TypeKind::Ufixed: + case TypeKind::Fixed: if (!_type.bits || !_type.places) return std::nullopt; - result["kind"] = "fixed"; + result["kind"] = _type.kind == TypeKind::Ufixed ? "ufixed" : "fixed"; result["bits"] = *_type.bits; result["places"] = *_type.places; break; - case langutil::SemanticDebugType::Kind::Bool: + case TypeKind::Bool: result["kind"] = "bool"; break; - case langutil::SemanticDebugType::Kind::Bytes: + case TypeKind::Bytes: result["kind"] = "bytes"; if (_type.bytes) result["size"] = *_type.bytes; break; - case langutil::SemanticDebugType::Kind::String: + case TypeKind::String: result["kind"] = "string"; break; - case langutil::SemanticDebugType::Kind::Address: + case TypeKind::Address: result["kind"] = "address"; if (_type.payable) result["payable"] = *_type.payable; break; - case langutil::SemanticDebugType::Kind::Contract: + case TypeKind::Contract: result["kind"] = "contract"; + if (_type.payable) + result["payable"] = *_type.payable; + if (_type.isLibrary && *_type.isLibrary) + result["library"] = true; + else if (_type.isInterface && *_type.isInterface) + result["interface"] = true; + attachDefinition(); + break; + case TypeKind::Enum: + { + result["kind"] = "enum"; + Json values = Json::array(); + for (std::string const& value: _type.enumValues) + values.emplace_back(value); + result["values"] = std::move(values); + attachDefinition(); + break; + } + case TypeKind::Alias: + { + std::optional underlying = singleWrapper(Role::Underlying); + if (!underlying) + return std::nullopt; + result["kind"] = "alias"; + result["contains"] = std::move(*underlying); + attachDefinition(); + break; + } + case TypeKind::Tuple: + { + std::optional elements = wrapperArray(Role::Member); + if (!elements) + return std::nullopt; + result["kind"] = "tuple"; + result["contains"] = std::move(*elements); + break; + } + case TypeKind::Array: + { + std::optional element = singleWrapper(Role::Element); + if (!element) + return std::nullopt; + result["kind"] = "array"; + result["contains"] = std::move(*element); + if (_type.count) + result["count"] = *_type.count; break; - default: + } + case TypeKind::Mapping: + { + std::optional key = singleWrapper(Role::Key); + std::optional value = singleWrapper(Role::Value); + if (!key || !value) + return std::nullopt; + result["kind"] = "mapping"; + result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; + break; + } + case TypeKind::Struct: + { + std::optional members = wrapperArray(Role::Member); + if (!members) + return std::nullopt; + result["kind"] = "struct"; + result["contains"] = std::move(*members); + attachDefinition(); + break; + } + case TypeKind::Function: + { + // The schema requires knowing whether the function follows internal or + // external call semantics. + if (!_type.externalFunction) + return std::nullopt; + std::optional parameters = wrappedTuple(Role::Parameter); + if (!parameters) + return std::nullopt; + result["kind"] = "function"; + result[*_type.externalFunction ? "external" : "internal"] = true; + Json contains{{"parameters", std::move(*parameters)}}; + if (!componentsWithRole(Role::Return).empty()) + { + std::optional returns = wrappedTuple(Role::Return); + if (!returns) + return std::nullopt; + contains["returns"] = std::move(*returns); + } + result["contains"] = std::move(contains); + attachDefinition(); + break; + } + case TypeKind::Unknown: return std::nullopt; } return result; } -static void collectEthdebugTypes(Json& _types, langutil::SemanticDebugDataTable const& _semanticDebugData) +/// Registers @a _type in the type resources table under @a _id together with +/// all composed types it references. Entries are registered before descending +/// so that recursive types terminate. +static void registerEthdebugType( + Json& _types, + std::string const& _id, + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + if (_types.contains(_id)) + return; + + std::optional lowered = ethdebugType(_type, true, _sourceIndices); + if (!lowered) + return; + _types[_id] = std::move(*lowered); + + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerEthdebugType(_types, *component.referenceID, *component.type, _sourceIndices); +} + +static void collectEthdebugTypes( + Json& _types, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices +) { for (auto const& entry: _semanticDebugData.entries()) { @@ -173,36 +652,16 @@ static void collectEthdebugTypes(Json& _types, langutil::SemanticDebugDataTable continue; for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) - { - if (!variable.typeID || !variable.ethdebugType || _types.contains(*variable.typeID)) - continue; - - if (std::optional type = ethdebugType(*variable.ethdebugType)) - _types[*variable.typeID] = std::move(*type); - } + if (variable.typeID && variable.ethdebugType) + registerEthdebugType(_types, *variable.typeID, *variable.ethdebugType, _sourceIndices); } } -static std::optional ethdebugStoragePointer(langutil::SemanticDebugPointer const& _pointer) +static bool isStorageBackedLocation(langutil::SemanticDebugVariableLocation const& _location) { - if ( - _pointer.pointerClass != langutil::SemanticDebugPointer::Class::Region || - !_pointer.location || - *_pointer.location != langutil::SemanticDebugPointer::Location::Storage || - !_pointer.slot - ) - return std::nullopt; - - Json result = Json::object(); - if (_pointer.name) - result["name"] = *_pointer.name; - result["location"] = "storage"; - result["slot"] = *_pointer.slot; - if (_pointer.offset) - result["offset"] = *_pointer.offset; - if (_pointer.length) - result["length"] = *_pointer.length; - return result; + return + _location.kind == langutil::SemanticDebugVariableLocation::Kind::Storage || + _location.kind == langutil::SemanticDebugVariableLocation::Kind::TransientStorage; } static void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable const& _semanticDebugData) @@ -217,18 +676,23 @@ static void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugData { if ( !variable.location || - variable.location->kind != langutil::SemanticDebugVariableLocation::Kind::Storage || + !isStorageBackedLocation(*variable.location) || !variable.location->pointerID || !variable.ethdebugPointer || _pointers.contains(*variable.location->pointerID) ) continue; - if (std::optional pointer = ethdebugStoragePointer(*variable.ethdebugPointer)) + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) + { + Json expect = Json::array(); + for (std::string const& parameter: variable.ethdebugPointer->expectedParameters) + expect.emplace_back(parameter); _pointers[*variable.location->pointerID] = Json{ - {"expect", Json::array()}, + {"expect", std::move(expect)}, {"for", std::move(*pointer)} }; + } } } } @@ -236,10 +700,11 @@ static void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugData static void collectEthdebugResources( Json& _types, Json& _pointers, - langutil::SemanticDebugDataTable const& _semanticDebugData + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices ) { - collectEthdebugTypes(_types, _semanticDebugData); + collectEthdebugTypes(_types, _semanticDebugData, _sourceIndices); collectEthdebugPointers(_pointers, _semanticDebugData); } @@ -283,11 +748,8 @@ static std::optional buildEthdebugPr for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) { // Program-level context currently carries named state variables, - // i.e. variables resolved to a storage location. - if ( - !variable.location || - variable.location->kind != langutil::SemanticDebugVariableLocation::Kind::Storage - ) + // i.e. variables resolved to a storage-backed location. + if (!variable.location || !isStorageBackedLocation(*variable.location)) continue; schema::program::Context::Variable contextVariable; @@ -305,14 +767,18 @@ static std::optional buildEthdebugPr ); if (variable.ethdebugType) - contextVariable.type = ethdebugType(*variable.ethdebugType); + contextVariable.type = ethdebugType(*variable.ethdebugType, false, &_sourceIndices); - if (variable.ethdebugPointer) - if (std::optional pointer = ethdebugStoragePointer(*variable.ethdebugPointer)) + // Pointers with expected template parameters (e.g. mapping keys) are + // not closed expressions; they are only exported as templates in the + // pointer resources. + if (variable.ethdebugPointer && variable.ethdebugPointer->expectedParameters.empty()) + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) { - // The context variable's identifier already names it; the bare - // pointer region must not carry an extra "name" property. - pointer->erase("name"); + // The context variable's identifier already names it; a bare + // top-level region does not need an extra "name" property. + if (pointer->is_object() && pointer->contains("location")) + pointer->erase("name"); contextVariable.pointer = std::move(*pointer); } @@ -1398,6 +1864,7 @@ Json CompilerStack::ethdebug() const solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); Json types = Json::object(); Json pointers = Json::object(); + std::map const sourceIndexMap = sourceIndices(); for (auto const& contractEntry: m_contracts) { Contract const& compiledContract = contractEntry.second; @@ -1405,9 +1872,9 @@ Json CompilerStack::ethdebug() const continue; if (compiledContract.yulSemanticDebugData) - collectEthdebugResources(types, pointers, *compiledContract.yulSemanticDebugData); + collectEthdebugResources(types, pointers, *compiledContract.yulSemanticDebugData, &sourceIndexMap); else - collectEthdebugResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract)); + collectEthdebugResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract), &sourceIndexMap); } return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types), std::move(pointers)); diff --git a/libyul/SemanticDebugDataTransfer.cpp b/libyul/SemanticDebugDataTransfer.cpp index d2f885b73879..7198b8c2451b 100644 --- a/libyul/SemanticDebugDataTransfer.cpp +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -160,23 +160,62 @@ std::set declaredVariableNames(Block const& _root) return names; } -bool stackPointerSurvives(SemanticDebugPointer const& _pointer, std::set const& _yulNames) +/// Collects identifiers of Variable expressions that are not bound within the +/// pointer itself. Scope definitions, list index names and template parameters +/// bind identifiers; whatever remains free must be provided from outside. For +/// internal stack pointers the free variables are generated Yul variable names. +void collectFreeVariables( + SemanticDebugPointerExpression const& _expression, + std::set const& _bound, + std::set& _free +) { - if (_pointer.pointerClass == SemanticDebugPointer::Class::Region) + if (_expression.kind == SemanticDebugPointerExpression::Kind::Variable && _expression.value) { - if (!_pointer.location || *_pointer.location != SemanticDebugPointer::Location::Stack || !_pointer.slot) - return true; - return _yulNames.count(*_pointer.slot) != 0; + if (!_bound.count(*_expression.value)) + _free.insert(*_expression.value); + return; } - if (_pointer.pointerClass == SemanticDebugPointer::Class::Group) - return std::all_of( - _pointer.group.begin(), - _pointer.group.end(), - [&](SemanticDebugPointer const& _part) { return stackPointerSurvives(_part, _yulNames); } - ); + // Lookup and Read reference region names, which live in a separate namespace. + for (SemanticDebugPointerExpression const& operand: _expression.operands) + collectFreeVariables(operand, _bound, _free); +} + +void collectFreeVariables( + SemanticDebugPointer const& _pointer, + std::set _bound, + std::set& _free +) +{ + for (std::string const& parameter: _pointer.expectedParameters) + _bound.insert(parameter); - return true; + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value()) + collectFreeVariables(**expression, _bound, _free); + + // Scope definitions are ordered: each definition may reference the earlier ones. + for (auto const& [definedName, definedValue]: _pointer.definitions) + { + collectFreeVariables(definedValue, _bound, _free); + _bound.insert(definedName); + } + + for (SemanticDebugPointer const& member: _pointer.group) + collectFreeVariables(member, _bound, _free); + + if (_pointer.listElement) + { + std::set elementBound = _bound; + if (_pointer.indexName) + elementBound.insert(*_pointer.indexName); + collectFreeVariables(*_pointer.listElement, std::move(elementBound), _free); + } + + for (auto const* subPointer: {&_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer) + collectFreeVariables(**subPointer, _bound, _free); } bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) @@ -188,7 +227,13 @@ bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set freeVariables; + collectFreeVariables(*_variable.ethdebugPointer, {}, freeVariables); + return std::all_of( + freeVariables.begin(), + freeVariables.end(), + [&](std::string const& _name) { return _yulNames.count(_name) != 0; } + ); } SemanticDebugVariable optimizedOutVariable(SemanticDebugVariable _variable) diff --git a/test/ethdebugSchemaTests/sources/a.sol b/test/ethdebugSchemaTests/sources/a.sol index 07736b72d8f9..fe84d71eee16 100644 --- a/test/ethdebugSchemaTests/sources/a.sol +++ b/test/ethdebugSchemaTests/sources/a.sol @@ -4,6 +4,9 @@ pragma solidity >=0.0; contract A1 { uint128 stored; bool enabled; + mapping(address => uint256) balances; + uint256[] values; + string label; function a(uint x) public pure { assert(x > 0); diff --git a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py index cf5f3a6afd8b..9c7b52b10e2f 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -129,6 +129,41 @@ def test_program_context_includes_state_variables(solc_output): "length": "0x01", } + # Complex state variables carry recursive type representations. + assert variables["balances"]["type"] == { + "kind": "mapping", + "contains": { + "key": {"type": {"kind": "address", "payable": False}}, + "value": {"type": {"kind": "uint", "bits": 256}}, + }, + } + # A mapping pointer expects the key as a template parameter, so it is not a + # closed expression and only appears as a template in the pointer resources. + assert "pointer" not in variables["balances"] + + assert variables["values"]["type"] == { + "kind": "array", + "contains": {"type": {"kind": "uint", "bits": 256}}, + } + values_pointer = variables["values"]["pointer"] + assert values_pointer["group"][0]["name"] == "values-length" + assert values_pointer["group"][0]["location"] == "storage" + assert values_pointer["group"][0]["slot"] == "0x02" + assert values_pointer["group"][1]["define"] == { + "values-data": {"$keccak256": [{"$wordsized": "0x02"}]} + } + values_list = values_pointer["group"][1]["in"]["list"] + assert values_list["count"] == {"$read": "values-length"} + assert values_list["each"] == "values-index" + assert values_list["is"]["slot"] == {"$sum": ["values-data", "values-index"]} + + assert variables["label"]["type"] == {"kind": "string"} + label_pointer = variables["label"]["pointer"] + assert label_pointer["group"][0]["name"] == "label-length-flag" + assert label_pointer["group"][0]["offset"] == {"$difference": ["$wordsize", "0x01"]} + conditional = label_pointer["group"][1] + assert "if" in conditional and "then" in conditional and "else" in conditional + # Contracts without state variables emit no program-level context. assert "context" not in programs[("a.sol", "A2")] @@ -155,13 +190,27 @@ def test_resources_include_standard_json_source_contents(standard_json_input, so def test_resources_include_type_and_pointer_tables(solc_output): - assert solc_output["ethdebug"]["resources"]["types"]["t_uint256"] == { + types = solc_output["ethdebug"]["resources"]["types"] + assert types["t_uint256"] == { "kind": "uint", "bits": 256, } + assert types["t_address"] == {"kind": "address", "payable": False} + + # Composed types reference their component types by ID into this table. + mapping_types = [entry for entry in types.values() if entry.get("kind") == "mapping"] + assert mapping_types == [{ + "kind": "mapping", + "contains": { + "key": {"type": {"id": "t_address"}}, + "value": {"type": {"id": "t_uint256"}}, + }, + }] + array_types = [entry for entry in types.values() if entry.get("kind") == "array"] + assert {"kind": "array", "contains": {"type": {"id": "t_uint256"}}} in array_types + assert {"kind": "string"} in types.values() pointers = solc_output["ethdebug"]["resources"]["pointers"] - assert all(pointer["expect"] == [] for pointer in pointers.values()) pointer_targets = [pointer["for"] for pointer in pointers.values()] assert { "name": "stored", @@ -177,6 +226,17 @@ def test_resources_include_type_and_pointer_tables(solc_output): "length": "0x01", } in pointer_targets + # Mapping pointers are exported as templates over their expected keys. + templates_with_parameters = [pointer for pointer in pointers.values() if pointer["expect"]] + assert templates_with_parameters == [{ + "expect": ["key"], + "for": { + "name": "balances", + "location": "storage", + "slot": {"$keccak256": [{"$wordsized": "key"}, {"$wordsized": "0x01"}]}, + }, + }] + def test_resources_and_compilation_share_compilation(solc_output): assert solc_output["ethdebug"]["resources"]["compilation"] == solc_output["ethdebug"]["compilation"] diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 40028549d446..df3023475acb 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -36,7 +36,7 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) ethdebugPointer.pointerClass = SemanticDebugPointer::Class::Region; ethdebugPointer.location = SemanticDebugPointer::Location::Stack; ethdebugPointer.name = "value"; - ethdebugPointer.slot = "pointer:value"; + ethdebugPointer.slot = SemanticDebugPointerExpression::variable("var_value"); // NOTE: Built imperatively instead of with nested designated initializers, // which crash MSVC with an internal compiler error. @@ -93,7 +93,132 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name, "value"); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot); - BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot, "pointer:value"); + BOOST_CHECK( + debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->kind == + SemanticDebugPointerExpression::Kind::Variable + ); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->value); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->value, "var_value"); +} + +BOOST_AUTO_TEST_CASE(pointer_expressions_compose) +{ + // keccak256($wordsized(key), $wordsized(0x02)) — the storage slot of a + // mapping value, parameterized by the template variable "key". + SemanticDebugPointerExpression slot = SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x02")) + }); + + BOOST_CHECK(slot.kind == SemanticDebugPointerExpression::Kind::Keccak256); + BOOST_REQUIRE_EQUAL(slot.operands.size(), 2); + BOOST_CHECK(slot.operands.at(0).kind == SemanticDebugPointerExpression::Kind::Resize); + BOOST_CHECK(!slot.operands.at(0).value); + BOOST_REQUIRE_EQUAL(slot.operands.at(0).operands.size(), 1); + BOOST_CHECK(slot.operands.at(0).operands.front().kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE_EQUAL(slot.operands.at(1).operands.size(), 1); + BOOST_CHECK(slot.operands.at(1).operands.front().kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(slot.operands.at(1).operands.front().value); + BOOST_CHECK_EQUAL(*slot.operands.at(1).operands.front().value, "0x02"); +} + +BOOST_AUTO_TEST_CASE(pointer_collections_compose) +{ + // group [ length region; define data := keccak256($wordsized(0x00)) in + // list over $read(length) ] — the storage layout of a dynamic array. + SemanticDebugPointer element = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "values-item", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("values-data"), + SemanticDebugPointerExpression::variable("values-index") + }) + ); + + std::vector members; + members.emplace_back(SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "values-length", + SemanticDebugPointerExpression::literal("0x00") + )); + members.emplace_back(SemanticDebugPointer::scope( + {{ + "values-data", + SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x00")) + }) + }}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::read("values-length"), + "values-index", + std::move(element) + ) + )); + SemanticDebugPointer pointer = SemanticDebugPointer::makeGroup(std::move(members)); + + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); + BOOST_CHECK(pointer.group.at(0).pointerClass == SemanticDebugPointer::Class::Region); + BOOST_CHECK(pointer.group.at(1).pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(pointer.group.at(1).definitions.size(), 1); + BOOST_CHECK_EQUAL(pointer.group.at(1).definitions.front().first, "values-data"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget); + BOOST_CHECK(pointer.group.at(1).scopeTarget->pointerClass == SemanticDebugPointer::Class::List); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->count); + BOOST_CHECK(pointer.group.at(1).scopeTarget->count->kind == SemanticDebugPointerExpression::Kind::Read); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->indexName); + BOOST_CHECK_EQUAL(*pointer.group.at(1).scopeTarget->indexName, "values-index"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->listElement); + BOOST_CHECK(pointer.group.at(1).scopeTarget->listElement->pointerClass == SemanticDebugPointer::Class::Region); +} + +BOOST_AUTO_TEST_CASE(recursive_types_reference_by_id) +{ + // struct Node { uint256 value; Node[] children; } — the array element cuts + // the recursion and keeps only the reference ID. + auto uintType = std::make_shared([]{ + SemanticDebugType type; + type.typeClass = SemanticDebugType::Class::Elementary; + type.kind = SemanticDebugType::Kind::Uint; + type.bits = 256; + return type; + }()); + + SemanticDebugTypeComponent cutElement; + cutElement.role = SemanticDebugTypeComponent::Role::Element; + cutElement.referenceID = "t_struct$_Node"; + + SemanticDebugType arrayType; + arrayType.typeClass = SemanticDebugType::Class::Complex; + arrayType.kind = SemanticDebugType::Kind::Array; + arrayType.components.emplace_back(std::move(cutElement)); + + SemanticDebugTypeComponent valueMember; + valueMember.role = SemanticDebugTypeComponent::Role::Member; + valueMember.name = "value"; + valueMember.referenceID = "t_uint256"; + valueMember.type = uintType; + + SemanticDebugTypeComponent childrenMember; + childrenMember.role = SemanticDebugTypeComponent::Role::Member; + childrenMember.name = "children"; + childrenMember.referenceID = "t_array$_t_struct$_Node"; + childrenMember.type = std::make_shared(std::move(arrayType)); + + SemanticDebugType nodeType; + nodeType.typeClass = SemanticDebugType::Class::Complex; + nodeType.kind = SemanticDebugType::Kind::Struct; + nodeType.definitionName = "Node"; + nodeType.components.emplace_back(std::move(valueMember)); + nodeType.components.emplace_back(std::move(childrenMember)); + + BOOST_REQUIRE_EQUAL(nodeType.components.size(), 2); + BOOST_REQUIRE(nodeType.components.at(1).type); + BOOST_REQUIRE_EQUAL(nodeType.components.at(1).type->components.size(), 1); + SemanticDebugTypeComponent const& element = nodeType.components.at(1).type->components.front(); + BOOST_CHECK(!element.type); + BOOST_REQUIRE(element.referenceID); + BOOST_CHECK_EQUAL(*element.referenceID, "t_struct$_Node"); } BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp index b03e978505c9..55b0e21c794a 100644 --- a/test/libsolidity/SemanticDebugData.cpp +++ b/test/libsolidity/SemanticDebugData.cpp @@ -86,6 +86,48 @@ std::vector stackSlots(VariableDeclaration const& _variable) return IRVariable(_variable).stackSlots(); } +void checkVariableExpression( + std::optional const& _expression, + std::string_view _identifier +) +{ + BOOST_REQUIRE(_expression); + BOOST_CHECK(_expression->kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE(_expression->value); + BOOST_CHECK_EQUAL(*_expression->value, std::string(_identifier)); +} + +void checkLiteralExpression( + std::optional const& _expression, + std::string_view _value +) +{ + BOOST_REQUIRE(_expression); + BOOST_CHECK(_expression->kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(_expression->value); + BOOST_CHECK_EQUAL(*_expression->value, std::string(_value)); +} + +/// Checks a `$keccak256($wordsized(...), $wordsized(...))` slot expression and +/// returns the unpadded operands for further inspection. +std::pair checkMappingSlotExpression( + SemanticDebugPointerExpression const& _expression +) +{ + BOOST_CHECK(_expression.kind == SemanticDebugPointerExpression::Kind::Keccak256); + BOOST_REQUIRE_EQUAL(_expression.operands.size(), 2); + for (SemanticDebugPointerExpression const& operand: _expression.operands) + { + BOOST_CHECK(operand.kind == SemanticDebugPointerExpression::Kind::Resize); + BOOST_CHECK(!operand.value); + BOOST_REQUIRE_EQUAL(operand.operands.size(), 1); + } + return { + &_expression.operands.at(0).operands.front(), + &_expression.operands.at(1).operands.front() + }; +} + void checkStackPointer( SemanticDebugPointer const& _pointer, std::string_view _name, @@ -101,8 +143,7 @@ void checkStackPointer( BOOST_CHECK(*_pointer.location == SemanticDebugPointer::Location::Stack); BOOST_REQUIRE(_pointer.name); BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); - BOOST_REQUIRE(_pointer.slot); - BOOST_CHECK_EQUAL(*_pointer.slot, _stackSlots.front()); + checkVariableExpression(_pointer.slot, _stackSlots.front()); BOOST_CHECK(_pointer.group.empty()); return; } @@ -119,11 +160,36 @@ void checkStackPointer( BOOST_CHECK(*stackPointer.location == SemanticDebugPointer::Location::Stack); BOOST_REQUIRE(stackPointer.name); BOOST_CHECK_EQUAL(*stackPointer.name, _stackSlots.at(slotIndex)); - BOOST_REQUIRE(stackPointer.slot); - BOOST_CHECK_EQUAL(*stackPointer.slot, _stackSlots.at(slotIndex)); + checkVariableExpression(stackPointer.slot, _stackSlots.at(slotIndex)); } } +void checkStorageRegion( + SemanticDebugPointer const& _pointer, + std::string_view _name, + std::string_view _slot, + std::optional _offset, + std::optional _length +) +{ + BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(_pointer.location); + BOOST_CHECK(*_pointer.location == SemanticDebugPointer::Location::Storage); + BOOST_REQUIRE(_pointer.name); + BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); + checkLiteralExpression(_pointer.slot, _slot); + + if (_offset) + checkLiteralExpression(_pointer.offset, *_offset); + else + BOOST_CHECK(!_pointer.offset); + + if (_length) + checkLiteralExpression(_pointer.length, *_length); + else + BOOST_CHECK(!_pointer.length); +} + void checkStoragePointer( SemanticDebugVariable const& _variable, ContractDefinition const& _contract, @@ -138,30 +204,7 @@ void checkStoragePointer( BOOST_CHECK_EQUAL(*_variable.location->pointerID, storagePointer(_contract, _variable)); BOOST_REQUIRE(_variable.ethdebugPointer); - SemanticDebugPointer const& pointer = *_variable.ethdebugPointer; - BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(pointer.location); - BOOST_CHECK(*pointer.location == SemanticDebugPointer::Location::Storage); - BOOST_REQUIRE(pointer.name); - BOOST_CHECK_EQUAL(*pointer.name, _variable.name); - BOOST_REQUIRE(pointer.slot); - BOOST_CHECK_EQUAL(*pointer.slot, std::string(_slot)); - - if (_offset) - { - BOOST_REQUIRE(pointer.offset); - BOOST_CHECK_EQUAL(*pointer.offset, std::string(*_offset)); - } - else - BOOST_CHECK(!pointer.offset); - - if (_length) - { - BOOST_REQUIRE(pointer.length); - BOOST_CHECK_EQUAL(*pointer.length, std::string(*_length)); - } - else - BOOST_CHECK(!pointer.length); + checkStorageRegion(*_variable.ethdebugPointer, _variable.name, _slot, _offset, _length); } SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) @@ -559,6 +602,400 @@ BOOST_AUTO_TEST_CASE(state_variables_include_storage_pointer_descriptors) BOOST_CHECK(!findVariableByName(*data, "ignoredImmutable")); } +BOOST_AUTO_TEST_CASE(mapping_state_variables_have_keccak_pointer_templates) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowances; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* balances = findVariableByName(*data, "balances"); + BOOST_REQUIRE(balances); + BOOST_REQUIRE(balances->location); + BOOST_CHECK(balances->location->kind == SemanticDebugVariableLocation::Kind::Storage); + BOOST_REQUIRE(balances->ethdebugPointer); + // The mapping key is not stored anywhere; it must be provided to the + // pointer template as the expected parameter "key". + BOOST_REQUIRE_EQUAL(balances->ethdebugPointer->expectedParameters.size(), 1); + BOOST_CHECK_EQUAL(balances->ethdebugPointer->expectedParameters.front(), "key"); + BOOST_CHECK(balances->ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(balances->ethdebugPointer->slot); + auto const [balancesKey, balancesSlot] = checkMappingSlotExpression(*balances->ethdebugPointer->slot); + BOOST_CHECK(balancesKey->kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE(balancesKey->value); + BOOST_CHECK_EQUAL(*balancesKey->value, "key"); + BOOST_CHECK(balancesSlot->kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(balancesSlot->value); + BOOST_CHECK_EQUAL(*balancesSlot->value, "0x00"); + + // The type descriptor composes key and value types. + BOOST_REQUIRE(balances->ethdebugType); + BOOST_CHECK(balances->ethdebugType->typeClass == SemanticDebugType::Class::Complex); + BOOST_CHECK(balances->ethdebugType->kind == SemanticDebugType::Kind::Mapping); + BOOST_REQUIRE_EQUAL(balances->ethdebugType->components.size(), 2); + SemanticDebugTypeComponent const& keyComponent = balances->ethdebugType->components.at(0); + BOOST_CHECK(keyComponent.role == SemanticDebugTypeComponent::Role::Key); + BOOST_REQUIRE(keyComponent.referenceID); + BOOST_CHECK_EQUAL(*keyComponent.referenceID, "t_address"); + BOOST_REQUIRE(keyComponent.type); + BOOST_CHECK(keyComponent.type->kind == SemanticDebugType::Kind::Address); + SemanticDebugTypeComponent const& valueComponent = balances->ethdebugType->components.at(1); + BOOST_CHECK(valueComponent.role == SemanticDebugTypeComponent::Role::Value); + BOOST_REQUIRE(valueComponent.type); + BOOST_CHECK(valueComponent.type->kind == SemanticDebugType::Kind::Uint); + + // Nested mappings chain the hashes: keccak256(key1 . keccak256(key . slot)). + SemanticDebugVariable const* allowances = findVariableByName(*data, "allowances"); + BOOST_REQUIRE(allowances); + BOOST_REQUIRE(allowances->ethdebugPointer); + BOOST_REQUIRE_EQUAL(allowances->ethdebugPointer->expectedParameters.size(), 2); + BOOST_CHECK_EQUAL(allowances->ethdebugPointer->expectedParameters.at(0), "key"); + BOOST_CHECK_EQUAL(allowances->ethdebugPointer->expectedParameters.at(1), "key1"); + BOOST_REQUIRE(allowances->ethdebugPointer->slot); + auto const [outerKey, innerHash] = checkMappingSlotExpression(*allowances->ethdebugPointer->slot); + BOOST_CHECK(outerKey->kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE(outerKey->value); + BOOST_CHECK_EQUAL(*outerKey->value, "key1"); + auto const [innerKey, innerSlot] = checkMappingSlotExpression(*innerHash); + BOOST_CHECK(innerKey->kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE(innerKey->value); + BOOST_CHECK_EQUAL(*innerKey->value, "key"); + BOOST_CHECK(innerSlot->kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(innerSlot->value); + BOOST_CHECK_EQUAL(*innerSlot->value, "0x01"); +} + +BOOST_AUTO_TEST_CASE(dynamic_array_state_variables_have_length_and_data_pointers) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + uint256[] values; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* values = findVariableByName(*data, "values"); + BOOST_REQUIRE(values); + BOOST_REQUIRE(values->ethdebugPointer); + BOOST_CHECK(values->ethdebugPointer->expectedParameters.empty()); + + // group [ length region at the base slot; + // define values-data := keccak256($wordsized(slot)) in + // list over $read(values-length) ] + SemanticDebugPointer const& pointer = *values->ethdebugPointer; + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); + + checkStorageRegion(pointer.group.at(0), "values-length", "0x00", std::nullopt, std::nullopt); + + SemanticDebugPointer const& dataScope = pointer.group.at(1); + BOOST_CHECK(dataScope.pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(dataScope.definitions.size(), 1); + BOOST_CHECK_EQUAL(dataScope.definitions.front().first, "values-data"); + BOOST_CHECK(dataScope.definitions.front().second.kind == SemanticDebugPointerExpression::Kind::Keccak256); + + BOOST_REQUIRE(dataScope.scopeTarget); + SemanticDebugPointer const& elementList = *dataScope.scopeTarget; + BOOST_CHECK(elementList.pointerClass == SemanticDebugPointer::Class::List); + BOOST_REQUIRE(elementList.count); + BOOST_CHECK(elementList.count->kind == SemanticDebugPointerExpression::Kind::Read); + BOOST_REQUIRE(elementList.count->value); + BOOST_CHECK_EQUAL(*elementList.count->value, "values-length"); + BOOST_REQUIRE(elementList.indexName); + BOOST_CHECK_EQUAL(*elementList.indexName, "values-index"); + BOOST_REQUIRE(elementList.listElement); + BOOST_CHECK(elementList.listElement->pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(elementList.listElement->slot); + BOOST_CHECK(elementList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); +} + +BOOST_AUTO_TEST_CASE(static_array_state_variables_use_element_lists) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + uint64[8] packed; + uint256[3] wide; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + // uint64 elements pack four to a slot: the element region derives slot and + // byte offset from the index. + SemanticDebugVariable const* packed = findVariableByName(*data, "packed"); + BOOST_REQUIRE(packed); + BOOST_REQUIRE(packed->ethdebugType); + BOOST_CHECK(packed->ethdebugType->kind == SemanticDebugType::Kind::Array); + BOOST_REQUIRE(packed->ethdebugType->count); + BOOST_CHECK_EQUAL(*packed->ethdebugType->count, "0x08"); + BOOST_REQUIRE(packed->ethdebugPointer); + SemanticDebugPointer const& packedList = *packed->ethdebugPointer; + BOOST_CHECK(packedList.pointerClass == SemanticDebugPointer::Class::List); + checkLiteralExpression(packedList.count, "0x08"); + BOOST_REQUIRE(packedList.listElement); + BOOST_REQUIRE(packedList.listElement->slot); + BOOST_CHECK(packedList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); + BOOST_REQUIRE(packedList.listElement->offset); + BOOST_CHECK(packedList.listElement->offset->kind == SemanticDebugPointerExpression::Kind::Product); + checkLiteralExpression(packedList.listElement->length, "0x08"); + + // uint256 elements advance one slot per index, starting after the two slots + // occupied by "packed". + SemanticDebugVariable const* wide = findVariableByName(*data, "wide"); + BOOST_REQUIRE(wide); + BOOST_REQUIRE(wide->ethdebugPointer); + SemanticDebugPointer const& wideList = *wide->ethdebugPointer; + BOOST_CHECK(wideList.pointerClass == SemanticDebugPointer::Class::List); + checkLiteralExpression(wideList.count, "0x03"); + BOOST_REQUIRE(wideList.listElement); + BOOST_REQUIRE(wideList.listElement->slot); + BOOST_CHECK(wideList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); + BOOST_REQUIRE_EQUAL(wideList.listElement->slot->operands.size(), 2); + BOOST_CHECK(wideList.listElement->slot->operands.at(0).kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(wideList.listElement->slot->operands.at(0).value); + BOOST_CHECK_EQUAL(*wideList.listElement->slot->operands.at(0).value, "0x02"); + BOOST_CHECK(wideList.listElement->slot->operands.at(1).kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_CHECK(!wideList.listElement->length); +} + +BOOST_AUTO_TEST_CASE(string_state_variables_use_conditional_pointers) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + string label; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* label = findVariableByName(*data, "label"); + BOOST_REQUIRE(label); + BOOST_REQUIRE(label->ethdebugPointer); + + // group [ length-flag byte region; conditional on (flag + 1) % 2: + // short value in place, long value at keccak256(slot) ] + SemanticDebugPointer const& pointer = *label->ethdebugPointer; + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); + + SemanticDebugPointer const& lengthFlag = pointer.group.at(0); + BOOST_CHECK(lengthFlag.pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(lengthFlag.name); + BOOST_CHECK_EQUAL(*lengthFlag.name, "label-length-flag"); + checkLiteralExpression(lengthFlag.slot, "0x00"); + BOOST_REQUIRE(lengthFlag.offset); + BOOST_CHECK(lengthFlag.offset->kind == SemanticDebugPointerExpression::Kind::Difference); + checkLiteralExpression(lengthFlag.length, "0x01"); + + SemanticDebugPointer const& value = pointer.group.at(1); + BOOST_CHECK(value.pointerClass == SemanticDebugPointer::Class::Conditional); + BOOST_REQUIRE(value.condition); + BOOST_CHECK(value.condition->kind == SemanticDebugPointerExpression::Kind::Remainder); + + BOOST_REQUIRE(value.thenPointer); + BOOST_CHECK(value.thenPointer->pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(value.thenPointer->definitions.size(), 1); + BOOST_CHECK_EQUAL(value.thenPointer->definitions.front().first, "label-length"); + BOOST_REQUIRE(value.thenPointer->scopeTarget); + checkVariableExpression(value.thenPointer->scopeTarget->length, "label-length"); + + BOOST_REQUIRE(value.elsePointer); + BOOST_CHECK(value.elsePointer->pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(value.elsePointer->group.size(), 2); + SemanticDebugPointer const& longData = value.elsePointer->group.at(1); + BOOST_CHECK(longData.pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(longData.definitions.size(), 2); + BOOST_CHECK_EQUAL(longData.definitions.at(0).first, "label-length"); + BOOST_CHECK_EQUAL(longData.definitions.at(1).first, "label-data"); + BOOST_CHECK(longData.definitions.at(1).second.kind == SemanticDebugPointerExpression::Kind::Keccak256); + BOOST_REQUIRE(longData.scopeTarget); + checkVariableExpression(longData.scopeTarget->slot, "label-data"); + checkVariableExpression(longData.scopeTarget->length, "label-length"); +} + +BOOST_AUTO_TEST_CASE(struct_state_variables_have_member_pointers) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + struct Point { + uint128 x; + uint128 y; + uint256 z; + } + + Point origin; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* origin = findVariableByName(*data, "origin"); + BOOST_REQUIRE(origin); + + BOOST_REQUIRE(origin->ethdebugType); + BOOST_CHECK(origin->ethdebugType->typeClass == SemanticDebugType::Class::Complex); + BOOST_CHECK(origin->ethdebugType->kind == SemanticDebugType::Kind::Struct); + BOOST_REQUIRE(origin->ethdebugType->definitionName); + BOOST_CHECK_EQUAL(*origin->ethdebugType->definitionName, "Point"); + BOOST_REQUIRE_EQUAL(origin->ethdebugType->components.size(), 3); + BOOST_REQUIRE(origin->ethdebugType->components.at(0).name); + BOOST_CHECK_EQUAL(*origin->ethdebugType->components.at(0).name, "x"); + BOOST_REQUIRE(origin->ethdebugType->components.at(2).name); + BOOST_CHECK_EQUAL(*origin->ethdebugType->components.at(2).name, "z"); + + // Members become individual regions: x and y pack into slot 0, z takes slot 1. + BOOST_REQUIRE(origin->ethdebugPointer); + SemanticDebugPointer const& pointer = *origin->ethdebugPointer; + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 3); + checkStorageRegion(pointer.group.at(0), "origin-x", "0x00", std::nullopt, "0x10"); + checkStorageRegion(pointer.group.at(1), "origin-y", "0x00", "0x10", "0x10"); + checkStorageRegion(pointer.group.at(2), "origin-z", "0x01", std::nullopt, std::nullopt); +} + +BOOST_AUTO_TEST_CASE(recursive_struct_types_are_cut_with_references) +{ + BOOST_REQUIRE(runFramework(R"( + contract C { + struct Node { + uint256 value; + Node[] children; + } + + Node root; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* root = findVariableByName(*data, "root"); + BOOST_REQUIRE(root); + + // The array element inside the struct refers back to the struct: the inline + // representation is cut and only the type reference remains. + BOOST_REQUIRE(root->ethdebugType); + BOOST_CHECK(root->ethdebugType->kind == SemanticDebugType::Kind::Struct); + BOOST_REQUIRE_EQUAL(root->ethdebugType->components.size(), 2); + SemanticDebugTypeComponent const& children = root->ethdebugType->components.at(1); + BOOST_REQUIRE(children.type); + BOOST_CHECK(children.type->kind == SemanticDebugType::Kind::Array); + BOOST_REQUIRE_EQUAL(children.type->components.size(), 1); + SemanticDebugTypeComponent const& element = children.type->components.front(); + BOOST_CHECK(!element.type); + BOOST_REQUIRE(element.referenceID); + BOOST_REQUIRE(root->typeID); + BOOST_CHECK_EQUAL(*element.referenceID, *root->typeID); + + // The pointer recursion falls back to a whole-struct region for the nested + // occurrence instead of recursing forever. + BOOST_REQUIRE(root->ethdebugPointer); + BOOST_CHECK(root->ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(root->ethdebugPointer->group.size(), 2); + SemanticDebugPointer const& childrenPointer = root->ethdebugPointer->group.at(1); + BOOST_CHECK(childrenPointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(childrenPointer.group.size(), 2); + BOOST_REQUIRE(childrenPointer.group.at(1).scopeTarget); + SemanticDebugPointer const& elementPointer = *childrenPointer.group.at(1).scopeTarget; + BOOST_CHECK(elementPointer.pointerClass == SemanticDebugPointer::Class::List); + BOOST_REQUIRE(elementPointer.listElement); + BOOST_CHECK(elementPointer.listElement->pointerClass == SemanticDebugPointer::Class::Region); + checkLiteralExpression(elementPointer.listElement->length, "0x40"); +} + +BOOST_AUTO_TEST_CASE(state_variable_types_cover_enums_aliases_and_contracts) +{ + BOOST_REQUIRE(runFramework(R"( + contract D {} + + contract C { + enum Mode { + Off, + On + } + + type Price is uint128; + + Mode mode; + Price price; + D other; + } + )", PipelineStage::Analysis)); + + ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); + BOOST_REQUIRE(contract); + + SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); + SemanticDebugData::ConstPtr data = table.find(contract->id()); + BOOST_REQUIRE(data); + + SemanticDebugVariable const* mode = findVariableByName(*data, "mode"); + BOOST_REQUIRE(mode); + BOOST_REQUIRE(mode->ethdebugType); + BOOST_CHECK(mode->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(mode->ethdebugType->kind == SemanticDebugType::Kind::Enum); + BOOST_REQUIRE_EQUAL(mode->ethdebugType->enumValues.size(), 2); + BOOST_CHECK_EQUAL(mode->ethdebugType->enumValues.at(0), "Off"); + BOOST_CHECK_EQUAL(mode->ethdebugType->enumValues.at(1), "On"); + BOOST_REQUIRE(mode->ethdebugType->definitionName); + BOOST_CHECK_EQUAL(*mode->ethdebugType->definitionName, "Mode"); + BOOST_REQUIRE(mode->ethdebugType->definitionLocation); + + SemanticDebugVariable const* price = findVariableByName(*data, "price"); + BOOST_REQUIRE(price); + BOOST_REQUIRE(price->ethdebugType); + BOOST_CHECK(price->ethdebugType->kind == SemanticDebugType::Kind::Alias); + BOOST_REQUIRE(price->ethdebugType->definitionName); + BOOST_CHECK_EQUAL(*price->ethdebugType->definitionName, "Price"); + BOOST_REQUIRE_EQUAL(price->ethdebugType->components.size(), 1); + BOOST_CHECK(price->ethdebugType->components.front().role == SemanticDebugTypeComponent::Role::Underlying); + BOOST_REQUIRE(price->ethdebugType->components.front().type); + BOOST_CHECK(price->ethdebugType->components.front().type->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(price->ethdebugType->components.front().type->bits); + BOOST_CHECK_EQUAL(*price->ethdebugType->components.front().type->bits, 128); + + SemanticDebugVariable const* other = findVariableByName(*data, "other"); + BOOST_REQUIRE(other); + BOOST_REQUIRE(other->ethdebugType); + BOOST_CHECK(other->ethdebugType->kind == SemanticDebugType::Kind::Contract); + BOOST_REQUIRE(other->ethdebugType->definitionName); + BOOST_CHECK_EQUAL(*other->ethdebugType->definitionName, "D"); +} + BOOST_AUTO_TEST_CASE(inherited_function_variables_have_semantic_metadata) { BOOST_REQUIRE(runFramework(R"( diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index 27df5462dfe2..082d0c6a6219 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -64,7 +64,7 @@ SemanticDebugPointer stackPointer(std::string _name) pointer.pointerClass = SemanticDebugPointer::Class::Region; pointer.location = SemanticDebugPointer::Location::Stack; pointer.name = _name; - pointer.slot = std::move(_name); + pointer.slot = SemanticDebugPointerExpression::variable(std::move(_name)); return pointer; } @@ -261,6 +261,127 @@ BOOST_AUTO_TEST_CASE(attach_marks_missing_stack_locations_optimized_out) checkStackVariableOptimizedOut(*funDef); } +BOOST_AUTO_TEST_CASE(bound_pointer_variables_do_not_affect_survival) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + 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), "item" (bound as a list index) and "key" (bound as a + // template parameter). Only var_x is a free Yul dependency, so the stack + // location must survive. + SemanticDebugPointer element = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "element", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("var_x"), + SemanticDebugPointerExpression::variable("aux"), + SemanticDebugPointerExpression::variable("item"), + SemanticDebugPointerExpression::variable("key") + }) + ); + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"aux", SemanticDebugPointerExpression::literal("0x01")}}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::literal("0x02"), + "item", + std::move(element) + ) + ); + pointer.expectedParameters = {"key"}; + + SemanticDebugVariable variable; + variable.name = "x"; + variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "var_x" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 23; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(23, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableSurvived(*funDef, "var_x"); +} + +BOOST_AUTO_TEST_CASE(free_pointer_variables_in_expressions_require_yul_names) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + 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. + SemanticDebugPointer pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "element", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("var_x"), + SemanticDebugPointerExpression::variable("var_missing") + }) + ); + + SemanticDebugVariable variable; + variable.name = "x"; + variable.location = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "var_x" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 23; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(23, 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) { OptimiserSettings optimiserSettings = OptimiserSettings::none(); From 0f4a72368fc4ae8d8897c9a981e2c152df421ea7 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 6 Jul 2026 17:28:00 +0200 Subject: [PATCH 20/47] ethdebug: Assert positive width for sized resize expressions --- liblangutil/SemanticDebugData.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 00073d0979d1..ca56657f6dea 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -18,6 +18,7 @@ #pragma once +#include #include #include @@ -231,6 +232,9 @@ struct SemanticDebugPointerExpression static SemanticDebugPointerExpression sized(unsigned _bytes, SemanticDebugPointerExpression _operand) { + // The ethdebug $sized expression requires a positive byte width; + // use wordSized() for word-sized resizing. + solAssert(_bytes > 0); SemanticDebugPointerExpression result; result.kind = Kind::Resize; result.value = std::to_string(_bytes); From 9f4a5246d45fce5f24243aa1ad6111da3503c049 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 6 Jul 2026 17:28:00 +0200 Subject: [PATCH 21/47] ethdebug: Move semantic metadata JSON lowering out of CompilerStack --- libsolidity/CMakeLists.txt | 2 + libsolidity/interface/CompilerStack.cpp | 699 +---------------------- libsolidity/interface/Ethdebug.cpp | 726 ++++++++++++++++++++++++ libsolidity/interface/Ethdebug.h | 66 +++ 4 files changed, 799 insertions(+), 694 deletions(-) create mode 100644 libsolidity/interface/Ethdebug.cpp create mode 100644 libsolidity/interface/Ethdebug.h diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 14e2d10ca2bb..a8ae465b18ed 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -146,6 +146,8 @@ set(sources interface/CompilerStack.cpp interface/CompilerStack.h interface/DebugSettings.h + interface/Ethdebug.cpp + interface/Ethdebug.h interface/FileReader.cpp interface/FileReader.h interface/ImportRemapper.cpp diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index fb60cff0f089..ad97330e3f3c 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -104,696 +105,6 @@ using solidity::util::errinfo_comment; static int g_compilerStackCounts = 0; -static evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( - langutil::SourceLocation const& _location, - unsigned _sourceID -); - -/// Serializes @a _location as an ethdebug source range when it points into a -/// known source unit. -static std::optional ethdebugSourceRange( - langutil::SourceLocation const& _location, - std::map const* _sourceIndices -) -{ - if ( - !_sourceIndices || - !_location.hasText() || - !_location.sourceName || - !_sourceIndices->count(*_location.sourceName) - ) - return std::nullopt; - return Json(ethdebugDeclarationRange(_location, _sourceIndices->at(*_location.sourceName))); -} - -/// Lowers an internal pointer expression to the ethdebug/format/pointer/expression -/// JSON grammar. Returns nullopt for malformed expressions. -static std::optional ethdebugPointerExpression(langutil::SemanticDebugPointerExpression const& _expression) -{ - using Kind = langutil::SemanticDebugPointerExpression::Kind; - - auto loweredOperands = [&]() -> std::optional { - Json operands = Json::array(); - for (langutil::SemanticDebugPointerExpression const& operand: _expression.operands) - if (std::optional lowered = ethdebugPointerExpression(operand)) - operands.emplace_back(std::move(*lowered)); - else - return std::nullopt; - return operands; - }; - - auto arithmetic = [&](std::string const& _operation, std::optional _arity) -> std::optional { - if (_arity && _expression.operands.size() != *_arity) - return std::nullopt; - std::optional operands = loweredOperands(); - if (!operands) - return std::nullopt; - return Json{{_operation, std::move(*operands)}}; - }; - - switch (_expression.kind) - { - case Kind::Literal: - case Kind::Variable: - if (!_expression.value) - return std::nullopt; - return Json(*_expression.value); - case Kind::WordSize: - return Json("$wordsize"); - case Kind::LookupSlot: - case Kind::LookupOffset: - case Kind::LookupLength: - { - if (!_expression.value) - return std::nullopt; - std::string const property = - _expression.kind == Kind::LookupSlot ? ".slot" : - _expression.kind == Kind::LookupOffset ? ".offset" : ".length"; - return Json{{property, *_expression.value}}; - } - case Kind::Read: - if (!_expression.value) - return std::nullopt; - return Json{{"$read", *_expression.value}}; - case Kind::Sum: - return arithmetic("$sum", std::nullopt); - case Kind::Product: - return arithmetic("$product", std::nullopt); - case Kind::Difference: - return arithmetic("$difference", 2); - case Kind::Quotient: - return arithmetic("$quotient", 2); - case Kind::Remainder: - return arithmetic("$remainder", 2); - case Kind::Keccak256: - return arithmetic("$keccak256", std::nullopt); - case Kind::Concat: - return arithmetic("$concat", std::nullopt); - case Kind::Resize: - { - if (_expression.operands.size() != 1) - return std::nullopt; - std::optional operand = ethdebugPointerExpression(_expression.operands.front()); - if (!operand) - return std::nullopt; - if (_expression.value) - return Json{{"$sized" + *_expression.value, std::move(*operand)}}; - return Json{{"$wordsized", std::move(*operand)}}; - } - case Kind::Unknown: - break; - } - return std::nullopt; -} - -/// Lowers an internal pointer descriptor to an ethdebug/format/pointer. Returns -/// nullopt whenever any part cannot be represented; a partial pointer would -/// mislead a consuming debugger. -static std::optional ethdebugPointer(langutil::SemanticDebugPointer const& _pointer) -{ - using Class = langutil::SemanticDebugPointer::Class; - using Location = langutil::SemanticDebugPointer::Location; - - auto loweredExpression = [](std::optional const& _expression) - -> std::optional - { - if (!_expression) - return std::nullopt; - return ethdebugPointerExpression(*_expression); - }; - - switch (_pointer.pointerClass) - { - case Class::Region: - { - if (!_pointer.location) - return std::nullopt; - - std::string locationName; - // Stack, storage and transient storage address word-sized slots; the - // byte-oriented locations address byte ranges via offset and length. - bool wordOriented = false; - switch (*_pointer.location) - { - case Location::Stack: - locationName = "stack"; - wordOriented = true; - break; - case Location::Storage: - locationName = "storage"; - wordOriented = true; - break; - case Location::Transient: - locationName = "transient"; - wordOriented = true; - break; - case Location::Memory: - locationName = "memory"; - break; - case Location::Calldata: - locationName = "calldata"; - break; - case Location::Returndata: - locationName = "returndata"; - break; - case Location::Code: - locationName = "code"; - break; - case Location::Unknown: - return std::nullopt; - } - - Json result = Json::object(); - if (_pointer.name) - result["name"] = *_pointer.name; - result["location"] = locationName; - - if (wordOriented) - { - std::optional slot = loweredExpression(_pointer.slot); - if (!slot) - return std::nullopt; - result["slot"] = std::move(*slot); - } - else if (!_pointer.offset || !_pointer.length) - return std::nullopt; - - if (_pointer.offset) - { - std::optional offset = loweredExpression(_pointer.offset); - if (!offset) - return std::nullopt; - result["offset"] = std::move(*offset); - } - if (_pointer.length) - { - std::optional length = loweredExpression(_pointer.length); - if (!length) - return std::nullopt; - result["length"] = std::move(*length); - } - return result; - } - case Class::Group: - { - if (_pointer.group.empty()) - return std::nullopt; - Json members = Json::array(); - for (langutil::SemanticDebugPointer const& member: _pointer.group) - if (std::optional lowered = ethdebugPointer(member)) - members.emplace_back(std::move(*lowered)); - else - return std::nullopt; - return Json{{"group", std::move(members)}}; - } - case Class::List: - { - if (!_pointer.indexName || !_pointer.listElement) - return std::nullopt; - std::optional count = loweredExpression(_pointer.count); - std::optional element = ethdebugPointer(*_pointer.listElement); - if (!count || !element) - return std::nullopt; - return Json{{"list", Json{ - {"count", std::move(*count)}, - {"each", *_pointer.indexName}, - {"is", std::move(*element)} - }}}; - } - case Class::Conditional: - { - if (!_pointer.thenPointer) - return std::nullopt; - std::optional condition = loweredExpression(_pointer.condition); - std::optional thenPointer = ethdebugPointer(*_pointer.thenPointer); - if (!condition || !thenPointer) - return std::nullopt; - Json result{{"if", std::move(*condition)}, {"then", std::move(*thenPointer)}}; - if (_pointer.elsePointer) - { - std::optional elsePointer = ethdebugPointer(*_pointer.elsePointer); - if (!elsePointer) - return std::nullopt; - result["else"] = std::move(*elsePointer); - } - return result; - } - case Class::Scope: - { - if (_pointer.definitions.empty() || !_pointer.scopeTarget) - return std::nullopt; - std::optional inner = ethdebugPointer(*_pointer.scopeTarget); - if (!inner) - return std::nullopt; - // Scope definitions are ordered, but JSON object members are not, so - // each definition becomes its own nested scope: ordering by structure. - for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) - { - std::optional value = ethdebugPointerExpression(definition->second); - if (!value) - return std::nullopt; - inner = Json{ - {"define", Json{{definition->first, std::move(*value)}}}, - {"in", std::move(*inner)} - }; - } - return inner; - } - case Class::TemplateReference: - { - if (!_pointer.templateName) - return std::nullopt; - Json result{{"template", *_pointer.templateName}}; - if (!_pointer.yields.empty()) - { - Json yields = Json::object(); - for (auto const& [producedName, newName]: _pointer.yields) - yields[producedName] = newName; - result["yields"] = std::move(yields); - } - return result; - } - case Class::Unknown: - break; - } - return std::nullopt; -} - -static std::optional ethdebugType( - langutil::SemanticDebugType const& _type, - bool _referenceComponents, - std::map const* _sourceIndices -); - -/// Lowers a composed type to an ethdebug type wrapper: `{"type": ...}` with an -/// optional `name`. With @a _referenceComponents the type is referenced by ID -/// into the type resources; otherwise it is inlined, falling back to an ID -/// reference where inlining is impossible (recursive types). -static std::optional ethdebugTypeWrapper( - langutil::SemanticDebugTypeComponent const& _component, - bool _referenceComponents, - std::map const* _sourceIndices -) -{ - Json wrapper = Json::object(); - if (_component.name) - wrapper["name"] = *_component.name; - - if ((_referenceComponents || !_component.type) && _component.referenceID) - { - wrapper["type"] = Json{{"id", *_component.referenceID}}; - return wrapper; - } - if (!_component.type) - return std::nullopt; - - if (std::optional inlined = ethdebugType(*_component.type, _referenceComponents, _sourceIndices)) - wrapper["type"] = std::move(*inlined); - else if (_component.referenceID) - wrapper["type"] = Json{{"id", *_component.referenceID}}; - else - return std::nullopt; - return wrapper; -} - -static std::optional ethdebugTypeDefinition( - langutil::SemanticDebugType const& _type, - std::map const* _sourceIndices -) -{ - Json definition = Json::object(); - if (_type.definitionName) - definition["name"] = *_type.definitionName; - if (_type.definitionLocation) - if (std::optional location = ethdebugSourceRange(*_type.definitionLocation, _sourceIndices)) - definition["location"] = std::move(*location); - if (definition.empty()) - return std::nullopt; - return definition; -} - -/// Lowers an internal type descriptor to an ethdebug/format/type. Composed types -/// are referenced by ID or inlined according to @a _referenceComponents; see -/// ethdebugTypeWrapper. Returns nullopt for types the ethdebug vocabulary cannot -/// express yet. -static std::optional ethdebugType( - langutil::SemanticDebugType const& _type, - bool _referenceComponents, - std::map const* _sourceIndices -) -{ - using TypeKind = langutil::SemanticDebugType::Kind; - using Role = langutil::SemanticDebugTypeComponent::Role; - - auto componentsWithRole = [&](Role _role) { - std::vector components; - for (langutil::SemanticDebugTypeComponent const& component: _type.components) - if (component.role == _role) - components.emplace_back(&component); - return components; - }; - - auto singleWrapper = [&](Role _role) -> std::optional { - std::vector components = componentsWithRole(_role); - if (components.size() != 1) - return std::nullopt; - return ethdebugTypeWrapper(*components.front(), _referenceComponents, _sourceIndices); - }; - - auto wrapperArray = [&](Role _role) -> std::optional { - Json wrappers = Json::array(); - for (langutil::SemanticDebugTypeComponent const* component: componentsWithRole(_role)) - if (std::optional wrapper = ethdebugTypeWrapper(*component, _referenceComponents, _sourceIndices)) - wrappers.emplace_back(std::move(*wrapper)); - else - return std::nullopt; - return wrappers; - }; - - auto wrappedTuple = [&](Role _role) -> std::optional { - std::optional wrappers = wrapperArray(_role); - if (!wrappers) - return std::nullopt; - return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; - }; - - Json result = Json::object(); - auto attachDefinition = [&]() { - if (std::optional definition = ethdebugTypeDefinition(_type, _sourceIndices)) - result["definition"] = std::move(*definition); - }; - - switch (_type.kind) - { - case TypeKind::Uint: - case TypeKind::Int: - if (!_type.bits) - return std::nullopt; - result["kind"] = _type.kind == TypeKind::Uint ? "uint" : "int"; - result["bits"] = *_type.bits; - break; - case TypeKind::Ufixed: - case TypeKind::Fixed: - if (!_type.bits || !_type.places) - return std::nullopt; - result["kind"] = _type.kind == TypeKind::Ufixed ? "ufixed" : "fixed"; - result["bits"] = *_type.bits; - result["places"] = *_type.places; - break; - case TypeKind::Bool: - result["kind"] = "bool"; - break; - case TypeKind::Bytes: - result["kind"] = "bytes"; - if (_type.bytes) - result["size"] = *_type.bytes; - break; - case TypeKind::String: - result["kind"] = "string"; - break; - case TypeKind::Address: - result["kind"] = "address"; - if (_type.payable) - result["payable"] = *_type.payable; - break; - case TypeKind::Contract: - result["kind"] = "contract"; - if (_type.payable) - result["payable"] = *_type.payable; - if (_type.isLibrary && *_type.isLibrary) - result["library"] = true; - else if (_type.isInterface && *_type.isInterface) - result["interface"] = true; - attachDefinition(); - break; - case TypeKind::Enum: - { - result["kind"] = "enum"; - Json values = Json::array(); - for (std::string const& value: _type.enumValues) - values.emplace_back(value); - result["values"] = std::move(values); - attachDefinition(); - break; - } - case TypeKind::Alias: - { - std::optional underlying = singleWrapper(Role::Underlying); - if (!underlying) - return std::nullopt; - result["kind"] = "alias"; - result["contains"] = std::move(*underlying); - attachDefinition(); - break; - } - case TypeKind::Tuple: - { - std::optional elements = wrapperArray(Role::Member); - if (!elements) - return std::nullopt; - result["kind"] = "tuple"; - result["contains"] = std::move(*elements); - break; - } - case TypeKind::Array: - { - std::optional element = singleWrapper(Role::Element); - if (!element) - return std::nullopt; - result["kind"] = "array"; - result["contains"] = std::move(*element); - if (_type.count) - result["count"] = *_type.count; - break; - } - case TypeKind::Mapping: - { - std::optional key = singleWrapper(Role::Key); - std::optional value = singleWrapper(Role::Value); - if (!key || !value) - return std::nullopt; - result["kind"] = "mapping"; - result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; - break; - } - case TypeKind::Struct: - { - std::optional members = wrapperArray(Role::Member); - if (!members) - return std::nullopt; - result["kind"] = "struct"; - result["contains"] = std::move(*members); - attachDefinition(); - break; - } - case TypeKind::Function: - { - // The schema requires knowing whether the function follows internal or - // external call semantics. - if (!_type.externalFunction) - return std::nullopt; - std::optional parameters = wrappedTuple(Role::Parameter); - if (!parameters) - return std::nullopt; - result["kind"] = "function"; - result[*_type.externalFunction ? "external" : "internal"] = true; - Json contains{{"parameters", std::move(*parameters)}}; - if (!componentsWithRole(Role::Return).empty()) - { - std::optional returns = wrappedTuple(Role::Return); - if (!returns) - return std::nullopt; - contains["returns"] = std::move(*returns); - } - result["contains"] = std::move(contains); - attachDefinition(); - break; - } - case TypeKind::Unknown: - return std::nullopt; - } - - return result; -} - -/// Registers @a _type in the type resources table under @a _id together with -/// all composed types it references. Entries are registered before descending -/// so that recursive types terminate. -static void registerEthdebugType( - Json& _types, - std::string const& _id, - langutil::SemanticDebugType const& _type, - std::map const* _sourceIndices -) -{ - if (_types.contains(_id)) - return; - - std::optional lowered = ethdebugType(_type, true, _sourceIndices); - if (!lowered) - return; - _types[_id] = std::move(*lowered); - - for (langutil::SemanticDebugTypeComponent const& component: _type.components) - if (component.referenceID && component.type) - registerEthdebugType(_types, *component.referenceID, *component.type, _sourceIndices); -} - -static void collectEthdebugTypes( - Json& _types, - langutil::SemanticDebugDataTable const& _semanticDebugData, - std::map const* _sourceIndices -) -{ - for (auto const& entry: _semanticDebugData.entries()) - { - auto const& debugData = entry.second; - if (!debugData) - continue; - - for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) - if (variable.typeID && variable.ethdebugType) - registerEthdebugType(_types, *variable.typeID, *variable.ethdebugType, _sourceIndices); - } -} - -static bool isStorageBackedLocation(langutil::SemanticDebugVariableLocation const& _location) -{ - return - _location.kind == langutil::SemanticDebugVariableLocation::Kind::Storage || - _location.kind == langutil::SemanticDebugVariableLocation::Kind::TransientStorage; -} - -static void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable const& _semanticDebugData) -{ - for (auto const& entry: _semanticDebugData.entries()) - { - auto const& debugData = entry.second; - if (!debugData) - continue; - - for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) - { - if ( - !variable.location || - !isStorageBackedLocation(*variable.location) || - !variable.location->pointerID || - !variable.ethdebugPointer || - _pointers.contains(*variable.location->pointerID) - ) - continue; - - if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) - { - Json expect = Json::array(); - for (std::string const& parameter: variable.ethdebugPointer->expectedParameters) - expect.emplace_back(parameter); - _pointers[*variable.location->pointerID] = Json{ - {"expect", std::move(expect)}, - {"for", std::move(*pointer)} - }; - } - } - } -} - -static void collectEthdebugResources( - Json& _types, - Json& _pointers, - langutil::SemanticDebugDataTable const& _semanticDebugData, - std::map const* _sourceIndices -) -{ - collectEthdebugTypes(_types, _semanticDebugData, _sourceIndices); - collectEthdebugPointers(_pointers, _semanticDebugData); -} - -static evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( - langutil::SourceLocation const& _location, - unsigned _sourceID -) -{ - namespace schema = evmasm::ethdebug::schema; - schema::materials::Reference reference; - reference.id = schema::materials::ID{static_cast(_sourceID)}; - reference.type = std::nullopt; - - schema::materials::SourceRange::Range range{ - .length = schema::data::Unsigned{_location.end - _location.start}, - .offset = schema::data::Unsigned{_location.start} - }; - - schema::materials::SourceRange sourceRange; - sourceRange.source = std::move(reference); - sourceRange.range = range; - return sourceRange; -} - -/// Builds the program-level ethdebug context: the contract's named state -/// variables, each with its declaration range, ethdebug type, and storage -/// pointer. Returns nullopt when there are no such variables. -static std::optional buildEthdebugProgramContext( - langutil::SemanticDebugDataTable const& _semanticDebugData, - std::map const& _sourceIndices -) -{ - namespace schema = evmasm::ethdebug::schema; - std::vector variables; - for (auto const& entry: _semanticDebugData.entries()) - { - auto const& debugData = entry.second; - if (!debugData) - continue; - - for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) - { - // Program-level context currently carries named state variables, - // i.e. variables resolved to a storage-backed location. - if (!variable.location || !isStorageBackedLocation(*variable.location)) - continue; - - schema::program::Context::Variable contextVariable; - contextVariable.identifier = variable.name; - - if ( - variable.declarationLocation && - variable.declarationLocation->hasText() && - variable.declarationLocation->sourceName && - _sourceIndices.count(*variable.declarationLocation->sourceName) - ) - contextVariable.declaration = ethdebugDeclarationRange( - *variable.declarationLocation, - _sourceIndices.at(*variable.declarationLocation->sourceName) - ); - - if (variable.ethdebugType) - contextVariable.type = ethdebugType(*variable.ethdebugType, false, &_sourceIndices); - - // Pointers with expected template parameters (e.g. mapping keys) are - // not closed expressions; they are only exported as templates in the - // pointer resources. - if (variable.ethdebugPointer && variable.ethdebugPointer->expectedParameters.empty()) - if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) - { - // The context variable's identifier already names it; a bare - // top-level region does not need an extra "name" property. - if (pointer->is_object() && pointer->contains("location")) - pointer->erase("name"); - contextVariable.pointer = std::move(*pointer); - } - - variables.emplace_back(std::move(contextVariable)); - } - } - - if (variables.empty()) - return std::nullopt; - - schema::program::Context context; - context.variables = std::move(variables); - return context; -} - CompilerStack::CompilerStack(ReadCallback::Callback _readFile): m_readFile{std::move(_readFile)}, m_objectOptimizer(std::make_shared()), @@ -1872,9 +1183,9 @@ Json CompilerStack::ethdebug() const continue; if (compiledContract.yulSemanticDebugData) - collectEthdebugResources(types, pointers, *compiledContract.yulSemanticDebugData, &sourceIndexMap); + Ethdebug::collectResources(types, pointers, *compiledContract.yulSemanticDebugData, &sourceIndexMap); else - collectEthdebugResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract), &sourceIndexMap); + Ethdebug::collectResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract), &sourceIndexMap); } return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types), std::move(pointers)); @@ -1925,8 +1236,8 @@ Json CompilerStack::ethdebug(Contract const& _contract, bool _runtime) const std::optional programContext = _contract.yulSemanticDebugData - ? buildEthdebugProgramContext(*_contract.yulSemanticDebugData, sourceIndexMap) - : buildEthdebugProgramContext(buildSemanticDebugDataTable(*_contract.contract), sourceIndexMap); + ? Ethdebug::programContext(*_contract.yulSemanticDebugData, sourceIndexMap) + : Ethdebug::programContext(buildSemanticDebugDataTable(*_contract.contract), sourceIndexMap); return evmasm::ethdebug::program( _contract.contract->name(), diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp new file mode 100644 index 000000000000..217239c3b3eb --- /dev/null +++ b/libsolidity/interface/Ethdebug.cpp @@ -0,0 +1,726 @@ +/* + 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 +/** + * Lowers internal semantic debug metadata to public ethdebug JSON. + */ + +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::frontend; + +namespace +{ +evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( + langutil::SourceLocation const& _location, + unsigned _sourceID +) +{ + namespace schema = evmasm::ethdebug::schema; + schema::materials::Reference reference; + reference.id = schema::materials::ID{static_cast(_sourceID)}; + reference.type = std::nullopt; + + schema::materials::SourceRange::Range range{ + .length = schema::data::Unsigned{_location.end - _location.start}, + .offset = schema::data::Unsigned{_location.start} + }; + + schema::materials::SourceRange sourceRange; + sourceRange.source = std::move(reference); + sourceRange.range = range; + return sourceRange; +} + +/// Serializes @a _location as an ethdebug source range when it points into a +/// known source unit. +std::optional ethdebugSourceRange( + langutil::SourceLocation const& _location, + std::map const* _sourceIndices +) +{ + if ( + !_sourceIndices || + !_location.hasText() || + !_location.sourceName || + !_sourceIndices->count(*_location.sourceName) + ) + return std::nullopt; + return Json(ethdebugDeclarationRange(_location, _sourceIndices->at(*_location.sourceName))); +} + +/// Lowers an internal pointer expression to the ethdebug/format/pointer/expression +/// JSON grammar. Returns nullopt for malformed expressions. +std::optional ethdebugPointerExpression(langutil::SemanticDebugPointerExpression const& _expression) +{ + using Kind = langutil::SemanticDebugPointerExpression::Kind; + + auto loweredOperands = [&]() -> std::optional { + Json operands = Json::array(); + for (langutil::SemanticDebugPointerExpression const& operand: _expression.operands) + if (std::optional lowered = ethdebugPointerExpression(operand)) + operands.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return operands; + }; + + auto arithmetic = [&](std::string const& _operation, std::optional _arity) -> std::optional { + if (_arity && _expression.operands.size() != *_arity) + return std::nullopt; + std::optional operands = loweredOperands(); + if (!operands) + return std::nullopt; + return Json{{_operation, std::move(*operands)}}; + }; + + switch (_expression.kind) + { + case Kind::Literal: + case Kind::Variable: + if (!_expression.value) + return std::nullopt; + return Json(*_expression.value); + case Kind::WordSize: + return Json("$wordsize"); + case Kind::LookupSlot: + case Kind::LookupOffset: + case Kind::LookupLength: + { + if (!_expression.value) + return std::nullopt; + std::string const property = + _expression.kind == Kind::LookupSlot ? ".slot" : + _expression.kind == Kind::LookupOffset ? ".offset" : ".length"; + return Json{{property, *_expression.value}}; + } + case Kind::Read: + if (!_expression.value) + return std::nullopt; + return Json{{"$read", *_expression.value}}; + case Kind::Sum: + return arithmetic("$sum", std::nullopt); + case Kind::Product: + return arithmetic("$product", std::nullopt); + case Kind::Difference: + return arithmetic("$difference", 2); + case Kind::Quotient: + return arithmetic("$quotient", 2); + case Kind::Remainder: + return arithmetic("$remainder", 2); + case Kind::Keccak256: + return arithmetic("$keccak256", std::nullopt); + case Kind::Concat: + return arithmetic("$concat", std::nullopt); + case Kind::Resize: + { + if (_expression.operands.size() != 1) + return std::nullopt; + std::optional operand = ethdebugPointerExpression(_expression.operands.front()); + if (!operand) + return std::nullopt; + if (_expression.value) + return Json{{"$sized" + *_expression.value, std::move(*operand)}}; + return Json{{"$wordsized", std::move(*operand)}}; + } + case Kind::Unknown: + break; + } + return std::nullopt; +} + +/// Lowers an internal pointer descriptor to an ethdebug/format/pointer. Returns +/// nullopt whenever any part cannot be represented; a partial pointer would +/// mislead a consuming debugger. +std::optional ethdebugPointer(langutil::SemanticDebugPointer const& _pointer) +{ + using Class = langutil::SemanticDebugPointer::Class; + using Location = langutil::SemanticDebugPointer::Location; + + auto loweredExpression = [](std::optional const& _expression) + -> std::optional + { + if (!_expression) + return std::nullopt; + return ethdebugPointerExpression(*_expression); + }; + + switch (_pointer.pointerClass) + { + case Class::Region: + { + if (!_pointer.location) + return std::nullopt; + + std::string locationName; + // Stack, storage and transient storage address word-sized slots; the + // byte-oriented locations address byte ranges via offset and length. + bool wordOriented = false; + switch (*_pointer.location) + { + case Location::Stack: + locationName = "stack"; + wordOriented = true; + break; + case Location::Storage: + locationName = "storage"; + wordOriented = true; + break; + case Location::Transient: + locationName = "transient"; + wordOriented = true; + break; + case Location::Memory: + locationName = "memory"; + break; + case Location::Calldata: + locationName = "calldata"; + break; + case Location::Returndata: + locationName = "returndata"; + break; + case Location::Code: + locationName = "code"; + break; + case Location::Unknown: + return std::nullopt; + } + + Json result = Json::object(); + if (_pointer.name) + result["name"] = *_pointer.name; + result["location"] = locationName; + + if (wordOriented) + { + std::optional slot = loweredExpression(_pointer.slot); + if (!slot) + return std::nullopt; + result["slot"] = std::move(*slot); + } + else if (!_pointer.offset || !_pointer.length) + return std::nullopt; + + if (_pointer.offset) + { + std::optional offset = loweredExpression(_pointer.offset); + if (!offset) + return std::nullopt; + result["offset"] = std::move(*offset); + } + if (_pointer.length) + { + std::optional length = loweredExpression(_pointer.length); + if (!length) + return std::nullopt; + result["length"] = std::move(*length); + } + return result; + } + case Class::Group: + { + if (_pointer.group.empty()) + return std::nullopt; + Json members = Json::array(); + for (langutil::SemanticDebugPointer const& member: _pointer.group) + if (std::optional lowered = ethdebugPointer(member)) + members.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return Json{{"group", std::move(members)}}; + } + case Class::List: + { + if (!_pointer.indexName || !_pointer.listElement) + return std::nullopt; + std::optional count = loweredExpression(_pointer.count); + std::optional element = ethdebugPointer(*_pointer.listElement); + if (!count || !element) + return std::nullopt; + return Json{{"list", Json{ + {"count", std::move(*count)}, + {"each", *_pointer.indexName}, + {"is", std::move(*element)} + }}}; + } + case Class::Conditional: + { + if (!_pointer.thenPointer) + return std::nullopt; + std::optional condition = loweredExpression(_pointer.condition); + std::optional thenPointer = ethdebugPointer(*_pointer.thenPointer); + if (!condition || !thenPointer) + return std::nullopt; + Json result{{"if", std::move(*condition)}, {"then", std::move(*thenPointer)}}; + if (_pointer.elsePointer) + { + std::optional elsePointer = ethdebugPointer(*_pointer.elsePointer); + if (!elsePointer) + return std::nullopt; + result["else"] = std::move(*elsePointer); + } + return result; + } + case Class::Scope: + { + if (_pointer.definitions.empty() || !_pointer.scopeTarget) + return std::nullopt; + std::optional inner = ethdebugPointer(*_pointer.scopeTarget); + if (!inner) + return std::nullopt; + // Scope definitions are ordered, but JSON object members are not, so + // each definition becomes its own nested scope: ordering by structure. + for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) + { + std::optional value = ethdebugPointerExpression(definition->second); + if (!value) + return std::nullopt; + inner = Json{ + {"define", Json{{definition->first, std::move(*value)}}}, + {"in", std::move(*inner)} + }; + } + return inner; + } + case Class::TemplateReference: + { + if (!_pointer.templateName) + return std::nullopt; + Json result{{"template", *_pointer.templateName}}; + if (!_pointer.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _pointer.yields) + yields[producedName] = newName; + result["yields"] = std::move(yields); + } + return result; + } + case Class::Unknown: + break; + } + return std::nullopt; +} + +std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +); + +/// Lowers a composed type to an ethdebug type wrapper: `{"type": ...}` with an +/// optional `name`. With @a _referenceComponents the type is referenced by ID +/// into the type resources; otherwise it is inlined, falling back to an ID +/// reference where inlining is impossible (recursive types). +std::optional ethdebugTypeWrapper( + langutil::SemanticDebugTypeComponent const& _component, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + Json wrapper = Json::object(); + if (_component.name) + wrapper["name"] = *_component.name; + + if ((_referenceComponents || !_component.type) && _component.referenceID) + { + wrapper["type"] = Json{{"id", *_component.referenceID}}; + return wrapper; + } + if (!_component.type) + return std::nullopt; + + if (std::optional inlined = ethdebugType(*_component.type, _referenceComponents, _sourceIndices)) + wrapper["type"] = std::move(*inlined); + else if (_component.referenceID) + wrapper["type"] = Json{{"id", *_component.referenceID}}; + else + return std::nullopt; + return wrapper; +} + +std::optional ethdebugTypeDefinition( + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + Json definition = Json::object(); + if (_type.definitionName) + definition["name"] = *_type.definitionName; + if (_type.definitionLocation) + if (std::optional location = ethdebugSourceRange(*_type.definitionLocation, _sourceIndices)) + definition["location"] = std::move(*location); + if (definition.empty()) + return std::nullopt; + return definition; +} + +/// Lowers an internal type descriptor to an ethdebug/format/type. Composed types +/// are referenced by ID or inlined according to @a _referenceComponents; see +/// ethdebugTypeWrapper. Returns nullopt for types the ethdebug vocabulary cannot +/// express yet. +std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + using TypeKind = langutil::SemanticDebugType::Kind; + using Role = langutil::SemanticDebugTypeComponent::Role; + + auto componentsWithRole = [&](Role _role) { + std::vector components; + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + components.emplace_back(&component); + return components; + }; + + auto singleWrapper = [&](Role _role) -> std::optional { + std::vector components = componentsWithRole(_role); + if (components.size() != 1) + return std::nullopt; + return ethdebugTypeWrapper(*components.front(), _referenceComponents, _sourceIndices); + }; + + auto wrapperArray = [&](Role _role) -> std::optional { + Json wrappers = Json::array(); + for (langutil::SemanticDebugTypeComponent const* component: componentsWithRole(_role)) + if (std::optional wrapper = ethdebugTypeWrapper(*component, _referenceComponents, _sourceIndices)) + wrappers.emplace_back(std::move(*wrapper)); + else + return std::nullopt; + return wrappers; + }; + + auto wrappedTuple = [&](Role _role) -> std::optional { + std::optional wrappers = wrapperArray(_role); + if (!wrappers) + return std::nullopt; + return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; + }; + + Json result = Json::object(); + auto attachDefinition = [&]() { + if (std::optional definition = ethdebugTypeDefinition(_type, _sourceIndices)) + result["definition"] = std::move(*definition); + }; + + switch (_type.kind) + { + case TypeKind::Uint: + case TypeKind::Int: + if (!_type.bits) + return std::nullopt; + result["kind"] = _type.kind == TypeKind::Uint ? "uint" : "int"; + result["bits"] = *_type.bits; + break; + case TypeKind::Ufixed: + case TypeKind::Fixed: + if (!_type.bits || !_type.places) + return std::nullopt; + result["kind"] = _type.kind == TypeKind::Ufixed ? "ufixed" : "fixed"; + result["bits"] = *_type.bits; + result["places"] = *_type.places; + break; + case TypeKind::Bool: + result["kind"] = "bool"; + break; + case TypeKind::Bytes: + result["kind"] = "bytes"; + if (_type.bytes) + result["size"] = *_type.bytes; + break; + case TypeKind::String: + result["kind"] = "string"; + break; + case TypeKind::Address: + result["kind"] = "address"; + if (_type.payable) + result["payable"] = *_type.payable; + break; + case TypeKind::Contract: + result["kind"] = "contract"; + if (_type.payable) + result["payable"] = *_type.payable; + if (_type.isLibrary && *_type.isLibrary) + result["library"] = true; + else if (_type.isInterface && *_type.isInterface) + result["interface"] = true; + attachDefinition(); + break; + case TypeKind::Enum: + { + result["kind"] = "enum"; + Json values = Json::array(); + for (std::string const& value: _type.enumValues) + values.emplace_back(value); + result["values"] = std::move(values); + attachDefinition(); + break; + } + case TypeKind::Alias: + { + std::optional underlying = singleWrapper(Role::Underlying); + if (!underlying) + return std::nullopt; + result["kind"] = "alias"; + result["contains"] = std::move(*underlying); + attachDefinition(); + break; + } + case TypeKind::Tuple: + { + std::optional elements = wrapperArray(Role::Member); + if (!elements) + return std::nullopt; + result["kind"] = "tuple"; + result["contains"] = std::move(*elements); + break; + } + case TypeKind::Array: + { + std::optional element = singleWrapper(Role::Element); + if (!element) + return std::nullopt; + result["kind"] = "array"; + result["contains"] = std::move(*element); + if (_type.count) + result["count"] = *_type.count; + break; + } + case TypeKind::Mapping: + { + std::optional key = singleWrapper(Role::Key); + std::optional value = singleWrapper(Role::Value); + if (!key || !value) + return std::nullopt; + result["kind"] = "mapping"; + result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; + break; + } + case TypeKind::Struct: + { + std::optional members = wrapperArray(Role::Member); + if (!members) + return std::nullopt; + result["kind"] = "struct"; + result["contains"] = std::move(*members); + attachDefinition(); + break; + } + case TypeKind::Function: + { + // The schema requires knowing whether the function follows internal or + // external call semantics. + if (!_type.externalFunction) + return std::nullopt; + std::optional parameters = wrappedTuple(Role::Parameter); + if (!parameters) + return std::nullopt; + result["kind"] = "function"; + result[*_type.externalFunction ? "external" : "internal"] = true; + Json contains{{"parameters", std::move(*parameters)}}; + if (!componentsWithRole(Role::Return).empty()) + { + std::optional returns = wrappedTuple(Role::Return); + if (!returns) + return std::nullopt; + contains["returns"] = std::move(*returns); + } + result["contains"] = std::move(contains); + attachDefinition(); + break; + } + case TypeKind::Unknown: + return std::nullopt; + } + + return result; +} + +/// Registers @a _type in the type resources table under @a _id together with +/// all composed types it references. Entries are registered before descending +/// so that recursive types terminate. +void registerEthdebugType( + Json& _types, + std::string const& _id, + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + if (_types.contains(_id)) + return; + + std::optional lowered = ethdebugType(_type, true, _sourceIndices); + if (!lowered) + return; + _types[_id] = std::move(*lowered); + + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerEthdebugType(_types, *component.referenceID, *component.type, _sourceIndices); +} + +void collectEthdebugTypes( + Json& _types, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices +) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + if (variable.typeID && variable.ethdebugType) + registerEthdebugType(_types, *variable.typeID, *variable.ethdebugType, _sourceIndices); + } +} + +bool isStorageBackedLocation(langutil::SemanticDebugVariableLocation const& _location) +{ + return + _location.kind == langutil::SemanticDebugVariableLocation::Kind::Storage || + _location.kind == langutil::SemanticDebugVariableLocation::Kind::TransientStorage; +} + +void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable const& _semanticDebugData) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + if ( + !variable.location || + !isStorageBackedLocation(*variable.location) || + !variable.location->pointerID || + !variable.ethdebugPointer || + _pointers.contains(*variable.location->pointerID) + ) + continue; + + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) + { + Json expect = Json::array(); + for (std::string const& parameter: variable.ethdebugPointer->expectedParameters) + expect.emplace_back(parameter); + _pointers[*variable.location->pointerID] = Json{ + {"expect", std::move(expect)}, + {"for", std::move(*pointer)} + }; + } + } + } +} + +} + +void Ethdebug::collectResources( + Json& _types, + Json& _pointers, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices +) +{ + collectEthdebugTypes(_types, _semanticDebugData, _sourceIndices); + collectEthdebugPointers(_pointers, _semanticDebugData); +} + +std::optional Ethdebug::programContext( + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const& _sourceIndices +) +{ + namespace schema = evmasm::ethdebug::schema; + std::vector variables; + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + // Program-level context currently carries named state variables, + // i.e. variables resolved to a storage-backed location. + if (!variable.location || !isStorageBackedLocation(*variable.location)) + continue; + + schema::program::Context::Variable contextVariable; + contextVariable.identifier = variable.name; + + if ( + variable.declarationLocation && + variable.declarationLocation->hasText() && + variable.declarationLocation->sourceName && + _sourceIndices.count(*variable.declarationLocation->sourceName) + ) + contextVariable.declaration = ethdebugDeclarationRange( + *variable.declarationLocation, + _sourceIndices.at(*variable.declarationLocation->sourceName) + ); + + if (variable.ethdebugType) + contextVariable.type = ethdebugType(*variable.ethdebugType, false, &_sourceIndices); + + // Pointers with expected template parameters (e.g. mapping keys) are + // not closed expressions; they are only exported as templates in the + // pointer resources. + if (variable.ethdebugPointer && variable.ethdebugPointer->expectedParameters.empty()) + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) + { + // The context variable's identifier already names it; a bare + // top-level region does not need an extra "name" property. + if (pointer->is_object() && pointer->contains("location")) + pointer->erase("name"); + contextVariable.pointer = std::move(*pointer); + } + + variables.emplace_back(std::move(contextVariable)); + } + } + + if (variables.empty()) + return std::nullopt; + + schema::program::Context context; + context.variables = std::move(variables); + return context; +} diff --git a/libsolidity/interface/Ethdebug.h b/libsolidity/interface/Ethdebug.h new file mode 100644 index 000000000000..4cf59448f77c --- /dev/null +++ b/libsolidity/interface/Ethdebug.h @@ -0,0 +1,66 @@ +/* + 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 +/** + * Lowers internal semantic debug metadata to public ethdebug JSON. + */ + +#pragma once + +#include + +#include + +#include + +#include +#include +#include + +namespace solidity::frontend +{ + +/// Lowers the compiler-internal semantic debug metadata (see +/// liblangutil/SemanticDebugData.h) to the public ethdebug JSON vocabulary: +/// type and pointer resource tables and the program-level context. +class Ethdebug +{ +public: + /// Collects ethdebug type and pointer resources from @a _semanticDebugData + /// into @a _types and @a _pointers. Types are registered under their compiler + /// type identifiers with composed types referenced by ID and registered + /// transitively; pointers become templates over their expected parameters. + /// @a _sourceIndices maps source unit names to ethdebug source IDs and may be + /// null, in which case definition source locations are omitted. + static void collectResources( + Json& _types, + Json& _pointers, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices + ); + + /// Builds the program-level ethdebug context: the contract's named state + /// variables, each with its declaration range, ethdebug type, and — when the + /// pointer needs no expected parameters — its storage pointer. + /// @returns nullopt when there are no such variables. + static std::optional programContext( + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const& _sourceIndices + ); +}; + +} From 68e2379792709ae4df273f700d12ab98b94453bf Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 20 Jul 2026 10:18:57 +0200 Subject: [PATCH 22/47] ethdebug: Describe variable data locations at EVM level Replace the Immutable and Constant location kinds with Returndata, Code and Computed so that locations describe machine state rather than Solidity-only language constructs. Immutables are memory-located while creation code initializes them and code-located at runtime; folded constants are code bytes while constant expressions that must execute are computed values. Rename the ambiguous fields to declarationSourceLocation and dataLocation, make the source identifier optional, and emit unnamed declarations such as unnamed return parameters instead of skipping them. The SemanticDebugData soltest suite asserted on the removed language-level kinds; it is superseded by the Ethdebug isoltest coverage added in a subsequent commit. --- liblangutil/SemanticDebugData.h | 13 +- .../codegen/ir/SemanticDebugDataBuilder.cpp | 26 +- libsolidity/interface/Ethdebug.cpp | 26 +- libyul/SemanticDebugDataTransfer.cpp | 6 +- test/CMakeLists.txt | 1 - test/liblangutil/DebugData.cpp | 17 +- test/libsolidity/SemanticDebugData.cpp | 1136 ----------------- test/libyul/DebugData.cpp | 36 +- 8 files changed, 68 insertions(+), 1193 deletions(-) delete mode 100644 test/libsolidity/SemanticDebugData.cpp diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index ca56657f6dea..6581feb5bcd6 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -40,8 +40,9 @@ struct SemanticDebugVariableLocation TransientStorage, Memory, Calldata, - Immutable, - Constant, + Returndata, + Code, + Computed, OptimizedOut }; @@ -493,12 +494,14 @@ struct SemanticDebugPointer struct SemanticDebugVariable { - std::string name; + /// Source-language identifier, if the variable has one. Unnamed return + /// parameters are still represented and leave this unset. + std::optional identifier; std::optional declarationAstID; - std::optional declarationLocation; + std::optional declarationSourceLocation; std::optional typeID; std::optional ethdebugType; - std::optional location; + std::optional dataLocation; std::optional ethdebugPointer; }; diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index e58562639688..92b2b4ef15b1 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -301,6 +301,11 @@ SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _y ); } +std::optional sourceIdentifier(VariableDeclaration const& _variable) +{ + return _variable.name().empty() ? std::nullopt : std::make_optional(_variable.name()); +} + PointerExpression literalExpression(u256 const& _value) { return PointerExpression::literal(toCompactHexWithPrefix(_value)); @@ -343,11 +348,15 @@ std::optional stackPointer(VariableDeclaration const& _var return std::nullopt; if (stackSlots.size() == 1) - return stackRegionPointer(_variable.name(), stackSlots.front()); + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + sourceIdentifier(_variable), + PointerExpression::variable(stackSlots.front()) + ); SemanticDebugPointer result; result.pointerClass = SemanticDebugPointer::Class::Group; - result.name = _variable.name(); + result.name = sourceIdentifier(_variable); for (std::string const& stackSlot: stackSlots) result.group.emplace_back(stackRegionPointer(stackSlot, stackSlot)); return result; @@ -709,12 +718,12 @@ std::optional ethdebugType(VariableDeclaration const& _variab SemanticDebugVariable baseSemanticVariable(VariableDeclaration const& _variable) { return { - .name = _variable.name(), + .identifier = sourceIdentifier(_variable), .declarationAstID = _variable.id(), - .declarationLocation = _variable.location(), + .declarationSourceLocation = _variable.location(), .typeID = typeID(_variable), .ethdebugType = ethdebugType(_variable), - .location = std::nullopt, + .dataLocation = std::nullopt, .ethdebugPointer = std::nullopt }; } @@ -722,7 +731,7 @@ SemanticDebugVariable baseSemanticVariable(VariableDeclaration const& _variable) SemanticDebugVariable stackSemanticVariable(VariableDeclaration const& _variable) { SemanticDebugVariable result = baseSemanticVariable(_variable); - result.location = stackLocation(_variable); + result.dataLocation = stackLocation(_variable); result.ethdebugPointer = stackPointer(_variable); return result; } @@ -736,7 +745,7 @@ SemanticDebugVariable storageSemanticVariable( ) { SemanticDebugVariable result = baseSemanticVariable(_variable); - result.location = storageLocation(_contract, _variable, _dataLocation); + result.dataLocation = storageLocation(_contract, _variable, _dataLocation); result.ethdebugPointer = storagePointer(_variable, _slot, _offset, _dataLocation); return result; } @@ -747,8 +756,7 @@ void appendVariables( ) { for (ASTPointer const& declaration: _declarations) - if (!declaration->name().empty()) - _variables.emplace_back(stackSemanticVariable(*declaration)); + _variables.emplace_back(stackSemanticVariable(*declaration)); } void appendCallableVariables(std::vector& _variables, FunctionDefinition const& _function) diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp index 217239c3b3eb..91d382d4bdbb 100644 --- a/libsolidity/interface/Ethdebug.cpp +++ b/libsolidity/interface/Ethdebug.cpp @@ -628,11 +628,11 @@ void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable c for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) { if ( - !variable.location || - !isStorageBackedLocation(*variable.location) || - !variable.location->pointerID || + !variable.dataLocation || + !isStorageBackedLocation(*variable.dataLocation) || + !variable.dataLocation->pointerID || !variable.ethdebugPointer || - _pointers.contains(*variable.location->pointerID) + _pointers.contains(*variable.dataLocation->pointerID) ) continue; @@ -641,7 +641,7 @@ void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable c Json expect = Json::array(); for (std::string const& parameter: variable.ethdebugPointer->expectedParameters) expect.emplace_back(parameter); - _pointers[*variable.location->pointerID] = Json{ + _pointers[*variable.dataLocation->pointerID] = Json{ {"expect", std::move(expect)}, {"for", std::move(*pointer)} }; @@ -680,21 +680,21 @@ std::optional Ethdebug::programConte { // Program-level context currently carries named state variables, // i.e. variables resolved to a storage-backed location. - if (!variable.location || !isStorageBackedLocation(*variable.location)) + if (!variable.dataLocation || !isStorageBackedLocation(*variable.dataLocation)) continue; schema::program::Context::Variable contextVariable; - contextVariable.identifier = variable.name; + contextVariable.identifier = variable.identifier; if ( - variable.declarationLocation && - variable.declarationLocation->hasText() && - variable.declarationLocation->sourceName && - _sourceIndices.count(*variable.declarationLocation->sourceName) + variable.declarationSourceLocation && + variable.declarationSourceLocation->hasText() && + variable.declarationSourceLocation->sourceName && + _sourceIndices.count(*variable.declarationSourceLocation->sourceName) ) contextVariable.declaration = ethdebugDeclarationRange( - *variable.declarationLocation, - _sourceIndices.at(*variable.declarationLocation->sourceName) + *variable.declarationSourceLocation, + _sourceIndices.at(*variable.declarationSourceLocation->sourceName) ); if (variable.ethdebugType) diff --git a/libyul/SemanticDebugDataTransfer.cpp b/libyul/SemanticDebugDataTransfer.cpp index 7198b8c2451b..eb8fa0e6e0b3 100644 --- a/libyul/SemanticDebugDataTransfer.cpp +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -221,8 +221,8 @@ void collectFreeVariables( bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) { if ( - !_variable.location || - _variable.location->kind != SemanticDebugVariableLocation::Kind::Stack || + !_variable.dataLocation || + _variable.dataLocation->kind != SemanticDebugVariableLocation::Kind::Stack || !_variable.ethdebugPointer ) return true; @@ -238,7 +238,7 @@ bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set("input.sol")}; + variable.declarationSourceLocation = SourceLocation{1, 6, std::make_shared("input.sol")}; variable.typeID = "type:uint256"; variable.ethdebugType = ethdebugType; - variable.location = location; + variable.dataLocation = location; variable.ethdebugPointer = ethdebugPointer; SemanticDebugData data; @@ -74,7 +74,8 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_REQUIRE(debugData->semanticDebugData->lexicalScopeID); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->lexicalScopeID, 17); BOOST_REQUIRE_EQUAL(debugData->semanticDebugData->variableDefinitions.size(), 1); - BOOST_CHECK_EQUAL(debugData->semanticDebugData->variableDefinitions.front().name, "value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().identifier); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().identifier, "value"); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().typeID); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().typeID, "type:uint256"); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType); @@ -82,10 +83,10 @@ BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->kind == SemanticDebugType::Kind::Uint); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits); BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits, 256); - BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location); - BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().location->pointerID); - BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().location->pointerID, "pointer:value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().dataLocation); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().dataLocation->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().dataLocation->pointerID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().dataLocation->pointerID, "pointer:value"); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer); BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Region); BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->location); diff --git a/test/libsolidity/SemanticDebugData.cpp b/test/libsolidity/SemanticDebugData.cpp deleted file mode 100644 index 55b0e21c794a..000000000000 --- a/test/libsolidity/SemanticDebugData.cpp +++ /dev/null @@ -1,1136 +0,0 @@ -/* - 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 -#include - -using namespace solidity; -using namespace solidity::frontend; -using namespace solidity::frontend::test; -using namespace solidity::langutil; - -namespace -{ - -std::string stackPointer(VariableDeclaration const& _variable) -{ - return "var_" + _variable.name() + "_" + std::to_string(_variable.id()); -} - -std::string storagePointer(ContractDefinition const& _contract, SemanticDebugVariable const& _variable) -{ - BOOST_REQUIRE(_variable.declarationAstID); - return "storage_" + std::to_string(_contract.id()) + "_" + std::to_string(*_variable.declarationAstID); -} - -FunctionDefinition const* findFunction(ContractDefinition const& _contract, std::string_view _name) -{ - for (FunctionDefinition const* candidate: _contract.definedFunctions()) - if (candidate->name() == _name) - return candidate; - return nullptr; -} - -ModifierDefinition const* findModifier(ContractDefinition const& _contract, std::string_view _name) -{ - for (ModifierDefinition const* candidate: _contract.functionModifiers()) - if (candidate->name() == _name) - return candidate; - return nullptr; -} - -SemanticDebugVariable const* findVariableByName(SemanticDebugData const& _data, std::string_view _name) -{ - for (SemanticDebugVariable const& variable: _data.variableDefinitions) - if (variable.name == _name) - return &variable; - - return nullptr; -} - -std::vector stackSlots(VariableDeclaration const& _variable) -{ - return IRVariable(_variable).stackSlots(); -} - -void checkVariableExpression( - std::optional const& _expression, - std::string_view _identifier -) -{ - BOOST_REQUIRE(_expression); - BOOST_CHECK(_expression->kind == SemanticDebugPointerExpression::Kind::Variable); - BOOST_REQUIRE(_expression->value); - BOOST_CHECK_EQUAL(*_expression->value, std::string(_identifier)); -} - -void checkLiteralExpression( - std::optional const& _expression, - std::string_view _value -) -{ - BOOST_REQUIRE(_expression); - BOOST_CHECK(_expression->kind == SemanticDebugPointerExpression::Kind::Literal); - BOOST_REQUIRE(_expression->value); - BOOST_CHECK_EQUAL(*_expression->value, std::string(_value)); -} - -/// Checks a `$keccak256($wordsized(...), $wordsized(...))` slot expression and -/// returns the unpadded operands for further inspection. -std::pair checkMappingSlotExpression( - SemanticDebugPointerExpression const& _expression -) -{ - BOOST_CHECK(_expression.kind == SemanticDebugPointerExpression::Kind::Keccak256); - BOOST_REQUIRE_EQUAL(_expression.operands.size(), 2); - for (SemanticDebugPointerExpression const& operand: _expression.operands) - { - BOOST_CHECK(operand.kind == SemanticDebugPointerExpression::Kind::Resize); - BOOST_CHECK(!operand.value); - BOOST_REQUIRE_EQUAL(operand.operands.size(), 1); - } - return { - &_expression.operands.at(0).operands.front(), - &_expression.operands.at(1).operands.front() - }; -} - -void checkStackPointer( - SemanticDebugPointer const& _pointer, - std::string_view _name, - std::vector const& _stackSlots -) -{ - BOOST_REQUIRE(!_stackSlots.empty()); - - if (_stackSlots.size() == 1) - { - BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(_pointer.location); - BOOST_CHECK(*_pointer.location == SemanticDebugPointer::Location::Stack); - BOOST_REQUIRE(_pointer.name); - BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); - checkVariableExpression(_pointer.slot, _stackSlots.front()); - BOOST_CHECK(_pointer.group.empty()); - return; - } - - BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE(_pointer.name); - BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); - BOOST_REQUIRE_EQUAL(_pointer.group.size(), _stackSlots.size()); - for (size_t slotIndex = 0; slotIndex < _stackSlots.size(); ++slotIndex) - { - SemanticDebugPointer const& stackPointer = _pointer.group.at(slotIndex); - BOOST_CHECK(stackPointer.pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(stackPointer.location); - BOOST_CHECK(*stackPointer.location == SemanticDebugPointer::Location::Stack); - BOOST_REQUIRE(stackPointer.name); - BOOST_CHECK_EQUAL(*stackPointer.name, _stackSlots.at(slotIndex)); - checkVariableExpression(stackPointer.slot, _stackSlots.at(slotIndex)); - } -} - -void checkStorageRegion( - SemanticDebugPointer const& _pointer, - std::string_view _name, - std::string_view _slot, - std::optional _offset, - std::optional _length -) -{ - BOOST_CHECK(_pointer.pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(_pointer.location); - BOOST_CHECK(*_pointer.location == SemanticDebugPointer::Location::Storage); - BOOST_REQUIRE(_pointer.name); - BOOST_CHECK_EQUAL(*_pointer.name, std::string(_name)); - checkLiteralExpression(_pointer.slot, _slot); - - if (_offset) - checkLiteralExpression(_pointer.offset, *_offset); - else - BOOST_CHECK(!_pointer.offset); - - if (_length) - checkLiteralExpression(_pointer.length, *_length); - else - BOOST_CHECK(!_pointer.length); -} - -void checkStoragePointer( - SemanticDebugVariable const& _variable, - ContractDefinition const& _contract, - std::string_view _slot, - std::optional _offset, - std::optional _length -) -{ - BOOST_REQUIRE(_variable.location); - BOOST_CHECK(_variable.location->kind == SemanticDebugVariableLocation::Kind::Storage); - BOOST_REQUIRE(_variable.location->pointerID); - BOOST_CHECK_EQUAL(*_variable.location->pointerID, storagePointer(_contract, _variable)); - - BOOST_REQUIRE(_variable.ethdebugPointer); - checkStorageRegion(*_variable.ethdebugPointer, _variable.name, _slot, _offset, _length); -} - -SemanticDebugData::ConstPtr findSemanticDebugData(langutil::DebugData::ConstPtr const& _debugData, int64_t _astID) -{ - if ( - _debugData && - _debugData->astID && - *_debugData->astID == _astID && - _debugData->semanticDebugData - ) - return _debugData->semanticDebugData; - - return nullptr; -} - -SemanticDebugData::ConstPtr findSemanticDebugData(yul::Block const& _block, int64_t _astID); - -SemanticDebugData::ConstPtr findSemanticDebugData(yul::FunctionDefinition const& _function, int64_t _astID) -{ - if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_function.debugData, _astID)) - return result; - return findSemanticDebugData(_function.body, _astID); -} - -SemanticDebugData::ConstPtr findSemanticDebugData(yul::Statement const& _statement, int64_t _astID) -{ - return std::visit([&](auto const& _node) -> SemanticDebugData::ConstPtr { - if constexpr (std::is_same_v, yul::FunctionDefinition>) - return findSemanticDebugData(_node, _astID); - else if constexpr (std::is_same_v, yul::Block>) - return findSemanticDebugData(_node, _astID); - else - return findSemanticDebugData(_node.debugData, _astID); - }, _statement); -} - -SemanticDebugData::ConstPtr findSemanticDebugData(yul::Block const& _block, int64_t _astID) -{ - if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_block.debugData, _astID)) - return result; - for (yul::Statement const& statement: _block.statements) - if (SemanticDebugData::ConstPtr result = findSemanticDebugData(statement, _astID)) - return result; - return nullptr; -} - -SemanticDebugData::ConstPtr findSemanticDebugData(yul::Object const& _object, int64_t _astID) -{ - if (_object.hasCode()) - if (SemanticDebugData::ConstPtr result = findSemanticDebugData(_object.code()->root(), _astID)) - return result; - - for (std::shared_ptr const& subObject: _object.subObjects) - if (auto const* object = dynamic_cast(subObject.get())) - if (SemanticDebugData::ConstPtr result = findSemanticDebugData(*object, _astID)) - return result; - - return nullptr; -} - -void checkFunctionVariableDebugData( - SemanticDebugData const& _data, - FunctionDefinition const& _function, - VariableDeclaration const& _parameter, - VariableDeclaration const& _returnVariable, - bool _requireInitialStackLocations = true -) -{ - BOOST_REQUIRE(_data.lexicalScopeID); - BOOST_CHECK_EQUAL(*_data.lexicalScopeID, _function.id()); - BOOST_REQUIRE_EQUAL(_data.variableDefinitions.size(), 2); - - SemanticDebugVariable const& parameterDebugData = _data.variableDefinitions.at(0); - BOOST_CHECK_EQUAL(parameterDebugData.name, "value"); - BOOST_REQUIRE(parameterDebugData.declarationAstID); - BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, _parameter.id()); - BOOST_REQUIRE(parameterDebugData.declarationLocation); - BOOST_REQUIRE(parameterDebugData.typeID); - BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); - BOOST_REQUIRE(parameterDebugData.ethdebugType); - BOOST_CHECK(parameterDebugData.ethdebugType->typeClass == SemanticDebugType::Class::Elementary); - BOOST_CHECK(parameterDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); - BOOST_REQUIRE(parameterDebugData.ethdebugType->bits); - BOOST_CHECK_EQUAL(*parameterDebugData.ethdebugType->bits, 256); - if (_requireInitialStackLocations) - { - BOOST_REQUIRE(parameterDebugData.location); - BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(parameterDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(_parameter)); - BOOST_REQUIRE(parameterDebugData.ethdebugPointer); - checkStackPointer(*parameterDebugData.ethdebugPointer, "value", stackSlots(_parameter)); - } - - SemanticDebugVariable const& returnDebugData = _data.variableDefinitions.at(1); - BOOST_CHECK_EQUAL(returnDebugData.name, "result"); - BOOST_REQUIRE(returnDebugData.declarationAstID); - BOOST_CHECK_EQUAL(*returnDebugData.declarationAstID, _returnVariable.id()); - BOOST_REQUIRE(returnDebugData.typeID); - BOOST_CHECK_EQUAL(*returnDebugData.typeID, "t_uint256"); - BOOST_REQUIRE(returnDebugData.ethdebugType); - BOOST_CHECK(returnDebugData.ethdebugType->typeClass == SemanticDebugType::Class::Elementary); - BOOST_CHECK(returnDebugData.ethdebugType->kind == SemanticDebugType::Kind::Uint); - BOOST_REQUIRE(returnDebugData.ethdebugType->bits); - BOOST_CHECK_EQUAL(*returnDebugData.ethdebugType->bits, 256); - if (_requireInitialStackLocations) - { - BOOST_REQUIRE(returnDebugData.location); - BOOST_CHECK(returnDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(returnDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*returnDebugData.location->pointerID, stackPointer(_returnVariable)); - BOOST_REQUIRE(returnDebugData.ethdebugPointer); - checkStackPointer(*returnDebugData.ethdebugPointer, "result", stackSlots(_returnVariable)); - } -} - -} - -class SemanticDebugDataFixture: public AnalysisFramework -{ - void setupCompiler(CompilerStack& _compiler) override - { - AnalysisFramework::setupCompiler(_compiler); - _compiler.setViaIR(true); - _compiler.setOptimiserSettings(OptimiserSettings::none()); - DebugInfoSelection selection = DebugInfoSelection::Default(); - selection.enable("ast-id"); - _compiler.selectDebugInfo(selection); - } -}; - -class EthdebugOnlySemanticDebugDataFixture: public AnalysisFramework -{ - void setupCompiler(CompilerStack& _compiler) override - { - AnalysisFramework::setupCompiler(_compiler); - _compiler.setViaIR(true); - _compiler.setOptimiserSettings(OptimiserSettings::none()); - DebugInfoSelection selection = DebugInfoSelection::None(); - selection.enable("ethdebug"); - _compiler.selectDebugInfo(selection); - } -}; - -BOOST_FIXTURE_TEST_SUITE(SemanticDebugDataTest, SemanticDebugDataFixture) - -BOOST_AUTO_TEST_CASE(function_parameters_and_return_variables) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - function f(uint256 value) public pure returns (uint256 result) { - return value; - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - FunctionDefinition const* function = findFunction(*contract, "f"); - BOOST_REQUIRE(function); - BOOST_REQUIRE_EQUAL(function->parameters().size(), 1); - BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); - - VariableDeclaration const& parameter = *function->parameters().front(); - VariableDeclaration const& returnVariable = *function->returnParameters().front(); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(function->id()); - BOOST_REQUIRE(data); - checkFunctionVariableDebugData(*data, *function, parameter, returnVariable); -} - -BOOST_AUTO_TEST_CASE(modifier_parameters_have_semantic_metadata) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - modifier guarded(uint256 guard) { - _; - } - - function f(uint256 value) public guarded(value) returns (uint256 result) { - return value; - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - ModifierDefinition const* modifier = findModifier(*contract, "guarded"); - BOOST_REQUIRE(modifier); - BOOST_REQUIRE_EQUAL(modifier->parameters().size(), 1); - - VariableDeclaration const& parameter = *modifier->parameters().front(); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(modifier->id()); - BOOST_REQUIRE(data); - BOOST_REQUIRE(data->lexicalScopeID); - BOOST_CHECK_EQUAL(*data->lexicalScopeID, modifier->id()); - BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 1); - - SemanticDebugVariable const& parameterDebugData = data->variableDefinitions.front(); - BOOST_CHECK_EQUAL(parameterDebugData.name, "guard"); - BOOST_REQUIRE(parameterDebugData.declarationAstID); - BOOST_CHECK_EQUAL(*parameterDebugData.declarationAstID, parameter.id()); - BOOST_REQUIRE(parameterDebugData.typeID); - BOOST_CHECK_EQUAL(*parameterDebugData.typeID, "t_uint256"); - BOOST_REQUIRE(parameterDebugData.location); - BOOST_CHECK(parameterDebugData.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(parameterDebugData.location->pointerID); - BOOST_CHECK_EQUAL(*parameterDebugData.location->pointerID, stackPointer(parameter)); - BOOST_REQUIRE(parameterDebugData.ethdebugPointer); - checkStackPointer(*parameterDebugData.ethdebugPointer, "guard", stackSlots(parameter)); -} - -BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_type_descriptors) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - function f( - uint256 amount, - int128 signedAmount, - bool enabled, - bytes16 tag, - bytes memory payload, - string memory label, - uint256[] memory values, - address payable recipient - ) public pure returns (bool ok) { - ok = enabled; - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - FunctionDefinition const* function = findFunction(*contract, "f"); - BOOST_REQUIRE(function); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(function->id()); - BOOST_REQUIRE(data); - BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 9); - - SemanticDebugVariable const* amount = findVariableByName(*data, "amount"); - BOOST_REQUIRE(amount); - BOOST_REQUIRE(amount->ethdebugType); - BOOST_CHECK(amount->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); - BOOST_CHECK(amount->ethdebugType->kind == SemanticDebugType::Kind::Uint); - BOOST_REQUIRE(amount->ethdebugType->bits); - BOOST_CHECK_EQUAL(*amount->ethdebugType->bits, 256); - - SemanticDebugVariable const* signedAmount = findVariableByName(*data, "signedAmount"); - BOOST_REQUIRE(signedAmount); - BOOST_REQUIRE(signedAmount->ethdebugType); - BOOST_CHECK(signedAmount->ethdebugType->kind == SemanticDebugType::Kind::Int); - BOOST_REQUIRE(signedAmount->ethdebugType->bits); - BOOST_CHECK_EQUAL(*signedAmount->ethdebugType->bits, 128); - - SemanticDebugVariable const* enabled = findVariableByName(*data, "enabled"); - BOOST_REQUIRE(enabled); - BOOST_REQUIRE(enabled->ethdebugType); - BOOST_CHECK(enabled->ethdebugType->kind == SemanticDebugType::Kind::Bool); - - SemanticDebugVariable const* tag = findVariableByName(*data, "tag"); - BOOST_REQUIRE(tag); - BOOST_REQUIRE(tag->ethdebugType); - BOOST_CHECK(tag->ethdebugType->kind == SemanticDebugType::Kind::Bytes); - BOOST_REQUIRE(tag->ethdebugType->bytes); - BOOST_CHECK_EQUAL(*tag->ethdebugType->bytes, 16); - - SemanticDebugVariable const* payload = findVariableByName(*data, "payload"); - BOOST_REQUIRE(payload); - BOOST_REQUIRE(payload->ethdebugType); - BOOST_CHECK(payload->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); - BOOST_CHECK(payload->ethdebugType->kind == SemanticDebugType::Kind::Bytes); - BOOST_REQUIRE(payload->ethdebugType->dataLocation); - BOOST_CHECK_EQUAL(*payload->ethdebugType->dataLocation, "memory"); - BOOST_REQUIRE(payload->ethdebugType->dynamic); - BOOST_CHECK(*payload->ethdebugType->dynamic); - - SemanticDebugVariable const* label = findVariableByName(*data, "label"); - BOOST_REQUIRE(label); - BOOST_REQUIRE(label->ethdebugType); - BOOST_CHECK(label->ethdebugType->kind == SemanticDebugType::Kind::String); - BOOST_REQUIRE(label->ethdebugType->dataLocation); - BOOST_CHECK_EQUAL(*label->ethdebugType->dataLocation, "memory"); - - SemanticDebugVariable const* values = findVariableByName(*data, "values"); - BOOST_REQUIRE(values); - BOOST_REQUIRE(values->ethdebugType); - BOOST_CHECK(values->ethdebugType->typeClass == SemanticDebugType::Class::Complex); - BOOST_CHECK(values->ethdebugType->kind == SemanticDebugType::Kind::Array); - BOOST_REQUIRE(values->ethdebugType->dataLocation); - BOOST_CHECK_EQUAL(*values->ethdebugType->dataLocation, "memory"); - BOOST_REQUIRE(values->ethdebugType->dynamic); - BOOST_CHECK(*values->ethdebugType->dynamic); - - SemanticDebugVariable const* recipient = findVariableByName(*data, "recipient"); - BOOST_REQUIRE(recipient); - BOOST_REQUIRE(recipient->ethdebugType); - BOOST_CHECK(recipient->ethdebugType->kind == SemanticDebugType::Kind::Address); - BOOST_REQUIRE(recipient->ethdebugType->payable); - BOOST_CHECK(*recipient->ethdebugType->payable); - - SemanticDebugVariable const* ok = findVariableByName(*data, "ok"); - BOOST_REQUIRE(ok); - BOOST_REQUIRE(ok->ethdebugType); - BOOST_CHECK(ok->ethdebugType->kind == SemanticDebugType::Kind::Bool); -} - -BOOST_AUTO_TEST_CASE(function_variables_include_ethdebug_pointer_descriptors) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - function f(uint256 value, bytes calldata payload) external pure returns (uint256 result) { - result = value + payload.length; - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - FunctionDefinition const* function = findFunction(*contract, "f"); - BOOST_REQUIRE(function); - BOOST_REQUIRE_EQUAL(function->parameters().size(), 2); - BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); - - VariableDeclaration const& value = *function->parameters().at(0); - VariableDeclaration const& payload = *function->parameters().at(1); - VariableDeclaration const& result = *function->returnParameters().front(); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(function->id()); - BOOST_REQUIRE(data); - BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 3); - - SemanticDebugVariable const* valueDebugData = findVariableByName(*data, "value"); - BOOST_REQUIRE(valueDebugData); - BOOST_REQUIRE(valueDebugData->ethdebugPointer); - checkStackPointer(*valueDebugData->ethdebugPointer, "value", stackSlots(value)); - - std::vector payloadStackSlots = stackSlots(payload); - BOOST_REQUIRE_GT(payloadStackSlots.size(), 1); - SemanticDebugVariable const* payloadDebugData = findVariableByName(*data, "payload"); - BOOST_REQUIRE(payloadDebugData); - BOOST_REQUIRE(payloadDebugData->ethdebugPointer); - checkStackPointer(*payloadDebugData->ethdebugPointer, "payload", payloadStackSlots); - - SemanticDebugVariable const* resultDebugData = findVariableByName(*data, "result"); - BOOST_REQUIRE(resultDebugData); - BOOST_REQUIRE(resultDebugData->ethdebugPointer); - checkStackPointer(*resultDebugData->ethdebugPointer, "result", stackSlots(result)); -} - -BOOST_AUTO_TEST_CASE(state_variables_include_storage_pointer_descriptors) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - uint128 packedA; - bool packedB; - uint256 wide; - uint256 constant ignoredConstant = 1; - uint256 immutable ignoredImmutable; - - constructor() { - ignoredImmutable = 2; - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - BOOST_REQUIRE(data->lexicalScopeID); - BOOST_CHECK_EQUAL(*data->lexicalScopeID, contract->id()); - BOOST_REQUIRE_EQUAL(data->variableDefinitions.size(), 3); - - SemanticDebugVariable const* packedA = findVariableByName(*data, "packedA"); - BOOST_REQUIRE(packedA); - checkStoragePointer(*packedA, *contract, "0x00", std::nullopt, "0x10"); - - SemanticDebugVariable const* packedB = findVariableByName(*data, "packedB"); - BOOST_REQUIRE(packedB); - checkStoragePointer(*packedB, *contract, "0x00", "0x10", "0x01"); - - SemanticDebugVariable const* wide = findVariableByName(*data, "wide"); - BOOST_REQUIRE(wide); - checkStoragePointer(*wide, *contract, "0x01", std::nullopt, std::nullopt); - - BOOST_CHECK(!findVariableByName(*data, "ignoredConstant")); - BOOST_CHECK(!findVariableByName(*data, "ignoredImmutable")); -} - -BOOST_AUTO_TEST_CASE(mapping_state_variables_have_keccak_pointer_templates) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - mapping(address => uint256) balances; - mapping(address => mapping(address => uint256)) allowances; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* balances = findVariableByName(*data, "balances"); - BOOST_REQUIRE(balances); - BOOST_REQUIRE(balances->location); - BOOST_CHECK(balances->location->kind == SemanticDebugVariableLocation::Kind::Storage); - BOOST_REQUIRE(balances->ethdebugPointer); - // The mapping key is not stored anywhere; it must be provided to the - // pointer template as the expected parameter "key". - BOOST_REQUIRE_EQUAL(balances->ethdebugPointer->expectedParameters.size(), 1); - BOOST_CHECK_EQUAL(balances->ethdebugPointer->expectedParameters.front(), "key"); - BOOST_CHECK(balances->ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(balances->ethdebugPointer->slot); - auto const [balancesKey, balancesSlot] = checkMappingSlotExpression(*balances->ethdebugPointer->slot); - BOOST_CHECK(balancesKey->kind == SemanticDebugPointerExpression::Kind::Variable); - BOOST_REQUIRE(balancesKey->value); - BOOST_CHECK_EQUAL(*balancesKey->value, "key"); - BOOST_CHECK(balancesSlot->kind == SemanticDebugPointerExpression::Kind::Literal); - BOOST_REQUIRE(balancesSlot->value); - BOOST_CHECK_EQUAL(*balancesSlot->value, "0x00"); - - // The type descriptor composes key and value types. - BOOST_REQUIRE(balances->ethdebugType); - BOOST_CHECK(balances->ethdebugType->typeClass == SemanticDebugType::Class::Complex); - BOOST_CHECK(balances->ethdebugType->kind == SemanticDebugType::Kind::Mapping); - BOOST_REQUIRE_EQUAL(balances->ethdebugType->components.size(), 2); - SemanticDebugTypeComponent const& keyComponent = balances->ethdebugType->components.at(0); - BOOST_CHECK(keyComponent.role == SemanticDebugTypeComponent::Role::Key); - BOOST_REQUIRE(keyComponent.referenceID); - BOOST_CHECK_EQUAL(*keyComponent.referenceID, "t_address"); - BOOST_REQUIRE(keyComponent.type); - BOOST_CHECK(keyComponent.type->kind == SemanticDebugType::Kind::Address); - SemanticDebugTypeComponent const& valueComponent = balances->ethdebugType->components.at(1); - BOOST_CHECK(valueComponent.role == SemanticDebugTypeComponent::Role::Value); - BOOST_REQUIRE(valueComponent.type); - BOOST_CHECK(valueComponent.type->kind == SemanticDebugType::Kind::Uint); - - // Nested mappings chain the hashes: keccak256(key1 . keccak256(key . slot)). - SemanticDebugVariable const* allowances = findVariableByName(*data, "allowances"); - BOOST_REQUIRE(allowances); - BOOST_REQUIRE(allowances->ethdebugPointer); - BOOST_REQUIRE_EQUAL(allowances->ethdebugPointer->expectedParameters.size(), 2); - BOOST_CHECK_EQUAL(allowances->ethdebugPointer->expectedParameters.at(0), "key"); - BOOST_CHECK_EQUAL(allowances->ethdebugPointer->expectedParameters.at(1), "key1"); - BOOST_REQUIRE(allowances->ethdebugPointer->slot); - auto const [outerKey, innerHash] = checkMappingSlotExpression(*allowances->ethdebugPointer->slot); - BOOST_CHECK(outerKey->kind == SemanticDebugPointerExpression::Kind::Variable); - BOOST_REQUIRE(outerKey->value); - BOOST_CHECK_EQUAL(*outerKey->value, "key1"); - auto const [innerKey, innerSlot] = checkMappingSlotExpression(*innerHash); - BOOST_CHECK(innerKey->kind == SemanticDebugPointerExpression::Kind::Variable); - BOOST_REQUIRE(innerKey->value); - BOOST_CHECK_EQUAL(*innerKey->value, "key"); - BOOST_CHECK(innerSlot->kind == SemanticDebugPointerExpression::Kind::Literal); - BOOST_REQUIRE(innerSlot->value); - BOOST_CHECK_EQUAL(*innerSlot->value, "0x01"); -} - -BOOST_AUTO_TEST_CASE(dynamic_array_state_variables_have_length_and_data_pointers) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - uint256[] values; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* values = findVariableByName(*data, "values"); - BOOST_REQUIRE(values); - BOOST_REQUIRE(values->ethdebugPointer); - BOOST_CHECK(values->ethdebugPointer->expectedParameters.empty()); - - // group [ length region at the base slot; - // define values-data := keccak256($wordsized(slot)) in - // list over $read(values-length) ] - SemanticDebugPointer const& pointer = *values->ethdebugPointer; - BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); - - checkStorageRegion(pointer.group.at(0), "values-length", "0x00", std::nullopt, std::nullopt); - - SemanticDebugPointer const& dataScope = pointer.group.at(1); - BOOST_CHECK(dataScope.pointerClass == SemanticDebugPointer::Class::Scope); - BOOST_REQUIRE_EQUAL(dataScope.definitions.size(), 1); - BOOST_CHECK_EQUAL(dataScope.definitions.front().first, "values-data"); - BOOST_CHECK(dataScope.definitions.front().second.kind == SemanticDebugPointerExpression::Kind::Keccak256); - - BOOST_REQUIRE(dataScope.scopeTarget); - SemanticDebugPointer const& elementList = *dataScope.scopeTarget; - BOOST_CHECK(elementList.pointerClass == SemanticDebugPointer::Class::List); - BOOST_REQUIRE(elementList.count); - BOOST_CHECK(elementList.count->kind == SemanticDebugPointerExpression::Kind::Read); - BOOST_REQUIRE(elementList.count->value); - BOOST_CHECK_EQUAL(*elementList.count->value, "values-length"); - BOOST_REQUIRE(elementList.indexName); - BOOST_CHECK_EQUAL(*elementList.indexName, "values-index"); - BOOST_REQUIRE(elementList.listElement); - BOOST_CHECK(elementList.listElement->pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(elementList.listElement->slot); - BOOST_CHECK(elementList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); -} - -BOOST_AUTO_TEST_CASE(static_array_state_variables_use_element_lists) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - uint64[8] packed; - uint256[3] wide; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - // uint64 elements pack four to a slot: the element region derives slot and - // byte offset from the index. - SemanticDebugVariable const* packed = findVariableByName(*data, "packed"); - BOOST_REQUIRE(packed); - BOOST_REQUIRE(packed->ethdebugType); - BOOST_CHECK(packed->ethdebugType->kind == SemanticDebugType::Kind::Array); - BOOST_REQUIRE(packed->ethdebugType->count); - BOOST_CHECK_EQUAL(*packed->ethdebugType->count, "0x08"); - BOOST_REQUIRE(packed->ethdebugPointer); - SemanticDebugPointer const& packedList = *packed->ethdebugPointer; - BOOST_CHECK(packedList.pointerClass == SemanticDebugPointer::Class::List); - checkLiteralExpression(packedList.count, "0x08"); - BOOST_REQUIRE(packedList.listElement); - BOOST_REQUIRE(packedList.listElement->slot); - BOOST_CHECK(packedList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); - BOOST_REQUIRE(packedList.listElement->offset); - BOOST_CHECK(packedList.listElement->offset->kind == SemanticDebugPointerExpression::Kind::Product); - checkLiteralExpression(packedList.listElement->length, "0x08"); - - // uint256 elements advance one slot per index, starting after the two slots - // occupied by "packed". - SemanticDebugVariable const* wide = findVariableByName(*data, "wide"); - BOOST_REQUIRE(wide); - BOOST_REQUIRE(wide->ethdebugPointer); - SemanticDebugPointer const& wideList = *wide->ethdebugPointer; - BOOST_CHECK(wideList.pointerClass == SemanticDebugPointer::Class::List); - checkLiteralExpression(wideList.count, "0x03"); - BOOST_REQUIRE(wideList.listElement); - BOOST_REQUIRE(wideList.listElement->slot); - BOOST_CHECK(wideList.listElement->slot->kind == SemanticDebugPointerExpression::Kind::Sum); - BOOST_REQUIRE_EQUAL(wideList.listElement->slot->operands.size(), 2); - BOOST_CHECK(wideList.listElement->slot->operands.at(0).kind == SemanticDebugPointerExpression::Kind::Literal); - BOOST_REQUIRE(wideList.listElement->slot->operands.at(0).value); - BOOST_CHECK_EQUAL(*wideList.listElement->slot->operands.at(0).value, "0x02"); - BOOST_CHECK(wideList.listElement->slot->operands.at(1).kind == SemanticDebugPointerExpression::Kind::Variable); - BOOST_CHECK(!wideList.listElement->length); -} - -BOOST_AUTO_TEST_CASE(string_state_variables_use_conditional_pointers) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - string label; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* label = findVariableByName(*data, "label"); - BOOST_REQUIRE(label); - BOOST_REQUIRE(label->ethdebugPointer); - - // group [ length-flag byte region; conditional on (flag + 1) % 2: - // short value in place, long value at keccak256(slot) ] - SemanticDebugPointer const& pointer = *label->ethdebugPointer; - BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); - - SemanticDebugPointer const& lengthFlag = pointer.group.at(0); - BOOST_CHECK(lengthFlag.pointerClass == SemanticDebugPointer::Class::Region); - BOOST_REQUIRE(lengthFlag.name); - BOOST_CHECK_EQUAL(*lengthFlag.name, "label-length-flag"); - checkLiteralExpression(lengthFlag.slot, "0x00"); - BOOST_REQUIRE(lengthFlag.offset); - BOOST_CHECK(lengthFlag.offset->kind == SemanticDebugPointerExpression::Kind::Difference); - checkLiteralExpression(lengthFlag.length, "0x01"); - - SemanticDebugPointer const& value = pointer.group.at(1); - BOOST_CHECK(value.pointerClass == SemanticDebugPointer::Class::Conditional); - BOOST_REQUIRE(value.condition); - BOOST_CHECK(value.condition->kind == SemanticDebugPointerExpression::Kind::Remainder); - - BOOST_REQUIRE(value.thenPointer); - BOOST_CHECK(value.thenPointer->pointerClass == SemanticDebugPointer::Class::Scope); - BOOST_REQUIRE_EQUAL(value.thenPointer->definitions.size(), 1); - BOOST_CHECK_EQUAL(value.thenPointer->definitions.front().first, "label-length"); - BOOST_REQUIRE(value.thenPointer->scopeTarget); - checkVariableExpression(value.thenPointer->scopeTarget->length, "label-length"); - - BOOST_REQUIRE(value.elsePointer); - BOOST_CHECK(value.elsePointer->pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(value.elsePointer->group.size(), 2); - SemanticDebugPointer const& longData = value.elsePointer->group.at(1); - BOOST_CHECK(longData.pointerClass == SemanticDebugPointer::Class::Scope); - BOOST_REQUIRE_EQUAL(longData.definitions.size(), 2); - BOOST_CHECK_EQUAL(longData.definitions.at(0).first, "label-length"); - BOOST_CHECK_EQUAL(longData.definitions.at(1).first, "label-data"); - BOOST_CHECK(longData.definitions.at(1).second.kind == SemanticDebugPointerExpression::Kind::Keccak256); - BOOST_REQUIRE(longData.scopeTarget); - checkVariableExpression(longData.scopeTarget->slot, "label-data"); - checkVariableExpression(longData.scopeTarget->length, "label-length"); -} - -BOOST_AUTO_TEST_CASE(struct_state_variables_have_member_pointers) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - struct Point { - uint128 x; - uint128 y; - uint256 z; - } - - Point origin; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* origin = findVariableByName(*data, "origin"); - BOOST_REQUIRE(origin); - - BOOST_REQUIRE(origin->ethdebugType); - BOOST_CHECK(origin->ethdebugType->typeClass == SemanticDebugType::Class::Complex); - BOOST_CHECK(origin->ethdebugType->kind == SemanticDebugType::Kind::Struct); - BOOST_REQUIRE(origin->ethdebugType->definitionName); - BOOST_CHECK_EQUAL(*origin->ethdebugType->definitionName, "Point"); - BOOST_REQUIRE_EQUAL(origin->ethdebugType->components.size(), 3); - BOOST_REQUIRE(origin->ethdebugType->components.at(0).name); - BOOST_CHECK_EQUAL(*origin->ethdebugType->components.at(0).name, "x"); - BOOST_REQUIRE(origin->ethdebugType->components.at(2).name); - BOOST_CHECK_EQUAL(*origin->ethdebugType->components.at(2).name, "z"); - - // Members become individual regions: x and y pack into slot 0, z takes slot 1. - BOOST_REQUIRE(origin->ethdebugPointer); - SemanticDebugPointer const& pointer = *origin->ethdebugPointer; - BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(pointer.group.size(), 3); - checkStorageRegion(pointer.group.at(0), "origin-x", "0x00", std::nullopt, "0x10"); - checkStorageRegion(pointer.group.at(1), "origin-y", "0x00", "0x10", "0x10"); - checkStorageRegion(pointer.group.at(2), "origin-z", "0x01", std::nullopt, std::nullopt); -} - -BOOST_AUTO_TEST_CASE(recursive_struct_types_are_cut_with_references) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - struct Node { - uint256 value; - Node[] children; - } - - Node root; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* root = findVariableByName(*data, "root"); - BOOST_REQUIRE(root); - - // The array element inside the struct refers back to the struct: the inline - // representation is cut and only the type reference remains. - BOOST_REQUIRE(root->ethdebugType); - BOOST_CHECK(root->ethdebugType->kind == SemanticDebugType::Kind::Struct); - BOOST_REQUIRE_EQUAL(root->ethdebugType->components.size(), 2); - SemanticDebugTypeComponent const& children = root->ethdebugType->components.at(1); - BOOST_REQUIRE(children.type); - BOOST_CHECK(children.type->kind == SemanticDebugType::Kind::Array); - BOOST_REQUIRE_EQUAL(children.type->components.size(), 1); - SemanticDebugTypeComponent const& element = children.type->components.front(); - BOOST_CHECK(!element.type); - BOOST_REQUIRE(element.referenceID); - BOOST_REQUIRE(root->typeID); - BOOST_CHECK_EQUAL(*element.referenceID, *root->typeID); - - // The pointer recursion falls back to a whole-struct region for the nested - // occurrence instead of recursing forever. - BOOST_REQUIRE(root->ethdebugPointer); - BOOST_CHECK(root->ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(root->ethdebugPointer->group.size(), 2); - SemanticDebugPointer const& childrenPointer = root->ethdebugPointer->group.at(1); - BOOST_CHECK(childrenPointer.pointerClass == SemanticDebugPointer::Class::Group); - BOOST_REQUIRE_EQUAL(childrenPointer.group.size(), 2); - BOOST_REQUIRE(childrenPointer.group.at(1).scopeTarget); - SemanticDebugPointer const& elementPointer = *childrenPointer.group.at(1).scopeTarget; - BOOST_CHECK(elementPointer.pointerClass == SemanticDebugPointer::Class::List); - BOOST_REQUIRE(elementPointer.listElement); - BOOST_CHECK(elementPointer.listElement->pointerClass == SemanticDebugPointer::Class::Region); - checkLiteralExpression(elementPointer.listElement->length, "0x40"); -} - -BOOST_AUTO_TEST_CASE(state_variable_types_cover_enums_aliases_and_contracts) -{ - BOOST_REQUIRE(runFramework(R"( - contract D {} - - contract C { - enum Mode { - Off, - On - } - - type Price is uint128; - - Mode mode; - Price price; - D other; - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(contract->id()); - BOOST_REQUIRE(data); - - SemanticDebugVariable const* mode = findVariableByName(*data, "mode"); - BOOST_REQUIRE(mode); - BOOST_REQUIRE(mode->ethdebugType); - BOOST_CHECK(mode->ethdebugType->typeClass == SemanticDebugType::Class::Elementary); - BOOST_CHECK(mode->ethdebugType->kind == SemanticDebugType::Kind::Enum); - BOOST_REQUIRE_EQUAL(mode->ethdebugType->enumValues.size(), 2); - BOOST_CHECK_EQUAL(mode->ethdebugType->enumValues.at(0), "Off"); - BOOST_CHECK_EQUAL(mode->ethdebugType->enumValues.at(1), "On"); - BOOST_REQUIRE(mode->ethdebugType->definitionName); - BOOST_CHECK_EQUAL(*mode->ethdebugType->definitionName, "Mode"); - BOOST_REQUIRE(mode->ethdebugType->definitionLocation); - - SemanticDebugVariable const* price = findVariableByName(*data, "price"); - BOOST_REQUIRE(price); - BOOST_REQUIRE(price->ethdebugType); - BOOST_CHECK(price->ethdebugType->kind == SemanticDebugType::Kind::Alias); - BOOST_REQUIRE(price->ethdebugType->definitionName); - BOOST_CHECK_EQUAL(*price->ethdebugType->definitionName, "Price"); - BOOST_REQUIRE_EQUAL(price->ethdebugType->components.size(), 1); - BOOST_CHECK(price->ethdebugType->components.front().role == SemanticDebugTypeComponent::Role::Underlying); - BOOST_REQUIRE(price->ethdebugType->components.front().type); - BOOST_CHECK(price->ethdebugType->components.front().type->kind == SemanticDebugType::Kind::Uint); - BOOST_REQUIRE(price->ethdebugType->components.front().type->bits); - BOOST_CHECK_EQUAL(*price->ethdebugType->components.front().type->bits, 128); - - SemanticDebugVariable const* other = findVariableByName(*data, "other"); - BOOST_REQUIRE(other); - BOOST_REQUIRE(other->ethdebugType); - BOOST_CHECK(other->ethdebugType->kind == SemanticDebugType::Kind::Contract); - BOOST_REQUIRE(other->ethdebugType->definitionName); - BOOST_CHECK_EQUAL(*other->ethdebugType->definitionName, "D"); -} - -BOOST_AUTO_TEST_CASE(inherited_function_variables_have_semantic_metadata) -{ - BOOST_REQUIRE(runFramework(R"( - contract Base { - modifier baseGuarded(uint256 guard) { - _; - } - - function inherited(uint256 value) public pure returns (uint256 result) { - return value; - } - } - - contract Derived is Base {} - )", PipelineStage::Analysis)); - - ContractDefinition const* base = retrieveContractByName(compiler().ast(""), "Base"); - BOOST_REQUIRE(base); - ContractDefinition const* derived = retrieveContractByName(compiler().ast(""), "Derived"); - BOOST_REQUIRE(derived); - FunctionDefinition const* function = findFunction(*base, "inherited"); - BOOST_REQUIRE(function); - ModifierDefinition const* modifier = findModifier(*base, "baseGuarded"); - BOOST_REQUIRE(modifier); - - // The derived contract's IR contains the inherited definitions with the base - // contract's AST IDs, so the derived contract's table must cover them. - SemanticDebugDataTable table = buildSemanticDebugDataTable(*derived); - SemanticDebugData::ConstPtr data = table.find(function->id()); - BOOST_REQUIRE(data); - checkFunctionVariableDebugData( - *data, - *function, - *function->parameters().front(), - *function->returnParameters().front() - ); - BOOST_CHECK(table.find(modifier->id())); -} - -BOOST_AUTO_TEST_CASE(free_functions_have_semantic_metadata) -{ - BOOST_REQUIRE(runFramework(R"( - function freeHelper(uint256 value) pure returns (uint256 result) { - return value + 1; - } - - contract C { - function f(uint256 value) public pure returns (uint256 result) { - return freeHelper(value); - } - } - )", PipelineStage::Analysis)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - - FunctionDefinition const* freeFunction = nullptr; - for (ASTPointer const& node: compiler().ast("").nodes()) - if (auto const* candidate = dynamic_cast(node.get())) - freeFunction = candidate; - BOOST_REQUIRE(freeFunction); - - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - SemanticDebugData::ConstPtr data = table.find(freeFunction->id()); - BOOST_REQUIRE(data); - checkFunctionVariableDebugData( - *data, - *freeFunction, - *freeFunction->parameters().front(), - *freeFunction->returnParameters().front() - ); -} - -BOOST_FIXTURE_TEST_CASE(ethdebug_debug_info_implies_ast_id, EthdebugOnlySemanticDebugDataFixture) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - function f(uint256 value) public pure returns (uint256 result) { - return value; - } - } - )", PipelineStage::Compilation)); - - // The AST ID comments are the join key for semantic metadata across the Yul text - // boundary. Selecting only ethdebug debug info must still produce them. - std::optional const& yulIR = compiler().yulIR("C"); - BOOST_REQUIRE(yulIR); - BOOST_CHECK(yulIR->find("@ast-id") != std::string::npos); -} - -BOOST_AUTO_TEST_CASE(function_variable_metadata_survives_generated_yul_reparse) -{ - BOOST_REQUIRE(runFramework(R"( - contract C { - function f(uint256 value) public pure returns (uint256 result) { - return value; - } - } - )", PipelineStage::Compilation)); - - ContractDefinition const* contract = retrieveContractByName(compiler().ast(""), "C"); - BOOST_REQUIRE(contract); - FunctionDefinition const* function = findFunction(*contract, "f"); - BOOST_REQUIRE(function); - BOOST_REQUIRE_EQUAL(function->parameters().size(), 1); - BOOST_REQUIRE_EQUAL(function->returnParameters().size(), 1); - - VariableDeclaration const& parameter = *function->parameters().front(); - VariableDeclaration const& returnVariable = *function->returnParameters().front(); - SemanticDebugDataTable table = buildSemanticDebugDataTable(*contract); - - std::optional const& yulIR = compiler().yulIR("C"); - BOOST_REQUIRE(yulIR); - - OptimiserSettings optimiserSettings = OptimiserSettings::none(); - optimiserSettings.yulOptimiserSteps = ""; - optimiserSettings.yulOptimiserCleanupSteps = ""; - yul::YulStack yulStack( - solidity::test::CommonOptions::get().evmVersion(), - optimiserSettings, - DebugInfoSelection::All(), - &compiler() - ); - BOOST_REQUIRE(yulStack.parseAndAnalyze("", *yulIR)); - - yulStack.attachSemanticDebugData(table); - SemanticDebugData::ConstPtr attachedData = findSemanticDebugData(*yulStack.parserResult(), function->id()); - BOOST_REQUIRE(attachedData); - checkFunctionVariableDebugData(*attachedData, *function, parameter, returnVariable); - - yulStack.optimize(); - - SemanticDebugData::ConstPtr reparsedData = findSemanticDebugData(*yulStack.parserResult(), function->id()); - BOOST_REQUIRE(reparsedData); - checkFunctionVariableDebugData(*reparsedData, *function, parameter, returnVariable, false); -} - -BOOST_AUTO_TEST_SUITE_END() diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp index 082d0c6a6219..fa4ad29c8b11 100644 --- a/test/libyul/DebugData.cpp +++ b/test/libyul/DebugData.cpp @@ -71,8 +71,8 @@ SemanticDebugPointer stackPointer(std::string _name) SemanticDebugDataTable stackVariableTable(int64_t _astID, std::string _name, std::string _slot) { SemanticDebugVariable variable; - variable.name = std::move(_name); - variable.location = SemanticDebugVariableLocation{ + variable.identifier = std::move(_name); + variable.dataLocation = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = _slot }; @@ -94,10 +94,10 @@ void checkStackVariableSurvived(FunctionDefinition const& _function, std::string SemanticDebugData const& data = *_function.debugData->semanticDebugData; BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); SemanticDebugVariable const& variable = data.variableDefinitions.front(); - BOOST_REQUIRE(variable.location); - BOOST_CHECK(variable.location->kind == SemanticDebugVariableLocation::Kind::Stack); - BOOST_REQUIRE(variable.location->pointerID); - BOOST_CHECK_EQUAL(*variable.location->pointerID, _slot); + BOOST_REQUIRE(variable.dataLocation); + BOOST_CHECK(variable.dataLocation->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(variable.dataLocation->pointerID); + BOOST_CHECK_EQUAL(*variable.dataLocation->pointerID, _slot); BOOST_REQUIRE(variable.ethdebugPointer); } @@ -108,9 +108,9 @@ void checkStackVariableOptimizedOut(FunctionDefinition const& _function) SemanticDebugData const& data = *_function.debugData->semanticDebugData; BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); SemanticDebugVariable const& variable = data.variableDefinitions.front(); - BOOST_REQUIRE(variable.location); - BOOST_CHECK(variable.location->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); - BOOST_CHECK(!variable.location->pointerID); + BOOST_REQUIRE(variable.dataLocation); + BOOST_CHECK(variable.dataLocation->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!variable.dataLocation->pointerID); BOOST_CHECK(!variable.ethdebugPointer); } @@ -196,8 +196,8 @@ BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) BOOST_REQUIRE(funDef->debugData->astID); SemanticDebugVariable variable; - variable.name = "value"; - variable.location = SemanticDebugVariableLocation{ + variable.identifier = "value"; + variable.dataLocation = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = "missing_slot" }; @@ -226,9 +226,9 @@ BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) SemanticDebugData const& reparsedDebugData = *reparsedFunDef->debugData->semanticDebugData; BOOST_REQUIRE_EQUAL(reparsedDebugData.variableDefinitions.size(), 1); SemanticDebugVariable const& reparsedVariable = reparsedDebugData.variableDefinitions.front(); - BOOST_REQUIRE(reparsedVariable.location); - BOOST_CHECK(reparsedVariable.location->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); - BOOST_CHECK(!reparsedVariable.location->pointerID); + BOOST_REQUIRE(reparsedVariable.dataLocation); + BOOST_CHECK(reparsedVariable.dataLocation->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!reparsedVariable.dataLocation->pointerID); BOOST_CHECK(!reparsedVariable.ethdebugPointer); } @@ -308,8 +308,8 @@ BOOST_AUTO_TEST_CASE(bound_pointer_variables_do_not_affect_survival) pointer.expectedParameters = {"key"}; SemanticDebugVariable variable; - variable.name = "x"; - variable.location = SemanticDebugVariableLocation{ + variable.identifier = "x"; + variable.dataLocation = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = "var_x" }; @@ -362,8 +362,8 @@ BOOST_AUTO_TEST_CASE(free_pointer_variables_in_expressions_require_yul_names) ); SemanticDebugVariable variable; - variable.name = "x"; - variable.location = SemanticDebugVariableLocation{ + variable.identifier = "x"; + variable.dataLocation = SemanticDebugVariableLocation{ .kind = SemanticDebugVariableLocation::Kind::Stack, .pointerID = "var_x" }; From a8e7fc6172ed5a1dad0e0d7c0182c3c9e53c2795 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 20 Jul 2026 10:19:17 +0200 Subject: [PATCH 23/47] ethdebug: Add versioned serialization for the semantic side table Everything that crosses the Solidity/Yul boundary needs a serialization format so that a serialized two-stage compilation can reproduce a one-stage compilation. Introduce a versioned JSON sidecar format ("solidity-ethdebug-semantic-data", version 1) with bidirectional conversion for the complete table, including recursive type descriptors, pointer expression trees, unattached scope records, and the source-language contract name needed by the public program output. Readers reject unknown formats, unsupported versions, malformed tagged values, and duplicate table keys; writers emit deterministic entry order. --- liblangutil/CMakeLists.txt | 2 + liblangutil/SemanticDebugDataSerDe.cpp | 810 ++++++++++++++++++ liblangutil/SemanticDebugDataSerDe.h | 48 ++ liblangutil/SemanticDebugDataTable.h | 16 + .../codegen/ir/SemanticDebugDataBuilder.cpp | 1 + test/liblangutil/DebugData.cpp | 117 +++ 6 files changed, 994 insertions(+) create mode 100644 liblangutil/SemanticDebugDataSerDe.cpp create mode 100644 liblangutil/SemanticDebugDataSerDe.h diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 38eda841ffe3..96132cf240f2 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -18,6 +18,8 @@ set(sources Scanner.h CharStreamProvider.h SemanticDebugData.h + SemanticDebugDataSerDe.cpp + SemanticDebugDataSerDe.h SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp new file mode 100644 index 000000000000..1edac9c85cb7 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -0,0 +1,810 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +void require(bool _condition, std::string const& _message) +{ + solRequire(_condition, SemanticDebugDataSerializationError, _message); +} + +void requireObject(Json const& _json, std::string const& _path) +{ + require(_json.is_object(), _path + " must be an object."); +} + +void requireArray(Json const& _json, std::string const& _path) +{ + require(_json.is_array(), _path + " must be an array."); +} + +Json const& requiredMember(Json const& _json, std::string const& _name, std::string const& _path) +{ + requireObject(_json, _path); + require(_json.contains(_name), _path + "." + _name + " is required."); + return _json.at(_name); +} + +std::string requiredString(Json const& _json, std::string const& _name, std::string const& _path) +{ + Json const& value = requiredMember(_json, _name, _path); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +template +T requiredInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + Json const& value = requiredMember(_json, _name, _path); + require(value.is_number_integer(), _path + "." + _name + " must be an integer."); + if constexpr (std::is_unsigned_v) + { + require( + value.is_number_unsigned() || value.get() >= 0, + _path + "." + _name + " must not be negative."); + Json::number_unsigned_t rawValue = value.get(); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + } + else + { + if (value.is_number_unsigned()) + require( + value.get() + <= static_cast(std::numeric_limits::max()), + _path + "." + _name + " is too large."); + else + { + Json::number_integer_t rawValue = value.get(); + require(rawValue >= std::numeric_limits::min(), _path + "." + _name + " is too small."); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + } + } + return value.get(); +} + +template +std::optional optionalValue(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + if constexpr (std::is_integral_v && std::is_unsigned_v && !std::is_same_v) + { + Json const& value = _json.at(_name); + require(value.is_number_integer(), _path + "." + _name + " must be an integer."); + require( + value.is_number_unsigned() || value.get() >= 0, + _path + "." + _name + " must not be negative."); + Json::number_unsigned_t rawValue = value.get(); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + return static_cast(rawValue); + } + else if constexpr (std::is_integral_v && std::is_signed_v) + return requiredInteger(_json, _name, _path); + try + { + return _json.at(_name).get(); + } + catch (Json::exception const& _exception) + { + solThrow( + SemanticDebugDataSerializationError, + _path + "." + _name + " has an invalid value: " + std::string(_exception.what())); + } +} + +std::optional optionalString(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + Json const& value = _json.at(_name); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +template +std::string enumToString(Enum _value, std::initializer_list> _values) +{ + for (auto const& [value, name]: _values) + if (_value == value) + return std::string(name); + solAssert(false, "Unhandled semantic debug data enum value."); +} + +template +Enum enumFromString( + std::string const& _name, + std::initializer_list> _values, + std::string const& _path) +{ + for (auto const& [value, name]: _values) + if (_name == name) + return value; + solThrow(SemanticDebugDataSerializationError, _path + " has unknown value \"" + _name + "\"."); +} + +std::string variableLocationKindToString(SemanticDebugVariableLocation::Kind _kind) +{ + using Kind = SemanticDebugVariableLocation::Kind; + return enumToString( + _kind, + {{Kind::Stack, "stack"}, + {Kind::Storage, "storage"}, + {Kind::TransientStorage, "transientStorage"}, + {Kind::Memory, "memory"}, + {Kind::Calldata, "calldata"}, + {Kind::Returndata, "returndata"}, + {Kind::Code, "code"}, + {Kind::Computed, "computed"}, + {Kind::OptimizedOut, "optimizedOut"} + }); +} + +SemanticDebugVariableLocation::Kind variableLocationKindFromString(std::string const& _kind, std::string const& _path) +{ + using Kind = SemanticDebugVariableLocation::Kind; + return enumFromString( + _kind, + {{Kind::Stack, "stack"}, + {Kind::Storage, "storage"}, + {Kind::TransientStorage, "transientStorage"}, + {Kind::Memory, "memory"}, + {Kind::Calldata, "calldata"}, + {Kind::Returndata, "returndata"}, + {Kind::Code, "code"}, + {Kind::Computed, "computed"}, + {Kind::OptimizedOut, "optimizedOut"} + }, + _path); +} + +std::string expressionKindToString(SemanticDebugPointerExpression::Kind _kind) +{ + using Kind = SemanticDebugPointerExpression::Kind; + return enumToString( + _kind, + { + {Kind::Unknown, "unknown"}, + {Kind::Literal, "literal"}, + {Kind::WordSize, "wordSize"}, + {Kind::Variable, "variable"}, + {Kind::LookupSlot, "lookupSlot"}, + {Kind::LookupOffset, "lookupOffset"}, + {Kind::LookupLength, "lookupLength"}, + {Kind::Read, "read"}, + {Kind::Sum, "sum"}, + {Kind::Product, "product"}, + {Kind::Difference, "difference"}, + {Kind::Quotient, "quotient"}, + {Kind::Remainder, "remainder"}, + {Kind::Keccak256, "keccak256"}, + {Kind::Concat, "concat"}, + {Kind::Resize, "resize"} + }); +} + +SemanticDebugPointerExpression::Kind expressionKindFromString(std::string const& _kind, std::string const& _path) +{ + using Kind = SemanticDebugPointerExpression::Kind; + return enumFromString( + _kind, + { + {Kind::Unknown, "unknown"}, + {Kind::Literal, "literal"}, + {Kind::WordSize, "wordSize"}, + {Kind::Variable, "variable"}, + {Kind::LookupSlot, "lookupSlot"}, + {Kind::LookupOffset, "lookupOffset"}, + {Kind::LookupLength, "lookupLength"}, + {Kind::Read, "read"}, + {Kind::Sum, "sum"}, + {Kind::Product, "product"}, + {Kind::Difference, "difference"}, + {Kind::Quotient, "quotient"}, + {Kind::Remainder, "remainder"}, + {Kind::Keccak256, "keccak256"}, + {Kind::Concat, "concat"}, + {Kind::Resize, "resize"} + }, + _path); +} + +std::string typeRoleToString(SemanticDebugTypeComponent::Role _role) +{ + using Role = SemanticDebugTypeComponent::Role; + return enumToString( + _role, + { + {Role::Element, "element"}, + {Role::Key, "key"}, + {Role::Value, "value"}, + {Role::Member, "member"}, + {Role::Parameter, "parameter"}, + {Role::Return, "return"}, + {Role::Underlying, "underlying"}, + {Role::Contract, "contract"} + }); +} + +SemanticDebugTypeComponent::Role typeRoleFromString(std::string const& _role, std::string const& _path) +{ + using Role = SemanticDebugTypeComponent::Role; + return enumFromString( + _role, + { + {Role::Element, "element"}, + {Role::Key, "key"}, + {Role::Value, "value"}, + {Role::Member, "member"}, + {Role::Parameter, "parameter"}, + {Role::Return, "return"}, + {Role::Underlying, "underlying"}, + {Role::Contract, "contract"} + }, + _path); +} + +std::string typeClassToString(SemanticDebugType::Class _class) +{ + using Class = SemanticDebugType::Class; + return enumToString( + _class, {{Class::Elementary, "elementary"}, {Class::Complex, "complex"}, {Class::Unknown, "unknown"}}); +} + +SemanticDebugType::Class typeClassFromString(std::string const& _class, std::string const& _path) +{ + using Class = SemanticDebugType::Class; + return enumFromString( + _class, {{Class::Elementary, "elementary"}, {Class::Complex, "complex"}, {Class::Unknown, "unknown"}}, _path); +} + +std::string typeKindToString(SemanticDebugType::Kind _kind) +{ + using Kind = SemanticDebugType::Kind; + return enumToString( + _kind, + { + {Kind::Uint, "uint"}, + {Kind::Int, "int"}, + {Kind::Ufixed, "ufixed"}, + {Kind::Fixed, "fixed"}, + {Kind::Bool, "bool"}, + {Kind::Bytes, "bytes"}, + {Kind::String, "string"}, + {Kind::Address, "address"}, + {Kind::Contract, "contract"}, + {Kind::Enum, "enum"}, + {Kind::Alias, "alias"}, + {Kind::Tuple, "tuple"}, + {Kind::Array, "array"}, + {Kind::Mapping, "mapping"}, + {Kind::Struct, "struct"}, + {Kind::Function, "function"}, + {Kind::Unknown, "unknown"} + }); +} + +SemanticDebugType::Kind typeKindFromString(std::string const& _kind, std::string const& _path) +{ + using Kind = SemanticDebugType::Kind; + return enumFromString( + _kind, + { + {Kind::Uint, "uint"}, + {Kind::Int, "int"}, + {Kind::Ufixed, "ufixed"}, + {Kind::Fixed, "fixed"}, + {Kind::Bool, "bool"}, + {Kind::Bytes, "bytes"}, + {Kind::String, "string"}, + {Kind::Address, "address"}, + {Kind::Contract, "contract"}, + {Kind::Enum, "enum"}, + {Kind::Alias, "alias"}, + {Kind::Tuple, "tuple"}, + {Kind::Array, "array"}, + {Kind::Mapping, "mapping"}, + {Kind::Struct, "struct"}, + {Kind::Function, "function"}, + {Kind::Unknown, "unknown"} + }, + _path); +} + +std::string pointerClassToString(SemanticDebugPointer::Class _class) +{ + using Class = SemanticDebugPointer::Class; + return enumToString( + _class, + { + {Class::Region, "region"}, + {Class::Group, "group"}, + {Class::List, "list"}, + {Class::Conditional, "conditional"}, + {Class::Scope, "scope"}, + {Class::TemplateReference, "templateReference"}, + {Class::Unknown, "unknown"} + }); +} + +SemanticDebugPointer::Class pointerClassFromString(std::string const& _class, std::string const& _path) +{ + using Class = SemanticDebugPointer::Class; + return enumFromString( + _class, + { + {Class::Region, "region"}, + {Class::Group, "group"}, + {Class::List, "list"}, + {Class::Conditional, "conditional"}, + {Class::Scope, "scope"}, + {Class::TemplateReference, "templateReference"}, + {Class::Unknown, "unknown"} + }, + _path); +} + +std::string pointerLocationToString(SemanticDebugPointer::Location _location) +{ + using Location = SemanticDebugPointer::Location; + return enumToString( + _location, + { + {Location::Stack, "stack"}, + {Location::Storage, "storage"}, + {Location::Transient, "transient"}, + {Location::Memory, "memory"}, + {Location::Calldata, "calldata"}, + {Location::Returndata, "returndata"}, + {Location::Code, "code"}, + {Location::Unknown, "unknown"} + }); +} + +SemanticDebugPointer::Location pointerLocationFromString(std::string const& _location, std::string const& _path) +{ + using Location = SemanticDebugPointer::Location; + return enumFromString( + _location, + { + {Location::Stack, "stack"}, + {Location::Storage, "storage"}, + {Location::Transient, "transient"}, + {Location::Memory, "memory"}, + {Location::Calldata, "calldata"}, + {Location::Returndata, "returndata"}, + {Location::Code, "code"}, + {Location::Unknown, "unknown"} + }, + _path); +} + +template +void setOptional(Json& _json, std::string const& _name, std::optional const& _value) +{ + if (_value) + _json[_name] = *_value; +} + +Json sourceLocationToJson(SourceLocation const& _location) +{ + Json result{{"start", _location.start}, {"end", _location.end}}; + if (_location.sourceName) + result["sourceName"] = *_location.sourceName; + return result; +} + +SourceLocation sourceLocationFromJson(Json const& _json, std::string const& _path) +{ + SourceLocation result; + result.start = requiredInteger(_json, "start", _path); + result.end = requiredInteger(_json, "end", _path); + if (std::optional sourceName = optionalString(_json, "sourceName", _path)) + result.sourceName = std::make_shared(std::move(*sourceName)); + return result; +} + +Json expressionToJson(SemanticDebugPointerExpression const& _expression); +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path); +Json typeToJson(SemanticDebugType const& _type); +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path); +Json pointerToJson(SemanticDebugPointer const& _pointer); +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path); + +Json expressionToJson(SemanticDebugPointerExpression const& _expression) +{ + Json result{{"kind", expressionKindToString(_expression.kind)}}; + setOptional(result, "value", _expression.value); + if (!_expression.operands.empty()) + { + result["operands"] = Json::array(); + for (auto const& operand: _expression.operands) + result["operands"].emplace_back(expressionToJson(operand)); + } + return result; +} + +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugPointerExpression result; + result.kind = expressionKindFromString(requiredString(_json, "kind", _path), _path + ".kind"); + result.value = optionalString(_json, "value", _path); + if (_json.contains("operands")) + { + Json const& operands = _json.at("operands"); + requireArray(operands, _path + ".operands"); + for (size_t index = 0; index < operands.size(); ++index) + result.operands.emplace_back( + expressionFromJson(operands.at(index), _path + ".operands[" + std::to_string(index) + "]")); + } + return result; +} + +Json typeComponentToJson(SemanticDebugTypeComponent const& _component) +{ + Json result{{"role", typeRoleToString(_component.role)}}; + setOptional(result, "name", _component.name); + setOptional(result, "referenceId", _component.referenceID); + if (_component.type) + result["type"] = typeToJson(*_component.type); + return result; +} + +SemanticDebugTypeComponent typeComponentFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugTypeComponent result; + result.role = typeRoleFromString(requiredString(_json, "role", _path), _path + ".role"); + result.name = optionalString(_json, "name", _path); + result.referenceID = optionalString(_json, "referenceId", _path); + if (_json.contains("type")) + result.type = std::make_shared(typeFromJson(_json.at("type"), _path + ".type")); + return result; +} + +Json typeToJson(SemanticDebugType const& _type) +{ + Json result{{"class", typeClassToString(_type.typeClass)}, {"kind", typeKindToString(_type.kind)}}; + setOptional(result, "bits", _type.bits); + setOptional(result, "places", _type.places); + setOptional(result, "bytes", _type.bytes); + setOptional(result, "payable", _type.payable); + setOptional(result, "isLibrary", _type.isLibrary); + setOptional(result, "isInterface", _type.isInterface); + if (!_type.enumValues.empty()) + result["enumValues"] = _type.enumValues; + setOptional(result, "count", _type.count); + setOptional(result, "externalFunction", _type.externalFunction); + if (!_type.components.empty()) + { + result["components"] = Json::array(); + for (auto const& component: _type.components) + result["components"].emplace_back(typeComponentToJson(component)); + } + setOptional(result, "definitionName", _type.definitionName); + if (_type.definitionLocation) + result["definitionLocation"] = sourceLocationToJson(*_type.definitionLocation); + setOptional(result, "dataLocation", _type.dataLocation); + setOptional(result, "dynamic", _type.dynamic); + return result; +} + +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugType result; + result.typeClass = typeClassFromString(requiredString(_json, "class", _path), _path + ".class"); + result.kind = typeKindFromString(requiredString(_json, "kind", _path), _path + ".kind"); + result.bits = optionalValue(_json, "bits", _path); + result.places = optionalValue(_json, "places", _path); + result.bytes = optionalValue(_json, "bytes", _path); + result.payable = optionalValue(_json, "payable", _path); + result.isLibrary = optionalValue(_json, "isLibrary", _path); + result.isInterface = optionalValue(_json, "isInterface", _path); + if (_json.contains("enumValues")) + { + Json const& enumValues = _json.at("enumValues"); + requireArray(enumValues, _path + ".enumValues"); + for (size_t index = 0; index < enumValues.size(); ++index) + { + require( + enumValues.at(index).is_string(), + _path + ".enumValues[" + std::to_string(index) + "] must be a string."); + result.enumValues.emplace_back(enumValues.at(index).get()); + } + } + result.count = optionalString(_json, "count", _path); + result.externalFunction = optionalValue(_json, "externalFunction", _path); + if (_json.contains("components")) + { + Json const& components = _json.at("components"); + requireArray(components, _path + ".components"); + for (size_t index = 0; index < components.size(); ++index) + result.components.emplace_back( + typeComponentFromJson(components.at(index), _path + ".components[" + std::to_string(index) + "]")); + } + result.definitionName = optionalString(_json, "definitionName", _path); + if (_json.contains("definitionLocation")) + result.definitionLocation + = sourceLocationFromJson(_json.at("definitionLocation"), _path + ".definitionLocation"); + result.dataLocation = optionalString(_json, "dataLocation", _path); + result.dynamic = optionalValue(_json, "dynamic", _path); + return result; +} + +Json pointerToJson(SemanticDebugPointer const& _pointer) +{ + Json result{{"class", pointerClassToString(_pointer.pointerClass)}}; + if (!_pointer.expectedParameters.empty()) + result["expectedParameters"] = _pointer.expectedParameters; + if (_pointer.location) + result["location"] = pointerLocationToString(*_pointer.location); + setOptional(result, "name", _pointer.name); + if (_pointer.slot) + result["slot"] = expressionToJson(*_pointer.slot); + if (_pointer.offset) + result["offset"] = expressionToJson(*_pointer.offset); + if (_pointer.length) + result["length"] = expressionToJson(*_pointer.length); + if (!_pointer.group.empty()) + { + result["group"] = Json::array(); + for (auto const& member: _pointer.group) + result["group"].emplace_back(pointerToJson(member)); + } + if (_pointer.count) + result["count"] = expressionToJson(*_pointer.count); + setOptional(result, "indexName", _pointer.indexName); + if (_pointer.listElement) + result["listElement"] = pointerToJson(*_pointer.listElement); + if (_pointer.condition) + result["condition"] = expressionToJson(*_pointer.condition); + if (_pointer.thenPointer) + result["thenPointer"] = pointerToJson(*_pointer.thenPointer); + if (_pointer.elsePointer) + result["elsePointer"] = pointerToJson(*_pointer.elsePointer); + if (!_pointer.definitions.empty()) + { + result["definitions"] = Json::array(); + for (auto const& [name, value]: _pointer.definitions) + result["definitions"].emplace_back(Json{{"name", name}, {"value", expressionToJson(value)}}); + } + if (_pointer.scopeTarget) + result["scopeTarget"] = pointerToJson(*_pointer.scopeTarget); + setOptional(result, "templateName", _pointer.templateName); + if (!_pointer.yields.empty()) + { + result["yields"] = Json::array(); + for (auto const& [name, value]: _pointer.yields) + result["yields"].emplace_back(Json{{"name", name}, {"value", value}}); + } + return result; +} + +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugPointer result; + result.pointerClass = pointerClassFromString(requiredString(_json, "class", _path), _path + ".class"); + if (_json.contains("expectedParameters")) + { + Json const& parameters = _json.at("expectedParameters"); + requireArray(parameters, _path + ".expectedParameters"); + for (size_t index = 0; index < parameters.size(); ++index) + { + require( + parameters.at(index).is_string(), + _path + ".expectedParameters[" + std::to_string(index) + "] must be a string."); + result.expectedParameters.emplace_back(parameters.at(index).get()); + } + } + if (std::optional location = optionalString(_json, "location", _path)) + result.location = pointerLocationFromString(*location, _path + ".location"); + result.name = optionalString(_json, "name", _path); + if (_json.contains("slot")) + result.slot = expressionFromJson(_json.at("slot"), _path + ".slot"); + if (_json.contains("offset")) + result.offset = expressionFromJson(_json.at("offset"), _path + ".offset"); + if (_json.contains("length")) + result.length = expressionFromJson(_json.at("length"), _path + ".length"); + if (_json.contains("group")) + { + Json const& group = _json.at("group"); + requireArray(group, _path + ".group"); + for (size_t index = 0; index < group.size(); ++index) + result.group.emplace_back( + pointerFromJson(group.at(index), _path + ".group[" + std::to_string(index) + "]")); + } + if (_json.contains("count")) + result.count = expressionFromJson(_json.at("count"), _path + ".count"); + result.indexName = optionalString(_json, "indexName", _path); + if (_json.contains("listElement")) + result.listElement = std::make_shared( + pointerFromJson(_json.at("listElement"), _path + ".listElement")); + if (_json.contains("condition")) + result.condition = expressionFromJson(_json.at("condition"), _path + ".condition"); + if (_json.contains("thenPointer")) + result.thenPointer = std::make_shared( + pointerFromJson(_json.at("thenPointer"), _path + ".thenPointer")); + if (_json.contains("elsePointer")) + result.elsePointer = std::make_shared( + pointerFromJson(_json.at("elsePointer"), _path + ".elsePointer")); + if (_json.contains("definitions")) + { + Json const& definitions = _json.at("definitions"); + requireArray(definitions, _path + ".definitions"); + for (size_t index = 0; index < definitions.size(); ++index) + { + std::string itemPath = _path + ".definitions[" + std::to_string(index) + "]"; + result.definitions.emplace_back( + requiredString(definitions.at(index), "name", itemPath), + expressionFromJson(requiredMember(definitions.at(index), "value", itemPath), itemPath + ".value")); + } + } + if (_json.contains("scopeTarget")) + result.scopeTarget = std::make_shared( + pointerFromJson(_json.at("scopeTarget"), _path + ".scopeTarget")); + result.templateName = optionalString(_json, "templateName", _path); + if (_json.contains("yields")) + { + Json const& yields = _json.at("yields"); + requireArray(yields, _path + ".yields"); + for (size_t index = 0; index < yields.size(); ++index) + { + std::string itemPath = _path + ".yields[" + std::to_string(index) + "]"; + result.yields.emplace_back( + requiredString(yields.at(index), "name", itemPath), + requiredString(yields.at(index), "value", itemPath)); + } + } + return result; +} + +Json variableLocationToJson(SemanticDebugVariableLocation const& _location) +{ + Json result{{"kind", variableLocationKindToString(_location.kind)}}; + setOptional(result, "pointerId", _location.pointerID); + return result; +} + +SemanticDebugVariableLocation variableLocationFromJson(Json const& _json, std::string const& _path) +{ + return { + .kind = variableLocationKindFromString(requiredString(_json, "kind", _path), _path + ".kind"), + .pointerID = optionalString(_json, "pointerId", _path)}; +} + +Json variableToJson(SemanticDebugVariable const& _variable) +{ + if (_variable.identifier && _variable.identifier->empty()) + solThrow(SemanticDebugDataSerializationError, "Variable identifier must not be empty."); + Json result = Json::object(); + setOptional(result, "identifier", _variable.identifier); + setOptional(result, "declarationAstId", _variable.declarationAstID); + if (_variable.declarationSourceLocation) + result["declarationSourceLocation"] = sourceLocationToJson(*_variable.declarationSourceLocation); + setOptional(result, "typeId", _variable.typeID); + if (_variable.ethdebugType) + result["type"] = typeToJson(*_variable.ethdebugType); + if (_variable.dataLocation) + result["dataLocation"] = variableLocationToJson(*_variable.dataLocation); + if (_variable.ethdebugPointer) + result["pointer"] = pointerToJson(*_variable.ethdebugPointer); + return result; +} + +SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugVariable result; + result.identifier = optionalString(_json, "identifier", _path); + if (result.identifier && result.identifier->empty()) + solThrow(SemanticDebugDataSerializationError, _path + ".identifier must not be empty."); + result.declarationAstID = optionalValue(_json, "declarationAstId", _path); + if (_json.contains("declarationSourceLocation")) + result.declarationSourceLocation = sourceLocationFromJson( + _json.at("declarationSourceLocation"), + _path + ".declarationSourceLocation" + ); + result.typeID = optionalString(_json, "typeId", _path); + if (_json.contains("type")) + result.ethdebugType = typeFromJson(_json.at("type"), _path + ".type"); + if (_json.contains("dataLocation")) + result.dataLocation = variableLocationFromJson(_json.at("dataLocation"), _path + ".dataLocation"); + if (_json.contains("pointer")) + result.ethdebugPointer = pointerFromJson(_json.at("pointer"), _path + ".pointer"); + return result; +} + +Json dataToJson(SemanticDebugData const& _data) +{ + Json result = Json::object(); + setOptional(result, "lexicalScopeId", _data.lexicalScopeID); + result["variables"] = Json::array(); + for (auto const& variable: _data.variableDefinitions) + result["variables"].emplace_back(variableToJson(variable)); + return result; +} + +SemanticDebugData dataFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugData result; + result.lexicalScopeID = optionalValue(_json, "lexicalScopeId", _path); + Json const& variables = requiredMember(_json, "variables", _path); + requireArray(variables, _path + ".variables"); + for (size_t index = 0; index < variables.size(); ++index) + result.variableDefinitions.emplace_back( + variableFromJson(variables.at(index), _path + ".variables[" + std::to_string(index) + "]")); + return result; +} + +} // namespace + +Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) +{ + Json result{ + {"format", std::string(SemanticDebugDataFormat)}, + {"version", SemanticDebugDataFormatVersion}, + {"entries", Json::array()}}; + setOptional(result, "contractName", _table.contractName()); + for (auto const& [astID, data]: _table.entries()) + { + require(data != nullptr, "Semantic debug data table contains a null entry."); + result["entries"].emplace_back(Json{{"astId", astID}, {"data", dataToJson(*data)}}); + } + return result; +} + +SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) +{ + requireObject(_json, "semantic debug data"); + std::string format = requiredString(_json, "format", "semantic debug data"); + require(format == SemanticDebugDataFormat, "Unsupported semantic debug data format \"" + format + "\"."); + unsigned version = requiredInteger(_json, "version", "semantic debug data"); + require( + version == SemanticDebugDataFormatVersion, + "Unsupported semantic debug data format version " + std::to_string(version) + "."); + std::optional contractName = optionalString(_json, "contractName", "semantic debug data"); + Json const& entries = requiredMember(_json, "entries", "semantic debug data"); + requireArray(entries, "semantic debug data.entries"); + + SemanticDebugDataTable result; + if (contractName) + result.setContractName(std::move(*contractName)); + for (size_t index = 0; index < entries.size(); ++index) + { + std::string path = "semantic debug data.entries[" + std::to_string(index) + "]"; + Json const& entry = entries.at(index); + int64_t astID = requiredInteger(entry, "astId", path); + require(!result.find(astID), path + " duplicates AST ID " + std::to_string(astID) + "."); + result.set( + astID, + std::make_shared( + dataFromJson(requiredMember(entry, "data", path), path + ".data"))); + } + return result; +} diff --git a/liblangutil/SemanticDebugDataSerDe.h b/liblangutil/SemanticDebugDataSerDe.h new file mode 100644 index 000000000000..68857c0ddc8d --- /dev/null +++ b/liblangutil/SemanticDebugDataSerDe.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 + +#pragma once + +#include + +#include +#include + +#include + +namespace solidity::langutil +{ + +/// The serialized semantic debug data is an internal, versioned sidecar for +/// Yul. It is intentionally distinct from the public ethdebug/format schemas. +inline constexpr std::string_view SemanticDebugDataFormat = "solidity-ethdebug-semantic-data"; +inline constexpr unsigned SemanticDebugDataFormatVersion = 1; + +struct SemanticDebugDataSerializationError: virtual util::Exception +{ +}; + +/// Serializes the complete AST-ID side table into its versioned JSON format. +Json semanticDebugDataToJson(SemanticDebugDataTable const& _table); + +/// Deserializes a versioned side table. Throws +/// SemanticDebugDataSerializationError if the input is malformed or uses an +/// unsupported format version. +SemanticDebugDataTable semanticDebugDataFromJson(Json const& _json); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h index 9084e4f8b3a5..e1062fc6c3ea 100644 --- a/liblangutil/SemanticDebugDataTable.h +++ b/liblangutil/SemanticDebugDataTable.h @@ -23,14 +23,29 @@ #include #include #include +#include #include namespace solidity::langutil { +/// Maps source-language AST origin IDs to semantic debug data. +/// The origin ID is sufficient for the current un-cloned Yul path. +/// A generated scope-instance discriminator must be added before semantic debug data can be +/// preserved through optimizations that clone or specialize Yul scopes with the same origin ID. class SemanticDebugDataTable { public: + void setContractName(std::string _contractName) + { + m_contractName = std::move(_contractName); + } + + std::optional const& contractName() const + { + return m_contractName; + } + void set(int64_t _astID, SemanticDebugData::ConstPtr _debugData) { m_byASTID[_astID] = std::move(_debugData); @@ -56,6 +71,7 @@ class SemanticDebugDataTable } private: + std::optional m_contractName; std::map m_byASTID; }; diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 92b2b4ef15b1..b8f406d46f6c 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -826,6 +826,7 @@ void addStorageVariables(SemanticDebugDataTable& _table, ContractDefinition cons SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable(ContractDefinition const& _contract) { SemanticDebugDataTable table; + table.setContractName(_contract.name()); addStorageVariables(table, _contract); diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 1f0ec6cf7953..a2d4145b4b53 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -17,6 +17,7 @@ // SPDX-License-Identifier: GPL-3.0 #include +#include #include #include @@ -239,6 +240,122 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) BOOST_CHECK(!table.find(std::nullopt)); } +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) +{ + SemanticDebugType elementType; + elementType.typeClass = SemanticDebugType::Class::Elementary; + elementType.kind = SemanticDebugType::Kind::Uint; + elementType.bits = 256; + + SemanticDebugTypeComponent component; + component.role = SemanticDebugTypeComponent::Role::Member; + component.name = "member"; + component.referenceID = "t_uint256"; + component.type = std::make_shared(elementType); + + SemanticDebugType type; + type.typeClass = SemanticDebugType::Class::Complex; + type.kind = SemanticDebugType::Kind::Struct; + type.components.emplace_back(std::move(component)); + type.definitionName = "Container"; + type.definitionLocation = SourceLocation{4, 20, std::make_shared("input.sol")}; + type.dataLocation = "storage"; + type.dynamic = false; + + SemanticDebugPointer templateReference; + templateReference.pointerClass = SemanticDebugPointer::Class::TemplateReference; + templateReference.expectedParameters = {"key"}; + templateReference.templateName = "mapping-value"; + templateReference.yields = {{"value", "renamed-value"}}; + + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"slot", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + })}}, + SemanticDebugPointer::conditional( + SemanticDebugPointerExpression::read("condition"), + std::move(templateReference), + SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "fallback", + SemanticDebugPointerExpression::variable("slot"), + SemanticDebugPointerExpression::literal("0x00"), + SemanticDebugPointerExpression::wordSize() + ) + ) + ); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.declarationAstID = 9; + variable.declarationSourceLocation = SourceLocation{21, 26, std::make_shared("input.sol")}; + variable.typeID = "t_struct$_Container"; + variable.ethdebugType = std::move(type); + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Storage, + .pointerID = "pointer:value" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 7; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.setContractName("ContainerContract"); + table.set(42, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["format"], SemanticDebugDataFormat); + BOOST_CHECK_EQUAL(serialized["version"], SemanticDebugDataFormatVersion); + BOOST_CHECK_EQUAL(serialized["contractName"], "ContainerContract"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_duplicate_ast_id) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["version"] = SemanticDebugDataFormatVersion + 1; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); + + serialized["version"] = SemanticDebugDataFormatVersion; + serialized["entries"] = Json::array({ + {{"astId", 1}, {"data", {{"variables", Json::array()}}}}, + {{"astId", 1}, {"data", {{"variables", Json::array()}}}} + }); + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_location_kinds_roundtrip) +{ + using Kind = SemanticDebugVariableLocation::Kind; + std::vector const kinds{ + Kind::Stack, + Kind::Storage, + Kind::TransientStorage, + Kind::Memory, + Kind::Calldata, + Kind::Returndata, + Kind::Code, + Kind::Computed, + Kind::OptimizedOut + }; + + SemanticDebugData data; + for (Kind kind: kinds) + { + SemanticDebugVariable variable; + variable.dataLocation = SemanticDebugVariableLocation{.kind = kind, .pointerID = std::nullopt}; + data.variableDefinitions.emplace_back(std::move(variable)); + } + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(data))); + Json const serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::langutil::test From d30f29919392c1e0866e2d235e2fddd8d8b13678 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 20 Jul 2026 10:19:41 +0200 Subject: [PATCH 24/47] test: Cover ethdebug semantic metadata production with isoltests --- libsolidity/interface/CompilerStack.cpp | 6 +++ libsolidity/interface/CompilerStack.h | 3 ++ test/libsolidity/EthdebugTest.cpp | 8 ++++ test/libsolidity/EthdebugTest.h | 3 +- .../semantic_function_variables.sol | 38 ++++++++++++++++++ ...emantic_inheritance_and_free_functions.sol | 26 +++++++++++++ .../semantic_storage_pointers.sol | 39 +++++++++++++++++++ .../semantic_type_descriptors.sol | 35 +++++++++++++++++ 8 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 test/libsolidity/ethdebugTests/semantic_function_variables.sol create mode 100644 test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol create mode 100644 test/libsolidity/ethdebugTests/semantic_storage_pointers.sol create mode 100644 test/libsolidity/ethdebugTests/semantic_type_descriptors.sol diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index ad97330e3f3c..6f96b61a840c 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -974,6 +974,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."); diff --git a/libsolidity/interface/CompilerStack.h b/libsolidity/interface/CompilerStack.h index 9e7b80dad72a..e3dee6a3d9e8 100644 --- a/libsolidity/interface/CompilerStack.h +++ b/libsolidity/interface/CompilerStack.h @@ -326,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; diff --git a/test/libsolidity/EthdebugTest.cpp b/test/libsolidity/EthdebugTest.cpp index 8681c2d4f3c4..7041fd4cd95c 100644 --- a/test/libsolidity/EthdebugTest.cpp +++ b/test/libsolidity/EthdebugTest.cpp @@ -20,6 +20,7 @@ #include #include +#include #include @@ -144,6 +145,13 @@ std::optional EthdebugTest::fetchOutput( return std::nullopt; return creation["contract"]; } + if (_outputName == "semantic") + { + auto const& semanticDebugData = compiler().yulSemanticDebugData(*resolved); + if (!semanticDebugData) + return std::nullopt; + return semanticDebugDataToJson(*semanticDebugData); + } } return std::nullopt; } diff --git a/test/libsolidity/EthdebugTest.h b/test/libsolidity/EthdebugTest.h index 7a0955230f01..fa9663561b07 100644 --- a/test/libsolidity/EthdebugTest.h +++ b/test/libsolidity/EthdebugTest.h @@ -39,7 +39,8 @@ namespace solidity::frontend::test /// /// Scope keys exposed to expectations: /// - Globals: `.resources`, `.compilation`. -/// - Per contract: `Contract.creation`, `Contract.runtime`, `Contract.contract`. +/// - Per contract: `Contract.creation`, `Contract.runtime`, `Contract.contract`, +/// `Contract.semantic` (the serialized internal semantic 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/libsolidity/ethdebugTests/semantic_function_variables.sol b/test/libsolidity/ethdebugTests/semantic_function_variables.sol new file mode 100644 index 000000000000..f140dd1b3074 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_function_variables.sol @@ -0,0 +1,38 @@ +contract C { + modifier guarded(bool enabled) { + require(enabled); + _; + } + + function f(uint256 value, bytes memory payload) + public + pure + guarded(true) + returns (uint256 result, bytes memory) + { + return (value, payload); + } +} +// ---- +// C.semantic.format: solidity-ethdebug-semantic-data +// C.semantic.version: 1 +// C.semantic.entries | length: 2 +// C.semantic.entries[0].data.variables | length: 1 +// C.semantic.entries[0].data.variables[0].identifier: enabled +// C.semantic.entries[0].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[0].data.variables[0].pointer.location: stack +// C.semantic.entries[0].data.variables[0].type.kind: bool +// C.semantic.entries[1].data.variables | length: 4 +// C.semantic.entries[1].data.variables[0].identifier: value +// C.semantic.entries[1].data.variables[0].type.kind: uint +// C.semantic.entries[1].data.variables[0].type.bits: 256 +// C.semantic.entries[1].data.variables[1].identifier: payload +// C.semantic.entries[1].data.variables[1].type.kind: bytes +// C.semantic.entries[1].data.variables[1].type.dataLocation: memory +// C.semantic.entries[1].data.variables[2].identifier: result +// C.semantic.entries[1].data.variables[3].identifier: +// C.semantic.entries[1].data.variables[3].declarationAstId: +// C.semantic.entries[1].data.variables[3].dataLocation.kind: stack +// C.semantic.entries[1].data.variables[3].pointer.name: +// C.semantic.entries[1].data.variables[3].pointer.location: stack +// C.semantic.entries[1].data.variables[3].type.kind: bytes diff --git a/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol b/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol new file mode 100644 index 000000000000..e9df4ab34ac5 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol @@ -0,0 +1,26 @@ +function helper(uint256 input) pure returns (uint256 output) { + return input; +} + +contract Base { + function inherited(uint256 value) public pure returns (uint256 result) { + return value; + } +} + +contract C is Base { + function callHelper(uint256 value) public pure returns (uint256 result) { + return helper(value); + } +} +// ---- +// C.semantic.entries | length: 3 +// C.semantic.entries[0].data.variables[0].identifier: input +// C.semantic.entries[0].data.variables[1].identifier: output +// C.semantic.entries[1].data.variables[0].identifier: value +// C.semantic.entries[1].data.variables[1].identifier: result +// C.semantic.entries[2].data.variables[0].identifier: value +// C.semantic.entries[2].data.variables[1].identifier: result +// C.semantic.entries[0].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[1].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[2].data.variables[0].dataLocation.kind: stack diff --git a/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol new file mode 100644 index 000000000000..77f7610683d7 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol @@ -0,0 +1,39 @@ +contract C { + struct Item { + uint128 first; + uint64 second; + } + + mapping(address => mapping(uint256 => Item)) balances; + uint16[8] packed; + uint256[] dynamicValues; + string label; + uint128 transient transientValue; +} +// ---- +// C.semantic.entries | length: 1 +// C.semantic.entries[0].data.variables | length: 5 +// C.semantic.entries[0].data.variables[0].identifier: balances +// C.semantic.entries[0].data.variables[0].dataLocation.kind: storage +// C.semantic.entries[0].data.variables[0].pointer.class: group +// C.semantic.entries[0].data.variables[0].pointer.expectedParameters: ["key", "key1"] +// C.semantic.entries[0].data.variables[0].pointer.group[0].location: storage +// C.semantic.entries[0].data.variables[0].pointer.group[0].slot.kind: keccak256 +// C.semantic.entries[0].data.variables[0].type.kind: mapping +// C.semantic.entries[0].data.variables[0].type.components[1].type.components[1].type.kind: struct +// C.semantic.entries[0].data.variables[1].identifier: packed +// C.semantic.entries[0].data.variables[1].pointer.class: list +// C.semantic.entries[0].data.variables[1].pointer.count.value: 0x08 +// C.semantic.entries[0].data.variables[1].pointer.listElement.offset.kind: product +// C.semantic.entries[0].data.variables[1].type.count: 0x08 +// C.semantic.entries[0].data.variables[2].identifier: dynamicValues +// C.semantic.entries[0].data.variables[2].pointer.class: group +// C.semantic.entries[0].data.variables[2].pointer.group[1].class: scope +// C.semantic.entries[0].data.variables[2].pointer.group[1].scopeTarget.class: list +// C.semantic.entries[0].data.variables[2].type.dynamic: true +// C.semantic.entries[0].data.variables[3].identifier: label +// C.semantic.entries[0].data.variables[3].pointer.group[1].class: conditional +// C.semantic.entries[0].data.variables[3].type.kind: string +// C.semantic.entries[0].data.variables[4].identifier: transientValue +// C.semantic.entries[0].data.variables[4].dataLocation.kind: transientStorage +// C.semantic.entries[0].data.variables[4].pointer.location: transient diff --git a/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol b/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol new file mode 100644 index 000000000000..3488cc3651ec --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol @@ -0,0 +1,35 @@ +type Amount is uint128; + +enum Choice { A, B, C } + +contract C { + struct Node { + uint256 value; + Node[] children; + } + + Amount amount; + Choice choice; + Node root; + C self; +} +// ---- +// C.semantic.entries[0].data.variables | length: 4 +// C.semantic.entries[0].data.variables[0].identifier: amount +// C.semantic.entries[0].data.variables[0].type.kind: alias +// C.semantic.entries[0].data.variables[0].type.definitionName: Amount +// C.semantic.entries[0].data.variables[0].type.components[0].role: underlying +// C.semantic.entries[0].data.variables[0].type.components[0].type.kind: uint +// C.semantic.entries[0].data.variables[0].type.components[0].type.bits: 128 +// C.semantic.entries[0].data.variables[1].identifier: choice +// C.semantic.entries[0].data.variables[1].type.kind: enum +// C.semantic.entries[0].data.variables[1].type.enumValues: ["A", "B", "C"] +// C.semantic.entries[0].data.variables[2].identifier: root +// C.semantic.entries[0].data.variables[2].type.kind: struct +// C.semantic.entries[0].data.variables[2].type.components[1].name: children +// C.semantic.entries[0].data.variables[2].type.components[1].type.kind: array +// C.semantic.entries[0].data.variables[2].type.components[1].type.components[0].type: +// C.semantic.entries[0].data.variables[2].type.components[1].type.components[0].referenceId: +// C.semantic.entries[0].data.variables[3].identifier: self +// C.semantic.entries[0].data.variables[3].type.kind: contract +// C.semantic.entries[0].data.variables[3].type.definitionName: C From 728d7806c20928baec7c66fb8aa242942bd7e551 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 20 Jul 2026 10:19:54 +0200 Subject: [PATCH 25/47] ethdebug: Serialize the semantic sidecar across compiler interfaces --- docs/using-the-compiler.rst | 21 ++- libsolidity/interface/CompilerStack.cpp | 8 +- libsolidity/interface/StandardCompiler.cpp | 118 ++++++++++++-- libsolidity/interface/StandardCompiler.h | 1 + libyul/YulStack.cpp | 15 +- libyul/YulStack.h | 8 +- solc/CommandLineInterface.cpp | 120 ++++++++++++++- solc/CommandLineInterface.h | 1 + solc/CommandLineParser.cpp | 43 +++++- solc/CommandLineParser.h | 3 + .../ethdebug_debuginfo_ssa_cfg/args | 2 +- .../standard_metadata_experimental/input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../output.json | 7 +- .../input.json | 2 +- .../input.json | 2 +- .../input.json | 2 +- test/ethdebugSchemaTests/input_file.json | 1 + test/libsolidity/StandardCompiler.cpp | 145 +++++++++++++++--- test/solc/CommandLineInterface.cpp | 87 +++++++++-- test/solc/CommandLineParser.cpp | 41 ++++- 22 files changed, 566 insertions(+), 64 deletions(-) diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index 04ad54a99aef..e962aa5b278a 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -275,6 +275,17 @@ Input Description } } }, + // Optional auxiliary inputs. + "auxiliaryInput": { + // Internal ethdebug semantic data sidecar for Yul input (experimental). + // This is the object emitted as the Solidity `irEthdebug` output. + "ethdebug": { + "format": "solidity-ethdebug-semantic-data", + "version": 1, + "contractName": "ContractName", + "entries": [/* ... */] + } + }, // Optional "settings": { @@ -391,7 +402,9 @@ Input Description // The snippet is quoted and follows the corresponding `@src` annotation. // - `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). - // - `ethdebug`: Ethdebug annotations (experimental). Automatically enabled when any ethdebug output is requested. + // - `ethdebug`: Ethdebug annotations (experimental). Depends on `ast-id`; explicitly selecting + // `ethdebug` without `ast-id` is an error. The required pair is automatically enabled when + // an ethdebug output is requested without an explicit `debugInfo` selection. // - `*`: Wildcard value that can be used to request all non-experimental components. "debugInfo": ["location", "snippet", "ast-id", "ethdebug"] }, @@ -444,6 +457,7 @@ Input Description // userdoc - User documentation (natspec) // metadata - Metadata // ir - Yul intermediate representation of the code before optimization + // irEthdebug - Internal ethdebug semantic data sidecar for the Yul IR (experimental) // irAst - AST of Yul intermediate representation of the code before optimization (experimental) // irOptimized - Intermediate representation after optimization // irOptimizedAst - AST of intermediate representation after optimization (experimental) @@ -602,6 +616,8 @@ Output Description "devdoc": {}, // Intermediate representation before optimization (string) "ir": "", + // Internal ethdebug semantic data sidecar for Yul (experimental) + "irEthdebug": {/* ... */}, // AST of intermediate representation before optimization "irAst": {/* ... */}, // Intermediate representation after optimization (string) @@ -769,7 +785,8 @@ The table below details all currently available experimental features. +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | Non-mainnet EVMs | ``evm`` | yes | ``--evm-version `` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ -| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, ``--debug-info ethdebug`` | +| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, ``--ir-ethdebug``, | +| | | | ``--ethdebug-input``, ``--debug-info ast-id,ethdebug`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | | | no | ``--yul-cfg-json`` | | SSA CFG + ``ssa-cfg`` +------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 6f96b61a840c..1e238ae8fdbc 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -298,11 +298,11 @@ void CompilerStack::setMetadataHash(MetadataHash _metadataHash) void CompilerStack::selectDebugInfo(DebugInfoSelection _debugInfoSelection) { solAssert(m_stackState < CompilationSuccessful, "Must select debug info components before compilation."); + solAssert( + !_debugInfoSelection.ethdebug || _debugInfoSelection.astID, + "Ethdebug semantic data requires AST ID debug information." + ); m_debugInfoSelection = _debugInfoSelection; - // Semantic debug metadata is reattached across the Yul text boundary using @ast-id - // comments as the join key. Without them the metadata would be silently lost. - if (m_debugInfoSelection.ethdebug) - m_debugInfoSelection.astID = true; } void CompilerStack::addSMTLib2Response(h256 const& _hash, std::string const& _response) diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index c8cb87504624..5df190fe8e0c 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -36,6 +37,7 @@ #include #include +#include #include #include @@ -168,7 +170,7 @@ bool hashMatchesContent(std::string const& _hash, std::string const& _content) bool isArtifactRequested(Json const& _outputSelection, std::string const& _artifact, bool _wildcardMatchesExperimental) { - static std::set experimental{"ir", "irAst", "irOptimized", "irOptimizedAst", "yulCFGJson", "ethdebug"}; + static std::set experimental{"ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "yulCFGJson", "ethdebug"}; for (auto const& selectedArtifactJson: _outputSelection) { std::string const& selectedArtifact = selectedArtifactJson.get(); @@ -177,7 +179,7 @@ bool isArtifactRequested(Json const& _outputSelection, std::string const& _artif boost::algorithm::starts_with(_artifact, selectedArtifact + ".") ) { - if (_artifact.find("ethdebug") != std::string::npos) + if (_artifact == "irEthdebug" || _artifact.find("ethdebug") != std::string::npos) // only accept exact matches for ethdebug, e.g. evm.bytecode.ethdebug return selectedArtifact == _artifact; return true; @@ -188,7 +190,7 @@ bool isArtifactRequested(Json const& _outputSelection, std::string const& _artif if (_artifact == "yulCFGJson") return false; // TODO: everything ethdebug related is only experimental for now, so it should not be matched by "*". - if (_artifact.find("ethdebug") != std::string::npos) + if (_artifact == "irEthdebug" || _artifact.find("ethdebug") != std::string::npos) return false; // "ir", "irOptimized" can only be matched by "*" if activated. if (experimental.count(_artifact) == 0 || _wildcardMatchesExperimental) @@ -275,7 +277,7 @@ bool isBinaryRequested(Json const& _outputSelection) // This does not include "evm.methodIdentifiers" on purpose! static std::vector const outputsThatRequireBinaries = std::vector{ "*", - "ir", "irAst", "irOptimized", "irOptimizedAst", "yulCFGJson", + "ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "yulCFGJson", "evm.gasEstimates", "evm.legacyAssembly", "evm.assembly" } + evmObjectComponents("bytecode") + evmObjectComponents("deployedBytecode"); @@ -324,7 +326,7 @@ bool isAnyEthdebugRequested(Json const& _outputSelection) return false; static std::array constexpr ethdebugArtifacts{ - "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", + "irEthdebug", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ethdebug.resources", "ethdebug.compilation" }; @@ -479,7 +481,7 @@ std::optional checkSourceKeys(Json const& _input, std::string const& _name std::optional checkAuxiliaryInputKeys(Json const& _input) { - static std::set keys{"smtlib2responses"}; + static std::set keys{"ethdebug", "smtlib2responses"}; return checkKeys(_input, keys, "auxiliaryInput"); } @@ -852,6 +854,26 @@ std::variant StandardCompiler::parseI ret.smtLib2Responses[hash] = response.get(); } } + + if (auxInputs.contains("ethdebug")) + { + if (ret.language != "Yul") + return formatFatalError( + Error::Type::JSONError, + "\"auxiliaryInput.ethdebug\" can only be used for Yul input." + ); + try + { + ret.semanticDebugData = semanticDebugDataFromJson(auxInputs["ethdebug"]); + } + catch (SemanticDebugDataSerializationError const& _exception) + { + return formatFatalError( + Error::Type::JSONError, + "Invalid \"auxiliaryInput.ethdebug\": " + stringOrDefault(_exception.comment()) + ); + } + } } Json const& settings = _input.value("settings", Json::object()); @@ -962,7 +984,6 @@ std::variant StandardCompiler::parseI Error::Type::JSONError, "To use 'snippet' with settings.debug.debugInfo you must select also 'location'." ); - ret.debugInfoSelection = debugInfoSelection.value(); } } @@ -1251,12 +1272,32 @@ std::variant StandardCompiler::parseI ret.modelCheckerSettings.timeout = modelCheckerSettings["timeout"].get(); } - if ((ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) || isAnyEthdebugRequested(ret.outputSelection)) + if ( + (ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) || + isAnyEthdebugRequested(ret.outputSelection) || + ret.semanticDebugData.has_value() + ) { if (ret.language != "Solidity" && ret.language != "Yul") return formatFatalError(Error::Type::FatalError, "'settings.debug.debugInfo' 'ethdebug' is only supported for languages 'Solidity' and 'Yul'."); } + static std::array constexpr semanticSidecarArtifacts{"irEthdebug"}; + if (ret.semanticDebugData || areArtifactsRequested(ret.outputSelection, semanticSidecarArtifacts)) + { + if (!ret.debugInfoSelection.has_value()) + { + ret.debugInfoSelection = DebugInfoSelection::Default(); + ret.debugInfoSelection->enable("ethdebug"); + } + else if (!ret.debugInfoSelection->ethdebug) + return formatFatalError( + Error::Type::FatalError, + "'ethdebug' needs to be enabled in 'settings.debug.debugInfo' when using 'irEthdebug' or " + "'auxiliaryInput.ethdebug'." + ); + } + if (isEthdebugProgramRequested(ret.outputSelection)) { if (ret.language == "Solidity" && !ret.viaIR) @@ -1278,12 +1319,24 @@ std::variant StandardCompiler::parseI { if (!ret.experimental) return formatFatalError(Error::Type::FatalError, "Ethdebug annotations are experimental and can only be included in 'settings.debug.debugInfo' by enabling the 'settings.experimental' option."); + // Implicitly enabled selections start from the default set and always contain ast-id, so + // only an explicit partial selection can fail this check. + if (!ret.debugInfoSelection->astID) + return formatFatalError( + Error::Type::JSONError, + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + ); } + if ( + ret.debugInfoSelection.has_value() && + ret.debugInfoSelection->ethdebug && + ret.optimiserSettings.runYulOptimiser + ) + solUnimplemented("Optimization is not yet supported with ethdebug."); + if (isEthdebugProgramRequested(ret.outputSelection)) { - if (ret.optimiserSettings.runYulOptimiser) - solUnimplemented("Optimization is not yet supported with ethdebug."); if (ret.viaSSACFG) solUnimplemented("SSA CFG codegen does not yet support ethdebug."); } @@ -1626,6 +1679,13 @@ Json StandardCompiler::compileSolidity(StandardCompiler::InputsAndSettings _inpu // IR if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "ir", wildcardMatchesExperimental)) contractData["ir"] = compilerStack.yulIR(contractName).value_or(""); + if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irEthdebug", wildcardMatchesExperimental)) + { + auto const& semanticDebugData = compilerStack.yulSemanticDebugData(contractName); + contractData["irEthdebug"] = semanticDebugDataToJson( + semanticDebugData ? *semanticDebugData : SemanticDebugDataTable{} + ); + } if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irAst", wildcardMatchesExperimental)) contractData["irAst"] = compilerStack.yulIRAst(contractName).value_or(Json{}); if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irOptimized", wildcardMatchesExperimental)) @@ -1799,9 +1859,13 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) solAssert(stack.hasErrors(), "No error reported, but parsing/analysis failed."); else { + if (_inputsAndSettings.semanticDebugData) + stack.attachSemanticDebugData(*_inputsAndSettings.semanticDebugData); contractName = stack.parserResult()->name; if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ir", wildcardMatchesExperimental)) output["contracts"][sourceName][contractName]["ir"] = stack.print(); + if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "irEthdebug", wildcardMatchesExperimental)) + output["contracts"][sourceName][contractName]["irEthdebug"] = semanticDebugDataToJson(stack.semanticDebugData()); if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ast", wildcardMatchesExperimental)) { @@ -1816,6 +1880,28 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) object.bytecode->link(_inputsAndSettings.libraries); if (deployedObject.bytecode) deployedObject.bytecode->link(_inputsAndSettings.libraries); + + if (_inputsAndSettings.semanticDebugData && stack.debugInfoSelection().ethdebug) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + std::string const& ethdebugContractName = + _inputsAndSettings.semanticDebugData->contractName().value_or(contractName); + auto addProgramContext = [&](MachineAssemblyObject& _object) + { + if (!_object.assembly || !_object.bytecode) + return; + _object.ethdebug = evmasm::ethdebug::program( + ethdebugContractName, + 0, + *_object.assembly, + *_object.bytecode, + Ethdebug::programContext(*_inputsAndSettings.semanticDebugData, sourceIndices) + ); + }; + addProgramContext(object); + addProgramContext(deployedObject); + } } for (auto const& error: stack.errors()) @@ -1890,9 +1976,19 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.resources")) { solAssert(_inputsAndSettings.experimental, ""); + Json types = Json::object(); + Json pointers = Json::object(); + if (_inputsAndSettings.semanticDebugData) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + Ethdebug::collectResources(types, pointers, *_inputsAndSettings.semanticDebugData, &sourceIndices); + } output["ethdebug"]["resources"] = evmasm::ethdebug::resources( {{.id = 0, .path = sourceName, .contents = sourceContents, .language = "Yul"}}, - VersionString + VersionString, + std::move(types), + std::move(pointers) ); } if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.compilation")) diff --git a/libsolidity/interface/StandardCompiler.h b/libsolidity/interface/StandardCompiler.h index cf2200d8cf12..0a9440aca201 100644 --- a/libsolidity/interface/StandardCompiler.h +++ b/libsolidity/interface/StandardCompiler.h @@ -82,6 +82,7 @@ class StandardCompiler OptimiserSettings optimiserSettings; std::optional debugInfoSelection; std::map libraries; + std::optional semanticDebugData; bool metadataLiteralSources = false; CompilerStack::MetadataFormat metadataFormat = CompilerStack::defaultMetadataFormat(); CompilerStack::MetadataHash metadataHash = CompilerStack::MetadataHash::IPFS; diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index 4f5633b8fd75..db535645931e 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -206,10 +206,12 @@ 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 metadata cannot be represented in the printed source. It is merged into the - // retained side table here and reattached by AST ID after the reparse. Entries that are not - // attached to any Yul node (e.g. contract-scope storage metadata) survive in the table itself. - collectSemanticDebugData(*m_parserResult, m_semanticDebugData); + // Semantic debug metadata crosses the public Yul boundary in its separately serialized sidecar, + // rather than in the printed source. When requested, merge it into the retained side table here + // and reattach it by AST ID after the internal reparse. Entries that are not attached to any Yul + // node (e.g. contract-scope storage metadata) survive in the table itself. + if (m_debugInfoSelection.ethdebug) + collectSemanticDebugData(*m_parserResult, m_semanticDebugData); std::string source = print(); YulStack cleanStack( @@ -229,7 +231,7 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); - if (!m_semanticDebugData.empty()) + 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, @@ -445,7 +447,10 @@ void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) { yulAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); yulAssert(m_parserResult, ""); + yulAssert(m_debugInfoSelection.ethdebug, "Semantic debug data was supplied without requesting ethdebug."); + if (_table.contractName()) + m_semanticDebugData.setContractName(*_table.contractName()); for (auto const& [astID, debugData]: _table.entries()) m_semanticDebugData.set(astID, debugData); diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 752fa9ea70d3..b9cf38b72a2f 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -99,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; @@ -150,6 +155,7 @@ class YulStack: public langutil::CharStreamProvider std::shared_ptr parserResult() const; void attachSemanticDebugData(langutil::SemanticDebugDataTable const& _table); + langutil::SemanticDebugDataTable const& semanticDebugData() const { return m_semanticDebugData; } Dialect const& dialect() const; diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index a3afa509868a..e03e5d379cc8 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include #include +#include #include #include @@ -270,6 +272,25 @@ void CommandLineInterface::handleIR(std::string const& _contractName) } } +void CommandLineInterface::handleIREthdebug(std::string const& _contractName) +{ + solAssert(CompilerInputModes.count(m_options.input.mode) == 1); + + if (!m_options.compiler.outputs.irEthdebug) + return; + + auto const& semanticDebugData = m_compiler->yulSemanticDebugData(_contractName); + std::string serialized = jsonPrint( + semanticDebugData ? semanticDebugDataToJson(*semanticDebugData) : + semanticDebugDataToJson(SemanticDebugDataTable{}), + m_options.formatting.json + ); + if (!m_options.output.dir.empty()) + createFile(m_compiler->filesystemFriendlyName(_contractName) + "_ir_ethdebug.json", serialized); + else + sout() << "IR ethdebug semantic data sidecar:" << std::endl << serialized << std::endl; +} + void CommandLineInterface::handleIRAst(std::string const& _contractName) { solAssert(CompilerInputModes.count(m_options.input.mode) == 1); @@ -952,6 +973,7 @@ void CommandLineInterface::compile() pipelineConfig.irCodegen = pipelineConfig.irOptimization || m_options.compiler.outputs.ir || + m_options.compiler.outputs.irEthdebug || m_options.compiler.outputs.irAstJson; pipelineConfig.bytecode = m_options.compiler.estimateGas || @@ -1282,12 +1304,71 @@ std::string CommandLineInterface::objectWithLinkRefsHex(evmasm::LinkerObject con void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) { solAssert(m_options.input.mode == InputMode::Assembler); + std::map semanticDebugDataBySource; + if (!m_options.input.ethdebugInputs.empty()) + { + for (std::string const& input: m_options.input.ethdebugInputs) + { + size_t const separator = input.find('='); + std::string sourceUnitName; + boost::filesystem::path sidecarPath; + if (separator == std::string::npos) + { + if (m_fileReader.sourceUnits().size() != 1 || m_options.input.ethdebugInputs.size() != 1) + solThrow( + CommandLineExecutionError, + "Unqualified --ethdebug-input requires exactly one strict assembly input; " + "use --ethdebug-input yul=file when compiling multiple inputs." + ); + sourceUnitName = m_fileReader.sourceUnits().begin()->first; + sidecarPath = input; + } + else + { + sourceUnitName = input.substr(0, separator); + sidecarPath = input.substr(separator + 1); + if (sourceUnitName.empty() || sidecarPath.empty()) + solThrow(CommandLineExecutionError, "Invalid --ethdebug-input mapping: " + input); + if (!m_fileReader.sourceUnits().contains(sourceUnitName)) + solThrow(CommandLineExecutionError, "Unknown Yul source in --ethdebug-input mapping: " + sourceUnitName); + } + + if (semanticDebugDataBySource.contains(sourceUnitName)) + solThrow(CommandLineExecutionError, "Duplicate --ethdebug-input for Yul source: " + sourceUnitName); + + Json json; + std::string parseError; + std::string contents; + try + { + contents = readFileAsString(sidecarPath); + } + catch (std::exception const& _exception) + { + solThrow(CommandLineExecutionError, "Could not read --ethdebug-input: "s + _exception.what()); + } + if (!jsonParseStrict(contents, json, &parseError)) + solThrow(CommandLineExecutionError, "Could not parse --ethdebug-input: " + parseError); + try + { + semanticDebugDataBySource.emplace(sourceUnitName, semanticDebugDataFromJson(json)); + } + catch (SemanticDebugDataSerializationError const& _exception) + { + solThrow( + CommandLineExecutionError, + "Invalid --ethdebug-input: " + stringOrDefault(_exception.comment()) + ); + } + } + } bool successful = true; std::map yulStacks; std::map objects; for (auto const& [sourceUnitName, yulSource]: m_fileReader.sourceUnits()) { + auto const semanticDebugData = semanticDebugDataBySource.find(sourceUnitName); auto& stack = yulStacks[sourceUnitName] = yul::YulStack( m_options.output.evmVersion, m_options.optimiserSettings(), @@ -1301,6 +1382,9 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) solAssert(stack.hasErrors(), "No error reported, but parsing/analysis failed."); else { + if (semanticDebugData != semanticDebugDataBySource.end()) + stack.attachSemanticDebugData(semanticDebugData->second); + if ( m_options.compiler.outputs.asmJson && stack.parserResult() && @@ -1317,6 +1401,20 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) yul::MachineAssemblyObject object = stack.assemble(_targetMachine, m_options.output.viaSSACFG); if (object.bytecode) object.bytecode->link(m_options.linker.libraries); + if (semanticDebugData != semanticDebugDataBySource.end() && object.assembly && object.bytecode) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + std::string const& ethdebugContractName = + semanticDebugData->second.contractName().value_or(stack.parserResult()->name); + object.ethdebug = evmasm::ethdebug::program( + ethdebugContractName, + 0, + *object.assembly, + *object.bytecode, + Ethdebug::programContext(semanticDebugData->second, sourceIndices) + ); + } objects.insert({sourceUnitName, std::move(object)}); } } @@ -1343,6 +1441,7 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) for (auto const& [sourceUnitName, yulSource]: m_fileReader.sourceUnits()) { + auto const semanticDebugData = semanticDebugDataBySource.find(sourceUnitName); solAssert(_targetMachine == yul::YulStack::Machine::EVM); yul::YulStack const& stack = yulStacks[sourceUnitName]; @@ -1350,11 +1449,21 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) if (m_options.compiler.outputs.ethdebugResources) { + Json types = Json::object(); + Json pointers = Json::object(); + if (semanticDebugData != semanticDebugDataBySource.end()) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + Ethdebug::collectResources(types, pointers, semanticDebugData->second, &sourceIndices); + } sout() << "======= Debug Data (ethdebug/format/info/resources) =======" << std::endl; sout() << util::jsonPrint( evmasm::ethdebug::resources( {{.id = 0, .path = sourceUnitName, .contents = yulSource, .language = "Yul"}}, - VersionString + VersionString, + std::move(types), + std::move(pointers) ), m_options.formatting.json ) << std::endl; @@ -1381,6 +1490,14 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) sout() << std::endl << "Pretty printed source:" << std::endl; sout() << stack.print() << std::endl; } + if (m_options.compiler.outputs.irEthdebug) + { + sout() << std::endl << "Ethdebug semantic data sidecar:" << std::endl; + sout() << jsonPrint( + semanticDebugDataToJson(stack.semanticDebugData()), + m_options.formatting.json + ) << std::endl; + } if (m_options.compiler.outputs.binary) { @@ -1462,6 +1579,7 @@ void CommandLineInterface::outputCompilationResults() handleBytecode(contract); handleIR(contract); + handleIREthdebug(contract); handleIRAst(contract); handleIROptimized(contract); handleIROptimizedAst(contract); diff --git a/solc/CommandLineInterface.h b/solc/CommandLineInterface.h index 3f8ddfac4631..b2018cd1c88f 100644 --- a/solc/CommandLineInterface.h +++ b/solc/CommandLineInterface.h @@ -105,6 +105,7 @@ class CommandLineInterface void handleBinary(std::string const& _contract); void handleOpcode(std::string const& _contract); void handleIR(std::string const& _contract); + void handleIREthdebug(std::string const& _contract); void handleIRAst(std::string const& _contract); void handleIROptimized(std::string const& _contract); void handleIROptimizedAst(std::string const& _contract); diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 54df68667b1a..267168d5a9a6 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -55,6 +55,7 @@ static std::string const g_strGas = "gas"; static std::string const g_strHelp = "help"; static std::string const g_strImportAst = "import-ast"; static std::string const g_strImportEvmAssemblerJson = "import-asm-json"; +static std::string const g_strEthdebugInput = "ethdebug-input"; static std::string const g_strInputFile = "input-file"; static std::string const g_strYul = "yul"; static std::string const g_strYulDialect = "yul-dialect"; @@ -162,6 +163,8 @@ std::vector const& CommandLineParser::experimentalOptionNames() static std::vector const names{ g_strImportAst, g_strImportEvmAssemblerJson, + g_strEthdebugInput, + "ir-ethdebug", "ir-ast-json", "ir-optimized-ast-json", "yul-cfg-json", @@ -468,6 +471,7 @@ void CommandLineParser::parseOutputSelection() static std::set const assemblerModeOutputs = { CompilerOutputs::componentName(&CompilerOutputs::asm_), CompilerOutputs::componentName(&CompilerOutputs::binary), + CompilerOutputs::componentName(&CompilerOutputs::irEthdebug), CompilerOutputs::componentName(&CompilerOutputs::irOptimized), CompilerOutputs::componentName(&CompilerOutputs::astCompactJson), CompilerOutputs::componentName(&CompilerOutputs::asmJson), @@ -700,6 +704,12 @@ General Information)").c_str(), po::value()->value_name(util::joinHumanReadable(g_yulDialectArgs, ",")), "Input dialect to use in assembly or yul mode." ) + ( + g_strEthdebugInput.c_str(), + po::value>()->composing()->value_name("file|yul=file"), + "(experimental) Attach a serialized ethdebug semantic data sidecar to strict assembly input. " + "Repeat yul=file for multiple Yul inputs." + ) ; desc.add(assemblyModeOptions); @@ -751,6 +761,7 @@ General Information)").c_str(), (CompilerOutputs::componentName(&CompilerOutputs::binaryRuntime).c_str(), "Binary of the runtime part of the contracts in hex.") (CompilerOutputs::componentName(&CompilerOutputs::abi).c_str(), "ABI specification of the contracts.") (CompilerOutputs::componentName(&CompilerOutputs::ir).c_str(), "Intermediate Representation (IR) of all contracts.") + (CompilerOutputs::componentName(&CompilerOutputs::irEthdebug).c_str(), "(experimental) Serialized ethdebug semantic data sidecar for the IR of all contracts.") (CompilerOutputs::componentName(&CompilerOutputs::irAstJson).c_str(), "(experimental) AST of Intermediate Representation (IR) of all contracts in a compact JSON format.") (CompilerOutputs::componentName(&CompilerOutputs::irOptimized).c_str(), "Optimized Intermediate Representation (IR) of all contracts.") (CompilerOutputs::componentName(&CompilerOutputs::irOptimizedAstJson).c_str(), "(experimental) AST of optimized Intermediate Representation (IR) of all contracts in a compact JSON format.") @@ -1071,7 +1082,8 @@ void CommandLineParser::processArgs() {g_strModelCheckerBMCLoopIterations, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, {g_strModelCheckerContracts, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, {g_strModelCheckerTargets, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, - {g_strViaSSACFG, {InputMode::Compiler, InputMode::CompilerWithASTImport, InputMode::Assembler}} + {g_strViaSSACFG, {InputMode::Compiler, InputMode::CompilerWithASTImport, InputMode::Assembler}}, + {g_strEthdebugInput, {InputMode::Assembler}} }; std::vector invalidOptionsForCurrentInputMode; for (auto const& [optionName, inputModes]: validOptionInputModeCombinations) @@ -1194,6 +1206,8 @@ void CommandLineParser::processArgs() if (m_options.output.debugInfoSelection->snippet && !m_options.output.debugInfoSelection->location) solThrow(CommandLineValidationError, "To use 'snippet' with --" + g_strDebugInfo + " you must select also 'location'."); + if (m_options.output.debugInfoSelection->ethdebug && !m_options.output.debugInfoSelection->astID) + solThrow(CommandLineValidationError, "To use 'ethdebug' with --" + g_strDebugInfo + " you must select also 'ast-id'."); } parseCombinedJsonOption(); @@ -1352,9 +1366,25 @@ void CommandLineParser::processArgs() if (dialect != g_strEVM) solThrow(CommandLineValidationError, "Invalid option for --" + g_strYulDialect + ": " + dialect); } + if (m_args.contains(g_strEthdebugInput)) + m_options.input.ethdebugInputs = m_args[g_strEthdebugInput].as>(); m_options.output.viaSSACFG = m_args.contains(g_strViaSSACFG); + if (!m_options.input.ethdebugInputs.empty() || m_options.compiler.outputs.irEthdebug) + { + if (!m_options.output.debugInfoSelection.has_value()) + { + m_options.output.debugInfoSelection = DebugInfoSelection::Default(); + m_options.output.debugInfoSelection->enable("ethdebug"); + } + else if (!m_options.output.debugInfoSelection->ethdebug) + solThrow( + CommandLineValidationError, + "--debug-info must contain ethdebug when using --ethdebug-input or --ir-ethdebug." + ); + } + if (m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) { if (m_options.output.viaSSACFG) @@ -1540,6 +1570,17 @@ void CommandLineParser::processArgs() } } + if (m_options.compiler.outputs.irEthdebug) + { + if (!m_options.output.debugInfoSelection.has_value()) + { + m_options.output.debugInfoSelection = DebugInfoSelection::Default(); + m_options.output.debugInfoSelection->enable("ethdebug"); + } + else if (!m_options.output.debugInfoSelection->ethdebug) + solThrow(CommandLineValidationError, "--debug-info must contain ethdebug when compiling with --ir-ethdebug."); + } + if ( m_options.output.debugInfoSelection.has_value() && m_options.output.debugInfoSelection->ethdebug && m_options.input.mode != InputMode::Compiler diff --git a/solc/CommandLineParser.h b/solc/CommandLineParser.h index 2593b6b2f886..62b0cc6b465a 100644 --- a/solc/CommandLineParser.h +++ b/solc/CommandLineParser.h @@ -78,6 +78,7 @@ struct CompilerOutputs {"bin-runtime", &CompilerOutputs::binaryRuntime}, {"abi", &CompilerOutputs::abi}, {"ir", &CompilerOutputs::ir}, + {"ir-ethdebug", &CompilerOutputs::irEthdebug}, {"ir-ast-json", &CompilerOutputs::irAstJson}, {"ir-optimized", &CompilerOutputs::irOptimized}, {"ir-optimized-ast-json", &CompilerOutputs::irOptimizedAstJson}, @@ -104,6 +105,7 @@ struct CompilerOutputs bool binaryRuntime = false; bool abi = false; bool ir = false; + bool irEthdebug = false; bool irAstJson = false; bool yulCFGJson = false; bool irOptimized = false; @@ -193,6 +195,7 @@ struct CommandLineOptions FileReader::FileSystemPathSet allowedDirectories; bool ignoreMissingFiles = false; bool noImportCallback = false; + std::vector ethdebugInputs; } input; struct Output diff --git a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args index 8dd599e3d575..ccd11791ab21 100644 --- a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args +++ b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args @@ -1 +1 @@ ---experimental --debug-info ethdebug --via-ssa-cfg +--experimental --debug-info ast-id,ethdebug --via-ssa-cfg diff --git a/test/cmdlineTests/standard_metadata_experimental/input.json b/test/cmdlineTests/standard_metadata_experimental/input.json index fddbca92eb2f..73922713c00c 100644 --- a/test/cmdlineTests/standard_metadata_experimental/input.json +++ b/test/cmdlineTests/standard_metadata_experimental/input.json @@ -8,6 +8,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json index 878b2b41f77a..89d120cdd9c3 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json index e1104f5b300c..fc5be60b592f 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/output.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/output.json index 91bcefcf44af..cc89844e0a1b 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/output.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/output.json @@ -61,7 +61,12 @@ interface C { ] }, "pointers": {}, - "types": {} + "types": { + "t_bytes32": { + "kind": "bytes", + "size": 32 + } + } } }, "sources": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json index 5fca11155a92..ecdb964bd0a3 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json @@ -8,7 +8,7 @@ "settings": { "viaIR": true, "debug": { - "debugInfo": ["ethdebug"] + "debugInfo": ["ast-id", "ethdebug"] }, "outputSelection": { "A.sol": { diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json index c459aadef6f2..5b9f3afce007 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["ir", "irOptimized", "evm.bytecode.ethdebug", "ethdebug.resources"]} } diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json index eac38e3db63d..6d936e39bdd7 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug"]} } diff --git a/test/ethdebugSchemaTests/input_file.json b/test/ethdebugSchemaTests/input_file.json index dc7038e6d02e..b67fc8a40b62 100644 --- a/test/ethdebugSchemaTests/input_file.json +++ b/test/ethdebugSchemaTests/input_file.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index c13704cc061a..443ca7ce2ba8 100644 --- a/test/libsolidity/StandardCompiler.cpp +++ b/test/libsolidity/StandardCompiler.cpp @@ -1934,36 +1934,48 @@ BOOST_AUTO_TEST_CASE(ethdebug_excluded_from_wildcards) BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) { + frontend::StandardCompiler compiler; + Json missingAstID = compiler.compile(generateExperimentalStandardJson( + true, + Json::array({"ethdebug"}), + Json::array({"ir"}) + )); + BOOST_CHECK(containsError( + missingAstID, + "JSONError", + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + )); + static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; @@ -2012,14 +2024,14 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), {} }, { @@ -2027,7 +2039,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) }, { generateExperimentalStandardJson( - true, Json::array({"ethdebug"}), { + true, Json::array({"ast-id", "ethdebug"}), { {"fileA", {{"contractA", Json::array({"evm.deployedBytecode.bin"})}}}, {"fileB", {{"contractB", Json::array({"evm.bytecode.bin"})}}} }, @@ -2039,15 +2051,14 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), SolidityAstCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), SolidityAstCode()), std::nullopt, }, }; - frontend::StandardCompiler compiler; for (auto const& test: tests) { Json result = compiler.compile(std::get<0>(test)); @@ -2060,7 +2071,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) { static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt }, { @@ -2068,7 +2079,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2076,7 +2087,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2096,21 +2107,21 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2269,6 +2280,102 @@ BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::d } } +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) +{ + frontend::StandardCompiler compiler; + Json firstInput = generateExperimentalStandardJson( + true, + {}, + Json::array({ + "ir", + "irEthdebug", + "evm.bytecode.object", + "evm.bytecode.ethdebug", + "evm.deployedBytecode.object", + "evm.deployedBytecode.ethdebug" + }), + SolidityCode({{ + "fileA", + "contract C { uint256 value; function f(uint256 argument) public { value = argument; } " + "function g(uint256 argument) public pure returns (uint256) { return argument; } }" + }}) + ); + Json firstResult = compiler.compile(firstInput); + BOOST_REQUIRE(containsAtMostWarnings(firstResult)); + Json const& firstContract = firstResult["contracts"]["fileA"]["C"]; + BOOST_REQUIRE(firstContract["ir"].is_string()); + BOOST_REQUIRE(firstContract["irEthdebug"].is_object()); + BOOST_REQUIRE(!firstContract["irEthdebug"]["entries"].empty()); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("argument") != std::string::npos); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("value") != std::string::npos); + + Json secondInput = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({ + "irEthdebug", + "evm.bytecode.object", + "evm.bytecode.ethdebug", + "evm.deployedBytecode.object", + "evm.deployedBytecode.ethdebug" + }), + YulCode({{"fileA.yul", firstContract["ir"]}}) + ); + secondInput["auxiliaryInput"]["ethdebug"] = firstContract["irEthdebug"]; + Json secondResult = compiler.compile(secondInput); + BOOST_REQUIRE(containsAtMostWarnings(secondResult)); + BOOST_REQUIRE(secondResult["contracts"]["fileA.yul"].is_object()); + Json const& secondContract = secondResult["contracts"]["fileA.yul"].begin().value(); + BOOST_REQUIRE(secondContract["evm"]["bytecode"]["ethdebug"].is_object()); + BOOST_CHECK(secondContract["evm"]["bytecode"]["object"] == firstContract["evm"]["bytecode"]["object"]); + BOOST_CHECK_EQUAL( + Json::diff( + firstContract["evm"]["bytecode"]["ethdebug"], + secondContract["evm"]["bytecode"]["ethdebug"] + ).dump(), + "[]" + ); + BOOST_CHECK( + secondContract["evm"]["deployedBytecode"]["object"] == + firstContract["evm"]["deployedBytecode"]["object"] + ); + BOOST_CHECK_EQUAL( + Json::diff( + firstContract["evm"]["deployedBytecode"]["ethdebug"], + secondContract["evm"]["deployedBytecode"]["ethdebug"] + ).dump(), + "[]" + ); + BOOST_CHECK(secondContract["irEthdebug"] == firstContract["irEthdebug"]); +} + +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_is_rejected_for_non_yul_input) +{ + frontend::StandardCompiler compiler; + Json input = generateExperimentalStandardJson(false, {}, Json::array({"ir"})); + input["auxiliaryInput"]["ethdebug"] = Json{ + {"format", "solidity-ethdebug-semantic-data"}, + {"version", 1}, + {"entries", Json::array()} + }; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "JSONError", "\"auxiliaryInput.ethdebug\" can only be used for Yul input.")); +} + +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_rejects_yul_optimization) +{ + frontend::StandardCompiler compiler; + Json input = generateExperimentalStandardJson( + false, + {}, + Json::array({"irEthdebug"}), + YulCode({{"fileA.yul", "object \"C\" { code { stop() } }"}}) + ); + input["settings"]["optimizer"] = {{"enabled", true}}; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "UnimplementedFeatureError", "Optimization is not yet supported with ethdebug.")); +} + BOOST_AUTO_TEST_CASE(no_experimental_import_ast_solidity_evmasm) { frontend::StandardCompiler compiler; diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 69a6d344f880..b5b16a7a3432 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -1431,13 +1431,13 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_incompatible_outputs) {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, }; for (auto const& test: supportedCLIFlagCombinations) @@ -1487,9 +1487,13 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) createFilesWithParentDirs({tempDir.path() / "input.sol"}, "pragma solidity >=0.0; contract C { function f() public pure {} }"); createFilesWithParentDirs({tempDir.path() / "input.yul"}, "{}"); static std::vector> erroneousCLIFlagCombinations{ + // ethdebug depends on ast-id. + { + {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + }, // --debug-info ethdebug with --optimize is not supported { - {"solc", "--experimental", "--debug-info", "ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, }, { {"solc", "--experimental", "--debug-info", "location", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, @@ -1503,22 +1507,22 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) }; static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, }, }; @@ -1606,6 +1610,69 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) } } +BOOST_AUTO_TEST_CASE(cli_ethdebug_semantic_sidecar_supports_two_stage_compilation) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + boost::filesystem::path inputPath = tempDir.path() / "input.sol"; + boost::filesystem::path outputDir = tempDir.path() / "output"; + createFilesWithParentDirs( + {inputPath}, + "contract C { uint256 value; function f(uint256 argument) public { value = argument; } }" + ); + boost::filesystem::create_directories(outputDir); + + OptionsReaderAndMessages firstStage = runCLI({ + "solc", + "--experimental", + "--ir", + "--ir-ethdebug", + "--output-dir", + outputDir.string(), + inputPath.string(), + }); + BOOST_REQUIRE(firstStage.success); + + boost::filesystem::path yulPath = outputDir / "C.yul"; + boost::filesystem::path sidecarPath = outputDir / "C_ir_ethdebug.json"; + BOOST_REQUIRE(boost::filesystem::is_regular_file(yulPath)); + BOOST_REQUIRE(boost::filesystem::is_regular_file(sidecarPath)); + BOOST_CHECK(readFileAsString(sidecarPath).find("argument") != std::string::npos); + + OptionsReaderAndMessages secondStage = runCLI({ + "solc", + "--strict-assembly", + "--experimental", + "--ethdebug-input", + sidecarPath.string(), + "--ir-ethdebug", + "--ethdebug-program", + yulPath.string(), + }); + BOOST_REQUIRE(secondStage.success); + BOOST_CHECK(secondStage.stderrContent.empty()); + BOOST_CHECK(secondStage.stdoutContent.find("Ethdebug semantic data sidecar:") != std::string::npos); + BOOST_CHECK(secondStage.stdoutContent.find("Debug Data (ethdebug/format/program):") != std::string::npos); + BOOST_CHECK(secondStage.stdoutContent.find("argument") != std::string::npos); + + boost::filesystem::path secondYulPath = outputDir / "D.yul"; + createFilesWithParentDirs({secondYulPath}, readFileAsString(yulPath)); + OptionsReaderAndMessages mappedStage = runCLI({ + "solc", + "--strict-assembly", + "--experimental", + "--ethdebug-input", + yulPath.string() + "=" + sidecarPath.string(), + "--ethdebug-input", + secondYulPath.string() + "=" + sidecarPath.string(), + "--ir-ethdebug", + yulPath.string(), + secondYulPath.string(), + }); + BOOST_REQUIRE(mappedStage.success); + BOOST_CHECK(mappedStage.stderrContent.empty()); + BOOST_CHECK(mappedStage.stdoutContent.find("argument") != std::string::npos); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::frontend::test diff --git a/test/solc/CommandLineParser.cpp b/test/solc/CommandLineParser.cpp index 5dd00e05aab8..9e9ffa809fb4 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -198,9 +198,9 @@ BOOST_AUTO_TEST_CASE(cli_mode_options) expectedOptions.formatting.withErrorIds = true; expectedOptions.compiler.outputs = { true, true, true, true, true, + true, true, true, false, true, true, true, true, true, true, - true, true, true, true, true, - true, true, true, + true, true, true, true, }; expectedOptions.compiler.estimateGas = true; expectedOptions.compiler.combinedJsonRequests = { @@ -650,13 +650,13 @@ BOOST_AUTO_TEST_CASE(invalid_optimizer_sequence_without_optimize) BOOST_AUTO_TEST_CASE(ethdebug) { // --ethdebug-program with explicit debug-info - CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir"}); + CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --ethdebug-program-runtime with explicit debug-info - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); @@ -680,12 +680,33 @@ BOOST_AUTO_TEST_CASE(ethdebug) BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --debug-info ethdebug with --ir only (no program output) - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ir, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // --ir-ethdebug emits the sidecar and implicitly enables ethdebug + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ir-ethdebug"}); + BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.irEthdebug, true); + BOOST_REQUIRE(commandLineOptions.output.debugInfoSelection.has_value()); + BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // --ethdebug-input attaches a sidecar in strict assembly mode + commandLineOptions = parseCommandLine({ + "solc", "contract.yul", "--strict-assembly", "--experimental", "--ethdebug-input", "debug.json" + }); + BOOST_REQUIRE_EQUAL(commandLineOptions.input.ethdebugInputs.size(), 1); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.front(), "debug.json"); + BOOST_REQUIRE(commandLineOptions.output.debugInfoSelection.has_value()); + BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // Repeated source=sidecar mappings pair multiple strict assembly inputs. + commandLineOptions = parseCommandLine({ + "solc", "a.yul", "b.yul", "--strict-assembly", "--experimental", + "--ethdebug-input", "a.yul=a.json", "--ethdebug-input", "b.yul=b.json" + }); + BOOST_REQUIRE_EQUAL(commandLineOptions.input.ethdebugInputs.size(), 2); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.at(0), "a.yul=a.json"); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.at(1), "b.yul=b.json"); // --ethdebug-resources does not require --via-ir commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-resources"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugResources, true); @@ -705,6 +726,7 @@ BOOST_AUTO_TEST_CASE(experimental_features_without_experimental_flag) std::vector const experimentalFeatures { "--import-ast", "--import-asm-json", + "--ir-ethdebug", "--ir-ast-json", "--ir-optimized-ast-json", "--yul-cfg-json", @@ -730,6 +752,15 @@ BOOST_AUTO_TEST_CASE(experimental_features_without_experimental_flag) std::vector const commandLineOptions{"solc", experimentalFeature, "contract.sol"}; BOOST_CHECK_EXCEPTION(parseCommandLine(commandLineOptions), CommandLineValidationError, hasCorrectMessage); } + + expectedErrorMessage = + "The following options are only available in experimental mode: --ethdebug-input. " + "To enable experimental mode, use the --experimental flag."; + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "--strict-assembly", "--ethdebug-input", "debug.json", "contract.yul"}), + CommandLineValidationError, + hasCorrectMessage + ); } BOOST_AUTO_TEST_CASE(via_ssa_cfg_smoke) From e5e142e7e1984f682f3fe7703b506b6789ed51c2 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 20 Jul 2026 10:20:00 +0200 Subject: [PATCH 26/47] docs: Rewrite the ethdebug internal metadata specification --- docs/internals/ethdebug_internal_metadata.rst | 676 +++++++++--------- 1 file changed, 347 insertions(+), 329 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index a31faa036170..2ae4f7c187b4 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -6,353 +6,371 @@ ETHDebug Internal Metadata .. warning:: - ETHDebug support is experimental. The internal representation described here is - intended for compiler development and may change before the public ETHDebug - output is stabilized. - -The compiler can emit debug information in the -`ethdebug format `_. The JSON outputs are -validated against the upstream schemas, but the compiler does not construct that -JSON directly during the Solidity-to-Yul lowering. This internal representation -is the compiler-side carrier for semantic information that needs to survive the -Yul pipeline before it can be lowered into public ETHDebug type and pointer -entities. - -This page describes the internal representation used for semantic debug metadata. -It complements :doc:`source_mappings`, which describe source ranges and bytecode -instruction mapping. - -Overview -======== + ETHDebug support and the interchange format described here are experimental. + They may change before ETHDebug output is stabilized. -The internal ETHDebug metadata flow is: - -1. Solidity analysis assigns AST IDs and type information to declarations. AST - IDs are stable within a single compilation and are the internal join key used - by this metadata pipeline. -2. The IR generator prints AST ID comments into generated Yul when AST ID debug - info is enabled. Semantic metadata reattachment relies on those comments - being present at Yul parse/reparse boundaries. Because of that, selecting - ``ethdebug`` debug info in ``CompilerStack`` implicitly enables ``ast-id`` - debug info as well. -3. The compiler builds a side table keyed by Solidity AST ID. -4. Generated Yul is parsed and analyzed into a ``YulStack``. -5. Semantic metadata is attached to Yul ``DebugData`` objects when the AST ID - in the Yul node's debug data has an entry in the side table. -6. If the Yul optimizer reparses optimized IR, semantic metadata is collected - before reparse and reattached afterward by AST ID. - -The AST ID is the join key. It allows semantic information from the Solidity AST -to survive the text-based Yul print/parse boundary. -The side table itself is not serialized; the serialized part that crosses the -Yul text boundary is the AST ID comment in the Yul source. +The compiler emits public debug information using the +`ethdebug format `_. +The structures on this page are not a second public ETHDebug format. +They are the compiler interchange model used to carry source-language semantics through Yul and later lower them to public ETHDebug types, pointers, and instruction contexts. -Core Structures -=============== +Design Requirements +=================== -``langutil::DebugData`` is the common debug payload carried by Yul AST nodes. -For ETHDebug, it contains: +The metadata pipeline has to preserve these compiler properties: -* the native Yul source location, -* the original Solidity source location, -* the optional Solidity AST ID, -* optional semantic debug metadata. +- A Solidity-to-Yul invocation followed by a Yul-to-bytecode invocation must be able to produce the same bytecode and debug metadata as a one-stage invocation. +- A language frontend that targets Yul must be able to supply its own semantic debug metadata without linking against in-memory Solidity compiler objects. +- Every value crossing the Solidity/Yul boundary must have a serialization format. +- Source-language declaration identity and generated-Yul instance identity must not be conflated. +- Location categories must describe EVM machine state rather than Solidity-only language constructs. -The semantic payload is represented by ``langutil::SemanticDebugData``. It -currently contains: +For these reasons, semantic metadata is a versioned JSON sidecar paired with Yul text. +The Yul text carries ``@ast-id`` comments that identify source-language origins. +The sidecar carries the semantic records associated with those origins. +Both artifacts are required at a compiler-to-compiler text boundary. -* ``lexicalScopeID``: the Solidity AST ID of the lexical scope represented by - this metadata, -* ``variableDefinitions``: the variables introduced in that scope. +Pipeline and Generation Time +============================ -Each ``SemanticDebugVariable`` contains: +The current Solidity pipeline performs the following steps: -* ``name``: Solidity source-level name, -* ``declarationAstID``: AST ID of the Solidity declaration, -* ``declarationLocation``: Solidity source location of the declaration, -* ``typeID``: compiler-internal Solidity type identifier, -* ``ethdebugType``: ETHDebug-oriented type descriptor derived from the Solidity - type, -* ``location``: initial internal variable location, -* ``ethdebugPointer``: ETHDebug-oriented pointer descriptor derived from the - initial internal location. +1. Solidity analysis assigns AST IDs and populates the existing type annotations on declarations. +2. IR generation emits Yul and, when requested, ``@ast-id`` comments. +3. After IR generation, ``frontend::buildSemanticDebugDataTable()`` builds semantic records on demand from the analyzed AST and from the same ``IRVariable`` naming rules used by code generation. +4. ``CompilerStack`` attaches the table after parsing generated Yul into a ``YulStack``. +5. ``YulStack`` carries semantic records on Yul ``DebugData`` objects and retains unattached records in its side table. +6. Before a requested Yul reparse, the stack collects attached records into the retained table and reattaches them after parsing the printed Yul. +7. The EVM code generator lowers surviving records into public ETHDebug resources and contexts. -The variable location is represented by ``SemanticDebugVariableLocation``. The -location kind can describe stack, storage, transient storage, memory, calldata, -immutable, constant, or optimized-out values. The current implementation -populates stack locations for function parameters and named return variables, -and storage locations for named state variables in persistent and transient -storage. +Semantic type descriptors are not new analysis annotations. +They are derived on demand from ``VariableDeclaration::annotation().type`` after analysis succeeds. +This avoids eagerly constructing ETHDebug-specific types when ETHDebug was not requested. +The builder also queries storage layout and ``IRVariable`` during IR code generation because those details are code-generation properties rather than type-analysis results. -Type Descriptors +No semantic table is carried through Yul when neither an ETHDebug artifact nor the ``ethdebug`` debug-info component is requested. + +Debug-Info Dependency +===================== + +Semantic metadata transfer currently depends on ``@ast-id`` comments. +Accordingly, ``ethdebug`` depends on the ``ast-id`` debug-info component. +This is an explicit dependency. +CLI and Standard JSON input reject an explicit selection containing ``ethdebug`` without ``ast-id``, just as ``snippet`` without ``location`` is rejected. +An output option that selects ETHDebug implicitly uses the complete required selection because the user did not provide a partial debug-info list. + +Core Structures +=============== + +``langutil::DebugData`` is the debug payload carried by Yul AST nodes. + +.. list-table:: ``DebugData`` fields relevant to ETHDebug + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``nativeLocation`` + - ``SourceLocation`` + - Location in the current Yul text. + * - ``originLocation`` + - ``SourceLocation`` + - Location in the source language. + * - ``astID`` + - optional integer + - Source-language AST origin copied from ``@ast-id``. + * - ``semanticDebugData`` + - optional ``SemanticDebugData`` pointer + - Semantic scope payload attached to this Yul node. + +``langutil::SemanticDebugData`` describes one semantic scope. + +.. list-table:: ``SemanticDebugData`` + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``lexicalScopeID`` + - optional integer + - Source-language AST identity of the scope origin. + * - ``variableDefinitions`` + - array of ``SemanticDebugVariable`` + - Bindings introduced by or visible through the scope record, in source order. + +``SemanticDebugVariable`` separates source identity, source position, static type, and current EVM data location. + +.. list-table:: ``SemanticDebugVariable`` + :header-rows: 1 + :widths: 27 25 48 + + * - Field + - Type + - Meaning + * - ``identifier`` + - optional string + - Source-language identifier. + It is absent for unnamed variables such as unnamed Solidity return parameters. + * - ``declarationAstID`` + - optional integer + - Identity of the source-language declaration. + Synthetic bindings may omit it. + * - ``declarationSourceLocation`` + - optional ``SourceLocation`` + - Source range of the declaration. + * - ``typeID`` + - optional string + - Compiler type identifier used as the exported type-resource key. + * - ``ethdebugType`` + - optional ``SemanticDebugType`` + - ETHDebug-oriented static type descriptor. + * - ``dataLocation`` + - optional ``SemanticDebugVariableLocation`` + - Current abstract EVM location of the value. + * - ``ethdebugPointer`` + - optional ``SemanticDebugPointer`` + - Pointer expression resolving the value in that location. + +The pointer expression is also the explicit mapping from a source-language variable to generated Yul variables. +For stack-backed Solidity variables, free variable expressions contain the exact names produced by ``IRVariable::stackSlots()``. +The declaration AST ID identifies the source variable, while those symbolic Yul names identify its generated representation. + +Scope Attachment ---------------- -``SemanticDebugType`` mirrors the ethdebug type vocabulary. Elementary kinds -carry their payload directly: bit width for ``uint``/``int``, bit width and -decimal places for ``fixed``/``ufixed``, byte size for static ``bytes``, -payability for addresses, payability and library/interface flags for contracts, -and the member name list for enums. - -Composed types are recursive. Each ``SemanticDebugType`` holds a list of -``SemanticDebugTypeComponent`` entries, where each component records: - -* ``role``: how the component is composed (array element, mapping key or value, - struct member or tuple element, function parameter or return, alias - underlying type, contract providing an external function), -* ``name``: the member or element name, if any, -* ``referenceID``: the stable compiler type identifier of the composed type, - usable as an ``{"id": ...}`` reference into the exported type resources, -* ``type``: the inline recursive representation. - -The inline representation of a component is cut (left null) when the composed -type is already being described further up the recursion path. This terminates -recursive types, e.g. structs that contain themselves through arrays or -mappings; the ``referenceID`` still identifies the type. - -Statically sized arrays record their element ``count``. User defined types -(contracts, enums, structs, aliases, functions) record the definition name and -source location. Function types record whether they follow internal or external -call semantics. - -Pointer Descriptors -------------------- +Semantic scope data belongs to the Yul node that introduces the corresponding scope. + +- Function and modifier records attach to the generated Yul ``FunctionDefinition``, not to its body block. +- A source block lowered to a distinct Yul block attaches to that ``Block``. +- A conditional or loop that introduces no separate source scope does not receive a scope payload merely because it contains a block. +- A conditional, loop clause, or catch clause that does introduce a source scope attaches its payload to the generated block representing that scope. +- Contract and file scopes with no corresponding Yul node remain side-table-only records and are still serialized. -``SemanticDebugPointer`` mirrors the ethdebug pointer schema. A pointer is -either a single region or one of the collection forms: - -* ``Region``: a data range in one location (stack, storage, transient storage, - memory, calldata, returndata, code). Word-oriented locations address by - ``slot`` with optional byte ``offset`` and ``length``; byte-oriented locations - address by ``offset`` and ``length``. -* ``Group``: an ordered composition of sub-pointers. -* ``List``: a dynamically sized repetition. ``count`` is an expression, and the - index is bound to ``indexName`` inside the repeated element pointer. -* ``Conditional``: chooses between ``thenPointer`` and the optional - ``elsePointer`` based on the non-zero-ness of ``condition``. -* ``Scope``: binds ordered auxiliary ``definitions`` (name/expression pairs) - inside a target pointer. Later definitions may reference earlier ones. -* ``TemplateReference``: refers to a pointer template defined elsewhere and can - rename the regions it produces. - -Slots, offsets, lengths, counts, conditions and scope definitions are -``SemanticDebugPointerExpression`` trees covering the ethdebug expression -grammar: literals, the ``$wordsize`` constant, variable references, region -lookups (``.slot``/``.offset``/``.length``), region reads (``$read``), -arithmetic (``$sum``, ``$difference``, ``$product``, ``$quotient``, -``$remainder``), hashing (``$keccak256``), concatenation (``$concat``) and -resizing (``$sized``/``$wordsized``). - -A root pointer additionally lists ``expectedParameters``: template variables -that must be bound externally before the pointer can be evaluated. Mapping keys -are the canonical example — the key is not stored anywhere, so the debugger -must provide it. Pointers with expected parameters are exported as pointer -templates, not as closed program-context pointers. - -Side Table -========== - -``langutil::SemanticDebugDataTable`` maps Solidity AST IDs to -``SemanticDebugData`` instances. - -This table is intentionally separate from the Yul AST because generated IR is -still passed through textual Yul at multiple points. The table lets the compiler -reattach semantic metadata whenever a Yul AST is reconstructed from text. -It is an in-memory compiler data structure, not a public output format. - -The current table entries are keyed by lexical-scope AST IDs, such as function -or modifier AST IDs. Variable declaration AST IDs are stored inside -``SemanticDebugVariable`` records as declaration identities; they are not -top-level keys in the table. - -The table is used in two places: - -* ``CompilerStack`` builds the table from the Solidity contract and attaches it - after generated IR is parsed into a ``YulStack``. This happens both for the - freshly generated IR and when the optimized IR is reloaded from text for EVM - code generation. -* ``YulStack`` retains the attached table as a member. ``reparse()`` merges - metadata collected from the current Yul AST into the retained table before - printing, then reattaches it to the new Yul AST by AST ID. Retaining the table - also preserves entries that are not attached to any Yul node, such as the - contract-scope storage metadata, which has no corresponding ``@ast-id`` - comment in generated Yul. - -The transfer between the table and Yul ASTs is implemented in -``libyul/SemanticDebugDataTransfer.h``. - -Current Producer +The transfer visitor can carry ``DebugData`` on all Yul node kinds, including names in parameter and return lists. +The producer is responsible for selecting the node that semantically owns a scope. + +Type Descriptors ================ -The current Solidity-side producer is -``frontend::buildSemanticDebugDataTable(ContractDefinition const&)``. - -It records named state variables, named function parameters, named modifier -parameters, and named function return variables. Functions and modifiers are -collected from all linearized base contracts, because inherited definitions are -compiled into the most derived contract's IR with their original AST IDs. Free -functions from the contract's source unit and all recursively referenced source -units are collected as well. -For each variable it stores: - -* the declaration name, -* the declaration AST ID, -* the declaration source location, -* the compiler type identifier, for example ``t_uint256``, -* an ETHDebug-oriented type descriptor, for example ``uint`` with ``bits = 256``, -* the initial location, -* an ETHDebug-oriented pointer descriptor for that initial location. - -The stack pointer is based on ``IRVariable`` and therefore matches the names used -by generated IR, such as ``var_value_42`` for a Solidity variable named -``value`` with AST ID ``42``. Multi-slot variables use the stack slot list -produced by ``IRVariable``. In the ETHDebug-oriented pointer descriptor, one-slot -variables become stack region pointers and multi-slot variables become groups of -stack region pointers. The stack slot is a symbolic variable expression holding -the generated Yul name; actual stack depths are only known after code -generation. - -The storage pointer is based on the compiler's existing storage layout -calculation. Named state variables use contract-scope semantic metadata keyed by -the contract AST ID. The pointer construction is recursive over the variable's -type, with the base slot threaded through as an expression: - -* Value types become a storage region with the base slot, and for packed - variables, the byte offset and byte length within the slot. -* Mappings register the key as an expected template parameter and describe the - value at ``$keccak256($wordsized(key), $wordsized(slot))``. Value-type keys - are padded to a word; ``bytes`` and ``string`` keys hash their raw bytes. - Nested mappings chain the hashes and expect one parameter per key. -* Dynamically sized arrays become a group of the length region at the base slot - and a list of element pointers starting at ``$keccak256($wordsized(slot))``, - with the element count read from the length region. -* Statically sized arrays become a list with a literal count. Elements narrower - than a word derive their slot and byte offset from the index (packed - elements); wider elements advance in whole slots, recursing into the element - type. -* ``bytes`` and ``string`` values become the canonical short/long conditional: - the doubled length lives in the last byte of the base slot; short values keep - their data in place while long values store ``2 * length + 1`` in the base - slot and their data at ``$keccak256($wordsized(slot))``. -* Structs become a group of member pointers using the struct storage layout, - recursing into each member type. Recursive structs and pathologically deep - compositions fall back to a region covering the struct's slots. -* Transient storage variables use the same construction with transient regions. - -Current Scope +``SemanticDebugType`` mirrors the public ETHDebug type vocabulary while retaining a small amount of compiler-only information needed during lowering. + +.. list-table:: Common ``SemanticDebugType`` fields + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``typeClass`` + - enum + - Elementary, complex, or unknown representation. + * - ``kind`` + - enum + - Integer, bytes, string, address, contract, enum, alias, tuple, array, mapping, struct, function, or unknown kind. + * - ``bits``, ``places``, ``bytes`` + - optional integers + - Width information for numeric and fixed-bytes types. + * - ``payable``, ``isLibrary``, ``isInterface`` + - optional booleans + - Address and contract properties. + * - ``enumValues`` + - array of strings + - Enum members in declaration order. + * - ``count`` + - optional hexadecimal string + - Fixed array length. + * - ``components`` + - array of ``SemanticDebugTypeComponent`` + - Recursive element, key, value, member, parameter, return, underlying, or contract components. + * - ``definitionName`` + - optional string + - Name of a user-defined type declaration. + * - ``definitionLocation`` + - optional ``SourceLocation`` + - Source range of that declaration. + * - ``dataLocation`` + - optional string + - Solidity type data-location annotation retained for type lowering. + * - ``dynamic`` + - optional boolean + - Whether an array-like type is dynamically sized. + +Every type component contains a role, an optional member name, an optional reference ID, and an optional inline type. +When recursively describing a type already present on the current recursion path, the inline type is omitted and the reference ID terminates the cycle. +This represents recursive structs without infinitely expanding them. + +Data Locations and Pointers +=========================== + +``SemanticDebugVariableLocation`` describes the EVM-level representation of a value. +It does not encode Solidity concepts such as ``constant`` or ``immutable`` as location kinds. + +.. list-table:: Variable data-location kinds + :header-rows: 1 + :widths: 25 75 + + * - Kind + - Meaning + * - ``Stack`` + - One or more EVM stack slots, initially represented by symbolic Yul variable names. + * - ``Storage`` + - Persistent storage. + * - ``TransientStorage`` + - Transaction-scoped transient storage. + * - ``Memory`` + - EVM memory. + * - ``Calldata`` + - Call input data. + * - ``Returndata`` + - Return data exposed by the EVM return-data buffer. + * - ``Code`` + - Bytes embedded in creation or runtime bytecode. + * - ``Computed`` + - A value with no persistent allocation that must be recomputed or rematerialized. + * - ``OptimizedOut`` + - No recoverable representation is available at this program point. + +Solidity immutables therefore have a memory location while creation code initializes them and a code location when read from deployed code. +A constant folded to an immediate may be represented by code bytes, while a constant expression that must execute is computed. +This distinction also works for future reference-type constants and for non-Solidity languages. + +``SemanticDebugPointer`` represents either one EVM region or a structured composition. + +.. list-table:: Pointer classes + :header-rows: 1 + :widths: 24 76 + + * - Class + - Meaning + * - ``Region`` + - A stack, storage, transient, memory, calldata, returndata, or code range. + * - ``Group`` + - An ordered group of pointers. + * - ``List`` + - A repeated element pointer with a count and bound index name. + * - ``Conditional`` + - A pointer selected by a condition. + * - ``Scope`` + - Ordered auxiliary definitions evaluated within a target pointer. + * - ``TemplateReference`` + - A reference to a separately exported pointer template. + +Slots, offsets, lengths, counts, and conditions are ``SemanticDebugPointerExpression`` trees. +The expression vocabulary covers literals, ``$wordsize``, variables, region lookups and reads, arithmetic, hashing, concatenation, and resizing. +Root pointers list externally bound ``expectedParameters`` such as mapping keys. +Pointers with expected parameters are exported as templates rather than closed program-context pointers. + +Location Changes +---------------- + +``dataLocation`` is the location valid for the program point represented by the containing debug context. +It is not permanently the declaration's initial location. +A value may move, split into multiple regions, be rematerialized, or become unavailable as optimization and code generation proceed. + +Optimizer transformations that preserve a value must rewrite its pointer expressions to the new Yul names or machine regions. +Transformations that clone or specialize code must clone the scope instance metadata as well. +If the compiler cannot describe a surviving value soundly, it must use ``OptimizedOut`` rather than retain a stale pointer. + +The implemented conservative rule checks all free Yul variable names used by a stack pointer after attachment or reparse. +If any required name is absent from the current Yul object, the location becomes ``OptimizedOut`` and the pointer is removed. +Bound names from pointer scopes, list indices, and template parameters are not mistaken for Yul dependencies. + +Identity and Optimizer Cloning +============================== + +A source-language AST ID is an origin identity, not a unique generated-code identity. +The same source function can produce multiple specialized or cloned Yul functions. +Those instances share declaration and type information but can have different current locations. + +The complete model therefore requires two identities: + +- ``originAstID`` identifies the source-language node and is preserved by ``@ast-id``. +- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance and is preserved in the serialized interchange data. + +The current version of ``SemanticDebugDataTable`` uses only the AST origin ID as its key. +That is sufficient for the un-cloned IR path currently supported by ETHDebug, where Yul optimization is rejected. +It must not be treated as sufficient for optimizer passes that clone or specialize functions. +Before those passes are enabled with ETHDebug, the table and Yul annotations must gain the instance discriminator so per-instance location updates cannot overwrite each other. + +Serialization ============= -This is not yet the complete ETHDebug variable model. The current scope covers: - -* function parameters, -* modifier parameters, -* named function return variables, -* named state variables in persistent and transient storage, -* recursive ETHDebug-oriented type descriptors for all elementary and composed - Solidity type categories, -* initial stack locations and ETHDebug-oriented stack pointer descriptors, -* initial storage locations and recursive ETHDebug-oriented storage pointer - descriptors, including mappings, arrays, ``bytes``/``string`` and structs. - -Local variables, memory pointers, calldata pointers, immutable and constant -values, and detailed optimizer location updates are still future work. The -current implementation exports schema-valid type entries (elementary and -composed) to ``ethdebug.resources.types`` and schema-valid pointer templates to -``ethdebug.resources.pointers``. It does not yet add per-instruction variable -contexts to the public ETHDebug JSON output. - -Type and Pointer Mapping -======================== - -The ``typeID`` is the compiler's existing internal type identifier, for example -``t_uint256``. It doubles as the key of the exported type resources and as the -``referenceID`` used by type components, so composed types can reference each -other by ID. - -The ``ethdebugType`` descriptor maps Solidity types to the public ETHDebug type -vocabulary, including recursive composition: array element types, mapping key -and value types, struct members, tuple elements, alias underlying types and -function parameter/return types are carried as components with reference IDs -and inline representations (see `Type Descriptors`_). - -The stack ``pointerID`` is an internal pointer into generated Yul stack slots. -Stack pointer descriptors use symbolic variable expressions holding the -generated Yul variable names, not runtime stack depths, so they remain internal -for now. - -The public resource exporter emits schema-valid type descriptors to -``ethdebug.resources.types``, keyed by compiler type ID. Composed resource -entries reference their component types with ``{"id": ...}`` into the same -table; every referenced component is registered as well. It also emits pointer -templates to ``ethdebug.resources.pointers`` for named state variables, keyed -by compiler-generated pointer IDs. The template's ``expect`` list carries the -pointer's expected parameters (mapping keys); the template body contains the -full recursive pointer. In the program-level context, types are inlined and -only closed pointers (without expected parameters) are attached to variables. - -Scope definitions inside exported pointers are order-sensitive, but JSON object -members are not ordered; the exporter therefore emits one nested -``define``/``in`` scope per definition so that ordering is structural. - -Future work should define: - -* memory, calldata, immutable, constant, and optimized-out variable location to - ETHDebug pointer mapping, -* optimizer rules for updating, splitting, merging, or removing variable - locations, -* schema-validated emission of per-instruction variable contexts. - -Optimizer Update Rules -====================== - -The first implemented optimizer rule is conservative and runs whenever semantic -metadata is attached to a Yul AST, including across the Yul text reparse -boundary and when optimized IR is reloaded from text for EVM code generation: - -* semantic metadata is collected before reparse and reattached afterward by AST - ID, -* surviving Yul variable names are collected separately for each object in the - Yul object tree, so a variable that only survives in the creation code is - still marked ``OptimizedOut`` in the deployed code, and vice versa, -* stack-backed semantic variables keep their locations if all free variables of - their pointer expressions still exist as Yul variables in the object the - metadata is attached to, -* stack-backed semantic variables become ``OptimizedOut`` if any free variable - no longer exists there. - -A pointer variable is free if it is not bound within the pointer itself. Scope -definitions, list index names and template parameters bind identifiers; -references to named regions live in a separate namespace and are never treated -as Yul variable dependencies. - -This avoids reporting stale stack locations after an optimizer pass removes the -generated Yul variables that originally held a Solidity value. - -The current rule does not yet infer new locations. If an optimizer pass renames, -splits, merges, inlines, or rematerializes a value, the compiler must eventually -provide an explicit debug-location update. Until those mappings exist, losing the -old stack slot is treated as ``OptimizedOut`` rather than guessed. +``liblangutil/SemanticDebugDataSerDe.h`` provides both serialization and deserialization. +The complete table, including recursive types, pointer expressions, source names, ordered definitions, and unattached scope records, is serialized. + +.. list-table:: Top-level JSON object + :header-rows: 1 + :widths: 22 22 56 + + * - Field + - Type + - Meaning + * - ``format`` + - string + - ``solidity-ethdebug-semantic-data``. + * - ``version`` + - integer + - Format version, currently ``1``. + * - ``contractName`` + - optional string + - Source-language contract name used by the public ETHDebug program object. + Generated Yul object names are not assumed to preserve it. + * - ``entries`` + - array + - Pairs containing ``astId`` and ``data``. + +Readers reject an unknown format, an unsupported version, malformed tagged values, and duplicate table keys. +Writers emit deterministic entry order because the table is ordered by key. + +Compiler Interfaces +------------------- + +Standard JSON uses these fields: + +- Solidity output ``contracts[][].ir`` contains the Yul text. +- Solidity output ``contracts[][].irEthdebug`` contains its semantic sidecar. +- Yul input ``auxiliaryInput.ethdebug`` supplies the sidecar for the Yul source. +- Yul output ``irEthdebug`` emits the retained sidecar again. + +The command-line interface uses these options: + +- ``--ir`` and ``--ir-ethdebug`` emit the Yul text and sidecar. +- ``--strict-assembly --ethdebug-input `` supplies an unqualified sidecar when there is exactly one Yul input. +- Repeated ``--ethdebug-input =`` options pair sidecars with multiple Yul inputs. + +The sidecar is deliberately separate from public ``ethdebug/format`` JSON. +It is accepted compiler input, not merely an in-memory cache. + +Complete Variable Model +======================= + +The intended producer covers every binding visible to source-level debugging, not only the subset already emitted by the implementation. + +- Named and unnamed function parameters and return parameters are included. +- Modifier parameters and local variables in ordinary blocks are included. +- Variables introduced by ``for``, ``if``, ``try``, success, ``catch``, error, and panic clauses are included in the precise scope where they become visible. +- State variables in persistent and transient storage are included. +- File-level constants are included. +- Every visible import alias is a separate binding with its alias identifier but shares the declaration identity and value description of the imported constant. +- Synthetic language bindings such as ``this``, ``super``, and ``msg`` are included even when there is no ``VariableDeclaration`` AST node. +- Values in memory, calldata, returndata, code, and computed form are included when they are observable. + +ETHDebug variable identifiers are optional. +An unnamed Solidity return parameter is therefore emitted with declaration, type, and pointer information but without ``identifier``. +Its declaration order and AST identity still map it to the corresponding Solidity return slot and generated ``IRVariable`` stack slots. + +The current Solidity producer implements function and modifier parameters, named and unnamed function returns, inherited and free-function records, and named persistent and transient state variables. +It recursively describes mappings, arrays, ``bytes``, strings, structs, aliases, enums, contracts, and function types. +Locals, clause variables, file-level constants and aliases, synthetic bindings, and non-storage reference locations remain implementation work under the model above. Testing ======== -The internal metadata plumbing is covered by focused tests: - -* ``DebugDataTest`` checks that ``DebugData`` can carry semantic metadata, that - the AST-ID side table resolves it, and that pointer expressions, pointer - collections and recursive type components compose as designed. -* ``YulDebugDataTest`` checks that semantic metadata survives Yul reparse by AST - ID, that missing stack locations are marked ``OptimizedOut``, and that the - free-variable analysis distinguishes Yul dependencies from identifiers bound - within the pointer (scope definitions, list indices, template parameters). -* ``SemanticDebugDataTest`` checks that Solidity function variables produce - semantic metadata with declaration IDs, type IDs, ETHDebug-oriented type - descriptors, initial stack locations, and ETHDebug-oriented pointer - descriptors. It covers the recursive state-variable constructions — packed - values, mappings (including nested ones), dynamic and static arrays, - ``string`` storage, structs and recursive struct fallbacks — as well as enum, - alias and contract type descriptors. It also verifies that function variable - metadata can be attached to generated Yul and survives the Yul reparse path. - -These tests deliberately target the internal model. Schema validation tests cover -the public ETHDebug JSON output separately, including the exported storage -pointer templates. +Solidity-side producer tests that compile source live in the Ethdebug isoltest suite. +The Ethdebug isoltest suite exposes the serialized semantic sidecar as ``Contract.semantic`` and covers function, modifier, unnamed-return, inheritance, free-function, type, and storage-pointer production. +Low-level data-model and Yul-transfer tests remain focused C++ unit tests. +``DebugDataTest`` covers the data model and bidirectional JSON round trips. +``YulDebugDataTest`` covers attachment, reparse survival, and conservative location invalidation. +Standard JSON and CLI tests cover serialized two-stage compilation and sidecar pairing. From a752fda41981c2213e3e165ec38aea059f7e13ba Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 23 Jul 2026 10:28:37 +0200 Subject: [PATCH 27/47] test: Avoid comparing Json against string_view, which is ambiguous on MSVC --- test/liblangutil/DebugData.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index a2d4145b4b53..2eac02505dd8 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -307,8 +307,8 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) table.set(42, std::make_shared(std::move(data))); Json serialized = semanticDebugDataToJson(table); - BOOST_CHECK_EQUAL(serialized["format"], SemanticDebugDataFormat); - BOOST_CHECK_EQUAL(serialized["version"], SemanticDebugDataFormatVersion); + BOOST_CHECK_EQUAL(serialized["format"].get(), SemanticDebugDataFormat); + BOOST_CHECK_EQUAL(serialized["version"].get(), SemanticDebugDataFormatVersion); BOOST_CHECK_EQUAL(serialized["contractName"], "ContainerContract"); BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); } From 9b0d173cde76472883ae189fcb0d737f9c33ea5a Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 23 Jul 2026 10:30:49 +0200 Subject: [PATCH 28/47] test: Run transient storage pointer isoltest only from Cancun on --- test/libsolidity/ethdebugTests/semantic_storage_pointers.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol index 77f7610683d7..1f98dec140b1 100644 --- a/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol +++ b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol @@ -10,6 +10,8 @@ contract C { string label; uint128 transient transientValue; } +// ==== +// EVMVersion: >=cancun // ---- // C.semantic.entries | length: 1 // C.semantic.entries[0].data.variables | length: 5 From 34c2193d89a150783ed5cd1615361c376765bd57 Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 23 Jul 2026 11:13:40 +0200 Subject: [PATCH 29/47] test: Build source maps explicitly instead of via nested braced initializers --- test/libsolidity/StandardCompiler.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index 443ca7ce2ba8..f83ad9d8f532 100644 --- a/test/libsolidity/StandardCompiler.cpp +++ b/test/libsolidity/StandardCompiler.cpp @@ -2283,6 +2283,10 @@ BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::d BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) { frontend::StandardCompiler compiler; + std::map firstSources; + firstSources["fileA"] = + "contract C { uint256 value; function f(uint256 argument) public { value = argument; } " + "function g(uint256 argument) public pure returns (uint256) { return argument; } }"; Json firstInput = generateExperimentalStandardJson( true, {}, @@ -2294,11 +2298,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) "evm.deployedBytecode.object", "evm.deployedBytecode.ethdebug" }), - SolidityCode({{ - "fileA", - "contract C { uint256 value; function f(uint256 argument) public { value = argument; } " - "function g(uint256 argument) public pure returns (uint256) { return argument; } }" - }}) + SolidityCode(std::move(firstSources)) ); Json firstResult = compiler.compile(firstInput); BOOST_REQUIRE(containsAtMostWarnings(firstResult)); @@ -2309,6 +2309,8 @@ BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) BOOST_CHECK(firstContract["irEthdebug"].dump().find("argument") != std::string::npos); BOOST_CHECK(firstContract["irEthdebug"].dump().find("value") != std::string::npos); + std::map secondSources; + secondSources["fileA.yul"] = firstContract["ir"]; Json secondInput = generateExperimentalStandardJson( false, Json::array({"ast-id", "ethdebug"}), @@ -2319,7 +2321,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) "evm.deployedBytecode.object", "evm.deployedBytecode.ethdebug" }), - YulCode({{"fileA.yul", firstContract["ir"]}}) + YulCode(std::move(secondSources)) ); secondInput["auxiliaryInput"]["ethdebug"] = firstContract["irEthdebug"]; Json secondResult = compiler.compile(secondInput); @@ -2365,11 +2367,13 @@ BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_is_rejected_for_non_yul_input) BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_rejects_yul_optimization) { frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; Json input = generateExperimentalStandardJson( false, {}, Json::array({"irEthdebug"}), - YulCode({{"fileA.yul", "object \"C\" { code { stop() } }"}}) + YulCode(std::move(yulSources)) ); input["settings"]["optimizer"] = {{"enabled", true}}; Json result = compiler.compile(input); From 3797dbd5a067d053dcbcec99ac9cc9c5d0693561 Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 24 Jul 2026 09:54:38 +0200 Subject: [PATCH 30/47] ethdebug: Normalize the Yul path in --ethdebug-input mappings --- solc/CommandLineInterface.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index e03e5d379cc8..0ba2ccd7051e 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -1325,12 +1325,15 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) } else { - sourceUnitName = input.substr(0, separator); + std::string const yulPath = input.substr(0, separator); sidecarPath = input.substr(separator + 1); - if (sourceUnitName.empty() || sidecarPath.empty()) + if (yulPath.empty() || sidecarPath.empty()) solThrow(CommandLineExecutionError, "Invalid --ethdebug-input mapping: " + input); + // The Yul file is given as a path exactly like the corresponding input argument and + // must be normalized the same way to match its source unit name. + sourceUnitName = m_fileReader.cliPathToSourceUnitName(yulPath); if (!m_fileReader.sourceUnits().contains(sourceUnitName)) - solThrow(CommandLineExecutionError, "Unknown Yul source in --ethdebug-input mapping: " + sourceUnitName); + solThrow(CommandLineExecutionError, "Unknown Yul source in --ethdebug-input mapping: " + yulPath); } if (semanticDebugDataBySource.contains(sourceUnitName)) From 12f62839523f23a72c497a17fe1c4cc7fdce3fd1 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 27 Jul 2026 10:22:09 +0200 Subject: [PATCH 31/47] docs: Specify optimizer debug info update rules --- docs/internals/ethdebug_internal_metadata.rst | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 2ae4f7c187b4..481940b7748f 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -277,6 +277,48 @@ The implemented conservative rule checks all free Yul variable names used by a s If any required name is absent from the current Yul object, the location becomes ``OptimizedOut`` and the pointer is removed. Bound names from pointer scopes, list indices, and template parameters are not mistaken for Yul dependencies. +Optimizer Update Rules +---------------------- + +Every transformation that rewrites Yul declares how it maintains the semantic side table. +Only the transformation knows whether a value survived unchanged, moved, merged with another value, or disappeared, so the obligation cannot be discharged by the code that runs it. +A pass with no declared strategy is treated as ``Drop``, so an undeclared pass degrades debug info instead of producing stale pointers. + +.. list-table:: Debug-info update strategies + :header-rows: 1 + :widths: 14 48 38 + + * - Strategy + - Obligation + - Passes + * - ``Preserve`` + - Copy the debug entry unchanged. + Valid only when the pass changes neither the names a pointer reads nor the region a value lives in. + - ``ForLoopInitRewriter``, ``ForLoopConditionIntoBody``, ``ForLoopConditionOutOfBody``, ``VarDeclInitializer``, ``FunctionHoister``, ``FunctionGrouper``, ``ConditionalUnsimplifier`` + * - ``Merge`` + - Keep the surviving value's entry when two values or two blocks collapse into one. + Record the discarded declarations against the same program point so both source names remain inspectable. + - ``CommonSubexpressionEliminator``, ``ExpressionJoiner``, ``ExpressionSimplifier``, ``ControlFlowSimplifier``, ``StructuralSimplifier``, ``BlockFlattener``, ``EquivalentFunctionCombiner``, ``LoadResolver`` + * - ``Remap`` + - Rewrite the pointer expressions to the new Yul names or the new region. + Renaming passes rewrite names; spilling passes rewrite a ``Stack`` location into a ``Memory`` region. + - ``Disambiguator``, ``NameSimplifier``, ``VarNameCleaner``, ``SSATransform``, ``SSAReverser``, ``ExpressionSplitter``, ``LoopInvariantCodeMotion``, ``StackToMemoryMover``, ``StackCompressor``, ``StackLimitEvader`` + * - ``Clone`` + - Duplicate the debug entry for each generated copy and give each copy its own ``scopeInstanceID``. + Without the instance discriminator the copies overwrite each other in the table. + - ``FullInliner``, ``FunctionSpecializer``, ``ExpressionInliner`` + * - ``Drop`` + - Set the location to ``OptimizedOut`` and remove the pointer. + The declaration, type, and source location are retained so the variable is still reported as belonging to the scope. + - ``DeadCodeEliminator``, ``UnusedPruner``, ``UnusedAssignEliminator``, ``UnusedStoreEliminator``, ``EqualStoreEliminator``, ``CircularReferencesPruner``, ``UnusedFunctionParameterPruner`` + +``Rematerialiser`` is a special case of ``Remap``. +Substituting a variable use by its defining expression can leave the variable itself unused and later pruned. +The value is then still recoverable by evaluating that expression, so its location becomes ``Computed`` rather than ``OptimizedOut``. + +A pass that cannot meet its obligation for a particular value falls back to ``Drop`` for that value alone, not for the whole scope. +Dropping a location is always sound, while keeping a pointer that no longer describes the value is not. + Identity and Optimizer Cloning ============================== From 9772f4bed4c3275ac42736e61a9ec0383932d84e Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 31 Jul 2026 16:06:06 +0200 Subject: [PATCH 32/47] ethdebug: Share one name table between each enum's two directions --- liblangutil/SemanticDebugDataSerDe.cpp | 330 +++++++++---------------- 1 file changed, 120 insertions(+), 210 deletions(-) diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp index 1edac9c85cb7..cd41aca04208 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -130,22 +131,116 @@ std::optional optionalString(Json const& _json, std::string const& return value.get(); } -template -std::string enumToString(Enum _value, std::initializer_list> _values) -{ - for (auto const& [value, name]: _values) +using VariableLocationKind = SemanticDebugVariableLocation::Kind; +constexpr std::pair variableLocationKindNames[]{ + {VariableLocationKind::Stack, "stack"}, + {VariableLocationKind::Storage, "storage"}, + {VariableLocationKind::TransientStorage, "transientStorage"}, + {VariableLocationKind::Memory, "memory"}, + {VariableLocationKind::Calldata, "calldata"}, + {VariableLocationKind::Returndata, "returndata"}, + {VariableLocationKind::Code, "code"}, + {VariableLocationKind::Computed, "computed"}, + {VariableLocationKind::OptimizedOut, "optimizedOut"} +}; + +using ExpressionKind = SemanticDebugPointerExpression::Kind; +constexpr std::pair expressionKindNames[]{ + {ExpressionKind::Unknown, "unknown"}, + {ExpressionKind::Literal, "literal"}, + {ExpressionKind::WordSize, "wordSize"}, + {ExpressionKind::Variable, "variable"}, + {ExpressionKind::LookupSlot, "lookupSlot"}, + {ExpressionKind::LookupOffset, "lookupOffset"}, + {ExpressionKind::LookupLength, "lookupLength"}, + {ExpressionKind::Read, "read"}, + {ExpressionKind::Sum, "sum"}, + {ExpressionKind::Product, "product"}, + {ExpressionKind::Difference, "difference"}, + {ExpressionKind::Quotient, "quotient"}, + {ExpressionKind::Remainder, "remainder"}, + {ExpressionKind::Keccak256, "keccak256"}, + {ExpressionKind::Concat, "concat"}, + {ExpressionKind::Resize, "resize"} +}; + +using TypeRole = SemanticDebugTypeComponent::Role; +constexpr std::pair typeRoleNames[]{ + {TypeRole::Element, "element"}, + {TypeRole::Key, "key"}, + {TypeRole::Value, "value"}, + {TypeRole::Member, "member"}, + {TypeRole::Parameter, "parameter"}, + {TypeRole::Return, "return"}, + {TypeRole::Underlying, "underlying"}, + {TypeRole::Contract, "contract"} +}; + +using TypeClass = SemanticDebugType::Class; +constexpr std::pair typeClassNames[]{ + {TypeClass::Elementary, "elementary"}, + {TypeClass::Complex, "complex"}, + {TypeClass::Unknown, "unknown"} +}; + +using TypeKind = SemanticDebugType::Kind; +constexpr std::pair typeKindNames[]{ + {TypeKind::Uint, "uint"}, + {TypeKind::Int, "int"}, + {TypeKind::Ufixed, "ufixed"}, + {TypeKind::Fixed, "fixed"}, + {TypeKind::Bool, "bool"}, + {TypeKind::Bytes, "bytes"}, + {TypeKind::String, "string"}, + {TypeKind::Address, "address"}, + {TypeKind::Contract, "contract"}, + {TypeKind::Enum, "enum"}, + {TypeKind::Alias, "alias"}, + {TypeKind::Tuple, "tuple"}, + {TypeKind::Array, "array"}, + {TypeKind::Mapping, "mapping"}, + {TypeKind::Struct, "struct"}, + {TypeKind::Function, "function"}, + {TypeKind::Unknown, "unknown"} +}; + +using PointerClass = SemanticDebugPointer::Class; +constexpr std::pair pointerClassNames[]{ + {PointerClass::Region, "region"}, + {PointerClass::Group, "group"}, + {PointerClass::List, "list"}, + {PointerClass::Conditional, "conditional"}, + {PointerClass::Scope, "scope"}, + {PointerClass::TemplateReference, "templateReference"}, + {PointerClass::Unknown, "unknown"} +}; + +using PointerLocation = SemanticDebugPointer::Location; +constexpr std::pair pointerLocationNames[]{ + {PointerLocation::Stack, "stack"}, + {PointerLocation::Storage, "storage"}, + {PointerLocation::Transient, "transient"}, + {PointerLocation::Memory, "memory"}, + {PointerLocation::Calldata, "calldata"}, + {PointerLocation::Returndata, "returndata"}, + {PointerLocation::Code, "code"}, + {PointerLocation::Unknown, "unknown"} +}; + +template +std::string enumToString(Enum _value, Names const& _names) +{ + for (auto const& [value, name]: _names) if (_value == value) return std::string(name); solAssert(false, "Unhandled semantic debug data enum value."); } -template -Enum enumFromString( - std::string const& _name, - std::initializer_list> _values, - std::string const& _path) +template +auto enumFromString(std::string const& _name, Names const& _names, std::string const& _path) + -> std::decay_tfirst)> { - for (auto const& [value, name]: _values) + for (auto const& [value, name]: _names) if (_name == name) return value; solThrow(SemanticDebugDataSerializationError, _path + " has unknown value \"" + _name + "\"."); @@ -153,260 +248,75 @@ Enum enumFromString( std::string variableLocationKindToString(SemanticDebugVariableLocation::Kind _kind) { - using Kind = SemanticDebugVariableLocation::Kind; - return enumToString( - _kind, - {{Kind::Stack, "stack"}, - {Kind::Storage, "storage"}, - {Kind::TransientStorage, "transientStorage"}, - {Kind::Memory, "memory"}, - {Kind::Calldata, "calldata"}, - {Kind::Returndata, "returndata"}, - {Kind::Code, "code"}, - {Kind::Computed, "computed"}, - {Kind::OptimizedOut, "optimizedOut"} - }); + return enumToString(_kind, variableLocationKindNames); } SemanticDebugVariableLocation::Kind variableLocationKindFromString(std::string const& _kind, std::string const& _path) { - using Kind = SemanticDebugVariableLocation::Kind; - return enumFromString( - _kind, - {{Kind::Stack, "stack"}, - {Kind::Storage, "storage"}, - {Kind::TransientStorage, "transientStorage"}, - {Kind::Memory, "memory"}, - {Kind::Calldata, "calldata"}, - {Kind::Returndata, "returndata"}, - {Kind::Code, "code"}, - {Kind::Computed, "computed"}, - {Kind::OptimizedOut, "optimizedOut"} - }, - _path); + return enumFromString(_kind, variableLocationKindNames, _path); } std::string expressionKindToString(SemanticDebugPointerExpression::Kind _kind) { - using Kind = SemanticDebugPointerExpression::Kind; - return enumToString( - _kind, - { - {Kind::Unknown, "unknown"}, - {Kind::Literal, "literal"}, - {Kind::WordSize, "wordSize"}, - {Kind::Variable, "variable"}, - {Kind::LookupSlot, "lookupSlot"}, - {Kind::LookupOffset, "lookupOffset"}, - {Kind::LookupLength, "lookupLength"}, - {Kind::Read, "read"}, - {Kind::Sum, "sum"}, - {Kind::Product, "product"}, - {Kind::Difference, "difference"}, - {Kind::Quotient, "quotient"}, - {Kind::Remainder, "remainder"}, - {Kind::Keccak256, "keccak256"}, - {Kind::Concat, "concat"}, - {Kind::Resize, "resize"} - }); + return enumToString(_kind, expressionKindNames); } SemanticDebugPointerExpression::Kind expressionKindFromString(std::string const& _kind, std::string const& _path) { - using Kind = SemanticDebugPointerExpression::Kind; - return enumFromString( - _kind, - { - {Kind::Unknown, "unknown"}, - {Kind::Literal, "literal"}, - {Kind::WordSize, "wordSize"}, - {Kind::Variable, "variable"}, - {Kind::LookupSlot, "lookupSlot"}, - {Kind::LookupOffset, "lookupOffset"}, - {Kind::LookupLength, "lookupLength"}, - {Kind::Read, "read"}, - {Kind::Sum, "sum"}, - {Kind::Product, "product"}, - {Kind::Difference, "difference"}, - {Kind::Quotient, "quotient"}, - {Kind::Remainder, "remainder"}, - {Kind::Keccak256, "keccak256"}, - {Kind::Concat, "concat"}, - {Kind::Resize, "resize"} - }, - _path); + return enumFromString(_kind, expressionKindNames, _path); } std::string typeRoleToString(SemanticDebugTypeComponent::Role _role) { - using Role = SemanticDebugTypeComponent::Role; - return enumToString( - _role, - { - {Role::Element, "element"}, - {Role::Key, "key"}, - {Role::Value, "value"}, - {Role::Member, "member"}, - {Role::Parameter, "parameter"}, - {Role::Return, "return"}, - {Role::Underlying, "underlying"}, - {Role::Contract, "contract"} - }); + return enumToString(_role, typeRoleNames); } SemanticDebugTypeComponent::Role typeRoleFromString(std::string const& _role, std::string const& _path) { - using Role = SemanticDebugTypeComponent::Role; - return enumFromString( - _role, - { - {Role::Element, "element"}, - {Role::Key, "key"}, - {Role::Value, "value"}, - {Role::Member, "member"}, - {Role::Parameter, "parameter"}, - {Role::Return, "return"}, - {Role::Underlying, "underlying"}, - {Role::Contract, "contract"} - }, - _path); + return enumFromString(_role, typeRoleNames, _path); } std::string typeClassToString(SemanticDebugType::Class _class) { - using Class = SemanticDebugType::Class; - return enumToString( - _class, {{Class::Elementary, "elementary"}, {Class::Complex, "complex"}, {Class::Unknown, "unknown"}}); + return enumToString(_class, typeClassNames); } SemanticDebugType::Class typeClassFromString(std::string const& _class, std::string const& _path) { - using Class = SemanticDebugType::Class; - return enumFromString( - _class, {{Class::Elementary, "elementary"}, {Class::Complex, "complex"}, {Class::Unknown, "unknown"}}, _path); + return enumFromString(_class, typeClassNames, _path); } std::string typeKindToString(SemanticDebugType::Kind _kind) { - using Kind = SemanticDebugType::Kind; - return enumToString( - _kind, - { - {Kind::Uint, "uint"}, - {Kind::Int, "int"}, - {Kind::Ufixed, "ufixed"}, - {Kind::Fixed, "fixed"}, - {Kind::Bool, "bool"}, - {Kind::Bytes, "bytes"}, - {Kind::String, "string"}, - {Kind::Address, "address"}, - {Kind::Contract, "contract"}, - {Kind::Enum, "enum"}, - {Kind::Alias, "alias"}, - {Kind::Tuple, "tuple"}, - {Kind::Array, "array"}, - {Kind::Mapping, "mapping"}, - {Kind::Struct, "struct"}, - {Kind::Function, "function"}, - {Kind::Unknown, "unknown"} - }); + return enumToString(_kind, typeKindNames); } SemanticDebugType::Kind typeKindFromString(std::string const& _kind, std::string const& _path) { - using Kind = SemanticDebugType::Kind; - return enumFromString( - _kind, - { - {Kind::Uint, "uint"}, - {Kind::Int, "int"}, - {Kind::Ufixed, "ufixed"}, - {Kind::Fixed, "fixed"}, - {Kind::Bool, "bool"}, - {Kind::Bytes, "bytes"}, - {Kind::String, "string"}, - {Kind::Address, "address"}, - {Kind::Contract, "contract"}, - {Kind::Enum, "enum"}, - {Kind::Alias, "alias"}, - {Kind::Tuple, "tuple"}, - {Kind::Array, "array"}, - {Kind::Mapping, "mapping"}, - {Kind::Struct, "struct"}, - {Kind::Function, "function"}, - {Kind::Unknown, "unknown"} - }, - _path); + return enumFromString(_kind, typeKindNames, _path); } std::string pointerClassToString(SemanticDebugPointer::Class _class) { - using Class = SemanticDebugPointer::Class; - return enumToString( - _class, - { - {Class::Region, "region"}, - {Class::Group, "group"}, - {Class::List, "list"}, - {Class::Conditional, "conditional"}, - {Class::Scope, "scope"}, - {Class::TemplateReference, "templateReference"}, - {Class::Unknown, "unknown"} - }); + return enumToString(_class, pointerClassNames); } SemanticDebugPointer::Class pointerClassFromString(std::string const& _class, std::string const& _path) { - using Class = SemanticDebugPointer::Class; - return enumFromString( - _class, - { - {Class::Region, "region"}, - {Class::Group, "group"}, - {Class::List, "list"}, - {Class::Conditional, "conditional"}, - {Class::Scope, "scope"}, - {Class::TemplateReference, "templateReference"}, - {Class::Unknown, "unknown"} - }, - _path); + return enumFromString(_class, pointerClassNames, _path); } std::string pointerLocationToString(SemanticDebugPointer::Location _location) { - using Location = SemanticDebugPointer::Location; - return enumToString( - _location, - { - {Location::Stack, "stack"}, - {Location::Storage, "storage"}, - {Location::Transient, "transient"}, - {Location::Memory, "memory"}, - {Location::Calldata, "calldata"}, - {Location::Returndata, "returndata"}, - {Location::Code, "code"}, - {Location::Unknown, "unknown"} - }); + return enumToString(_location, pointerLocationNames); } SemanticDebugPointer::Location pointerLocationFromString(std::string const& _location, std::string const& _path) { - using Location = SemanticDebugPointer::Location; - return enumFromString( - _location, - { - {Location::Stack, "stack"}, - {Location::Storage, "storage"}, - {Location::Transient, "transient"}, - {Location::Memory, "memory"}, - {Location::Calldata, "calldata"}, - {Location::Returndata, "returndata"}, - {Location::Code, "code"}, - {Location::Unknown, "unknown"} - }, - _path); + return enumFromString(_location, pointerLocationNames, _path); } + template void setOptional(Json& _json, std::string const& _name, std::optional const& _value) { From fc136796050fa036c7b85cca0354f19a684a83cb Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 3 Aug 2026 14:27:57 +0200 Subject: [PATCH 33/47] docs: Answer the type vocabulary and expression questions --- docs/internals/ethdebug_internal_metadata.rst | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 481940b7748f..ae67b0ee8314 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -197,6 +197,22 @@ Type Descriptors - optional boolean - Whether an array-like type is dynamically sized. +The ``kind`` enumeration follows `ethdebug/format types `_, which does not name every Solidity type directly. +Three cases are worth stating explicitly. + +**User-defined value types** have no separate kind. +They are represented as ``alias``, carrying the declaration's name and source range and a single ``Underlying`` component for the wrapped type. +A consumer that does not care about the distinction can follow the component and treat the value as its underlying type. + +**Array slices** are not represented today: they produce the ``unknown`` kind. +Only calldata slices exist, and their representation is identical to an ordinary calldata array - offset and length both on stack - so a pointer for them is expressible with what is already here. +Naming them is a gap in the type vocabulary rather than in the pointer model. + +**The enumeration is closed, and that is a known limitation.** +A 32-byte address type, and the contract and external function types that contain one, cannot be described without adding kinds - as can any future type the language grows. +``unknown`` is the only escape hatch, and it discards the information rather than deferring it. +Whether this vocabulary should stay closed, gain a versioned extension point, or defer entirely to the published ethdebug type schema is an open question that should be settled before the format is depended upon. + Every type component contains a role, an optional member name, an optional reference ID, and an optional inline type. When recursively describing a type already present on the current recursion path, the inline type is omitted and the reference ID terminates the cycle. This represents recursive structs without infinitely expanding them. @@ -258,7 +274,34 @@ This distinction also works for future reference-type constants and for non-Soli - A reference to a separately exported pointer template. Slots, offsets, lengths, counts, and conditions are ``SemanticDebugPointerExpression`` trees. -The expression vocabulary covers literals, ``$wordsize``, variables, region lookups and reads, arithmetic, hashing, concatenation, and resizing. +The vocabulary is the one defined by `ethdebug/format pointer expressions `_, restricted to the subset the compiler currently produces: + +.. list-table:: Expression kinds + :header-rows: 1 + :widths: 32 68 + + * - Kind + - Meaning + * - ``Literal`` + - A constant, written as a hexadecimal string. + * - ``WordSize`` + - The machine word size, ``$wordsize``. + * - ``Variable`` + - A name bound by an enclosing ``Scope``, ``List`` index, or ``expectedParameters``. + * - ``RegionLookup`` + - A named property of another region, such as its offset or length. + * - ``RegionRead`` + - The value stored in another region, ``$read``. + * - ``Arithmetic`` + - ``$sum``, ``$difference``, ``$product``, ``$quotient``, ``$remainder``. + * - ``Keccak256`` + - ``$keccak256`` of a concatenation, used for mapping and dynamic array slots. + * - ``Concat`` + - ``$concat``, the byte concatenation the hash is taken over. + * - ``Resize`` + - ``$resize``, adjusting a value to a given byte width. + +The remaining expression kinds in the ethdebug specification are accepted by the reader but not emitted by the Solidity producer. Root pointers list externally bound ``expectedParameters`` such as mapping keys. Pointers with expected parameters are exported as templates rather than closed program-context pointers. @@ -301,7 +344,8 @@ A pass with no declared strategy is treated as ``Drop``, so an undeclared pass d - ``CommonSubexpressionEliminator``, ``ExpressionJoiner``, ``ExpressionSimplifier``, ``ControlFlowSimplifier``, ``StructuralSimplifier``, ``BlockFlattener``, ``EquivalentFunctionCombiner``, ``LoadResolver`` * - ``Remap`` - Rewrite the pointer expressions to the new Yul names or the new region. - Renaming passes rewrite names; spilling passes rewrite a ``Stack`` location into a ``Memory`` region. + Renaming passes rewrite names. + Spilling passes move where the value is *found*, so the pointer changes from a ``Stack`` to a ``Memory`` region; the data itself is not relocated by the debug info. - ``Disambiguator``, ``NameSimplifier``, ``VarNameCleaner``, ``SSATransform``, ``SSAReverser``, ``ExpressionSplitter``, ``LoopInvariantCodeMotion``, ``StackToMemoryMover``, ``StackCompressor``, ``StackLimitEvader`` * - ``Clone`` - Duplicate the debug entry for each generated copy and give each copy its own ``scopeInstanceID``. @@ -361,7 +405,9 @@ The complete table, including recursive types, pointer expressions, source names Generated Yul object names are not assumed to preserve it. * - ``entries`` - array - - Pairs containing ``astId`` and ``data``. + - Objects containing ``astId`` and ``data``, where ``data`` is a ``SemanticDebugData`` value. + This is not ``langutil::DebugData``, and it does not repeat the AST ID: the ID appears once, as the entry key. + The table is an array rather than an object because AST IDs are integers and JSON object keys are not, and because an ordered array makes the writer's output deterministic without relying on key ordering rules. Readers reject an unknown format, an unsupported version, malformed tagged values, and duplicate table keys. Writers emit deterministic entry order because the table is ordered by key. From c76b9870ea53a8d9c92176c25a136989f22d0238 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 3 Aug 2026 14:29:07 +0200 Subject: [PATCH 34/47] docs: Say where pointer templates and type descriptors actually live --- docs/internals/ethdebug_internal_metadata.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index ae67b0ee8314..a1c994bf0885 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -126,6 +126,8 @@ Core Structures * - ``ethdebugType`` - optional ``SemanticDebugType`` - ETHDebug-oriented static type descriptor. + It is the value that gets registered in the public ``ethdebug.resources.types`` output under ``typeID``, not a second copy of it: variables sharing a type share the ID, and the descriptor is written once per ID. + It is carried through the sidecar rather than resolved at analysis time because the Yul boundary has to be crossable by a producer that is not Solidity, and such a producer has no analysis output to refer to. * - ``dataLocation`` - optional ``SemanticDebugVariableLocation`` - Current abstract EVM location of the value. @@ -271,7 +273,7 @@ This distinction also works for future reference-type constants and for non-Soli * - ``Scope`` - Ordered auxiliary definitions evaluated within a target pointer. * - ``TemplateReference`` - - A reference to a separately exported pointer template. + - A reference to a separately exported pointer template, by ``templateName``, together with the ``yields`` bindings that name its results. Slots, offsets, lengths, counts, and conditions are ``SemanticDebugPointerExpression`` trees. The vocabulary is the one defined by `ethdebug/format pointer expressions `_, restricted to the subset the compiler currently produces: @@ -305,6 +307,11 @@ The remaining expression kinds in the ethdebug specification are accepted by the Root pointers list externally bound ``expectedParameters`` such as mapping keys. Pointers with expected parameters are exported as templates rather than closed program-context pointers. +Templates are not a second store. +They are exactly the entries of the public `ethdebug.resources.pointers `_ output, keyed by the ``pointerID`` recorded on a variable's data location, each holding the parameters it expects and the pointer they parameterise. +A ``TemplateReference`` in the sidecar names one of those keys. +The sidecar carries the reference so that a variable can point at a shared template without repeating it; the template itself lives in the resource output, which is where a consumer reads it from. + Location Changes ---------------- From 652ddcd013e38511969167c02c8854af203af71e Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 3 Aug 2026 15:49:20 +0200 Subject: [PATCH 35/47] docs: Specify how pointers survive the optimizer --- docs/internals/ethdebug_internal_metadata.rst | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index a1c994bf0885..d08fe3db8010 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -63,6 +63,7 @@ Core Structures =============== ``langutil::DebugData`` is the debug payload carried by Yul AST nodes. +The public format this feeds is the `ethdebug/format specification `_; the structures below mirror its `program `_, `type `_, and `pointer `_ schemas, and differ from them only where the compiler needs information that the public format does not carry. .. list-table:: ``DebugData`` fields relevant to ETHDebug :header-rows: 1 @@ -135,9 +136,34 @@ Core Structures - optional ``SemanticDebugPointer`` - Pointer expression resolving the value in that location. -The pointer expression is also the explicit mapping from a source-language variable to generated Yul variables. -For stack-backed Solidity variables, free variable expressions contain the exact names produced by ``IRVariable::stackSlots()``. -The declaration AST ID identifies the source variable, while those symbolic Yul names identify its generated representation. +The pointer expression is the mapping from a source-language variable to where its value can be found. +How it refers to that place depends on whether the address is known while generating code, and the distinction is what makes the information survive optimisation. + +**Statically addressed values** - storage, transient storage, and code - are resolved during Yul code generation. +A state variable's pointer is its slot and offset, written as literals or as an expression over externally bound parameters such as mapping keys. +No Yul variable name appears in it, so no optimiser pass can invalidate it: there is nothing to rewrite. + +**Stack- and memory-backed values** cannot be resolved that early, because the stack slot does not exist until the Yul-to-EVM transform has run. +For these the pointer is not the carrier. +The variable's identity travels on the ``DebugData`` of the Yul node that produces the value, which the optimiser already propagates as it rewrites code, and the pointer is completed at emission time, once a slot has been assigned. + +**Yul variable names are not a durable handle, and pointer expressions do not use them as one.** +Consider a storage read that code generation emits as its own local:: + + let _1 := read_from_storage_split_offset_0_t_uint256(0x00) + let _2 := foo(_1) + +An optimiser pass may fold this to ``let _2 := foo(read_from_storage_split_offset_0_t_uint256(0x00))``. +A pointer that said "read the stack slot named ``_1``" would then name a variable that no longer exists. +Under the rule above it never said that: the variable is in storage, so its pointer is ``{"location": "storage", "slot": "0x00"}``, which the fold does not touch. + +This costs one thing worth stating. +The debug info no longer records that ``_1`` held the value at that moment - only that the source variable exists in the enclosing scope and where its value can be read from. +That is a deliberate trade: a consumer can always compute the value from the resolved pointer, and nothing else in the format depends on knowing which generated local happened to carry it. + +**Pointer expressions never contain arbitrary Yul.** +Embedding the generating expression would make the sidecar a second copy of the program: every optimiser pass would have to rewrite the embedded code as well as the code itself, and the expressions would be as large and as complex as whatever the user wrote. +The vocabulary above is closed for that reason. Scope Attachment ---------------- @@ -350,8 +376,8 @@ A pass with no declared strategy is treated as ``Drop``, so an undeclared pass d Record the discarded declarations against the same program point so both source names remain inspectable. - ``CommonSubexpressionEliminator``, ``ExpressionJoiner``, ``ExpressionSimplifier``, ``ControlFlowSimplifier``, ``StructuralSimplifier``, ``BlockFlattener``, ``EquivalentFunctionCombiner``, ``LoadResolver`` * - ``Remap`` - - Rewrite the pointer expressions to the new Yul names or the new region. - Renaming passes rewrite names. + - Rewrite what the entry refers to when the pass moves it. + Renaming passes rewrite the Yul names an entry is attached to; because a statically addressed pointer holds no Yul name, renaming cannot reach one. Spilling passes move where the value is *found*, so the pointer changes from a ``Stack`` to a ``Memory`` region; the data itself is not relocated by the debug info. - ``Disambiguator``, ``NameSimplifier``, ``VarNameCleaner``, ``SSATransform``, ``SSAReverser``, ``ExpressionSplitter``, ``LoopInvariantCodeMotion``, ``StackToMemoryMover``, ``StackCompressor``, ``StackLimitEvader`` * - ``Clone`` From aade6856b0c35e83d1c5727889b0a24688ae5a05 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 3 Aug 2026 16:30:36 +0200 Subject: [PATCH 36/47] ethdebug: Separate a Yul local from a bound variable in pointer expressions --- docs/internals/ethdebug_internal_metadata.rst | 5 ++++- liblangutil/SemanticDebugData.h | 20 +++++++++++++++++-- liblangutil/SemanticDebugDataSerDe.cpp | 1 + .../codegen/ir/SemanticDebugDataBuilder.cpp | 7 ++++--- libsolidity/interface/Ethdebug.cpp | 6 ++++++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index d08fe3db8010..92b809f03410 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -315,7 +315,10 @@ The vocabulary is the one defined by `ethdebug/format pointer expressions }` — the slot defined for the referenced region. LookupSlot, /// `{".offset": }` — the offset defined for the referenced region. @@ -123,6 +131,14 @@ struct SemanticDebugPointerExpression return result; } + static SemanticDebugPointerExpression yulLocal(std::string _yulName) + { + SemanticDebugPointerExpression result; + result.kind = Kind::YulLocal; + result.value = std::move(_yulName); + return result; + } + static SemanticDebugPointerExpression lookupSlot(std::string _region) { SemanticDebugPointerExpression result; diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp index cd41aca04208..e82c03e491de 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -150,6 +150,7 @@ constexpr std::pair expressionKindNames[]{ {ExpressionKind::Literal, "literal"}, {ExpressionKind::WordSize, "wordSize"}, {ExpressionKind::Variable, "variable"}, + {ExpressionKind::YulLocal, "yulLocal"}, {ExpressionKind::LookupSlot, "lookupSlot"}, {ExpressionKind::LookupOffset, "lookupOffset"}, {ExpressionKind::LookupLength, "lookupLength"}, diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index b8f406d46f6c..ce9eb1771c4e 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -292,12 +292,13 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _yulVariable) { - // Yul variable names stand in for stack depths that are only known after - // code generation, so the slot is a symbolic variable expression. + // A Yul name stands in for a stack depth that is only known after the + // Yul-to-EVM transform. It is not a variable a consumer can bind, so it is + // kept as its own kind and never published as though it were a slot. return SemanticDebugPointer::region( SemanticDebugPointer::Location::Stack, std::move(_name), - PointerExpression::variable(_yulVariable) + PointerExpression::yulLocal(_yulVariable) ); } diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp index 91d382d4bdbb..c9e037ea51b3 100644 --- a/libsolidity/interface/Ethdebug.cpp +++ b/libsolidity/interface/Ethdebug.cpp @@ -111,6 +111,12 @@ std::optional ethdebugPointerExpression(langutil::SemanticDebugPointerExpr if (!_expression.value) return std::nullopt; return Json(*_expression.value); + case Kind::YulLocal: + // A Yul local names a stack depth that is not known yet. Emitting the + // name would put an identifier where a slot belongs, so the pointer + // holding it is dropped instead - the variable is still described, it + // just has no published location until stack layout is available. + return std::nullopt; case Kind::WordSize: return Json("$wordsize"); case Kind::LookupSlot: From 51cb5ec9bf370981c3c1b2a8834c262fda3a696b Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 3 Aug 2026 16:37:38 +0200 Subject: [PATCH 37/47] docs: Stop claiming scopeInstanceID is serialized --- docs/internals/ethdebug_internal_metadata.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 92b809f03410..b45264f6be65 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -409,7 +409,9 @@ Those instances share declaration and type information but can have different cu The complete model therefore requires two identities: - ``originAstID`` identifies the source-language node and is preserved by ``@ast-id``. -- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance and is preserved in the serialized interchange data. +- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance. + It is part of the model, not yet of the serialized format: no field carries it today, and the reader rejects a table with a repeated AST ID. + Extending the serialized key to the pair is what admitting cloned code requires. The current version of ``SemanticDebugDataTable`` uses only the AST origin ID as its key. That is sufficient for the un-cloned IR path currently supported by ETHDebug, where Yul optimization is rejected. @@ -444,6 +446,8 @@ The complete table, including recursive types, pointer expressions, source names - Objects containing ``astId`` and ``data``, where ``data`` is a ``SemanticDebugData`` value. This is not ``langutil::DebugData``, and it does not repeat the AST ID: the ID appears once, as the entry key. The table is an array rather than an object because AST IDs are integers and JSON object keys are not, and because an ordered array makes the writer's output deterministic without relying on key ordering rules. + AST IDs are unique here because serialization happens at code generation, before any pass that could clone a scope; the reader enforces this by rejecting a repeated ID. + A two-level keying only becomes necessary when cloned code is admitted - see Identity and Optimizer Cloning. Readers reject an unknown format, an unsupported version, malformed tagged values, and duplicate table keys. Writers emit deterministic entry order because the table is ordered by key. From 65697d98e4c51bfcd5d70c1d414c68759384de07 Mon Sep 17 00:00:00 2001 From: djole Date: Tue, 4 Aug 2026 17:28:04 +0200 Subject: [PATCH 38/47] ethdebug: Resolve the type vocabulary --- docs/internals/ethdebug_internal_metadata.rst | 19 ++++++++++-------- liblangutil/SemanticDebugData.h | 3 +++ liblangutil/SemanticDebugDataSerDe.cpp | 1 + .../codegen/ir/SemanticDebugDataBuilder.cpp | 20 ++++++++++++++++++- libsolidity/interface/Ethdebug.cpp | 12 +++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index b45264f6be65..5758bb919d11 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -196,10 +196,11 @@ Type Descriptors - Elementary, complex, or unknown representation. * - ``kind`` - enum - - Integer, bytes, string, address, contract, enum, alias, tuple, array, mapping, struct, function, or unknown kind. + - Integer, bytes, string, address, contract, enum, alias, tuple, array, mapping, slice, struct, function, or unknown kind. * - ``bits``, ``places``, ``bytes`` - optional integers - Width information for numeric and fixed-bytes types. + ``bytes`` is also the representation width of address-carrying kinds: ``address`` and ``contract`` set it to ``20`` today, and a wider address is the same kind with a different width rather than a new kind. * - ``payable``, ``isLibrary``, ``isInterface`` - optional booleans - Address and contract properties. @@ -232,14 +233,16 @@ Three cases are worth stating explicitly. They are represented as ``alias``, carrying the declaration's name and source range and a single ``Underlying`` component for the wrapped type. A consumer that does not care about the distinction can follow the component and treat the value as its underlying type. -**Array slices** are not represented today: they produce the ``unknown`` kind. -Only calldata slices exist, and their representation is identical to an ordinary calldata array - offset and length both on stack - so a pointer for them is expressible with what is already here. -Naming them is a gap in the type vocabulary rather than in the pointer model. +**Array slices** are the ``slice`` kind, with the element type as an ``Element`` component and dynamic sizing. +Their representation is identical to an ordinary calldata array - offset and length both on stack - and the public format has no slice kind, so they are published as dynamic arrays; the sidecar keeps the distinction for the compiler's own use. -**The enumeration is closed, and that is a known limitation.** -A 32-byte address type, and the contract and external function types that contain one, cannot be described without adding kinds - as can any future type the language grows. -``unknown`` is the only escape hatch, and it discards the information rather than deferring it. -Whether this vocabulary should stay closed, gain a versioned extension point, or defer entirely to the published ethdebug type schema is an open question that should be settled before the format is depended upon. +**Address width is data, not a kind.** +``address`` and ``contract`` carry their representation width in ``bytes``, today always ``20``. +A migration to 32-byte addresses changes the value of that field and nothing else in this format; the contract and external function types that contain an address inherit the same rule. + +**The vocabulary is extended through the format version.** +New kinds are introduced by incrementing the serialization version, and readers already reject a version they do not support, so an old reader fails loudly rather than misreading a new kind. +``unknown`` remains reserved for types the producer genuinely cannot describe, not as an escape hatch for types the vocabulary has yet to name. Every type component contains a role, an optional member name, an optional reference ID, and an optional inline type. When recursively describing a type already present on the current recursion path, the inline type is omitted and the reference ID terminates the cycle. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index a831622d62d6..4cb289b9002a 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -324,6 +324,9 @@ struct SemanticDebugType Tuple, Array, Mapping, + /// A view over a section of an array. Same representation as the array + /// it slices; the element type is its `Element` component. + Slice, Struct, Function, Unknown diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp index e82c03e491de..cc8df1225179 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -200,6 +200,7 @@ constexpr std::pair typeKindNames[]{ {TypeKind::Tuple, "tuple"}, {TypeKind::Array, "array"}, {TypeKind::Mapping, "mapping"}, + {TypeKind::Slice, "slice"}, {TypeKind::Struct, "struct"}, {TypeKind::Function, "function"}, {TypeKind::Unknown, "unknown"} diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index ce9eb1771c4e..f9c307afaa08 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -102,6 +102,9 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO result.typeClass = SemanticDebugType::Class::Elementary; result.kind = SemanticDebugType::Kind::Address; result.payable = addressType.stateMutability() == StateMutability::Payable; + // The representation width is data, not part of the kind: a future + // 32-byte address is the same kind with a different width. + result.bytes = 20; break; } case Type::Category::Integer: @@ -162,6 +165,8 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO result.typeClass = SemanticDebugType::Class::Elementary; result.kind = SemanticDebugType::Kind::Contract; result.payable = contractType.isPayable(); + // A contract value is an address; its width travels the same way. + result.bytes = 20; if (contractType.contractDefinition().isLibrary()) result.isLibrary = true; if (contractType.contractDefinition().isInterface()) @@ -274,9 +279,22 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO )); break; } + case Type::Category::ArraySlice: + { + auto const& sliceType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Slice; + result.dynamic = true; + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Element, + std::nullopt, + *sliceType.arrayType().baseType(), + _typesOnPath + )); + break; + } case Type::Category::RationalNumber: case Type::Category::StringLiteral: - case Type::Category::ArraySlice: case Type::Category::TypeType: case Type::Category::Modifier: case Type::Category::Magic: diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp index c9e037ea51b3..cb5158bdd34e 100644 --- a/libsolidity/interface/Ethdebug.cpp +++ b/libsolidity/interface/Ethdebug.cpp @@ -525,6 +525,18 @@ std::optional ethdebugType( result["count"] = *_type.count; break; } + case TypeKind::Slice: + { + // The public format has no slice kind. A slice's representation is the + // dynamic array it views, so that is what it is published as; the + // sidecar keeps the distinction for the compiler's own use. + std::optional element = singleWrapper(Role::Element); + if (!element) + return std::nullopt; + result["kind"] = "array"; + result["contains"] = std::move(*element); + break; + } case TypeKind::Mapping: { std::optional key = singleWrapper(Role::Key); From 9949090fa99cd4c0d92d40ff4c93c177223d5dd0 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 5 Aug 2026 14:25:18 +0200 Subject: [PATCH 39/47] ethdebug: Key debug entries by (astId, instance) end to end --- docs/internals/ethdebug_internal_metadata.rst | 19 ++++++----- liblangutil/SemanticDebugDataSerDe.cpp | 19 ++++++++--- liblangutil/SemanticDebugDataTable.h | 32 ++++++++++++++----- libyul/SemanticDebugDataTransfer.cpp | 4 +-- libyul/YulStack.cpp | 4 +-- test/liblangutil/DebugData.cpp | 25 +++++++++++++++ 6 files changed, 77 insertions(+), 26 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 5758bb919d11..2afd9599a592 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -412,12 +412,10 @@ Those instances share declaration and type information but can have different cu The complete model therefore requires two identities: - ``originAstID`` identifies the source-language node and is preserved by ``@ast-id``. -- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance. - It is part of the model, not yet of the serialized format: no field carries it today, and the reader rejects a table with a repeated AST ID. - Extending the serialized key to the pair is what admitting cloned code requires. +- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance and is serialized as the entry's ``instance`` field, defaulting to ``0``. + The table key is the ``(astId, instance)`` pair, so admitting cloned code is not a format change: code generation emits instance ``0`` only, and cloning passes assign fresh instances to the copies they make. -The current version of ``SemanticDebugDataTable`` uses only the AST origin ID as its key. -That is sufficient for the un-cloned IR path currently supported by ETHDebug, where Yul optimization is rejected. +On the un-cloned IR path currently supported by ETHDebug, where Yul optimization is rejected, every entry has instance ``0`` and the AST ID alone is in practice unique. It must not be treated as sufficient for optimizer passes that clone or specialize functions. Before those passes are enabled with ETHDebug, the table and Yul annotations must gain the instance discriminator so per-instance location updates cannot overwrite each other. @@ -446,11 +444,12 @@ The complete table, including recursive types, pointer expressions, source names Generated Yul object names are not assumed to preserve it. * - ``entries`` - array - - Objects containing ``astId`` and ``data``, where ``data`` is a ``SemanticDebugData`` value. - This is not ``langutil::DebugData``, and it does not repeat the AST ID: the ID appears once, as the entry key. - The table is an array rather than an object because AST IDs are integers and JSON object keys are not, and because an ordered array makes the writer's output deterministic without relying on key ordering rules. - AST IDs are unique here because serialization happens at code generation, before any pass that could clone a scope; the reader enforces this by rejecting a repeated ID. - A two-level keying only becomes necessary when cloned code is admitted - see Identity and Optimizer Cloning. + - Objects containing ``astId``, an optional ``instance``, and ``data``, where ``data`` is a ``SemanticDebugData`` value. + This is not ``langutil::DebugData``, and it does not repeat the AST ID: the key appears once, on the entry. + The table is an array rather than an object because the key is a pair of integers and JSON object keys are neither, and because an ordered array makes the writer's output deterministic without relying on key ordering rules. + Entries are keyed by ``(astId, instance)``. + ``instance`` defaults to ``0`` and is omitted when zero, which is the only value code generation produces - so un-cloned output carries no discriminator at all. + A pass that clones a scope gives each copy its own instance; the reader rejects a repeated pair. Readers reject an unknown format, an unsupported version, malformed tagged values, and duplicate table keys. Writers emit deterministic entry order because the table is ordered by key. diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp index cc8df1225179..57769d647282 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -683,10 +683,16 @@ Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) {"version", SemanticDebugDataFormatVersion}, {"entries", Json::array()}}; setOptional(result, "contractName", _table.contractName()); - for (auto const& [astID, data]: _table.entries()) + for (auto const& [key, data]: _table.entries()) { require(data != nullptr, "Semantic debug data table contains a null entry."); - result["entries"].emplace_back(Json{{"astId", astID}, {"data", dataToJson(*data)}}); + Json entry{{"astId", key.first}, {"data", dataToJson(*data)}}; + // Instance 0 is the only value code generation produces, so it is + // omitted and today's output is unchanged; a cloned scope carries its + // discriminator explicitly. + if (key.second != 0) + entry["instance"] = key.second; + result["entries"].emplace_back(std::move(entry)); } return result; } @@ -712,9 +718,14 @@ SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) std::string path = "semantic debug data.entries[" + std::to_string(index) + "]"; Json const& entry = entries.at(index); int64_t astID = requiredInteger(entry, "astId", path); - require(!result.find(astID), path + " duplicates AST ID " + std::to_string(astID) + "."); + int64_t instance = optionalValue(entry, "instance", path).value_or(0); + SemanticDebugDataTable::Key const key{astID, instance}; + require( + !result.find(key), + path + " duplicates AST ID " + std::to_string(astID) + + (instance != 0 ? " (instance " + std::to_string(instance) + ")" : "") + "."); result.set( - astID, + key, std::make_shared( dataFromJson(requiredMember(entry, "data", path), path + ".data"))); } diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h index e1062fc6c3ea..5da97218a15d 100644 --- a/liblangutil/SemanticDebugDataTable.h +++ b/liblangutil/SemanticDebugDataTable.h @@ -46,33 +46,49 @@ class SemanticDebugDataTable return m_contractName; } + /// An entry is identified by the source AST ID together with a scope + /// instance discriminator. Code generation produces instance 0 only; + /// passes that clone code give each copy its own instance, so the copies + /// do not overwrite each other. Keying by the pair from the start means + /// admitting cloned code later is not a format change. + using Key = std::pair; + + void set(Key _key, SemanticDebugData::ConstPtr _debugData) + { + m_byKey[_key] = std::move(_debugData); + } + void set(int64_t _astID, SemanticDebugData::ConstPtr _debugData) { - m_byASTID[_astID] = std::move(_debugData); + set(Key{_astID, 0}, std::move(_debugData)); + } + + SemanticDebugData::ConstPtr find(Key _key) const + { + auto const it = m_byKey.find(_key); + return it == m_byKey.end() ? nullptr : it->second; } SemanticDebugData::ConstPtr find(std::optional _astID) const { if (!_astID) return nullptr; - - auto const it = m_byASTID.find(*_astID); - return it == m_byASTID.end() ? nullptr : it->second; + return find(Key{*_astID, 0}); } bool empty() const { - return m_byASTID.empty(); + return m_byKey.empty(); } - std::map const& entries() const + std::map const& entries() const { - return m_byASTID; + return m_byKey; } private: std::optional m_contractName; - std::map m_byASTID; + std::map m_byKey; }; } // namespace solidity::langutil diff --git a/libyul/SemanticDebugDataTransfer.cpp b/libyul/SemanticDebugDataTransfer.cpp index eb8fa0e6e0b3..5b925780b9e2 100644 --- a/libyul/SemanticDebugDataTransfer.cpp +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -275,8 +275,8 @@ SemanticDebugDataTable updateSemanticDebugDataLocations( ) { SemanticDebugDataTable result; - for (auto const& [astID, debugData]: _table.entries()) - result.set(astID, updateSemanticDebugDataLocations(debugData, _yulNames)); + for (auto const& [key, debugData]: _table.entries()) + result.set(key, updateSemanticDebugDataLocations(debugData, _yulNames)); return result; } diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index db535645931e..9d5b9a4e7399 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -451,8 +451,8 @@ void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) if (_table.contractName()) m_semanticDebugData.setContractName(*_table.contractName()); - for (auto const& [astID, debugData]: _table.entries()) - m_semanticDebugData.set(astID, debugData); + for (auto const& [key, debugData]: _table.entries()) + m_semanticDebugData.set(key, debugData); // Applying instead of blindly reattaching validates stack locations against the // current Yul code. This matters when the table is attached to already optimized diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 2eac02505dd8..54e3cc5aaabe 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -327,6 +327,31 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_duplic BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); } +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_distinguishes_scope_instances) +{ + // A cloned scope keeps its AST ID and gets its own instance; the two + // entries must coexist, round-trip, and collide only on the full pair. + SemanticDebugDataTable table; + table.set({9, 0}, std::make_shared()); + table.set({9, 2}, std::make_shared()); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["entries"].size(), 2); + // Instance 0 is the default and stays implicit, so un-cloned output is + // unchanged by the discriminator's existence. + BOOST_CHECK(!serialized["entries"][0].contains("instance")); + BOOST_CHECK_EQUAL(serialized["entries"][1]["instance"].get(), 2); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_CHECK(read.find({9, 0}) != nullptr); + BOOST_CHECK(read.find({9, 2}) != nullptr); + BOOST_CHECK(read.find({9, 1}) == nullptr); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); + + serialized["entries"].emplace_back(Json{{"astId", 9}, {"instance", 2}, {"data", Json::object()}}); + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_location_kinds_roundtrip) { using Kind = SemanticDebugVariableLocation::Kind; From 321af53ff3153f97d6292b4704d10177410190aa Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 5 Aug 2026 15:28:04 +0200 Subject: [PATCH 40/47] ethdebug: Location updates, so a value that moves can be described --- docs/internals/ethdebug_internal_metadata.rst | 32 +++++++++++++++- liblangutil/SemanticDebugData.h | 19 ++++++++++ liblangutil/SemanticDebugDataSerDe.cpp | 37 +++++++++++++++++++ test/liblangutil/DebugData.cpp | 29 +++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 2afd9599a592..d4eed3410f6e 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -359,6 +359,35 @@ The implemented conservative rule checks all free Yul variable names used by a s If any required name is absent from the current Yul object, the location becomes ``OptimizedOut`` and the pointer is removed. Bound names from pointer scopes, list indices, and template parameters are not mistaken for Yul dependencies. +Location Updates +~~~~~~~~~~~~~~~~ + +A single per-scope location cannot describe a value that moves during its lifetime. +``SSATransform`` gives one source variable a different generated name at each assignment; ``Rematerialiser`` replaces stored values with recomputation at individual uses. +For these, an entry carries ``locationUpdates`` - the analogue of an ``llvm.dbg.value`` intrinsic: + +.. list-table:: ``SemanticDebugLocationUpdate`` fields + :header-rows: 1 + :widths: 28 24 48 + + * - Field + - Type + - Meaning + * - ``variableAstId`` + - integer + - Declaration AST ID of the variable being rebound. + * - ``dataLocation`` + - ``SemanticDebugVariableLocation`` + - The location valid from the carrying node onward. + * - ``pointer`` + - optional ``SemanticDebugPointer`` + - Absent when the location kind carries no address - ``OptimizedOut`` in particular. + +An update takes effect at the Yul node its payload is attached to and holds until the next update for the same variable within the scope, or the scope's end. +Scope entries define variables; statement-level entries update them. +A ``Drop`` is an update to ``OptimizedOut``. +Code generation emits no updates, so the field is absent from un-optimized output; only passes whose effect a static location cannot express produce them. + Optimizer Update Rules ---------------------- @@ -395,7 +424,8 @@ A pass with no declared strategy is treated as ``Drop``, so an undeclared pass d The declaration, type, and source location are retained so the variable is still reported as belonging to the scope. - ``DeadCodeEliminator``, ``UnusedPruner``, ``UnusedAssignEliminator``, ``UnusedStoreEliminator``, ``EqualStoreEliminator``, ``CircularReferencesPruner``, ``UnusedFunctionParameterPruner`` -``Rematerialiser`` is a special case of ``Remap``. +``SSATransform`` and ``Rematerialiser`` cannot be expressed as a whole-scope ``Remap``, because the answer to "where is the variable" changes between program points. +They emit location updates instead - see `Location Updates`_. Substituting a variable use by its defining expression can leave the variable itself unused and later pruned. The value is then still recoverable by evaluating that expression, so its location becomes ``Computed`` rather than ``OptimizedOut``. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 4cb289b9002a..f4d3e8e9e482 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -524,12 +524,31 @@ struct SemanticDebugVariable std::optional ethdebugPointer; }; +/// Rebinds a variable's current location from the carrying Yul node onward, +/// within the variable's scope, until the next update - the analogue of an +/// `llvm.dbg.value` intrinsic. A single per-scope location cannot describe a +/// pass that gives one source variable different generated names over its +/// lifetime (SSATransform) or replaces stored values with recomputation +/// (Rematerialiser); this record can, and Drop is an update to OptimizedOut. +struct SemanticDebugLocationUpdate +{ + /// Declaration AST ID of the variable being rebound. + int64_t variableAstID = 0; + SemanticDebugVariableLocation dataLocation; + /// Absent when the location kind carries no address - OptimizedOut in + /// particular. + std::optional ethdebugPointer; +}; + struct SemanticDebugData { using ConstPtr = std::shared_ptr; std::optional lexicalScopeID; std::vector variableDefinitions = {}; + /// Location rebindings taking effect at the node this payload is attached + /// to. Scope entries define variables; statement-level entries update them. + std::vector locationUpdates = {}; }; } // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerDe.cpp index 57769d647282..20e0b681d26f 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerDe.cpp @@ -652,6 +652,27 @@ SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _pa return result; } +Json locationUpdateToJson(SemanticDebugLocationUpdate const& _update) +{ + Json result{ + {"variableAstId", _update.variableAstID}, + {"dataLocation", variableLocationToJson(_update.dataLocation)}}; + if (_update.ethdebugPointer) + result["pointer"] = pointerToJson(*_update.ethdebugPointer); + return result; +} + +SemanticDebugLocationUpdate locationUpdateFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugLocationUpdate result; + result.variableAstID = requiredInteger(_json, "variableAstId", _path); + result.dataLocation + = variableLocationFromJson(requiredMember(_json, "dataLocation", _path), _path + ".dataLocation"); + if (_json.contains("pointer")) + result.ethdebugPointer = pointerFromJson(_json.at("pointer"), _path + ".pointer"); + return result; +} + Json dataToJson(SemanticDebugData const& _data) { Json result = Json::object(); @@ -659,6 +680,14 @@ Json dataToJson(SemanticDebugData const& _data) result["variables"] = Json::array(); for (auto const& variable: _data.variableDefinitions) result["variables"].emplace_back(variableToJson(variable)); + // Omitted while empty, which is all code generation produces - only + // optimizer passes emit rebindings. + if (!_data.locationUpdates.empty()) + { + result["locationUpdates"] = Json::array(); + for (auto const& update: _data.locationUpdates) + result["locationUpdates"].emplace_back(locationUpdateToJson(update)); + } return result; } @@ -671,6 +700,14 @@ SemanticDebugData dataFromJson(Json const& _json, std::string const& _path) for (size_t index = 0; index < variables.size(); ++index) result.variableDefinitions.emplace_back( variableFromJson(variables.at(index), _path + ".variables[" + std::to_string(index) + "]")); + if (_json.contains("locationUpdates")) + { + Json const& updates = _json.at("locationUpdates"); + requireArray(updates, _path + ".locationUpdates"); + for (size_t index = 0; index < updates.size(); ++index) + result.locationUpdates.emplace_back(locationUpdateFromJson( + updates.at(index), _path + ".locationUpdates[" + std::to_string(index) + "]")); + } return result; } diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 54e3cc5aaabe..22fcb4219093 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -327,6 +327,35 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_duplic BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); } +BOOST_AUTO_TEST_CASE(semantic_debug_data_location_updates_roundtrip) +{ + // The dbg.value analogue: a statement-level rebinding of a variable's + // location, here a spill to memory and a drop to optimized-out. + SemanticDebugLocationUpdate spilled; + spilled.variableAstID = 42; + spilled.dataLocation = {SemanticDebugVariableLocation::Kind::Memory, "pointer:spill"}; + spilled.ethdebugPointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Memory, + "spill", + SemanticDebugPointerExpression::literal("0x80")); + + SemanticDebugLocationUpdate dropped; + dropped.variableAstID = 42; + dropped.dataLocation = {SemanticDebugVariableLocation::Kind::OptimizedOut, std::nullopt}; + + SemanticDebugData data; + data.locationUpdates = {spilled, dropped}; + SemanticDebugDataTable table; + table.set(7, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + // Absent pointer must stay absent - OptimizedOut has no address. + Json const& updates = serialized["entries"][0]["data"]["locationUpdates"]; + BOOST_CHECK_EQUAL(updates.size(), 2); + BOOST_CHECK(!updates[1].contains("pointer")); +} + BOOST_AUTO_TEST_CASE(semantic_debug_data_table_distinguishes_scope_instances) { // A cloned scope keeps its AST ID and gets its own instance; the two From 14604568c31036563d374bd0ab4a721ae5e4f27a Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 5 Aug 2026 15:56:21 +0200 Subject: [PATCH 41/47] ethdebug: Share type descriptors by ID in the serialized sidecar --- docs/internals/ethdebug_internal_metadata.rst | 6 +- liblangutil/SemanticDebugDataSerDe.cpp | 60 ++++++++++++++++--- test/liblangutil/DebugData.cpp | 42 +++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index d4eed3410f6e..79e27969fac1 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -127,7 +127,7 @@ The public format this feeds is the `ethdebug/format specification #include +#include +#include #include #include #include @@ -611,7 +613,7 @@ SemanticDebugVariableLocation variableLocationFromJson(Json const& _json, std::s .pointerID = optionalString(_json, "pointerId", _path)}; } -Json variableToJson(SemanticDebugVariable const& _variable) +Json variableToJson(SemanticDebugVariable const& _variable, std::set const& _tabledTypeIDs) { if (_variable.identifier && _variable.identifier->empty()) solThrow(SemanticDebugDataSerializationError, "Variable identifier must not be empty."); @@ -621,7 +623,9 @@ Json variableToJson(SemanticDebugVariable const& _variable) if (_variable.declarationSourceLocation) result["declarationSourceLocation"] = sourceLocationToJson(*_variable.declarationSourceLocation); setOptional(result, "typeId", _variable.typeID); - if (_variable.ethdebugType) + // A descriptor whose type ID is in the shared table is not repeated per + // variable; one without an ID has nowhere else to live and stays inline. + if (_variable.ethdebugType && !(_variable.typeID && _tabledTypeIDs.count(*_variable.typeID))) result["type"] = typeToJson(*_variable.ethdebugType); if (_variable.dataLocation) result["dataLocation"] = variableLocationToJson(*_variable.dataLocation); @@ -630,7 +634,10 @@ Json variableToJson(SemanticDebugVariable const& _variable) return result; } -SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _path) +SemanticDebugVariable variableFromJson( + Json const& _json, + std::map const& _sharedTypes, + std::string const& _path) { SemanticDebugVariable result; result.identifier = optionalString(_json, "identifier", _path); @@ -645,6 +652,9 @@ SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _pa result.typeID = optionalString(_json, "typeId", _path); if (_json.contains("type")) result.ethdebugType = typeFromJson(_json.at("type"), _path + ".type"); + else if (result.typeID) + if (auto const it = _sharedTypes.find(*result.typeID); it != _sharedTypes.end()) + result.ethdebugType = it->second; if (_json.contains("dataLocation")) result.dataLocation = variableLocationFromJson(_json.at("dataLocation"), _path + ".dataLocation"); if (_json.contains("pointer")) @@ -673,13 +683,13 @@ SemanticDebugLocationUpdate locationUpdateFromJson(Json const& _json, std::strin return result; } -Json dataToJson(SemanticDebugData const& _data) +Json dataToJson(SemanticDebugData const& _data, std::set const& _tabledTypeIDs) { Json result = Json::object(); setOptional(result, "lexicalScopeId", _data.lexicalScopeID); result["variables"] = Json::array(); for (auto const& variable: _data.variableDefinitions) - result["variables"].emplace_back(variableToJson(variable)); + result["variables"].emplace_back(variableToJson(variable, _tabledTypeIDs)); // Omitted while empty, which is all code generation produces - only // optimizer passes emit rebindings. if (!_data.locationUpdates.empty()) @@ -691,7 +701,10 @@ Json dataToJson(SemanticDebugData const& _data) return result; } -SemanticDebugData dataFromJson(Json const& _json, std::string const& _path) +SemanticDebugData dataFromJson( + Json const& _json, + std::map const& _sharedTypes, + std::string const& _path) { SemanticDebugData result; result.lexicalScopeID = optionalValue(_json, "lexicalScopeId", _path); @@ -699,7 +712,8 @@ SemanticDebugData dataFromJson(Json const& _json, std::string const& _path) requireArray(variables, _path + ".variables"); for (size_t index = 0; index < variables.size(); ++index) result.variableDefinitions.emplace_back( - variableFromJson(variables.at(index), _path + ".variables[" + std::to_string(index) + "]")); + variableFromJson( + variables.at(index), _sharedTypes, _path + ".variables[" + std::to_string(index) + "]")); if (_json.contains("locationUpdates")) { Json const& updates = _json.at("locationUpdates"); @@ -720,10 +734,29 @@ Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) {"version", SemanticDebugDataFormatVersion}, {"entries", Json::array()}}; setOptional(result, "contractName", _table.contractName()); + + // Types are written once, keyed by type ID, rather than inline in every + // variable that has one: the distinct types are few and the descriptors + // are not. Two variables with one type ID have one type by construction - + // the ID is the compiler's type identifier - so first-wins is not a choice + // between descriptors. + Json sharedTypes = Json::object(); + std::set tabledTypeIDs; + for (auto const& [key, data]: _table.entries()) + if (data) + for (auto const& variable: data->variableDefinitions) + if (variable.typeID && variable.ethdebugType && !tabledTypeIDs.count(*variable.typeID)) + { + sharedTypes[*variable.typeID] = typeToJson(*variable.ethdebugType); + tabledTypeIDs.insert(*variable.typeID); + } + if (!tabledTypeIDs.empty()) + result["types"] = std::move(sharedTypes); + for (auto const& [key, data]: _table.entries()) { require(data != nullptr, "Semantic debug data table contains a null entry."); - Json entry{{"astId", key.first}, {"data", dataToJson(*data)}}; + Json entry{{"astId", key.first}, {"data", dataToJson(*data, tabledTypeIDs)}}; // Instance 0 is the only value code generation produces, so it is // omitted and today's output is unchanged; a cloned scope carries its // discriminator explicitly. @@ -747,6 +780,15 @@ SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) Json const& entries = requiredMember(_json, "entries", "semantic debug data"); requireArray(entries, "semantic debug data.entries"); + std::map sharedTypes; + if (_json.contains("types")) + { + Json const& types = _json.at("types"); + requireObject(types, "semantic debug data.types"); + for (auto const& [typeID, descriptor]: types.items()) + sharedTypes.emplace(typeID, typeFromJson(descriptor, "semantic debug data.types." + typeID)); + } + SemanticDebugDataTable result; if (contractName) result.setContractName(std::move(*contractName)); @@ -764,7 +806,7 @@ SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) result.set( key, std::make_shared( - dataFromJson(requiredMember(entry, "data", path), path + ".data"))); + dataFromJson(requiredMember(entry, "data", path), sharedTypes, path + ".data"))); } return result; } diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 22fcb4219093..7f114fdc420e 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -327,6 +327,48 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_duplic BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); } +BOOST_AUTO_TEST_CASE(semantic_debug_data_types_are_shared_by_id) +{ + SemanticDebugType uintType; + uintType.typeClass = SemanticDebugType::Class::Elementary; + uintType.kind = SemanticDebugType::Kind::Uint; + uintType.bits = 256; + + auto makeVariable = [&](std::string _name) { + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.typeID = "t_uint256"; + variable.ethdebugType = uintType; + return variable; + }; + SemanticDebugData data; + data.variableDefinitions = {makeVariable("a"), makeVariable("b")}; + + // A descriptor without a type ID has nowhere else to live and stays inline. + SemanticDebugVariable inlineOnly; + inlineOnly.identifier = "c"; + inlineOnly.ethdebugType = uintType; + data.variableDefinitions.emplace_back(std::move(inlineOnly)); + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["types"].size(), 1); + Json const& variables = serialized["entries"][0]["data"]["variables"]; + BOOST_CHECK(!variables[0].contains("type")); + BOOST_CHECK(!variables[1].contains("type")); + BOOST_CHECK(variables[2].contains("type")); + + // Reading re-inflates the shared descriptor onto each variable. + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + auto entry = read.find(1); + BOOST_REQUIRE(entry != nullptr); + BOOST_REQUIRE(entry->variableDefinitions[0].ethdebugType.has_value()); + BOOST_CHECK(entry->variableDefinitions[0].ethdebugType->bits == 256); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + BOOST_AUTO_TEST_CASE(semantic_debug_data_location_updates_roundtrip) { // The dbg.value analogue: a statement-level rebinding of a variable's From 0189c6f44cc16bb5b43c6bee7da5ae4bbe229764 Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 6 Aug 2026 12:00:08 +0200 Subject: [PATCH 42/47] ethdebug: Rename SemanticDebugDataSerDe to SemanticDebugDataSerialization --- docs/internals/ethdebug_internal_metadata.rst | 2 +- liblangutil/CMakeLists.txt | 4 ++-- ...cDebugDataSerDe.cpp => SemanticDebugDataSerialization.cpp} | 2 +- ...anticDebugDataSerDe.h => SemanticDebugDataSerialization.h} | 0 libsolidity/interface/StandardCompiler.cpp | 2 +- solc/CommandLineInterface.cpp | 2 +- test/liblangutil/DebugData.cpp | 2 +- test/libsolidity/EthdebugTest.cpp | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename liblangutil/{SemanticDebugDataSerDe.cpp => SemanticDebugDataSerialization.cpp} (99%) rename liblangutil/{SemanticDebugDataSerDe.h => SemanticDebugDataSerialization.h} (100%) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 79e27969fac1..12209d9a18d4 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -452,7 +452,7 @@ Before those passes are enabled with ETHDebug, the table and Yul annotations mus Serialization ============= -``liblangutil/SemanticDebugDataSerDe.h`` provides both serialization and deserialization. +``liblangutil/SemanticDebugDataSerialization.h`` provides both serialization and deserialization. The complete table, including recursive types, pointer expressions, source names, ordered definitions, and unattached scope records, is serialized. .. list-table:: Top-level JSON object diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 96132cf240f2..fdd0a26fee31 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -18,8 +18,8 @@ set(sources Scanner.h CharStreamProvider.h SemanticDebugData.h - SemanticDebugDataSerDe.cpp - SemanticDebugDataSerDe.h + SemanticDebugDataSerialization.cpp + SemanticDebugDataSerialization.h SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h diff --git a/liblangutil/SemanticDebugDataSerDe.cpp b/liblangutil/SemanticDebugDataSerialization.cpp similarity index 99% rename from liblangutil/SemanticDebugDataSerDe.cpp rename to liblangutil/SemanticDebugDataSerialization.cpp index c68f9100426e..6e6419ece0de 100644 --- a/liblangutil/SemanticDebugDataSerDe.cpp +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -16,7 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 -#include +#include #include diff --git a/liblangutil/SemanticDebugDataSerDe.h b/liblangutil/SemanticDebugDataSerialization.h similarity index 100% rename from liblangutil/SemanticDebugDataSerDe.h rename to liblangutil/SemanticDebugDataSerialization.h diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index 5df190fe8e0c..ccf6e018d74a 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -37,7 +37,7 @@ #include #include -#include +#include #include #include diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index 0ba2ccd7051e..95e7898be914 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -46,7 +46,7 @@ #include #include -#include +#include #include #include diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 7f114fdc420e..50d822c05be2 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -17,7 +17,7 @@ // SPDX-License-Identifier: GPL-3.0 #include -#include +#include #include #include diff --git a/test/libsolidity/EthdebugTest.cpp b/test/libsolidity/EthdebugTest.cpp index 7041fd4cd95c..f84310847ec2 100644 --- a/test/libsolidity/EthdebugTest.cpp +++ b/test/libsolidity/EthdebugTest.cpp @@ -20,7 +20,7 @@ #include #include -#include +#include #include From 8a1ee752dff0f9e491da8482d26cc11d02eb7a74 Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 6 Aug 2026 15:49:21 +0200 Subject: [PATCH 43/47] Update docs/internals/ethdebug_internal_metadata.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kamil ƚliwak --- docs/internals/ethdebug_internal_metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 12209d9a18d4..7294a5847add 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -63,7 +63,7 @@ Core Structures =============== ``langutil::DebugData`` is the debug payload carried by Yul AST nodes. -The public format this feeds is the `ethdebug/format specification `_; the structures below mirror its `program `_, `type `_, and `pointer `_ schemas, and differ from them only where the compiler needs information that the public format does not carry. +The public format this feeds is the `ethdebug/format specification `_; the structures below mirror its `program `_, `type `_, and `pointer `_ schemas, and differ from them only where the compiler needs information that the public format does not carry. .. list-table:: ``DebugData`` fields relevant to ETHDebug :header-rows: 1 From e09ced23fa3eb97912b23770bc818002fa5e34eb Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 6 Aug 2026 17:07:01 +0200 Subject: [PATCH 44/47] ethdebug: Remove the type-level dataLocation annotation --- docs/internals/ethdebug_internal_metadata.rst | 3 --- liblangutil/SemanticDebugData.h | 3 +-- .../SemanticDebugDataSerialization.cpp | 2 -- .../codegen/ir/SemanticDebugDataBuilder.cpp | 20 ------------------- test/liblangutil/DebugData.cpp | 1 - 5 files changed, 1 insertion(+), 28 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 7294a5847add..30f434c0c565 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -219,9 +219,6 @@ Type Descriptors * - ``definitionLocation`` - optional ``SourceLocation`` - Source range of that declaration. - * - ``dataLocation`` - - optional string - - Solidity type data-location annotation retained for type lowering. * - ``dynamic`` - optional boolean - Whether an array-like type is dynamically sized. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index f4d3e8e9e482..01908aa4bd64 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -360,8 +360,7 @@ struct SemanticDebugType std::optional definitionName; std::optional definitionLocation; - // Internal annotations that have no direct ethdebug type schema equivalent. - std::optional dataLocation; + // Internal annotation with no direct ethdebug type schema equivalent. std::optional dynamic; }; diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp index 6e6419ece0de..229fd2bb3d04 100644 --- a/liblangutil/SemanticDebugDataSerialization.cpp +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -425,7 +425,6 @@ Json typeToJson(SemanticDebugType const& _type) setOptional(result, "definitionName", _type.definitionName); if (_type.definitionLocation) result["definitionLocation"] = sourceLocationToJson(*_type.definitionLocation); - setOptional(result, "dataLocation", _type.dataLocation); setOptional(result, "dynamic", _type.dynamic); return result; } @@ -467,7 +466,6 @@ SemanticDebugType typeFromJson(Json const& _json, std::string const& _path) if (_json.contains("definitionLocation")) result.definitionLocation = sourceLocationFromJson(_json.at("definitionLocation"), _path + ".definitionLocation"); - result.dataLocation = optionalString(_json, "dataLocation", _path); result.dynamic = optionalValue(_json, "dynamic", _path); return result; } diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index f9c307afaa08..787de47e1586 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -43,23 +43,6 @@ namespace using PointerExpression = SemanticDebugPointerExpression; -std::string dataLocationName(DataLocation _location) -{ - switch (_location) - { - case DataLocation::Storage: - return "storage"; - case DataLocation::Transient: - return "transient"; - case DataLocation::CallData: - return "calldata"; - case DataLocation::Memory: - return "memory"; - } - solAssert(false, "Invalid data location."); - return ""; -} - SemanticDebugType semanticType(Type const& _type, std::set& _typesOnPath); /// Wraps a composed type. The inline representation is cut when the composed @@ -139,7 +122,6 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO case Type::Category::Array: { auto const& arrayType = dynamic_cast(_type); - result.dataLocation = dataLocationName(arrayType.location()); result.dynamic = arrayType.isDynamicallySized(); if (arrayType.isByteArrayOrString()) { @@ -179,7 +161,6 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO auto const& structType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Struct; - result.dataLocation = dataLocationName(structType.location()); setDefinition(result, structType.structDefinition()); for (ASTPointer const& member: structType.structDefinition().members()) if (member->annotation().type) @@ -235,7 +216,6 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO auto const& mappingType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Mapping; - result.dataLocation = "storage"; result.components.emplace_back(typeComponent( SemanticDebugTypeComponent::Role::Key, std::nullopt, diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index 50d822c05be2..cf826783df81 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -259,7 +259,6 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) type.components.emplace_back(std::move(component)); type.definitionName = "Container"; type.definitionLocation = SourceLocation{4, 20, std::make_shared("input.sol")}; - type.dataLocation = "storage"; type.dynamic = false; SemanticDebugPointer templateReference; From 6fd875c0fc3afd22303619adbcd3759cad3ca21b Mon Sep 17 00:00:00 2001 From: djole Date: Thu, 6 Aug 2026 17:51:38 +0200 Subject: [PATCH 45/47] docs: Cite where the type reference ID comes from in the ethdebug spec --- docs/internals/ethdebug_internal_metadata.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index 30f434c0c565..ede08f5caae7 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -124,6 +124,7 @@ The public format this feeds is the `ethdebug/format specification `_ - producer-defined by the spec, and this producer uses its native type identifier as the value. * - ``ethdebugType`` - optional ``SemanticDebugType`` - ETHDebug-oriented static type descriptor. From 1bf1f271498a41328d08d281c2f7e4100c4eed36 Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 7 Aug 2026 13:49:34 +0200 Subject: [PATCH 46/47] ethdebug: Serialize sidecar types as the ethdebug type schema --- docs/internals/ethdebug_internal_metadata.rst | 10 +- liblangutil/SemanticDebugData.h | 2 - .../SemanticDebugDataSerialization.cpp | 462 ++++++++++++++---- .../codegen/ir/SemanticDebugDataBuilder.cpp | 2 - test/liblangutil/DebugData.cpp | 1 - 5 files changed, 368 insertions(+), 109 deletions(-) diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst index ede08f5caae7..758944b7e820 100644 --- a/docs/internals/ethdebug_internal_metadata.rst +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -128,7 +128,8 @@ The public format this feeds is the `ethdebug/format specification `_, which does not name every Solidity type directly. Three cases are worth stating explicitly. @@ -472,8 +470,10 @@ The complete table, including recursive types, pointer expressions, source names Generated Yul object names are not assumed to preserve it. * - ``types`` - optional object - - Type descriptors shared across variables, keyed by ``typeId`` and written once each. + - Type documents in the shape of the `ethdebug type schema `_, keyed by ``typeId`` and written once each. + The table is normalized: a composed type with an ID of its own is a table entry, and its containers refer to it with the spec's `type reference `_ form ``{"id": ...}`` - which is also how a recursive type closes its cycle. A variable whose descriptor is here carries only the ID; a descriptor without an ID stays inline on its variable. + Three deviations from the public schema, all deliberate: definition locations reference sources by name, because source indices are per-invocation and the sidecar must survive into another one; the compiler's ``slice`` kind and the address-width ``size`` appear under the versioned-extension rule; and a type the producer cannot name uses the schema's own class-only form. * - ``entries`` - array - Objects containing ``astId``, an optional ``instance``, and ``data``, where ``data`` is a ``SemanticDebugData`` value. diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h index 01908aa4bd64..0b592633b8af 100644 --- a/liblangutil/SemanticDebugData.h +++ b/liblangutil/SemanticDebugData.h @@ -360,8 +360,6 @@ struct SemanticDebugType std::optional definitionName; std::optional definitionLocation; - // Internal annotation with no direct ethdebug type schema equivalent. - std::optional dynamic; }; /// Internal representation of an ethdebug/format/pointer: a single region of EVM diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp index 229fd2bb3d04..b8d5ad61aea7 100644 --- a/liblangutil/SemanticDebugDataSerialization.cpp +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -20,6 +20,7 @@ #include +#include #include #include #include @@ -167,18 +168,6 @@ constexpr std::pair expressionKindNames[]{ {ExpressionKind::Resize, "resize"} }; -using TypeRole = SemanticDebugTypeComponent::Role; -constexpr std::pair typeRoleNames[]{ - {TypeRole::Element, "element"}, - {TypeRole::Key, "key"}, - {TypeRole::Value, "value"}, - {TypeRole::Member, "member"}, - {TypeRole::Parameter, "parameter"}, - {TypeRole::Return, "return"}, - {TypeRole::Underlying, "underlying"}, - {TypeRole::Contract, "contract"} -}; - using TypeClass = SemanticDebugType::Class; constexpr std::pair typeClassNames[]{ {TypeClass::Elementary, "elementary"}, @@ -270,16 +259,6 @@ SemanticDebugPointerExpression::Kind expressionKindFromString(std::string const& return enumFromString(_kind, expressionKindNames, _path); } -std::string typeRoleToString(SemanticDebugTypeComponent::Role _role) -{ - return enumToString(_role, typeRoleNames); -} - -SemanticDebugTypeComponent::Role typeRoleFromString(std::string const& _role, std::string const& _path) -{ - return enumFromString(_role, typeRoleNames, _path); -} - std::string typeClassToString(SemanticDebugType::Class _class) { return enumToString(_class, typeClassNames); @@ -290,11 +269,6 @@ SemanticDebugType::Class typeClassFromString(std::string const& _class, std::str return enumFromString(_class, typeClassNames, _path); } -std::string typeKindToString(SemanticDebugType::Kind _kind) -{ - return enumToString(_kind, typeKindNames); -} - SemanticDebugType::Kind typeKindFromString(std::string const& _kind, std::string const& _path) { return enumFromString(_kind, typeKindNames, _path); @@ -382,91 +356,334 @@ SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string return result; } -Json typeComponentToJson(SemanticDebugTypeComponent const& _component) +/// The sidecar serializes types in the shape of the public ethdebug type +/// schema: what the table holds *is* the type schema, with references by ID +/// (`{"id": ...}`) between entries, exactly as the spec's type/reference +/// mechanism defines. Two stated deviations, documented in the internals doc: +/// definition locations reference sources by name, since source indices are +/// per-invocation, and the compiler's own `slice` kind and address-width +/// `size` appear under the versioned-extension rule. +Json typeToJson(SemanticDebugType const& _type); + +/// The wrapper form: `{"name"?, "type": }`. A component +/// with a reference ID is emitted as a reference - its definition lives in the +/// shared table - and one without an ID is inlined, having nowhere else to +/// live. +Json typeWrapperToJson(SemanticDebugTypeComponent const& _component) +{ + Json wrapper = Json::object(); + if (_component.name) + wrapper["name"] = *_component.name; + if (_component.referenceID) + wrapper["type"] = Json{{"id", *_component.referenceID}}; + else if (_component.type) + wrapper["type"] = typeToJson(*_component.type); + return wrapper; +} + +Json typeWrapperArrayToJson( + SemanticDebugType const& _type, SemanticDebugTypeComponent::Role _role) +{ + Json wrappers = Json::array(); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + wrappers.emplace_back(typeWrapperToJson(component)); + return wrappers; +} + +std::optional typeSingleWrapperToJson( + SemanticDebugType const& _type, SemanticDebugTypeComponent::Role _role) { - Json result{{"role", typeRoleToString(_component.role)}}; - setOptional(result, "name", _component.name); - setOptional(result, "referenceId", _component.referenceID); - if (_component.type) - result["type"] = typeToJson(*_component.type); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + return typeWrapperToJson(component); + return std::nullopt; +} + +Json typeToJson(SemanticDebugType const& _type) +{ + using Kind = SemanticDebugType::Kind; + using Role = SemanticDebugTypeComponent::Role; + + Json result = Json::object(); + auto attachDefinition = [&]() { + Json definition = Json::object(); + if (_type.definitionName) + definition["name"] = *_type.definitionName; + if (_type.definitionLocation) + definition["location"] = sourceLocationToJson(*_type.definitionLocation); + if (!definition.empty()) + result["definition"] = std::move(definition); + }; + auto attachSingle = [&](Role _role) { + if (std::optional wrapper = typeSingleWrapperToJson(_type, _role)) + result["contains"] = std::move(*wrapper); + }; + + switch (_type.kind) + { + case Kind::Uint: + case Kind::Int: + result["kind"] = _type.kind == Kind::Uint ? "uint" : "int"; + setOptional(result, "bits", _type.bits); + break; + case Kind::Ufixed: + case Kind::Fixed: + result["kind"] = _type.kind == Kind::Ufixed ? "ufixed" : "fixed"; + setOptional(result, "bits", _type.bits); + setOptional(result, "places", _type.places); + break; + case Kind::Bool: + result["kind"] = "bool"; + break; + case Kind::Bytes: + result["kind"] = "bytes"; + setOptional(result, "size", _type.bytes); + break; + case Kind::String: + result["kind"] = "string"; + break; + case Kind::Address: + result["kind"] = "address"; + setOptional(result, "payable", _type.payable); + setOptional(result, "size", _type.bytes); + break; + case Kind::Contract: + result["kind"] = "contract"; + setOptional(result, "payable", _type.payable); + if (_type.isLibrary && *_type.isLibrary) + result["library"] = true; + else if (_type.isInterface && *_type.isInterface) + result["interface"] = true; + setOptional(result, "size", _type.bytes); + attachDefinition(); + break; + case Kind::Enum: + { + result["kind"] = "enum"; + Json values = Json::array(); + for (std::string const& value: _type.enumValues) + values.emplace_back(value); + result["values"] = std::move(values); + attachDefinition(); + break; + } + case Kind::Alias: + result["kind"] = "alias"; + attachSingle(Role::Underlying); + attachDefinition(); + break; + case Kind::Tuple: + result["kind"] = "tuple"; + result["contains"] = typeWrapperArrayToJson(_type, Role::Member); + break; + case Kind::Array: + result["kind"] = "array"; + attachSingle(Role::Element); + setOptional(result, "count", _type.count); + break; + case Kind::Slice: + result["kind"] = "slice"; + attachSingle(Role::Element); + break; + case Kind::Mapping: + { + Json contains = Json::object(); + if (std::optional key = typeSingleWrapperToJson(_type, Role::Key)) + contains["key"] = std::move(*key); + if (std::optional value = typeSingleWrapperToJson(_type, Role::Value)) + contains["value"] = std::move(*value); + result["kind"] = "mapping"; + result["contains"] = std::move(contains); + break; + } + case Kind::Struct: + result["kind"] = "struct"; + result["contains"] = typeWrapperArrayToJson(_type, Role::Member); + attachDefinition(); + break; + case Kind::Function: + { + result["kind"] = "function"; + if (_type.externalFunction) + result[*_type.externalFunction ? "external" : "internal"] = true; + Json contains{{"parameters", + Json{{"type", Json{{"kind", "tuple"}, {"contains", typeWrapperArrayToJson(_type, Role::Parameter)}}}}}}; + bool hasReturns = false; + for (SemanticDebugTypeComponent const& component: _type.components) + hasReturns = hasReturns || component.role == Role::Return; + if (hasReturns) + contains["returns"] + = Json{{"type", Json{{"kind", "tuple"}, {"contains", typeWrapperArrayToJson(_type, Role::Return)}}}}; + result["contains"] = std::move(contains); + attachDefinition(); + break; + } + case Kind::Unknown: + // The schema's own form for a type it cannot name: the class alone. + result["class"] = typeClassToString(_type.typeClass); + break; + } return result; } -SemanticDebugTypeComponent typeComponentFromJson(Json const& _json, std::string const& _path) +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path); + +SemanticDebugTypeComponent typeWrapperFromJson( + Json const& _json, SemanticDebugTypeComponent::Role _role, std::string const& _path) { SemanticDebugTypeComponent result; - result.role = typeRoleFromString(requiredString(_json, "role", _path), _path + ".role"); + result.role = _role; result.name = optionalString(_json, "name", _path); - result.referenceID = optionalString(_json, "referenceId", _path); if (_json.contains("type")) - result.type = std::make_shared(typeFromJson(_json.at("type"), _path + ".type")); + { + Json const& type = _json.at("type"); + requireObject(type, _path + ".type"); + // `{"id": ...}` is the spec's type reference; anything else is inline. + if (type.contains("id") && !type.contains("kind") && !type.contains("class")) + { + Json const& id = type.at("id"); + require(id.is_string(), _path + ".type.id must be a string."); + result.referenceID = id.get(); + } + else + result.type = std::make_shared(typeFromJson(type, _path + ".type")); + } return result; } -Json typeToJson(SemanticDebugType const& _type) +void typeWrappersFromJson( + SemanticDebugType& _result, + Json const& _wrappers, + SemanticDebugTypeComponent::Role _role, + std::string const& _path) { - Json result{{"class", typeClassToString(_type.typeClass)}, {"kind", typeKindToString(_type.kind)}}; - setOptional(result, "bits", _type.bits); - setOptional(result, "places", _type.places); - setOptional(result, "bytes", _type.bytes); - setOptional(result, "payable", _type.payable); - setOptional(result, "isLibrary", _type.isLibrary); - setOptional(result, "isInterface", _type.isInterface); - if (!_type.enumValues.empty()) - result["enumValues"] = _type.enumValues; - setOptional(result, "count", _type.count); - setOptional(result, "externalFunction", _type.externalFunction); - if (!_type.components.empty()) - { - result["components"] = Json::array(); - for (auto const& component: _type.components) - result["components"].emplace_back(typeComponentToJson(component)); - } - setOptional(result, "definitionName", _type.definitionName); - if (_type.definitionLocation) - result["definitionLocation"] = sourceLocationToJson(*_type.definitionLocation); - setOptional(result, "dynamic", _type.dynamic); - return result; + requireArray(_wrappers, _path); + for (size_t index = 0; index < _wrappers.size(); ++index) + _result.components.emplace_back( + typeWrapperFromJson(_wrappers.at(index), _role, _path + "[" + std::to_string(index) + "]")); } SemanticDebugType typeFromJson(Json const& _json, std::string const& _path) { + using Kind = SemanticDebugType::Kind; + using Role = SemanticDebugTypeComponent::Role; + SemanticDebugType result; - result.typeClass = typeClassFromString(requiredString(_json, "class", _path), _path + ".class"); + if (!_json.contains("kind")) + { + // The class-only form: a type the producer could not name. + result.kind = Kind::Unknown; + result.typeClass + = typeClassFromString(requiredString(_json, "class", _path), _path + ".class"); + return result; + } + result.kind = typeKindFromString(requiredString(_json, "kind", _path), _path + ".kind"); + switch (result.kind) + { + case Kind::Uint: + case Kind::Int: + case Kind::Ufixed: + case Kind::Fixed: + case Kind::Bool: + case Kind::Bytes: + case Kind::String: + case Kind::Address: + case Kind::Contract: + case Kind::Enum: + result.typeClass = SemanticDebugType::Class::Elementary; + break; + default: + result.typeClass = SemanticDebugType::Class::Complex; + break; + } + result.bits = optionalValue(_json, "bits", _path); result.places = optionalValue(_json, "places", _path); - result.bytes = optionalValue(_json, "bytes", _path); + result.bytes = optionalValue(_json, "size", _path); result.payable = optionalValue(_json, "payable", _path); - result.isLibrary = optionalValue(_json, "isLibrary", _path); - result.isInterface = optionalValue(_json, "isInterface", _path); - if (_json.contains("enumValues")) + if (optionalValue(_json, "library", _path).value_or(false)) + result.isLibrary = true; + if (optionalValue(_json, "interface", _path).value_or(false)) + result.isInterface = true; + result.count = optionalString(_json, "count", _path); + if (_json.contains("external")) + result.externalFunction = true; + else if (_json.contains("internal")) + result.externalFunction = false; + + if (_json.contains("values")) { - Json const& enumValues = _json.at("enumValues"); - requireArray(enumValues, _path + ".enumValues"); - for (size_t index = 0; index < enumValues.size(); ++index) + Json const& values = _json.at("values"); + requireArray(values, _path + ".values"); + for (size_t index = 0; index < values.size(); ++index) { require( - enumValues.at(index).is_string(), - _path + ".enumValues[" + std::to_string(index) + "] must be a string."); - result.enumValues.emplace_back(enumValues.at(index).get()); + values.at(index).is_string(), + _path + ".values[" + std::to_string(index) + "] must be a string."); + result.enumValues.emplace_back(values.at(index).get()); } } - result.count = optionalString(_json, "count", _path); - result.externalFunction = optionalValue(_json, "externalFunction", _path); - if (_json.contains("components")) + + if (_json.contains("definition")) { - Json const& components = _json.at("components"); - requireArray(components, _path + ".components"); - for (size_t index = 0; index < components.size(); ++index) - result.components.emplace_back( - typeComponentFromJson(components.at(index), _path + ".components[" + std::to_string(index) + "]")); + Json const& definition = _json.at("definition"); + requireObject(definition, _path + ".definition"); + result.definitionName = optionalString(definition, "name", _path + ".definition"); + if (definition.contains("location")) + result.definitionLocation + = sourceLocationFromJson(definition.at("location"), _path + ".definition.location"); + } + + if (_json.contains("contains")) + { + Json const& contains = _json.at("contains"); + std::string const path = _path + ".contains"; + switch (result.kind) + { + case Kind::Alias: + result.components.emplace_back(typeWrapperFromJson(contains, Role::Underlying, path)); + break; + case Kind::Array: + case Kind::Slice: + result.components.emplace_back(typeWrapperFromJson(contains, Role::Element, path)); + break; + case Kind::Tuple: + case Kind::Struct: + typeWrappersFromJson(result, contains, Role::Member, path); + break; + case Kind::Mapping: + requireObject(contains, path); + if (contains.contains("key")) + result.components.emplace_back(typeWrapperFromJson(contains.at("key"), Role::Key, path + ".key")); + if (contains.contains("value")) + result.components.emplace_back( + typeWrapperFromJson(contains.at("value"), Role::Value, path + ".value")); + break; + case Kind::Function: + { + requireObject(contains, path); + auto wrappedTuple = [&](std::string const& _member, Role _role) { + if (!contains.contains(_member)) + return; + Json const& wrapper = contains.at(_member); + requireObject(wrapper, path + "." + _member); + Json const& tuple = requiredMember(wrapper, "type", path + "." + _member); + typeWrappersFromJson( + result, + requiredMember(tuple, "contains", path + "." + _member + ".type"), + _role, + path + "." + _member + ".type.contains"); + }; + wrappedTuple("parameters", Role::Parameter); + wrappedTuple("returns", Role::Return); + break; + } + default: + break; + } } - result.definitionName = optionalString(_json, "definitionName", _path); - if (_json.contains("definitionLocation")) - result.definitionLocation - = sourceLocationFromJson(_json.at("definitionLocation"), _path + ".definitionLocation"); - result.dynamic = optionalValue(_json, "dynamic", _path); return result; } @@ -733,23 +950,41 @@ Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) {"entries", Json::array()}}; setOptional(result, "contractName", _table.contractName()); - // Types are written once, keyed by type ID, rather than inline in every - // variable that has one: the distinct types are few and the descriptors - // are not. Two variables with one type ID have one type by construction - - // the ID is the compiler's type identifier - so first-wins is not a choice - // between descriptors. - Json sharedTypes = Json::object(); - std::set tabledTypeIDs; + // Types are written once, keyed by type ID, and the table is normalized: + // a composed type with an ID of its own becomes a table entry, and the + // containing type references it as `{"id": ...}` - the spec's type + // reference form. Two variables with one type ID have one type by + // construction, the ID being the compiler's type identifier. The table is + // sorted by ID so the writer's output is deterministic. + std::map orderedTypes; + std::function registerType + = [&](std::string const& _id, SemanticDebugType const& _type) { + if (orderedTypes.count(_id)) + return; + // Present before descending, so a recursive type terminates. + orderedTypes.emplace(_id, Json::object()); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerType(*component.referenceID, *component.type); + orderedTypes[_id] = typeToJson(_type); + }; for (auto const& [key, data]: _table.entries()) if (data) for (auto const& variable: data->variableDefinitions) - if (variable.typeID && variable.ethdebugType && !tabledTypeIDs.count(*variable.typeID)) - { - sharedTypes[*variable.typeID] = typeToJson(*variable.ethdebugType); - tabledTypeIDs.insert(*variable.typeID); - } - if (!tabledTypeIDs.empty()) + if (variable.typeID && variable.ethdebugType) + registerType(*variable.typeID, *variable.ethdebugType); + + std::set tabledTypeIDs; + if (!orderedTypes.empty()) + { + Json sharedTypes = Json::object(); + for (auto const& [id, descriptor]: orderedTypes) + { + sharedTypes[id] = descriptor; + tabledTypeIDs.insert(id); + } result["types"] = std::move(sharedTypes); + } for (auto const& [key, data]: _table.entries()) { @@ -778,13 +1013,42 @@ SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) Json const& entries = requiredMember(_json, "entries", "semantic debug data"); requireArray(entries, "semantic debug data.entries"); - std::map sharedTypes; + std::map protoTypes; if (_json.contains("types")) { Json const& types = _json.at("types"); requireObject(types, "semantic debug data.types"); for (auto const& [typeID, descriptor]: types.items()) - sharedTypes.emplace(typeID, typeFromJson(descriptor, "semantic debug data.types." + typeID)); + protoTypes.emplace(typeID, typeFromJson(descriptor, "semantic debug data.types." + typeID)); + } + + // Re-attach referenced types, leaving a reference alone exactly where it + // closes a cycle - which reconstructs the shape the builder produced and + // keeps the reference IDs that public emission registers types by. + std::function&)> expand + = [&](SemanticDebugType const& _proto, std::set& _path) -> SemanticDebugType { + SemanticDebugType expanded = _proto; + for (SemanticDebugTypeComponent& component: expanded.components) + { + if (component.type) + component.type + = std::make_shared(expand(*component.type, _path)); + else if (component.referenceID && !_path.count(*component.referenceID)) + if (auto const it = protoTypes.find(*component.referenceID); it != protoTypes.end()) + { + _path.insert(*component.referenceID); + component.type + = std::make_shared(expand(it->second, _path)); + _path.erase(*component.referenceID); + } + } + return expanded; + }; + std::map sharedTypes; + for (auto const& [typeID, proto]: protoTypes) + { + std::set path{typeID}; + sharedTypes.emplace(typeID, expand(proto, path)); } SemanticDebugDataTable result; diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp index 787de47e1586..95d84b5a284a 100644 --- a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -122,7 +122,6 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO case Type::Category::Array: { auto const& arrayType = dynamic_cast(_type); - result.dynamic = arrayType.isDynamicallySized(); if (arrayType.isByteArrayOrString()) { result.typeClass = SemanticDebugType::Class::Elementary; @@ -264,7 +263,6 @@ SemanticDebugType semanticType(Type const& _type, std::set& _typesO auto const& sliceType = dynamic_cast(_type); result.typeClass = SemanticDebugType::Class::Complex; result.kind = SemanticDebugType::Kind::Slice; - result.dynamic = true; result.components.emplace_back(typeComponent( SemanticDebugTypeComponent::Role::Element, std::nullopt, diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp index cf826783df81..5fdf324aaf0a 100644 --- a/test/liblangutil/DebugData.cpp +++ b/test/liblangutil/DebugData.cpp @@ -259,7 +259,6 @@ BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) type.components.emplace_back(std::move(component)); type.definitionName = "Container"; type.definitionLocation = SourceLocation{4, 20, std::make_shared("input.sol")}; - type.dynamic = false; SemanticDebugPointer templateReference; templateReference.pointerClass = SemanticDebugPointer::Class::TemplateReference; From 7ab444ed0435f07705b4e09a7f6ff0af7dd839a2 Mon Sep 17 00:00:00 2001 From: djole Date: Fri, 7 Aug 2026 16:41:49 +0200 Subject: [PATCH 47/47] Fix coding style in the type table lambdas --- .../SemanticDebugDataSerialization.cpp | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp index b8d5ad61aea7..4ee06f506714 100644 --- a/liblangutil/SemanticDebugDataSerialization.cpp +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -957,17 +957,17 @@ Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) // construction, the ID being the compiler's type identifier. The table is // sorted by ID so the writer's output is deterministic. std::map orderedTypes; - std::function registerType - = [&](std::string const& _id, SemanticDebugType const& _type) { - if (orderedTypes.count(_id)) - return; - // Present before descending, so a recursive type terminates. - orderedTypes.emplace(_id, Json::object()); - for (SemanticDebugTypeComponent const& component: _type.components) - if (component.referenceID && component.type) - registerType(*component.referenceID, *component.type); - orderedTypes[_id] = typeToJson(_type); - }; + std::function registerType; + registerType = [&](std::string const& _id, SemanticDebugType const& _type) { + if (orderedTypes.count(_id)) + return; + // Present before descending, so a recursive type terminates. + orderedTypes.emplace(_id, Json::object()); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerType(*component.referenceID, *component.type); + orderedTypes[_id] = typeToJson(_type); + }; for (auto const& [key, data]: _table.entries()) if (data) for (auto const& variable: data->variableDefinitions) @@ -1025,25 +1025,23 @@ SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) // Re-attach referenced types, leaving a reference alone exactly where it // closes a cycle - which reconstructs the shape the builder produced and // keeps the reference IDs that public emission registers types by. - std::function&)> expand - = [&](SemanticDebugType const& _proto, std::set& _path) -> SemanticDebugType { - SemanticDebugType expanded = _proto; - for (SemanticDebugTypeComponent& component: expanded.components) - { - if (component.type) - component.type - = std::make_shared(expand(*component.type, _path)); - else if (component.referenceID && !_path.count(*component.referenceID)) - if (auto const it = protoTypes.find(*component.referenceID); it != protoTypes.end()) - { - _path.insert(*component.referenceID); - component.type - = std::make_shared(expand(it->second, _path)); - _path.erase(*component.referenceID); - } - } - return expanded; - }; + std::function&)> expand; + expand = [&](SemanticDebugType const& _proto, std::set& _path) -> SemanticDebugType { + SemanticDebugType expanded = _proto; + for (SemanticDebugTypeComponent& component: expanded.components) + { + if (component.type) + component.type = std::make_shared(expand(*component.type, _path)); + else if (component.referenceID && !_path.count(*component.referenceID)) + if (auto const it = protoTypes.find(*component.referenceID); it != protoTypes.end()) + { + _path.insert(*component.referenceID); + component.type = std::make_shared(expand(it->second, _path)); + _path.erase(*component.referenceID); + } + } + return expanded; + }; std::map sharedTypes; for (auto const& [typeID, proto]: protoTypes) {