diff --git a/Changelog.md b/Changelog.md index d05d6fcd3717..8e2fed7d17c4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,11 +8,14 @@ Language Features: Compiler Features: * Commandline Interface: Remove support for the experimental Language Server Protocol (LSP) mode. +* Commandline Interface: Selecting an ethdebug output no longer implicitly enables the `ethdebug` and `ast-id` components of `--debug-info`; they have to be selected explicitly. * EVM-ASM Optimizer: Improve performance of block deduplicator. * General: Improve performance throughout the compiler using Boost's flat versions of unordered set and map. * General: Remove support for the experimental Generic Solidity prototype (`pragma experimental solidity`). * SMTChecker: Emit a deprecation warning for the BMC engine. * SMTChecker: Support `block.slotnum`. +* Standard JSON Interface: Selecting an ethdebug output no longer implicitly enables the `ethdebug` and `ast-id` components of `settings.debug.debugInfo`; they have to be selected explicitly. +* ethdebug: Populate `ethdebug/format/info/resources` with the type documents and the pointer templates of the state variables in storage and transient storage, and publish the state variables in a program-level context (experimental). * Standard JSON Interface: Fix the entire output being replaced by a `JSONError` ("Error writing output JSON.") when an error message quotes a long source line and truncating it splits a multi-byte character. Bugfixes: diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index e7a3ccda380d..fbe84c254e7b 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -392,7 +392,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`; selecting + // `ethdebug` without `ast-id` is an error. Requesting a program ethdebug output + // does not change this selection, which must then contain `ethdebug`. // - `*`: Wildcard value that can be used to request all non-experimental components. "debugInfo": ["location", "snippet", "ast-id", "ethdebug"] }, @@ -770,7 +772,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``, | +| | | | ``--debug-info ast-id,ethdebug`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | | | no | ``--yul-cfg-json`` | | SSA CFG + ``ssa-cfg`` +------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/libevmasm/Ethdebug.cpp b/libevmasm/Ethdebug.cpp index 0b341d7e8df2..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,17 +186,22 @@ 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) }; } -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..3af9c463e6b8 100644 --- a/libevmasm/Ethdebug.h +++ b/libevmasm/Ethdebug.h @@ -21,8 +21,11 @@ #include #include +#include #include +#include + namespace solidity::evmasm::ethdebug { @@ -35,10 +38,21 @@ 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(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/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/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 054d01d91881..f590d95b2aa9 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -17,6 +17,12 @@ set(sources Scanner.cpp Scanner.h CharStreamProvider.h + SemanticDebugData.h + SemanticDebugDataAnalysis.cpp + SemanticDebugDataAnalysis.h + SemanticDebugDataSerialization.cpp + SemanticDebugDataSerialization.h + SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h new file mode 100644 index 000000000000..22457a830859 --- /dev/null +++ b/liblangutil/SemanticDebugData.h @@ -0,0 +1,542 @@ +/* + 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 +/** + * The compiler structures of the semantic debug info specified in + * docs/internals/ethdebug_internal_debug_info.rst: scope records with their + * variables, phases and ethdebug pointers. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +// The SemanticDebugData prefix names the compiler structures of the semantic +// debug info specified in docs/internals/ethdebug_internal_debug_info.rst: +// the scope records attached to Yul nodes, their variables and pointers, and +// the side table that carries them across a textual Yul boundary. + +/// Whether a variable's value can be recovered at a program point. The region +/// a materialized value lives in is not duplicated here: the variable's pointer +/// already records it. +enum class SemanticDebugVariablePhase +{ + /// The value has a concrete representation described by the variable's pointer. + Materialized, + /// The value has no region because the program recomputes it where it is + /// used. No recovery recipe is carried; the phase only records that + /// recomputation would be possible in principle, unlike OptimizedOut. + Computed, + /// No recoverable representation is available at this program point. + OptimizedOut +}; + +/// Source range in the shape of the ethdebug source range schema. The source +/// is identified by the same ID the accompanying ethdebug.compilation record +/// uses; a path is never used as an ID, and the Yul stage transports the ID +/// unchanged without interpreting it. +struct SemanticDebugSourceRange +{ + uint64_t sourceID = 0; + int offset = 0; + int length = 0; + + /// The range of @a _location in the source with ID @a _sourceID, or + /// nothing when the location is empty or the source has no ID. + static std::optional fromLocation( + SourceLocation const& _location, + std::optional _sourceID + ) + { + if (!_sourceID || _location.start < 0 || _location.end < _location.start) + return std::nullopt; + return SemanticDebugSourceRange{*_sourceID, _location.start, _location.end - _location.start}; + } + + bool operator==(SemanticDebugSourceRange const&) const = default; +}; + +/// Node in an ethdebug pointer expression tree. Expressions evaluate to +/// unsigned values and may reference named regions and variables bound by +/// 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, emitted as written. A variable that + /// nothing binds is a dependency on the surrounding Yul code, like a + /// Yul local, and is treated as one by the survival check. + Variable, + /// A generated Yul local standing in for a stack depth that is not + /// known until the Yul-to-EVM transform has run. + /// + /// Nothing binds the name, so a consumer cannot resolve it, and emitting + /// it would put a Yul identifier where the public format expects a + /// slot. It is internal only, serialized as `{"$$yulLocal": ...}` with + /// the compiler's `$$` prefix that cannot clash with the schema's own + /// `$` forms, and must be resolved or removed before public emission, + /// which publishes no pointer still carrying it. + YulLocal, + /// `{".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 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; + 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 + ) + { + // 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); + result.operands.emplace_back(std::move(_operand)); + return result; + } +}; + + +/// Internal representation of an ethdebug 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. + /// The template is defined either by an enclosing Templates pointer or + /// by the pointer template table of the sidecar. + TemplateReference, + /// A pointer with locally defined templates: @a templates are available + /// by name to template references inside @a scopeTarget. + Templates, + Unknown + }; + + enum class Location + { + Stack, + Storage, + Transient, + Memory, + Calldata, + Returndata, + Code, + Unknown + }; + + Class pointerClass = Class::Unknown; + + /// Template parameters (the pointer template's `expect` list) that must be + /// bound externally before this pointer can be evaluated, e.g. mapping + /// keys. Meaningful on a root pointer and on template definitions, in the + /// sidecar's pointer table as well as in local `templates` blocks. + std::vector expectedParameters; + + // Class::Region + std::optional location; + std::optional name; + /// 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 and Class::Templates share the target: the `in` pointer. + std::vector> definitions; + std::shared_ptr scopeTarget; + + // Class::TemplateReference + std::optional templateName; + std::vector> yields; + + // Class::Templates + std::vector>> templates; + + 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; + } + + static SemanticDebugPointer templateReference( + std::string _templateName, + std::vector> _yields = {} + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::TemplateReference; + result.templateName = std::move(_templateName); + result.yields = std::move(_yields); + return result; + } + + static SemanticDebugPointer withTemplates( + std::vector> _templates, + SemanticDebugPointer _target + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Templates; + for (auto& [templateName, definition]: _templates) + result.templates.emplace_back( + std::move(templateName), + std::make_shared(std::move(definition)) + ); + result.scopeTarget = std::make_shared(std::move(_target)); + return result; + } +}; + +/// One binding introduced by a scope record: the source declaration, its type +/// and, when the value is materialized, the pointer that resolves it. +struct SemanticDebugVariable +{ + /// 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 declarationSourceRange; + std::optional typeID; + /// Whether the value is materialized, computable or unavailable. A + /// materialized variable has a pointer; the other phases do not. + SemanticDebugVariablePhase phase = SemanticDebugVariablePhase::OptimizedOut; + std::optional pointer; +}; + +/// Changes a variable's phase or pointer 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 phase and pointer 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 a Drop is an update to +/// OptimizedOut. +struct SemanticDebugVariableUpdate +{ + /// Declaration AST ID of the variable being updated. + int64_t variableASTID = 0; + /// Replacement phase valid from the carrying node onward. Omitted when + /// only the pointer changes. + std::optional phase; + /// Replacement pointer for a materialized value. A change to Computed or + /// OptimizedOut clears the previous pointer. + std::optional pointer; +}; + +/// The semantic scope described by an attached Yul node. On a node that +/// introduces a source scope it defines the scope's variables; on a +/// finer-grained node it carries only variable updates. A variable appears in +/// exactly one scope record; nested records do not repeat enclosing bindings. +struct SemanticDebugScope +{ + using ConstPtr = std::shared_ptr; + + /// Bindings introduced by the scope, in source order. + std::vector variableDefinitions = {}; + /// Phase or pointer changes taking effect at the node this payload is + /// attached to. Scope records define variables; statement-level records + /// update them. + std::vector variableUpdates = {}; +}; + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataAnalysis.cpp b/liblangutil/SemanticDebugDataAnalysis.cpp new file mode 100644 index 000000000000..806a6086fbf4 --- /dev/null +++ b/liblangutil/SemanticDebugDataAnalysis.cpp @@ -0,0 +1,224 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +/// Collects the free names of pointers, resolving template references. The +/// free names of a template are computed once per template, with the +/// template's own parameters bound, and cached; a template that references +/// itself, directly or through other templates, contributes nothing to its +/// own free names. +class FreeNameCollector +{ +public: + explicit FreeNameCollector(SemanticDebugDataTable const& _table): m_table(_table) {} + + std::set collect(SemanticDebugPointer const& _pointer) + { + std::set free; + std::set bound; + LocalTemplates localTemplates; + visit(_pointer, bound, localTemplates, free); + return free; + } + +private: + using LocalTemplates = std::map>; + + void visit( + SemanticDebugPointerExpression const& _expression, + std::set const& _bound, + std::set& _free + ) + { + using Kind = SemanticDebugPointerExpression::Kind; + // A Yul local is a dependency on the current Yul code by definition; + // nothing binds it. An unbound variable is a dependency as well. + if (_expression.kind == Kind::YulLocal && _expression.value) + _free.insert(*_expression.value); + else if (_expression.kind == Kind::Variable && _expression.value) + { + if (!_bound.count(*_expression.value)) + _free.insert(*_expression.value); + } + else + // Lookups and reads name regions, which live in a separate namespace. + for (SemanticDebugPointerExpression const& operand: _expression.operands) + visit(operand, _bound, _free); + } + + void visit( + SemanticDebugPointer const& _pointer, + std::set& _bound, + LocalTemplates& _localTemplates, + std::set& _free + ) + { + ScopedNames parameters{_bound, _pointer.expectedParameters}; + + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value()) + visit(**expression, _bound, _free); + + // Scope definitions are ordered: each may reference the earlier ones. + std::vector definedNames; + for (auto const& [definedName, definedValue]: _pointer.definitions) + { + visit(definedValue, _bound, _free); + if (_bound.insert(definedName).second) + definedNames.emplace_back(definedName); + } + + for (SemanticDebugPointer const& member: _pointer.group) + visit(member, _bound, _localTemplates, _free); + + if (_pointer.listElement) + { + ScopedNames index{_bound, _pointer.indexName ? std::vector{*_pointer.indexName} : std::vector{}}; + visit(*_pointer.listElement, _bound, _localTemplates, _free); + } + + // Locally defined templates are visible to references inside the target. + std::vector addedTemplates; + if (_pointer.pointerClass == SemanticDebugPointer::Class::Templates) + for (auto const& [templateName, definition]: _pointer.templates) + if (definition && _localTemplates.emplace(templateName, definition).second) + addedTemplates.emplace_back(templateName); + + if (_pointer.pointerClass == SemanticDebugPointer::Class::TemplateReference && _pointer.templateName) + for (std::string const& name: freeNamesOfTemplate(*_pointer.templateName, _localTemplates)) + if (!_bound.count(name)) + _free.insert(name); + + for (auto const* subPointer: {&_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer) + visit(**subPointer, _bound, _localTemplates, _free); + + for (std::string const& templateName: addedTemplates) + _localTemplates.erase(templateName); + for (std::string const& definedName: definedNames) + _bound.erase(definedName); + } + + /// The free names of a template, with its own parameters bound. Local + /// templates shadow the table's; results for the table's templates are + /// cached, results for local ones are recomputed per reference. + std::set freeNamesOfTemplate(std::string const& _name, LocalTemplates& _localTemplates) + { + if (m_resolving.count(_name)) + return {}; + + SemanticDebugPointer const* definition = nullptr; + bool fromTable = false; + if (auto const it = _localTemplates.find(_name); it != _localTemplates.end()) + definition = it->second.get(); + else if ((definition = m_table.findPointerTemplate(_name))) + { + fromTable = true; + if (auto const cached = m_tableTemplateFreeNames.find(_name); cached != m_tableTemplateFreeNames.end()) + return cached->second; + } + if (!definition) + return {}; + + m_resolving.insert(_name); + std::set free; + std::set bound; + visit(*definition, bound, _localTemplates, free); + m_resolving.erase(_name); + + if (fromTable) + m_tableTemplateFreeNames.emplace(_name, free); + return free; + } + + /// Binds names for the lifetime of the object, restoring the previous + /// state afterwards. + class ScopedNames + { + public: + ScopedNames(std::set& _bound, std::vector const& _names): m_bound(_bound) + { + for (std::string const& name: _names) + if (m_bound.insert(name).second) + m_added.emplace_back(name); + } + ~ScopedNames() + { + for (std::string const& name: m_added) + m_bound.erase(name); + } + + private: + std::set& m_bound; + std::vector m_added; + }; + + SemanticDebugDataTable const& m_table; + std::set m_resolving; + std::map> m_tableTemplateFreeNames; +}; + +bool hasInternalExpression(SemanticDebugPointerExpression const& _expression) +{ + if (_expression.kind == SemanticDebugPointerExpression::Kind::YulLocal) + return true; + for (SemanticDebugPointerExpression const& operand: _expression.operands) + if (hasInternalExpression(operand)) + return true; + return false; +} + +} + +std::set langutil::semanticDebugPointerFreeNames( + SemanticDebugPointer const& _pointer, + SemanticDebugDataTable const& _table +) +{ + return FreeNameCollector{_table}.collect(_pointer); +} + +bool langutil::semanticDebugPointerHasInternalExpression(SemanticDebugPointer const& _pointer) +{ + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value() && hasInternalExpression(**expression)) + return true; + for (auto const& [definedName, definedValue]: _pointer.definitions) + if (hasInternalExpression(definedValue)) + return true; + for (SemanticDebugPointer const& member: _pointer.group) + if (semanticDebugPointerHasInternalExpression(member)) + return true; + for (auto const& [templateName, definition]: _pointer.templates) + if (definition && semanticDebugPointerHasInternalExpression(*definition)) + return true; + for (auto const* subPointer: {&_pointer.listElement, &_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer && semanticDebugPointerHasInternalExpression(**subPointer)) + return true; + return false; +} diff --git a/liblangutil/SemanticDebugDataAnalysis.h b/liblangutil/SemanticDebugDataAnalysis.h new file mode 100644 index 000000000000..44ca653b9cb6 --- /dev/null +++ b/liblangutil/SemanticDebugDataAnalysis.h @@ -0,0 +1,50 @@ +/* + 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 +/** + * Analyses of ethdebug pointers shared by the Yul side table transfer and the + * public emission. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace solidity::langutil +{ + +/// The names that @a _pointer reads and nothing inside it binds: generated Yul +/// locals (`$$yulLocal`) and variables not bound by a `define`, a list index or +/// a template parameter. Template references are followed through the local +/// templates blocks enclosing them and through the pointer templates of +/// @a _table; a reference to an unknown template contributes nothing. +std::set semanticDebugPointerFreeNames( + SemanticDebugPointer const& _pointer, + SemanticDebugDataTable const& _table +); + +/// Whether @a _pointer, including the templates it defines locally, still +/// contains a compiler-internal expression such as `$$yulLocal`. Templates in +/// the sidecar's table are not followed: they are rejected on deserialization +/// when they contain one. +bool semanticDebugPointerHasInternalExpression(SemanticDebugPointer const& _pointer); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp new file mode 100644 index 000000000000..b0cb212c5088 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -0,0 +1,1042 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +/// Sidecars nest pointers and expressions recursively; a document deeper than +/// this is rejected rather than parsed with unbounded recursion. +constexpr size_t maximumNestingDepth = 256; + +/// Expressions internal to the compiler use this prefix, which cannot clash +/// with the schema's own `$` forms. +constexpr std::string_view internalExpressionPrefix = "$$"; +constexpr std::string_view yulLocalKey = "$$yulLocal"; + +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."); +} + +/// Rejects members outside @a _allowed: the format is versioned, so an +/// unknown member is a mistake rather than an extension. +void requireOnlyMembers(Json const& _json, std::set const& _allowed, std::string const& _path) +{ + requireObject(_json, _path); + for (auto const& [name, value]: _json.items()) + require(_allowed.count(name), _path + " has an unknown member \"" + name + "\"."); +} + +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 integerValue(Json const& _value, std::string const& _path) +{ + require(_value.is_number_integer(), _path + " must be an integer."); + if constexpr (std::is_unsigned_v) + { + require( + _value.is_number_unsigned() || _value.get() >= 0, + _path + " must not be negative." + ); + require(_value.get() <= std::numeric_limits::max(), _path + " is too large."); + } + else if (_value.is_number_unsigned()) + require( + _value.get() <= static_cast(std::numeric_limits::max()), + _path + " is too large." + ); + else + { + Json::number_integer_t const rawValue = _value.get(); + require(rawValue >= std::numeric_limits::min(), _path + " is too small."); + require(rawValue <= std::numeric_limits::max(), _path + " is too large."); + } + return _value.get(); +} + +template +T requiredInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + return integerValue(requiredMember(_json, _name, _path), _path + "." + _name); +} + +template +std::optional optionalInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + return integerValue(_json.at(_name), _path + "." + _name); +} + +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 +void setOptional(Json& _json, std::string const& _name, std::optional const& _value) +{ + if (_value) + _json[_name] = *_value; +} + +/// Phase values use the compiler's dash-separated convention for enumeration +/// values, like `ast-id` in the debug info selection. +using Phase = SemanticDebugVariablePhase; +constexpr std::pair phaseNames[]{ + {Phase::Materialized, "materialized"}, + {Phase::Computed, "computed"}, + {Phase::OptimizedOut, "optimized-out"} +}; + +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"} +}; + +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 +auto enumFromString(std::string const& _name, Names const& _names, std::string const& _path) + -> std::decay_tfirst)> +{ + for (auto const& [value, name]: _names) + if (_name == name) + return value; + solThrow(SemanticDebugDataSerializationError, _path + " has unknown value \"" + _name + "\"."); +} + +/// The pointer schema's identifier grammar: `^[a-zA-Z_\-]+[a-zA-Z0-9$_\-]*$`. +bool isIdentifier(std::string_view _text) +{ + auto const isStart = [](char _c) { return std::isalpha(static_cast(_c)) || _c == '_' || _c == '-'; }; + auto const isRest = [&](char _c) { return isStart(_c) || std::isdigit(static_cast(_c)) || _c == '$'; }; + if (_text.empty() || !isStart(_text.front())) + return false; + for (char const c: _text) + if (!isRest(c)) + return false; + return true; +} + +/// A literal in its canonical form: `0x` followed by at least one hex digit. +bool isHexLiteral(std::string_view _text) +{ + if (_text.size() < 3 || _text.substr(0, 2) != "0x") + return false; + for (char const c: _text.substr(2)) + if (!std::isxdigit(static_cast(c))) + return false; + return true; +} + +/// A positive decimal number without leading zeros, as `$sized` requires. +bool isPositiveDecimal(std::string_view _text) +{ + if (_text.empty() || _text.front() == '0') + return false; + for (char const c: _text) + if (!std::isdigit(static_cast(c))) + return false; + return true; +} + +/// Word-oriented locations address by slot, byte-oriented ones by offset and +/// length, as the schema's segment and slice addressing schemes require. +bool isWordOriented(PointerLocation _location) +{ + return _location == PointerLocation::Stack || _location == PointerLocation::Storage || _location == PointerLocation::Transient; +} + +void requireRegionFields(SemanticDebugPointer const& _region, std::string const& _path) +{ + require(_region.location.has_value() && *_region.location != PointerLocation::Unknown, _path + " must state a location."); + require(!_region.name || isIdentifier(*_region.name), _path + ".name must be an identifier."); + if (isWordOriented(*_region.location)) + require(_region.slot.has_value(), _path + " must address its slot."); + else + require(_region.offset.has_value() && _region.length.has_value(), _path + " must address its offset and length."); +} + +/// Source ranges are serialized in the shape of the schema's +/// materials/source-range. The source ID is the one the accompanying +/// ethdebug.compilation record uses, transported as an opaque number. +Json sourceRangeToJson(SemanticDebugSourceRange const& _range) +{ + require(_range.offset >= 0 && _range.length >= 0, "Cannot serialize an invalid source range."); + return Json{ + {"source", Json{{"id", _range.sourceID}}}, + {"range", Json{{"length", _range.length}, {"offset", _range.offset}}} + }; +} + +SemanticDebugSourceRange sourceRangeFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"source", "range"}, _path); + SemanticDebugSourceRange result; + Json const& source = requiredMember(_json, "source", _path); + requireOnlyMembers(source, {"id"}, _path + ".source"); + result.sourceID = requiredInteger(source, "id", _path + ".source"); + Json const& range = requiredMember(_json, "range", _path); + requireOnlyMembers(range, {"offset", "length"}, _path + ".range"); + result.offset = requiredInteger(range, "offset", _path + ".range"); + result.length = requiredInteger(range, "length", _path + ".range"); + require(result.offset >= 0 && result.length >= 0, _path + ".range must not be negative."); + return result; +} + +Json expressionToJson(SemanticDebugPointerExpression const& _expression); +Json pointerBodyToJson(SemanticDebugPointer const& _pointer); +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path, size_t _depth); +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path, size_t _depth); +SemanticDebugPointer templateFromJson(Json const& _json, std::string const& _path, size_t _depth); + +/// Pointer expressions are serialized in the ethdebug pointer expression +/// grammar itself: `0x` strings for literals, identifiers for bound variables, +/// `"$wordsize"`, and single-key objects for lookups, reads, arithmetic and +/// resizing. The one internal form, `{"$$yulLocal": ...}`, has no schema +/// counterpart: it names a stack depth that is not known yet. +Json expressionToJson(SemanticDebugPointerExpression const& _expression) +{ + using Kind = SemanticDebugPointerExpression::Kind; + + auto const operands = [&](size_t _arity = 0) { + require( + _arity == 0 ? !_expression.operands.empty() : _expression.operands.size() == _arity, + "Pointer expression has the wrong number of operands." + ); + Json result = Json::array(); + for (SemanticDebugPointerExpression const& operand: _expression.operands) + result.emplace_back(expressionToJson(operand)); + return result; + }; + auto const value = [&]() -> std::string const& { + require(_expression.value.has_value(), "Pointer expression lacks its value."); + return *_expression.value; + }; + + switch (_expression.kind) + { + case Kind::Literal: + require(isHexLiteral(value()), "Pointer expression literal \"" + value() + "\" is not a hex literal."); + return Json(value()); + case Kind::Variable: + require(isIdentifier(value()), "Pointer expression variable \"" + value() + "\" is not an identifier."); + return Json(value()); + case Kind::YulLocal: + return Json{{std::string(yulLocalKey), value()}}; + case Kind::WordSize: + return Json("$wordsize"); + case Kind::LookupSlot: + return Json{{".slot", value()}}; + case Kind::LookupOffset: + return Json{{".offset", value()}}; + case Kind::LookupLength: + return Json{{".length", value()}}; + case Kind::Read: + return Json{{"$read", value()}}; + case Kind::Sum: + return Json{{"$sum", operands()}}; + case Kind::Product: + return Json{{"$product", operands()}}; + case Kind::Difference: + return Json{{"$difference", operands(2)}}; + case Kind::Quotient: + return Json{{"$quotient", operands(2)}}; + case Kind::Remainder: + return Json{{"$remainder", operands(2)}}; + case Kind::Keccak256: + return Json{{"$keccak256", operands()}}; + case Kind::Concat: + return Json{{"$concat", operands()}}; + case Kind::Resize: + require(_expression.operands.size() == 1, "A resize expression takes exactly one operand."); + if (!_expression.value) + return Json{{"$wordsized", expressionToJson(_expression.operands.front())}}; + require(isPositiveDecimal(*_expression.value), "A sized resize expression needs a positive byte width."); + return Json{{"$sized" + *_expression.value, expressionToJson(_expression.operands.front())}}; + case Kind::Unknown: + break; + } + solThrow(SemanticDebugDataSerializationError, "Cannot serialize an unknown pointer expression."); +} + +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + using Kind = SemanticDebugPointerExpression::Kind; + require(_depth < maximumNestingDepth, _path + " is nested too deeply."); + SemanticDebugPointerExpression result; + + if (_json.is_number_integer()) + { + require(_json.is_number_unsigned() || _json.get() >= 0, _path + " must not be negative."); + return SemanticDebugPointerExpression::literal(toCompactHexWithPrefix(u256(_json.get()))); + } + if (_json.is_string()) + { + std::string text = _json.get(); + if (text == "$wordsize") + return SemanticDebugPointerExpression::wordSize(); + if (isHexLiteral(text)) + return SemanticDebugPointerExpression::literal(std::move(text)); + require(isIdentifier(text), _path + " is neither a hex literal nor an identifier."); + return SemanticDebugPointerExpression::variable(std::move(text)); + } + + requireObject(_json, _path); + require(_json.size() == 1, _path + " must have exactly one member."); + std::string const key = _json.begin().key(); + Json const& value = _json.begin().value(); + std::string const valuePath = _path + "." + key; + + auto const regionName = [&](Kind _kind) { + require(value.is_string() && isIdentifier(value.get()), valuePath + " must name a region."); + result.kind = _kind; + result.value = value.get(); + }; + auto const operands = [&](Kind _kind, size_t _arity = 0) { + requireArray(value, valuePath); + require( + _arity == 0 ? !value.empty() : value.size() == _arity, + valuePath + " has the wrong number of operands." + ); + result.kind = _kind; + for (size_t index = 0; index < value.size(); ++index) + result.operands.emplace_back( + expressionFromJson(value.at(index), valuePath + "[" + std::to_string(index) + "]", _depth + 1) + ); + }; + + if (key == ".slot") + regionName(Kind::LookupSlot); + else if (key == ".offset") + regionName(Kind::LookupOffset); + else if (key == ".length") + regionName(Kind::LookupLength); + else if (key == "$read") + regionName(Kind::Read); + else if (key == yulLocalKey) + { + require(value.is_string() && !value.get().empty(), valuePath + " must name a Yul variable."); + result.kind = Kind::YulLocal; + result.value = value.get(); + } + else if (key == "$sum") + operands(Kind::Sum); + else if (key == "$product") + operands(Kind::Product); + else if (key == "$difference") + operands(Kind::Difference, 2); + else if (key == "$quotient") + operands(Kind::Quotient, 2); + else if (key == "$remainder") + operands(Kind::Remainder, 2); + else if (key == "$keccak256") + operands(Kind::Keccak256); + else if (key == "$concat") + operands(Kind::Concat); + else if (key == "$wordsized") + { + result.kind = Kind::Resize; + result.operands.emplace_back(expressionFromJson(value, valuePath, _depth + 1)); + } + else if (key.starts_with("$sized")) + { + std::string width = key.substr(6); + require(isPositiveDecimal(width), _path + ": malformed resize key \"" + key + "\"."); + result.kind = Kind::Resize; + result.value = std::move(width); + result.operands.emplace_back(expressionFromJson(value, valuePath, _depth + 1)); + } + else if (key.starts_with(internalExpressionPrefix)) + solThrow(SemanticDebugDataSerializationError, _path + ": unknown internal pointer expression \"" + key + "\"."); + else + solThrow(SemanticDebugDataSerializationError, _path + ": unknown pointer expression key \"" + key + "\"."); + return result; +} + +/// Pointers are serialized as ethdebug pointer documents. The class is +/// structural, exactly as in the schema: a region has a `location`, a group +/// has `group`, a list has `list`, a conditional has `if`, a scope is a +/// `define`/`in` chain, a template reference has `template`, and locally +/// defined templates are a `templates`/`in` pair. +Json pointerBodyToJson(SemanticDebugPointer const& _pointer) +{ + using Class = SemanticDebugPointer::Class; + + switch (_pointer.pointerClass) + { + case Class::Region: + { + requireRegionFields(_pointer, "Region pointer"); + Json result = Json::object(); + setOptional(result, "name", _pointer.name); + result["location"] = enumToString(*_pointer.location, pointerLocationNames); + for (auto const& [name, expression]: { + std::pair{"slot", &_pointer.slot}, std::pair{"offset", &_pointer.offset}, std::pair{"length", &_pointer.length} + }) + if (expression->has_value()) + result[name] = expressionToJson(**expression); + return result; + } + case Class::Group: + { + require(!_pointer.group.empty(), "A group pointer must have at least one member."); + Json members = Json::array(); + for (SemanticDebugPointer const& member: _pointer.group) + members.emplace_back(semanticDebugPointerToJson(member)); + return Json{{"group", std::move(members)}}; + } + case Class::List: + { + require( + _pointer.indexName.has_value() && isIdentifier(*_pointer.indexName) && _pointer.listElement != nullptr, + "A list pointer must bind an index name and an element." + ); + Json list = Json::object(); + if (_pointer.count) + list["count"] = expressionToJson(*_pointer.count); + list["each"] = *_pointer.indexName; + list["is"] = semanticDebugPointerToJson(*_pointer.listElement); + return Json{{"list", std::move(list)}}; + } + case Class::Conditional: + { + require( + _pointer.condition.has_value() && _pointer.thenPointer != nullptr, + "A conditional pointer must have a condition and a consequent." + ); + Json result{ + {"if", expressionToJson(*_pointer.condition)}, + {"then", semanticDebugPointerToJson(*_pointer.thenPointer)} + }; + if (_pointer.elsePointer) + result["else"] = semanticDebugPointerToJson(*_pointer.elsePointer); + return result; + } + case Class::Scope: + { + require( + !_pointer.definitions.empty() && _pointer.scopeTarget != nullptr, + "A scope pointer must define at least one name for its target." + ); + // Scope definitions are ordered while JSON object members are not, + // so each definition becomes its own define/in level. + Json inner = semanticDebugPointerToJson(*_pointer.scopeTarget); + for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) + { + require(isIdentifier(definition->first), "A scope definition must bind an identifier."); + inner = Json{ + {"define", Json{{definition->first, expressionToJson(definition->second)}}}, + {"in", std::move(inner)} + }; + } + return inner; + } + case Class::TemplateReference: + { + require( + _pointer.templateName.has_value() && isIdentifier(*_pointer.templateName), + "A template reference must name its template." + ); + Json result{{"template", *_pointer.templateName}}; + if (!_pointer.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _pointer.yields) + { + require(isIdentifier(producedName) && isIdentifier(newName), "A template reference must yield identifiers."); + yields[producedName] = newName; + } + result["yields"] = std::move(yields); + } + return result; + } + case Class::Templates: + { + require(_pointer.scopeTarget != nullptr, "A templates pointer must have a target."); + Json templates = Json::object(); + for (auto const& [templateName, definition]: _pointer.templates) + { + require(isIdentifier(templateName) && definition != nullptr, "A local pointer template must have a name and a definition."); + templates[templateName] = semanticDebugPointerTemplateToJson(*definition); + } + return Json{{"templates", std::move(templates)}, {"in", semanticDebugPointerToJson(*_pointer.scopeTarget)}}; + } + case Class::Unknown: + break; + } + solThrow(SemanticDebugDataSerializationError, "Cannot serialize an unknown pointer class."); +} + +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + using Class = SemanticDebugPointer::Class; + + require(_depth < maximumNestingDepth, _path + " is nested too deeply."); + requireObject(_json, _path); + + SemanticDebugPointer result; + if (_json.contains("location")) + { + requireOnlyMembers(_json, {"name", "location", "slot", "offset", "length"}, _path); + result.pointerClass = Class::Region; + result.name = optionalString(_json, "name", _path); + result.location = enumFromString(requiredString(_json, "location", _path), pointerLocationNames, _path + ".location"); + for (auto const& [name, expression]: { + std::pair{"slot", &result.slot}, std::pair{"offset", &result.offset}, std::pair{"length", &result.length} + }) + if (_json.contains(name)) + *expression = expressionFromJson(_json.at(name), _path + "." + name, _depth + 1); + requireRegionFields(result, _path); + } + else if (_json.contains("group")) + { + requireOnlyMembers(_json, {"group"}, _path); + result.pointerClass = Class::Group; + Json const& members = _json.at("group"); + requireArray(members, _path + ".group"); + require(!members.empty(), _path + ".group must have at least one member."); + for (size_t index = 0; index < members.size(); ++index) + result.group.emplace_back( + pointerFromJson(members.at(index), _path + ".group[" + std::to_string(index) + "]", _depth + 1) + ); + } + else if (_json.contains("list")) + { + requireOnlyMembers(_json, {"list"}, _path); + result.pointerClass = Class::List; + Json const& list = _json.at("list"); + requireOnlyMembers(list, {"count", "each", "is"}, _path + ".list"); + if (list.contains("count")) + result.count = expressionFromJson(list.at("count"), _path + ".list.count", _depth + 1); + result.indexName = requiredString(list, "each", _path + ".list"); + require(isIdentifier(*result.indexName), _path + ".list.each must be an identifier."); + result.listElement = std::make_shared( + pointerFromJson(requiredMember(list, "is", _path + ".list"), _path + ".list.is", _depth + 1) + ); + } + else if (_json.contains("if")) + { + requireOnlyMembers(_json, {"if", "then", "else"}, _path); + result.pointerClass = Class::Conditional; + result.condition = expressionFromJson(_json.at("if"), _path + ".if", _depth + 1); + result.thenPointer = std::make_shared( + pointerFromJson(requiredMember(_json, "then", _path), _path + ".then", _depth + 1) + ); + if (_json.contains("else")) + result.elsePointer = std::make_shared( + pointerFromJson(_json.at("else"), _path + ".else", _depth + 1) + ); + } + else if (_json.contains("define")) + { + // A define/in chain is folded back into one ordered definition list. + // The members of one define object cannot reference each other, so + // their order within that object does not matter. + result.pointerClass = Class::Scope; + Json const* current = &_json; + std::string path = _path; + size_t depth = _depth; + while (current->contains("define")) + { + require(depth < maximumNestingDepth, path + " is nested too deeply."); + requireOnlyMembers(*current, {"define", "in"}, path); + Json const& define = current->at("define"); + requireObject(define, path + ".define"); + require(!define.empty(), path + ".define must define at least one name."); + for (auto const& [name, value]: define.items()) + { + require(isIdentifier(name), path + ".define." + name + " is not an identifier."); + result.definitions.emplace_back(name, expressionFromJson(value, path + ".define." + name, depth + 1)); + } + current = &requiredMember(*current, "in", path); + path += ".in"; + ++depth; + } + result.scopeTarget = std::make_shared(pointerFromJson(*current, path, depth)); + } + else if (_json.contains("template")) + { + requireOnlyMembers(_json, {"template", "yields"}, _path); + result.pointerClass = Class::TemplateReference; + result.templateName = requiredString(_json, "template", _path); + require(isIdentifier(*result.templateName), _path + ".template must be an identifier."); + if (_json.contains("yields")) + { + Json const& yields = _json.at("yields"); + requireObject(yields, _path + ".yields"); + for (auto const& [producedName, newName]: yields.items()) + { + require( + isIdentifier(producedName) && newName.is_string() && isIdentifier(newName.get()), + _path + ".yields." + producedName + " must map an identifier to an identifier." + ); + result.yields.emplace_back(producedName, newName.get()); + } + } + } + else if (_json.contains("templates")) + { + requireOnlyMembers(_json, {"templates", "in"}, _path); + result.pointerClass = Class::Templates; + Json const& templates = _json.at("templates"); + requireObject(templates, _path + ".templates"); + for (auto const& [templateName, definition]: templates.items()) + { + require(isIdentifier(templateName), _path + ".templates." + templateName + " is not an identifier."); + result.templates.emplace_back( + templateName, + std::make_shared( + templateFromJson(definition, _path + ".templates." + templateName, _depth + 1) + ) + ); + } + result.scopeTarget = std::make_shared( + pointerFromJson(requiredMember(_json, "in", _path), _path + ".in", _depth + 1) + ); + } + else + solThrow( + SemanticDebugDataSerializationError, + _path + " is not a pointer: expected one of location, group, list, if, define, template or templates." + ); + return result; +} + +SemanticDebugPointer templateFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + requireOnlyMembers(_json, {"expect", "for"}, _path); + Json const& expect = requiredMember(_json, "expect", _path); + requireArray(expect, _path + ".expect"); + // Templates occur only in the table and in local templates blocks; a + // template in a pointer position, including this one's body, is rejected. + SemanticDebugPointer result = pointerFromJson(requiredMember(_json, "for", _path), _path + ".for", _depth + 1); + for (size_t index = 0; index < expect.size(); ++index) + { + std::string const parameterPath = _path + ".expect[" + std::to_string(index) + "]"; + require(expect.at(index).is_string(), parameterPath + " must be a string."); + result.expectedParameters.emplace_back(expect.at(index).get()); + require(isIdentifier(result.expectedParameters.back()), parameterPath + " must be an identifier."); + } + return result; +} + +/// A materialized variable has a pointer; a computed or optimized-out +/// variable does not. +void requirePhaseConsistency(std::optional _phase, bool _hasPointer, std::string const& _path) +{ + if (!_phase) + return; + if (*_phase == Phase::Materialized) + require(_hasPointer, _path + ": a materialized variable must have a pointer."); + else + require(!_hasPointer, _path + ": a " + enumToString(*_phase, phaseNames) + " variable must not have a pointer."); +} + +Json variableToJson(SemanticDebugVariable const& _variable) +{ + require(!_variable.identifier || !_variable.identifier->empty(), "Variable identifier must not be empty."); + requirePhaseConsistency(_variable.phase, _variable.pointer.has_value(), "variable"); + Json result = Json::object(); + setOptional(result, "identifier", _variable.identifier); + setOptional(result, "declarationASTID", _variable.declarationASTID); + if (_variable.declarationSourceRange) + result["declarationSourceRange"] = sourceRangeToJson(*_variable.declarationSourceRange); + setOptional(result, "typeID", _variable.typeID); + result["phase"] = enumToString(_variable.phase, phaseNames); + if (_variable.pointer) + result["pointer"] = semanticDebugPointerToJson(*_variable.pointer); + return result; +} + +SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers( + _json, {"identifier", "declarationASTID", "declarationSourceRange", "typeID", "phase", "pointer"}, _path + ); + SemanticDebugVariable result; + result.identifier = optionalString(_json, "identifier", _path); + require(!result.identifier || !result.identifier->empty(), _path + ".identifier must not be empty."); + result.declarationASTID = optionalInteger(_json, "declarationASTID", _path); + if (_json.contains("declarationSourceRange")) + result.declarationSourceRange = sourceRangeFromJson(_json.at("declarationSourceRange"), _path + ".declarationSourceRange"); + result.typeID = optionalString(_json, "typeID", _path); + result.phase = enumFromString(requiredString(_json, "phase", _path), phaseNames, _path + ".phase"); + if (_json.contains("pointer")) + result.pointer = pointerFromJson(_json.at("pointer"), _path + ".pointer", 0); + requirePhaseConsistency(result.phase, result.pointer.has_value(), _path); + return result; +} + +Json variableUpdateToJson(SemanticDebugVariableUpdate const& _update) +{ + require(_update.phase.has_value() || _update.pointer.has_value(), "A variable update must change the phase or the pointer."); + requirePhaseConsistency(_update.phase, _update.pointer.has_value(), "variable update"); + Json result{{"variableASTID", _update.variableASTID}}; + if (_update.phase) + result["phase"] = enumToString(*_update.phase, phaseNames); + if (_update.pointer) + result["pointer"] = semanticDebugPointerToJson(*_update.pointer); + return result; +} + +SemanticDebugVariableUpdate variableUpdateFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"variableASTID", "phase", "pointer"}, _path); + SemanticDebugVariableUpdate result; + result.variableASTID = requiredInteger(_json, "variableASTID", _path); + if (_json.contains("phase")) + result.phase = enumFromString(requiredString(_json, "phase", _path), phaseNames, _path + ".phase"); + if (_json.contains("pointer")) + result.pointer = pointerFromJson(_json.at("pointer"), _path + ".pointer", 0); + require(result.phase.has_value() || result.pointer.has_value(), _path + " must change the phase or the pointer."); + requirePhaseConsistency(result.phase, result.pointer.has_value(), _path); + return result; +} + +Json scopeToJson(SemanticDebugScope const& _scope) +{ + Json result = Json::object(); + result["variableDefinitions"] = Json::array(); + for (auto const& variable: _scope.variableDefinitions) + result["variableDefinitions"].emplace_back(variableToJson(variable)); + // Omitted while empty, which is all code generation produces: only + // optimizer passes emit updates. + if (!_scope.variableUpdates.empty()) + { + result["variableUpdates"] = Json::array(); + for (auto const& update: _scope.variableUpdates) + result["variableUpdates"].emplace_back(variableUpdateToJson(update)); + } + return result; +} + +SemanticDebugScope scopeFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"variableDefinitions", "variableUpdates"}, _path); + SemanticDebugScope result; + Json const& variables = requiredMember(_json, "variableDefinitions", _path); + requireArray(variables, _path + ".variableDefinitions"); + for (size_t index = 0; index < variables.size(); ++index) + result.variableDefinitions.emplace_back( + variableFromJson(variables.at(index), _path + ".variableDefinitions[" + std::to_string(index) + "]") + ); + if (_json.contains("variableUpdates")) + { + Json const& updates = _json.at("variableUpdates"); + requireArray(updates, _path + ".variableUpdates"); + for (size_t index = 0; index < updates.size(); ++index) + result.variableUpdates.emplace_back( + variableUpdateFromJson(updates.at(index), _path + ".variableUpdates[" + std::to_string(index) + "]") + ); + } + return result; +} + +/// The keys of the two-level scope map are the canonical decimal forms of the +/// AST ID and the instance: an optional sign for the AST ID, digits without +/// leading zeros, within the 64-bit range. +int64_t decimalKey(std::string const& _text, std::string const& _path) +{ + std::string_view digits = _text; + bool const negative = !digits.empty() && digits.front() == '-'; + if (negative) + digits.remove_prefix(1); + bool canonical = !digits.empty() && (digits == "0" ? !negative : digits.front() != '0'); + for (char const digit: digits) + if (!std::isdigit(static_cast(digit))) + canonical = false; + require(canonical, _path + " key \"" + _text + "\" is not a decimal integer."); + + int64_t value = 0; + auto const [end, error] = std::from_chars(_text.data(), _text.data() + _text.size(), value); + require(error == std::errc{} && end == _text.data() + _text.size(), _path + " key \"" + _text + "\" is out of range."); + return value; +} + +int64_t astIDKey(std::string const& _text, std::string const& _path) +{ + return decimalKey(_text, _path); +} + +int64_t instanceKey(std::string const& _text, std::string const& _path) +{ + int64_t const instance = decimalKey(_text, _path); + require(instance >= 0, _path + " key \"" + _text + "\" must not be negative."); + return instance; +} + +/// A template reference must resolve against a local templates block in the +/// same pointer document or against the sidecar's pointer template table. +void requireResolvableTemplateReferences( + SemanticDebugPointer const& _pointer, + std::set const& _localTemplates, + SemanticDebugDataTable const& _table, + std::string const& _path +) +{ + using Class = SemanticDebugPointer::Class; + auto const recurse = [&](std::shared_ptr const& _sub, std::string const& _subPath) { + if (_sub) + requireResolvableTemplateReferences(*_sub, _localTemplates, _table, _subPath); + }; + + switch (_pointer.pointerClass) + { + case Class::TemplateReference: + require( + _pointer.templateName.has_value() && + (_localTemplates.count(*_pointer.templateName) || _table.findPointerTemplate(*_pointer.templateName)), + _path + " references unknown pointer template \"" + _pointer.templateName.value_or("") + "\"." + ); + return; + case Class::Templates: + { + std::set localTemplates = _localTemplates; + for (auto const& [templateName, definition]: _pointer.templates) + { + recurse(definition, _path + ".templates." + templateName); + localTemplates.insert(templateName); + } + if (_pointer.scopeTarget) + requireResolvableTemplateReferences(*_pointer.scopeTarget, localTemplates, _table, _path + ".in"); + return; + } + case Class::Group: + for (size_t index = 0; index < _pointer.group.size(); ++index) + requireResolvableTemplateReferences( + _pointer.group[index], _localTemplates, _table, _path + ".group[" + std::to_string(index) + "]" + ); + return; + case Class::List: + recurse(_pointer.listElement, _path + ".list.is"); + return; + case Class::Conditional: + recurse(_pointer.thenPointer, _path + ".then"); + recurse(_pointer.elsePointer, _path + ".else"); + return; + case Class::Scope: + recurse(_pointer.scopeTarget, _path + ".in"); + return; + case Class::Region: + case Class::Unknown: + return; + } +} + +void requireResolvableTemplateReferences(SemanticDebugDataTable const& _table) +{ + for (auto const& [templateName, definition]: _table.pointerTemplates()) + requireResolvableTemplateReferences(definition, {}, _table, "sidecar.resources.pointers." + templateName); + for (auto const& [key, scope]: _table.scopes()) + { + if (!scope) + continue; + std::string const path = "sidecar.scopes." + std::to_string(key.first) + "." + std::to_string(key.second); + for (size_t index = 0; index < scope->variableDefinitions.size(); ++index) + if (auto const& pointer = scope->variableDefinitions[index].pointer) + requireResolvableTemplateReferences( + *pointer, {}, _table, path + ".variableDefinitions[" + std::to_string(index) + "].pointer" + ); + for (size_t index = 0; index < scope->variableUpdates.size(); ++index) + if (auto const& pointer = scope->variableUpdates[index].pointer) + requireResolvableTemplateReferences( + *pointer, {}, _table, path + ".variableUpdates[" + std::to_string(index) + "].pointer" + ); + } +} + +} // namespace + +Json langutil::semanticDebugPointerToJson(SemanticDebugPointer const& _pointer) +{ + if (_pointer.expectedParameters.empty()) + return pointerBodyToJson(_pointer); + return semanticDebugPointerTemplateToJson(_pointer); +} + +Json langutil::semanticDebugPointerTemplateToJson(SemanticDebugPointer const& _template) +{ + Json expect = Json::array(); + for (std::string const& parameter: _template.expectedParameters) + { + require(isIdentifier(parameter), "A template parameter must be an identifier."); + expect.emplace_back(parameter); + } + return Json{{"expect", std::move(expect)}, {"for", pointerBodyToJson(_template)}}; +} + +Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) +{ + Json result{ + {"format", std::string(SemanticDebugDataFormat)}, + {"version", SemanticDebugDataFormatVersion} + }; + setOptional(result, "contractName", _table.contractName()); + + // The resource tables mirror the public ethdebug.resources object: type + // documents are written verbatim, they already are the public type + // schema, and pointer templates in the schema's template form. + if (!_table.types().empty() || !_table.pointerTemplates().empty()) + { + Json types = Json::object(); + for (auto const& [id, document]: _table.types()) + types[id] = document; + Json pointers = Json::object(); + for (auto const& [name, definition]: _table.pointerTemplates()) + pointers[name] = semanticDebugPointerTemplateToJson(definition); + result["resources"] = Json{{"types", std::move(types)}, {"pointers", std::move(pointers)}}; + } + + // Scope records form a two-level map from the decimal AST ID to the + // decimal instance, with instance 0 written explicitly. The JSON library + // orders object members by key, which keeps the output deterministic. + Json scopes = Json::object(); + for (auto const& [key, scope]: _table.scopes()) + { + require(scope != nullptr, "Semantic debug data table contains a null scope record."); + scopes[std::to_string(key.first)][std::to_string(key.second)] = scopeToJson(*scope); + } + result["scopes"] = std::move(scopes); + return result; +} + +SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) +{ + std::string const root = "sidecar"; + requireOnlyMembers(_json, {"format", "version", "contractName", "resources", "scopes"}, root); + std::string const format = requiredString(_json, "format", root); + require(format == SemanticDebugDataFormat, "Unsupported semantic debug data format \"" + format + "\"."); + unsigned const version = requiredInteger(_json, "version", root); + require(version == SemanticDebugDataFormatVersion, "Unsupported semantic debug data format version " + std::to_string(version) + "."); + + SemanticDebugDataTable result; + if (std::optional contractName = optionalString(_json, "contractName", root)) + result.setContractName(std::move(*contractName)); + + if (_json.contains("resources")) + { + Json const& resources = _json.at("resources"); + requireOnlyMembers(resources, {"types", "pointers"}, root + ".resources"); + if (resources.contains("types")) + { + Json const& types = resources.at("types"); + requireObject(types, root + ".resources.types"); + for (auto const& [typeID, document]: types.items()) + { + requireObject(document, root + ".resources.types." + typeID); + result.setType(typeID, document); + } + } + if (resources.contains("pointers")) + { + Json const& pointers = resources.at("pointers"); + requireObject(pointers, root + ".resources.pointers"); + for (auto const& [name, definition]: pointers.items()) + { + std::string const path = root + ".resources.pointers." + name; + require(isIdentifier(name), path + " is not an identifier."); + SemanticDebugPointer pointerTemplate = templateFromJson(definition, path, 0); + // Pointer templates are public resources, published verbatim: + // an expression internal to the compiler has no place in them. + require( + !semanticDebugPointerHasInternalExpression(pointerTemplate), + path + " must not contain an internal expression." + ); + result.setPointerTemplate(name, std::move(pointerTemplate)); + } + } + } + + Json const& scopes = requiredMember(_json, "scopes", root); + requireObject(scopes, root + ".scopes"); + for (auto const& [astIDText, instances]: scopes.items()) + { + std::string const astIDPath = root + ".scopes." + astIDText; + int64_t const astID = astIDKey(astIDText, root + ".scopes"); + requireObject(instances, astIDPath); + for (auto const& [instanceText, scope]: instances.items()) + result.set( + SemanticDebugDataTable::Key{astID, instanceKey(instanceText, astIDPath)}, + std::make_shared(scopeFromJson(scope, astIDPath + "." + instanceText)) + ); + } + + requireResolvableTemplateReferences(result); + return result; +} diff --git a/liblangutil/SemanticDebugDataSerialization.h b/liblangutil/SemanticDebugDataSerialization.h new file mode 100644 index 000000000000..42128d83f30e --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.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 +/** + * JSON serialization of the semantic debug info side table: the ethdebug + * sidecar that crosses a textual Yul boundary. + * + * The envelope and the variable records are compiler-specific and versioned. + * The type and pointer documents inside them follow the public ethdebug + * schemas, with the single internal `$$yulLocal` expression for a generated + * Yul local whose stack depth is not known yet. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace solidity::langutil +{ + +inline constexpr std::string_view SemanticDebugDataFormat = "solidity-ethdebug-semantic-data"; +inline constexpr unsigned SemanticDebugDataFormatVersion = 1; + +/// Raised for a sidecar that is malformed and for a pointer that cannot be +/// represented in the ethdebug pointer schema. +struct SemanticDebugDataSerializationError: virtual util::Exception {}; + +/// Serializes the side table: the resource tables and every scope record, +/// attached to a Yul node or not. +Json semanticDebugDataToJson(SemanticDebugDataTable const& _table); + +/// Deserializes a sidecar, rejecting an unknown format, an unsupported +/// version, malformed fields and template references that resolve neither to +/// a local templates block nor to the sidecar's pointer table. +SemanticDebugDataTable semanticDebugDataFromJson(Json const& _json); + +/// Serializes @a _pointer as an ethdebug pointer document; a root pointer that +/// expects parameters takes the schema's template form. Public emission uses +/// the same writer, after checking that no internal expression remains. +Json semanticDebugPointerToJson(SemanticDebugPointer const& _pointer); + +/// Serializes @a _template as an ethdebug pointer template document, i.e. in +/// the `expect`/`for` form also when it expects no parameters. +Json semanticDebugPointerTemplateToJson(SemanticDebugPointer const& _template); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h new file mode 100644 index 000000000000..b7627e716b93 --- /dev/null +++ b/liblangutil/SemanticDebugDataTable.h @@ -0,0 +1,119 @@ +/* + 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 +/** + * The semantic debug info side table: scope records keyed by source AST + * origin, together with the type and pointer template resources. + */ + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +/// The semantic debug info side table: scope records keyed by source-language +/// AST origin and generated-scope instance, together with the resource tables +/// (types and pointer templates) that the records reference by ID. +class SemanticDebugDataTable +{ +public: + /// A scope record 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. In Yul text the instance travels in the + /// `@ast-id-instance` annotation next to `@ast-id`. + using Key = std::pair; + + void setContractName(std::string _contractName) { m_contractName = std::move(_contractName); } + std::optional const& contractName() const { return m_contractName; } + + void set(Key _key, SemanticDebugScope::ConstPtr _scope) { m_scopes[_key] = std::move(_scope); } + void set(int64_t _astID, SemanticDebugScope::ConstPtr _scope) { set(Key{_astID, 0}, std::move(_scope)); } + + SemanticDebugScope::ConstPtr find(Key _key) const + { + auto const it = m_scopes.find(_key); + return it == m_scopes.end() ? nullptr : it->second; + } + SemanticDebugScope::ConstPtr find(int64_t _astID, int64_t _instance = 0) const + { + return find(Key{_astID, _instance}); + } + + std::map const& scopes() const { return m_scopes; } + + /// Type documents in the shape of the public ethdebug type schema, keyed + /// by the compiler's type identifier. This is the only type store: scope + /// records reference into it by ID, and both the sidecar's resources and + /// the public ethdebug.resources.types output are this map, verbatim. + void setType(std::string _id, Json _document) { m_types[std::move(_id)] = std::move(_document); } + Json const* findType(std::string const& _id) const + { + auto const it = m_types.find(_id); + return it == m_types.end() ? nullptr : &it->second; + } + std::map const& types() const { return m_types; } + + /// Pointer templates keyed by producer-defined name, mirroring the public + /// ethdebug.resources.pointers table. Variables reference an entry with the + /// pointer schema's `template` reference, the same way they reference types. + void setPointerTemplate(std::string _name, SemanticDebugPointer _template) + { + m_pointerTemplates[std::move(_name)] = std::move(_template); + } + SemanticDebugPointer const* findPointerTemplate(std::string const& _name) const + { + auto const it = m_pointerTemplates.find(_name); + return it == m_pointerTemplates.end() ? nullptr : &it->second; + } + std::map const& pointerTemplates() const { return m_pointerTemplates; } + + /// Adds the content of @a _other: its contract name if it has one, and its + /// scope records, types and pointer templates, replacing entries with the + /// same key. + void merge(SemanticDebugDataTable const& _other) + { + if (_other.m_contractName) + m_contractName = _other.m_contractName; + for (auto const& [key, scope]: _other.m_scopes) + m_scopes[key] = scope; + for (auto const& [id, document]: _other.m_types) + m_types[id] = document; + for (auto const& [name, definition]: _other.m_pointerTemplates) + m_pointerTemplates[name] = definition; + } + + bool empty() const { return m_scopes.empty() && m_types.empty() && m_pointerTemplates.empty(); } + +private: + std::optional m_contractName; + std::map m_scopes; + std::map m_types; + std::map m_pointerTemplates; +}; + +} // namespace solidity::langutil diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 49bdb3d0932a..a8ae465b18ed 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 @@ -144,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/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp new file mode 100644 index 000000000000..03ea100a8256 --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -0,0 +1,874 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::langutil; + +namespace +{ + +using PointerExpression = SemanticDebugPointerExpression; + +/// Type documents under construction, together with the compilation's source +/// indices: declaration ranges are published with the same numeric source IDs +/// the ethdebug.compilation record uses, never with paths. +struct TypeRegistry +{ + std::map documents; + std::map const* sourceIndices = nullptr; +}; + +std::optional sourceID(TypeRegistry const& _types, SourceLocation const& _location) +{ + if (!_types.sourceIndices || !_location.sourceName) + return std::nullopt; + auto const it = _types.sourceIndices->find(*_location.sourceName); + if (it == _types.sourceIndices->end()) + return std::nullopt; + return it->second; +} + +/// An ethdebug identifier must not start with `$`, which a Solidity identifier +/// may. Every name the producer publishes passes through here. +std::string identifier(std::string const& _name) +{ + solAssert(!_name.empty()); + return _name.front() == '$' ? "_" + _name : _name; +} + +/// Registers the ethdebug/format/type document for @a _type - and every type +/// it composes - into @a _types under the compiler's type identifiers. There +/// is no intermediate representation: what this builds is what the sidecar +/// serializes and what ethdebug.resources.types publishes. Wrappers reference +/// composed types by `{"id": ...}`, and the placeholder inserted before +/// descending is what terminates recursive types. +/// @returns false for a type the schema has no document for, such as a +/// literal or a magic type; such a type composes into no document either. +bool registerTypeDocument(TypeRegistry& _types, Type const& _type); + +std::optional typeWrapper(TypeRegistry& _types, std::optional _name, Type const& _type) +{ + if (!registerTypeDocument(_types, _type)) + return std::nullopt; + Json wrapper = Json::object(); + if (_name) + wrapper["name"] = std::move(*_name); + wrapper["type"] = Json{{"id", _type.identifier()}}; + return wrapper; +} + +/// The wrappers of @a _types in order, or nothing if any of them has no document. +std::optional typeWrappers(TypeRegistry& _types, std::vector const& _componentTypes) +{ + Json wrappers = Json::array(); + for (Type const* componentType: _componentTypes) + { + if (!componentType) + continue; + std::optional wrapper = typeWrapper(_types, std::nullopt, *componentType); + if (!wrapper) + return std::nullopt; + wrappers.emplace_back(std::move(*wrapper)); + } + return wrappers; +} + +Json typeDefinition(TypeRegistry const& _types, Declaration const& _declaration) +{ + Json definition = Json::object(); + if (!_declaration.name().empty()) + definition["name"] = _declaration.name(); + SourceLocation const& location = _declaration.location(); + if (std::optional source = sourceID(_types, location); source && location.hasText()) + definition["location"] = Json{ + {"range", Json{{"length", location.end - location.start}, {"offset", location.start}}}, + {"source", Json{{"id", *source}}} + }; + return definition; +} + +std::optional typeDocument(TypeRegistry& _types, Type const& _type) +{ + Json result = Json::object(); + auto attachDefinition = [&](Declaration const& _declaration) { + Json definition = typeDefinition(_types, _declaration); + if (!definition.empty()) + result["definition"] = std::move(definition); + }; + auto contains = [&](Type const& _componentType) -> bool { + std::optional wrapper = typeWrapper(_types, std::nullopt, _componentType); + if (wrapper) + result["contains"] = std::move(*wrapper); + return wrapper.has_value(); + }; + + switch (_type.category()) + { + case Type::Category::Address: + { + auto const& addressType = dynamic_cast(_type); + result["kind"] = "address"; + result["payable"] = addressType.stateMutability() == StateMutability::Payable; + break; + } + case Type::Category::Integer: + { + auto const& integerType = dynamic_cast(_type); + result["kind"] = integerType.isSigned() ? "int" : "uint"; + result["bits"] = integerType.numBits(); + break; + } + case Type::Category::FixedPoint: + { + auto const& fixedPointType = dynamic_cast(_type); + result["kind"] = fixedPointType.isSigned() ? "fixed" : "ufixed"; + result["bits"] = fixedPointType.numBits(); + result["places"] = fixedPointType.fractionalDigits(); + break; + } + case Type::Category::Bool: + result["kind"] = "bool"; + break; + case Type::Category::FixedBytes: + { + auto const& bytesType = dynamic_cast(_type); + result["kind"] = "bytes"; + result["size"] = bytesType.numBytes(); + break; + } + case Type::Category::Array: + { + auto const& arrayType = dynamic_cast(_type); + if (arrayType.isByteArrayOrString()) + { + result["kind"] = arrayType.isString() ? "string" : "bytes"; + break; + } + result["kind"] = "array"; + if (!arrayType.isDynamicallySized()) + result["count"] = toCompactHexWithPrefix(arrayType.length()); + if (!contains(*arrayType.baseType())) + return std::nullopt; + break; + } + case Type::Category::ArraySlice: + { + // A slice's representation is the dynamic array it views, and no + // consumer distinguishes the two, so no separate kind is carried. + auto const& sliceType = dynamic_cast(_type); + result["kind"] = "array"; + if (!contains(*sliceType.arrayType().baseType())) + return std::nullopt; + break; + } + case Type::Category::Contract: + { + auto const& contractType = dynamic_cast(_type); + result["kind"] = "contract"; + result["payable"] = contractType.isPayable(); + if (contractType.contractDefinition().isLibrary()) + result["library"] = true; + else if (contractType.contractDefinition().isInterface()) + result["interface"] = true; + attachDefinition(contractType.contractDefinition()); + break; + } + case Type::Category::Struct: + { + auto const& structType = dynamic_cast(_type); + Json members = Json::array(); + for (ASTPointer const& member: structType.structDefinition().members()) + { + if (!member->annotation().type) + continue; + std::optional wrapper = typeWrapper(_types, member->name(), *member->annotation().type); + if (!wrapper) + return std::nullopt; + members.emplace_back(std::move(*wrapper)); + } + result["kind"] = "struct"; + result["contains"] = std::move(members); + attachDefinition(structType.structDefinition()); + break; + } + case Type::Category::Enum: + { + auto const& enumType = dynamic_cast(_type); + Json values = Json::array(); + for (ASTPointer const& member: enumType.enumDefinition().members()) + values.emplace_back(member->name()); + result["kind"] = "enum"; + result["values"] = std::move(values); + attachDefinition(enumType.enumDefinition()); + break; + } + case Type::Category::UserDefinedValueType: + { + auto const& aliasType = dynamic_cast(_type); + result["kind"] = "alias"; + if (!contains(aliasType.underlyingType())) + return std::nullopt; + attachDefinition(aliasType.definition()); + break; + } + case Type::Category::Tuple: + { + auto const& tupleType = dynamic_cast(_type); + std::optional members = typeWrappers(_types, tupleType.components()); + if (!members) + return std::nullopt; + result["kind"] = "tuple"; + result["contains"] = std::move(*members); + break; + } + case Type::Category::Mapping: + { + auto const& mappingType = dynamic_cast(_type); + std::optional key = typeWrapper(_types, std::nullopt, *mappingType.keyType()); + std::optional value = typeWrapper(_types, std::nullopt, *mappingType.valueType()); + if (!key || !value) + return std::nullopt; + result["kind"] = "mapping"; + result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; + break; + } + case Type::Category::Function: + { + auto const& functionType = dynamic_cast(_type); + result["kind"] = "function"; + if (functionType.kind() == FunctionType::Kind::Internal) + result["internal"] = true; + else if (functionType.kind() == FunctionType::Kind::External) + result["external"] = true; + + auto wrappedTuple = [&](std::vector const& _memberTypes) -> std::optional { + std::optional wrappers = typeWrappers(_types, _memberTypes); + if (!wrappers) + return std::nullopt; + return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; + }; + std::optional parameters = wrappedTuple(functionType.parameterTypes()); + if (!parameters) + return std::nullopt; + Json contained{{"parameters", std::move(*parameters)}}; + if (!functionType.returnParameterTypes().empty()) + { + std::optional returns = wrappedTuple(functionType.returnParameterTypes()); + if (!returns) + return std::nullopt; + contained["returns"] = std::move(*returns); + } + result["contains"] = std::move(contained); + + if (functionType.hasDeclaration()) + attachDefinition(functionType.declaration()); + break; + } + case Type::Category::RationalNumber: + case Type::Category::StringLiteral: + case Type::Category::TypeType: + case Type::Category::Modifier: + case Type::Category::Magic: + case Type::Category::Module: + case Type::Category::InaccessibleDynamic: + // Compile-time-only types have no runtime representation to describe. + return std::nullopt; + } + return result; +} + +bool registerTypeDocument(TypeRegistry& _types, Type const& _type) +{ + std::string const id = _type.identifier(); + if (_types.documents.count(id)) + return true; + // Present before descending, so a recursive type terminates. + _types.documents.emplace(id, Json::object()); + if (std::optional document = typeDocument(_types, _type)) + { + _types.documents[id] = std::move(*document); + return true; + } + _types.documents.erase(id); + return false; +} + +/// The region of one stack slot. 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 expression kind and never +/// published as though it were a slot. +SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _yulVariable) +{ + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + std::move(_name), + PointerExpression::yulLocal(_yulVariable) + ); +} + +std::optional sourceIdentifier(VariableDeclaration const& _variable) +{ + return _variable.name().empty() ? std::nullopt : std::make_optional(identifier(_variable.name())); +} + +PointerExpression literalExpression(u256 const& _value) +{ + return PointerExpression::literal(toCompactHexWithPrefix(_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) +{ + 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)}); +} + +/// The name the state variable's pointer template is published under: the +/// location and the AST IDs of the most derived contract and the variable, so +/// that an inherited variable at a different slot in another contract of the +/// same compilation does not clash. +std::string stateVariablePointerTemplateName( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + SemanticDebugPointer::Location _location +) +{ + solAssert( + _location == SemanticDebugPointer::Location::Storage || + _location == SemanticDebugPointer::Location::Transient + ); + std::string const prefix = _location == SemanticDebugPointer::Location::Storage ? "storage_" : "transient_"; + return prefix + std::to_string(_contract.id()) + "_" + std::to_string(_variable.id()); +} + +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 SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + sourceIdentifier(_variable), + PointerExpression::yulLocal(stackSlots.front()) + ); + + SemanticDebugPointer result; + result.pointerClass = SemanticDebugPointer::Class::Group; + result.name = sourceIdentifier(_variable); + for (std::string const& stackSlot: stackSlots) + result.group.emplace_back(stackRegionPointer(stackSlot, stackSlot)); + return result; +} + +/// Builds the pointer of a state variable in storage or transient storage. One +/// builder instance describes one root pointer; mapping keys encountered +/// anywhere in it become template parameters collected in @a expectedParameters. +class StateVariablePointerBuilder +{ +public: + explicit StateVariablePointerBuilder(SemanticDebugPointer::Location _location): + m_location(_location) + {} + + 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_location, + _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_location, 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_location, + 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_location, + 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_location, + _name, + _slot, + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + + SemanticDebugPointer longLengthRegion = SemanticDebugPointer::region(m_location, 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_location, + _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_location; + std::vector m_expectedParameters; + std::set m_structsOnPath; + unsigned m_depth = 0; +}; + +SemanticDebugPointer stateVariablePointer( + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _location +) +{ + solAssert(_variable.annotation().type, "State variable type expected."); + + StateVariablePointerBuilder builder{_location}; + std::optional offset; + if (_offset != 0) + offset = literalExpression(_offset); + SemanticDebugPointer result = builder.build( + *_variable.annotation().type, + literalExpression(_slot), + std::move(offset), + identifier(_variable.name()) + ); + result.expectedParameters = builder.takeExpectedParameters(); + return result; +} + +/// The record of @a _variable without its phase and pointer: identity, source +/// range and, when the type has a document, the type reference. +SemanticDebugVariable variableRecord(TypeRegistry& _types, VariableDeclaration const& _variable) +{ + SemanticDebugVariable result; + result.identifier = sourceIdentifier(_variable); + result.declarationASTID = _variable.id(); + result.declarationSourceRange = + SemanticDebugSourceRange::fromLocation(_variable.location(), sourceID(_types, _variable.location())); + if (_variable.annotation().type && registerTypeDocument(_types, *_variable.annotation().type)) + result.typeID = _variable.annotation().type->identifier(); + return result; +} + +/// The record of a parameter or return variable, materialized in the Yul locals +/// of its stack slots. +SemanticDebugVariable stackVariableRecord(TypeRegistry& _types, VariableDeclaration const& _variable) +{ + SemanticDebugVariable result = variableRecord(_types, _variable); + // A value without stack slots has nothing to materialize. + if (std::optional pointer = stackPointer(_variable)) + { + result.phase = SemanticDebugVariablePhase::Materialized; + result.pointer = std::move(*pointer); + } + else + result.phase = SemanticDebugVariablePhase::OptimizedOut; + return result; +} + +/// The record of a state variable. Its pointer is a reusable template published +/// in the pointer resources under a producer-defined name; the record +/// references it the same way it references its type. +SemanticDebugVariable stateVariableRecord( + TypeRegistry& _types, + SemanticDebugDataTable& _table, + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _location +) +{ + SemanticDebugVariable result = variableRecord(_types, _variable); + std::string const templateName = stateVariablePointerTemplateName(_contract, _variable, _location); + _table.setPointerTemplate(templateName, stateVariablePointer(_variable, _slot, _offset, _location)); + result.phase = SemanticDebugVariablePhase::Materialized; + result.pointer = SemanticDebugPointer::templateReference(templateName); + return result; +} + +void appendStackVariables( + TypeRegistry& _types, + std::vector& _variables, + std::vector> const& _declarations +) +{ + for (ASTPointer const& declaration: _declarations) + _variables.emplace_back(stackVariableRecord(_types, *declaration)); +} + +void setScope(SemanticDebugDataTable& _table, int64_t _astID, std::vector _variables) +{ + if (_variables.empty()) + return; + _table.set(_astID, std::make_shared(SemanticDebugScope{ + .variableDefinitions = std::move(_variables) + })); +} + +/// The scope of a function: its parameters and return variables, in that order. +void addFunction(TypeRegistry& _types, SemanticDebugDataTable& _table, FunctionDefinition const& _function) +{ + std::vector variables; + appendStackVariables(_types, variables, _function.parameters()); + appendStackVariables(_types, variables, _function.returnParameters()); + setScope(_table, _function.id(), std::move(variables)); +} + +/// The scope of a modifier: its parameters. +void addModifier(TypeRegistry& _types, SemanticDebugDataTable& _table, ModifierDefinition const& _modifier) +{ + std::vector variables; + appendStackVariables(_types, variables, _modifier.parameters()); + setScope(_table, _modifier.id(), std::move(variables)); +} + +/// The scope of the contract: its state variables in storage and in transient +/// storage, each with a pointer template in the resources. +void addStateVariables(TypeRegistry& _types, 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; + auto append = [&](DataLocation _dataLocation, SemanticDebugPointer::Location _location) { + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(_dataLocation)) + if (!variable->name().empty()) + variables.emplace_back(stateVariableRecord(_types, _table, _contract, *variable, slot, offset, _location)); + }; + append(DataLocation::Storage, SemanticDebugPointer::Location::Storage); + append(DataLocation::Transient, SemanticDebugPointer::Location::Transient); + setScope(_table, _contract.id(), std::move(variables)); +} + +} + +SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable( + ContractDefinition const& _contract, + std::map const& _sourceIndices +) +{ + SemanticDebugDataTable table; + table.setContractName(_contract.name()); + + TypeRegistry types{{}, &_sourceIndices}; + addStateVariables(types, table, _contract); + + // Inherited functions and modifiers are compiled into the most derived + // contract's IR with the AST IDs of their original definitions, so every + // linearized base contract contributes its scopes. + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + { + for (FunctionDefinition const* function: contract->definedFunctions()) + addFunction(types, table, *function); + for (ModifierDefinition const* modifier: contract->functionModifiers()) + addModifier(types, table, *modifier); + } + + // Free functions and internal library functions reachable through imports + // are compiled into the contract's IR as well. A library's external + // functions live in the library's own IR and are covered by its own table. + 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())) + addFunction(types, table, *freeFunction); + else if (auto const* library = dynamic_cast(node.get()); library && library->isLibrary()) + for (FunctionDefinition const* function: library->definedFunctions()) + if (function->visibility() <= Visibility::Internal) + addFunction(types, table, *function); + } + + for (auto& [id, document]: types.documents) + table.setType(id, std::move(document)); + + return table; +} diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.h b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h new file mode 100644 index 000000000000..402e9c30f600 --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.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 +/** + * Builds the semantic debug info of a contract from analysis results. + */ + +#pragma once + +#include + +#include +#include + +namespace solidity::frontend +{ + +class ContractDefinition; + +/// Builds the semantic debug info table of @a _contract from analysis results: +/// type documents, storage pointer templates and the scope records of the +/// contract's state variables, functions and modifiers. @a _sourceIndices maps +/// source unit names to the numeric source IDs of the ethdebug.compilation +/// record, which declaration ranges are published with. +langutil::SemanticDebugDataTable buildSemanticDebugDataTable( + ContractDefinition const& _contract, + std::map const& _sourceIndices +); + +} // namespace solidity::frontend diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b2b6f95625ae..30356422ab6b 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include +#include #include #include @@ -79,6 +81,7 @@ #include #include +#include #include @@ -295,6 +298,10 @@ 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; } @@ -1161,7 +1168,24 @@ 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); + // The resource tables are derived from analysis results alone, so + // ethdebug.resources is available right after analysis. + ethdebug::Resources resources; + std::map const sourceIndexMap = sourceIndices(); + for (auto const& contractEntry: m_contracts) + { + Contract const& compiledContract = contractEntry.second; + if (!compiledContract.contract) + continue; + resources.merge(ethdebug::resources(buildSemanticDebugDataTable(*compiledContract.contract, sourceIndexMap))); + } + + return evmasm::ethdebug::resources( + ethdebugSources(), + VersionString, + std::move(resources.types), + std::move(resources.pointers) + ); } Json CompilerStack::ethdebugCompilation() const @@ -1204,8 +1228,16 @@ 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())); + + return evmasm::ethdebug::program( + _contract.contract->name(), + sourceIndexMap.at(_contract.contract->sourceUnitName()), + *assembly, + object, + ethdebug::programContext(buildSemanticDebugDataTable(*_contract.contract, sourceIndexMap)) + ); } bytes CompilerStack::cborMetadata(std::string const& _contractName, bool _forIR) const diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp new file mode 100644 index 000000000000..387d4af5ad9e --- /dev/null +++ b/libsolidity/interface/Ethdebug.cpp @@ -0,0 +1,137 @@ +/* + 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; +using namespace solidity::frontend; + +namespace schema = solidity::evmasm::ethdebug::schema; + +namespace +{ + +/// The declaration range already carries the source ID of the compilation +/// record, so it is published as is. +schema::materials::SourceRange declarationRange(SemanticDebugSourceRange const& _range) +{ + schema::materials::Reference reference; + reference.id = schema::materials::ID{static_cast(_range.sourceID)}; + reference.type = std::nullopt; + + schema::materials::SourceRange sourceRange; + sourceRange.source = std::move(reference); + sourceRange.range = schema::materials::SourceRange::Range{ + .length = schema::data::Unsigned{_range.length}, + .offset = schema::data::Unsigned{_range.offset} + }; + return sourceRange; +} + +} + +void ethdebug::Resources::merge(Resources const& _other) +{ + for (auto const& [id, document]: _other.types.items()) + types[id] = document; + for (auto const& [name, definition]: _other.pointers.items()) + pointers[name] = definition; +} + +ethdebug::Resources ethdebug::resources(SemanticDebugDataTable const& _table) +{ + Resources result; + for (auto const& [id, document]: _table.types()) + result.types[id] = document; + // Deserialization rejects templates carrying an internal expression, and + // the producer never puts one into a template, so the writer publishes + // exactly the sidecar's table. + for (auto const& [name, definition]: _table.pointerTemplates()) + result.pointers[name] = semanticDebugPointerTemplateToJson(definition); + return result; +} + +std::optional ethdebug::programContext(SemanticDebugDataTable const& _table) +{ + using Class = SemanticDebugPointer::Class; + + std::vector variables; + for (auto const& [key, scope]: _table.scopes()) + { + if (!scope) + continue; + + for (SemanticDebugVariable const& variable: scope->variableDefinitions) + { + if (variable.phase != SemanticDebugVariablePhase::Materialized || !variable.pointer) + continue; + + // A pointer that reads a generated Yul local, or a variable that + // nothing binds, is only meaningful once the Yul-to-EVM transform + // has resolved it; the program-level context carries the closed + // pointers, i.e. those of the state variables. + if (!semanticDebugPointerFreeNames(*variable.pointer, _table).empty()) + continue; + + // A template reference is resolved against the table; the template + // body is inlined, so the reference's renaming of regions does not + // apply, and the context variable's own identifier names the value. + SemanticDebugPointer const* pointer = &*variable.pointer; + if (pointer->pointerClass == Class::TemplateReference) + { + pointer = pointer->templateName ? _table.findPointerTemplate(*pointer->templateName) : nullptr; + if (!pointer) + continue; + } + + schema::program::Context::Variable contextVariable; + contextVariable.identifier = variable.identifier; + if (variable.declarationSourceRange) + contextVariable.declaration = declarationRange(*variable.declarationSourceRange); + // The type resources carry the document; the context references it. + if (variable.typeID) + contextVariable.type = Json{{"id", *variable.typeID}}; + if (pointer->expectedParameters.empty()) + { + Json lowered = semanticDebugPointerToJson(*pointer); + // The context variable's identifier already names it; a bare + // top-level region does not need an extra "name" property. + if (lowered.contains("location")) + lowered.erase("name"); + contextVariable.pointer = std::move(lowered); + } + 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..9e82a0429b1d --- /dev/null +++ b/libsolidity/interface/Ethdebug.h @@ -0,0 +1,61 @@ +/* + 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 the internal semantic debug info to the public ethdebug resource + * tables and program context. + */ + +#pragma once + +#include + +#include + +#include + +#include + +namespace solidity::frontend::ethdebug +{ + +/// The public ethdebug resource tables of a side table: the type documents +/// verbatim and the pointer templates in the schema's template form, under +/// the producer-defined names the sidecar keys them by. +struct Resources +{ + Json types = Json::object(); + Json pointers = Json::object(); + + /// Adds the tables of @a _other, replacing entries with the same key. + void merge(Resources const& _other); +}; + +Resources resources(langutil::SemanticDebugDataTable const& _table); + +/// The program-level ethdebug context: every materialized variable whose +/// pointer is closed, i.e. reads neither a generated Yul local nor an unbound +/// variable, with its declaration range, its type reference and, when its +/// pointer template expects no parameters, the pointer itself. A template that +/// expects parameters, such as mapping keys, stays in the resources for the +/// consumer to instantiate, and the variable is listed without a pointer. +/// @returns nothing when no variable qualifies. +std::optional programContext( + langutil::SemanticDebugDataTable const& _table +); + +} diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index c8cb87504624..2c7840e36212 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -962,7 +962,6 @@ std::variant StandardCompiler::parseI Error::Type::JSONError, "To use 'snippet' with settings.debug.debugInfo you must select also 'location'." ); - ret.debugInfoSelection = debugInfoSelection.value(); } } @@ -1257,33 +1256,40 @@ std::variant StandardCompiler::parseI return formatFatalError(Error::Type::FatalError, "'settings.debug.debugInfo' 'ethdebug' is only supported for languages 'Solidity' and 'Yul'."); } + // Selecting an ethdebug output does not modify the debug info selection, just + // as 'irOptimized' does not imply the optimizer: the selection must contain + // 'ethdebug' explicitly. + bool const ethdebugSelected = ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug; + if (isEthdebugProgramRequested(ret.outputSelection)) { if (ret.language == "Solidity" && !ret.viaIR) return formatFatalError(Error::Type::FatalError, "'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' can only be selected as output, if 'viaIR' was set."); - if (!ret.debugInfoSelection.has_value()) - { - ret.debugInfoSelection = DebugInfoSelection::Default(); - ret.debugInfoSelection->enable("ethdebug"); - } - else - { - if (!ret.debugInfoSelection->ethdebug && ret.language == "Solidity") - return formatFatalError(Error::Type::FatalError, "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output."); - } + if (!ethdebugSelected) + return formatFatalError(Error::Type::FatalError, "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output."); } if (ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) { 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."); + 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."); } @@ -1802,7 +1808,6 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) 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, "ast", wildcardMatchesExperimental)) { Json sourceResult; diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 54df68667b1a..ca56a04a8ae3 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -642,7 +642,7 @@ General Information)").c_str(), ("Debug info components to be included in the produced EVM assembly and Yul code. " "Value can be 'all', 'none' or a comma-separated list containing one or more of the " "following components: " + util::joinHumanReadable(DebugInfoSelection::AllExceptExperimental().selectedNames()) + - ", or ethdebug (experimental). " + ", or ethdebug (experimental, requires ast-id). " "Note that 'all' does not include experimental components.").c_str() ) ( @@ -1194,6 +1194,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(); @@ -1355,7 +1357,21 @@ void CommandLineParser::processArgs() m_options.output.viaSSACFG = m_args.contains(g_strViaSSACFG); - if (m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) + // Selecting an ethdebug output does not modify the debug info selection, + // just as --ir-optimized does not imply --optimize: the selection must + // contain ethdebug explicitly. + bool const ethdebugSelected = + m_options.output.debugInfoSelection.has_value() && m_options.output.debugInfoSelection->ethdebug; + if ((m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) && !ethdebugSelected) + solThrow( + CommandLineValidationError, + fmt::format( + "--debug-info must contain ethdebug when using --{} / --{}.", + CompilerOutputs::componentName(&CompilerOutputs::ethdebugProgram), + CompilerOutputs::componentName(&CompilerOutputs::ethdebugProgramRuntime) + ) + ); + if (ethdebugSelected) { if (m_options.output.viaSSACFG) solUnimplemented("ethdebug is not yet supported with --" + g_strViaSSACFG + "."); @@ -1363,12 +1379,6 @@ void CommandLineParser::processArgs() solUnimplemented( "Optimization (using --" + g_strOptimize + ") is not yet supported with ethdebug." ); - - if (!m_options.output.debugInfoSelection.has_value()) - { - m_options.output.debugInfoSelection = DebugInfoSelection::Default(); - m_options.output.debugInfoSelection->enable("ethdebug"); - } } return; } @@ -1525,19 +1535,14 @@ void CommandLineParser::processArgs() enableEthdebugProgramMessage + " output can only be selected, if --via-ir was specified." ); - 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 " + enableEthdebugProgramMessage + "." - ); - } + // Selecting an ethdebug output does not modify the debug info selection, + // just as --ir-optimized does not imply --optimize: the selection must + // contain ethdebug explicitly. + if (!m_options.output.debugInfoSelection.has_value() || !m_options.output.debugInfoSelection->ethdebug) + solThrow( + CommandLineValidationError, + "--debug-info must contain ethdebug when using " + enableEthdebugProgramMessage + "." + ); } if ( 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/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/ethdebug_on_abstract/args b/test/cmdlineTests/ethdebug_on_abstract/args index 7a58d286f809..c98066148481 100644 --- a/test/cmdlineTests/ethdebug_on_abstract/args +++ b/test/cmdlineTests/ethdebug_on_abstract/args @@ -1 +1 @@ ---experimental --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 +--experimental --debug-info location,snippet,ast-id,ethdebug --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/ethdebug_on_interface/args b/test/cmdlineTests/ethdebug_on_interface/args index 7a58d286f809..c98066148481 100644 --- a/test/cmdlineTests/ethdebug_on_interface/args +++ b/test/cmdlineTests/ethdebug_on_interface/args @@ -1 +1 @@ ---experimental --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 +--experimental --debug-info location,snippet,ast-id,ethdebug --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/args b/test/cmdlineTests/ethdebug_program_without_debug_info/args new file mode 100644 index 000000000000..eda454f50546 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/args @@ -0,0 +1 @@ +--experimental --ethdebug-program --via-ir diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/err b/test/cmdlineTests/ethdebug_program_without_debug_info/err new file mode 100644 index 000000000000..53d9566da3c4 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/err @@ -0,0 +1 @@ +Error: --debug-info must contain ethdebug when using --ethdebug-program / --ethdebug-program-runtime. diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/exit b/test/cmdlineTests/ethdebug_program_without_debug_info/exit new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/exit @@ -0,0 +1 @@ +1 diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol b/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol new file mode 100644 index 000000000000..cd008c45b465 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity >=0.0; + +interface C { + function f() external; +} diff --git a/test/cmdlineTests/metadata_experimental/args b/test/cmdlineTests/metadata_experimental/args index fa925f16ed8f..714483671a52 100644 --- a/test/cmdlineTests/metadata_experimental/args +++ b/test/cmdlineTests/metadata_experimental/args @@ -1 +1 @@ ---metadata --experimental --via-ir --ethdebug-program --ethdebug-compilation --no-cbor-metadata --pretty-json --json-indent 4 +--metadata --experimental --debug-info location,snippet,ast-id,ethdebug --via-ir --ethdebug-program --ethdebug-compilation --no-cbor-metadata --pretty-json --json-indent 4 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_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/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_ethdebug_parse_only_incompatible/input.json b/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json index 17afe568db11..974e7d4dc0d5 100644 --- a/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json +++ b/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json @@ -6,6 +6,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "stopAfter": "parsing", "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json index b83cf6508863..c9a29bb95183 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json index ff8dd714040e..0e136310fb02 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json index 514012ee7c20..68a55cda99c6 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json index 514012ee7c20..68a55cda99c6 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json index 9bab955c8985..45c72569d7e4 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json index 1a3fb90988e2..ac4f27d71e8a 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json index 23698d7c12a5..0f5907d3b05c 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json index ce2ae6f33e60..1b06b73f5a45 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json index 1034d8d37f05..d2b5feaf8481 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json index ba4046a64848..1b5cd2785737 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json new file mode 100644 index 000000000000..a4dc2be0f83f --- /dev/null +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json @@ -0,0 +1,27 @@ +{ + "language": "Solidity", + "sources": { + "a.sol": { + "content": "//SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.0;\ncontract A1 { function a(uint x) public pure { assert(x > 0); } } contract A2 { function a(uint x) public pure { assert(x > 0); } }" + }, + "b.sol": { + "content": "//SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.0;\ncontract A1 { function b(uint x) public pure { assert(x > 0); } } contract B2 { function b(uint x) public pure { assert(x > 0); } }" + } + }, + "settings": { + "experimental": true, + "viaIR": true, + "optimizer": { + "enabled": false + }, + "outputSelection": { + "*": { + "*": [ + "evm.bytecode.ethdebug", + "ir", + "ethdebug.resources" + ] + } + } + } +} diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json new file mode 100644 index 000000000000..eb6ccad4bbd0 --- /dev/null +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json @@ -0,0 +1,11 @@ +{ + "errors": [ + { + "component": "general", + "formattedMessage": "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output.", + "message": "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output.", + "severity": "error", + "type": "FatalError" + } + ] +} 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_output_selection_ethdebug_no_viair/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json index 574a8b1c43cd..270e8fdd1a7e 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "outputSelection": { "*": { "*": [ diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json index e1084799b98f..a733f6b9bc09 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json index 235e3c855fe6..be68769dda4f 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json b/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json index d5ad9153e9c9..16d99b03fb19 100644 --- a/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json +++ b/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json @@ -6,6 +6,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaSSACFG": true, "outputSelection": { 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/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json b/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json index c0ae2d50fedb..2fc0385db13b 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json index 617c93f688a7..835adb066024 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ir", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json b/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json index 5abd87a0db38..4d08c9ef3cd5 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "irOptimized", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json index ff39d837e10f..6306f980b983 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.deployedBytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json index 713c5c84231a..720f12391d01 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.deployedBytecode.ethdebug", "evm.bytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_ir/input.json index 617c93f688a7..835adb066024 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ir", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json b/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json index abcb5e96dfcd..44646f574ba9 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "irOptimize", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json index 8074135b4f3b..6a35ee35c102 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "optimizer": { "enabled": true diff --git a/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json b/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json index 417a42a31177..683ca199c41e 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "optimizer": { "enabled": true diff --git a/test/cmdlineTests/via_ssa_cfg_ethdebug/args b/test/cmdlineTests/via_ssa_cfg_ethdebug/args index 7c85efbc7f3c..bd920fce0d5e 100644 --- a/test/cmdlineTests/via_ssa_cfg_ethdebug/args +++ b/test/cmdlineTests/via_ssa_cfg_ethdebug/args @@ -1 +1 @@ ---experimental --via-ssa-cfg --ethdebug-program --ethdebug-compilation +--experimental --debug-info location,snippet,ast-id,ethdebug --via-ssa-cfg --ethdebug-program --ethdebug-compilation 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/ethdebugSchemaTests/sources/a.sol b/test/ethdebugSchemaTests/sources/a.sol index feada97f0246..fe84d71eee16 100644 --- a/test/ethdebugSchemaTests/sources/a.sol +++ b/test/ethdebugSchemaTests/sources/a.sol @@ -2,6 +2,12 @@ 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 bc176072a0fe..c57d909f79e2 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -104,6 +104,77 @@ 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"] + } + types = solc_output["ethdebug"]["resources"]["types"] + + def referenced_type(variable): + reference = variable["type"] + assert set(reference) == {"id"} + return types[reference["id"]] + + assert referenced_type(variables["stored"]) == {"kind": "uint", "bits": 128} + assert variables["stored"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "length": "0x10", + } + assert referenced_type(variables["enabled"]) == {"kind": "bool"} + assert variables["enabled"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } + + # Complex type resources reference their component resources by ID. + assert referenced_type(variables["balances"]) == { + "kind": "mapping", + "contains": { + "key": {"type": {"id": "t_address"}}, + "value": {"type": {"id": "t_uint256"}}, + }, + } + # 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 referenced_type(variables["values"]) == { + "kind": "array", + "contains": {"type": {"id": "t_uint256"}}, + } + 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 referenced_type(variables["label"]) == {"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")] + + 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 = { @@ -125,9 +196,53 @@ 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"] == {} - assert solc_output["ethdebug"]["resources"]["pointers"] == {} +def test_resources_include_type_and_pointer_tables(solc_output): + 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"] + 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 + + # 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): diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp new file mode 100644 index 000000000000..a559b984de3b --- /dev/null +++ b/test/liblangutil/DebugData.cpp @@ -0,0 +1,539 @@ +/* + 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 + +namespace solidity::langutil::test +{ + +namespace +{ + +SemanticDebugPointer storageRegion(std::string _name, std::string _slot) +{ + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + std::move(_name), + SemanticDebugPointerExpression::literal(std::move(_slot)) + ); +} + +} + +BOOST_AUTO_TEST_SUITE(DebugDataTest) + +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") + }) + ); + SemanticDebugPointer pointer = SemanticDebugPointer::makeGroup({ + storageRegion("values-length", "0x00"), + SemanticDebugPointer::scope( + {{"values-data", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x00")) + })}}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::read("values-length"), + "values-index", + std::move(element) + ) + ) + }); + + 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->indexName); + BOOST_CHECK_EQUAL(*pointer.group.at(1).scopeTarget->indexName, "values-index"); + 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->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 table is + // normalized, so the cycle is closed by an `{"id": ...}` reference and the + // documents pass through serialization untouched. + SemanticDebugDataTable table; + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.setType("t_array$_t_struct$_Node", Json{ + {"kind", "array"}, + {"contains", Json{{"type", Json{{"id", "t_struct$_Node"}}}}}}); + table.setType("t_struct$_Node", Json{ + {"kind", "struct"}, + {"contains", Json::array({ + Json{{"name", "value"}, {"type", Json{{"id", "t_uint256"}}}}, + Json{{"name", "children"}, {"type", Json{{"id", "t_array$_t_struct$_Node"}}}}})}, + {"definition", Json{{"name", "Node"}}}}); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["resources"]["types"].size(), 3); + BOOST_CHECK_EQUAL( + serialized["resources"]["types"]["t_array$_t_struct$_Node"]["contains"]["type"]["id"].get(), + "t_struct$_Node"); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.findType("t_struct$_Node") != nullptr); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) +{ + SemanticDebugScope scope; + auto semanticDebugScope = std::make_shared(std::move(scope)); + + SemanticDebugDataTable table; + BOOST_CHECK(table.empty()); + + table.set(23, semanticDebugScope); + + BOOST_CHECK(!table.empty()); + BOOST_CHECK(table.find(23) == semanticDebugScope); + BOOST_CHECK(!table.find(24)); + BOOST_CHECK(!table.find(23, 1)); + BOOST_CHECK(table.find(SemanticDebugDataTable::Key{23, 0}) == semanticDebugScope); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) +{ + Json containerType{ + {"kind", "struct"}, + {"contains", Json::array({Json{{"name", "member"}, {"type", Json{{"id", "t_uint256"}}}}})}, + {"definition", Json{ + {"name", "Container"}, + {"location", Json{ + {"range", Json{{"length", 16}, {"offset", 4}}}, + {"source", Json{{"id", 0}}}}}}}}; + + // A mapping value template, published in the pointer resources and + // instantiated by the consumer with the key. + SemanticDebugPointer mappingValue = storageRegion("value", "0x00"); + mappingValue.slot = SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + }); + mappingValue.expectedParameters = {"key"}; + + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"slot", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + })}}, + SemanticDebugPointer::conditional( + SemanticDebugPointerExpression::read("condition"), + SemanticDebugPointer::templateReference("mapping-value", {{"value", "renamed-value"}}), + SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "fallback", + SemanticDebugPointerExpression::variable("slot"), + SemanticDebugPointerExpression::literal("0x00"), + SemanticDebugPointerExpression::wordSize() + ) + ) + ); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.declarationASTID = 9; + variable.declarationSourceRange = SemanticDebugSourceRange{0, 21, 5}; + variable.typeID = "t_struct$_Container"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = std::move(pointer); + + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.setContractName("ContainerContract"); + table.setType("t_struct$_Container", std::move(containerType)); + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.setPointerTemplate("mapping-value", std::move(mappingValue)); + table.set(42, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["format"].get(), SemanticDebugDataFormat); + BOOST_CHECK_EQUAL(serialized["version"].get(), SemanticDebugDataFormatVersion); + BOOST_CHECK_EQUAL(serialized["contractName"], "ContainerContract"); + // The pointer template table mirrors ethdebug.resources.pointers. + BOOST_CHECK(serialized["resources"]["pointers"]["mapping-value"]["expect"] == Json::array({"key"})); + // Field names are identical in the JSON form and the compiler structures. + Json const& serializedVariable = serialized["scopes"]["42"]["0"]["variableDefinitions"][0]; + BOOST_CHECK_EQUAL(serializedVariable["declarationASTID"].get(), 9); + BOOST_CHECK_EQUAL(serializedVariable["declarationSourceRange"]["source"]["id"].get(), 0); + BOOST_CHECK_EQUAL(serializedVariable["declarationSourceRange"]["range"]["offset"].get(), 21); + BOOST_CHECK_EQUAL(serializedVariable["typeID"].get(), "t_struct$_Container"); + BOOST_CHECK_EQUAL(serializedVariable["phase"].get(), "materialized"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_malformed_keys) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["version"] = SemanticDebugDataFormatVersion + 1; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); + serialized["version"] = SemanticDebugDataFormatVersion; + + Json const emptyScope{{"variableDefinitions", Json::array()}}; + for (std::string const& astIDKey: {"abc", "01", "1.5", "", "-0"}) + { + Json malformed = serialized; + malformed["scopes"][astIDKey] = Json{{"0", emptyScope}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + } + for (std::string const& instanceKey: {"-1", "x", "00"}) + { + Json malformed = serialized; + malformed["scopes"]["1"] = Json{{instanceKey, emptyScope}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + } + + // A negative AST ID is unusual but within the documented int64 range. + serialized["scopes"]["-1"] = Json{{"0", emptyScope}}; + BOOST_CHECK(semanticDebugDataFromJson(serialized).find(-1) != nullptr); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_types_are_shared_by_id) +{ + auto makeVariable = [&](std::string _name) { + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.typeID = "t_uint256"; + return variable; + }; + SemanticDebugScope scope; + scope.variableDefinitions = {makeVariable("a"), makeVariable("b")}; + + SemanticDebugDataTable table; + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.set(1, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["resources"]["types"].size(), 1); + Json const& variables = serialized["scopes"]["1"]["0"]["variableDefinitions"]; + // Variables carry only the ID; the document lives in the table. + BOOST_CHECK(!variables[0].contains("type")); + BOOST_CHECK(!variables[1].contains("type")); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.findType("t_uint256") != nullptr); + BOOST_CHECK_EQUAL((*read.findType("t_uint256"))["bits"].get(), 256); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_updates_roundtrip) +{ + // The dbg.value analogue: a statement-level rebinding of a variable, here + // a spill to memory that keeps the phase and a drop to optimized-out. + SemanticDebugVariableUpdate spilled; + spilled.variableASTID = 42; + spilled.pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Memory, + "spill", + std::nullopt, + SemanticDebugPointerExpression::literal("0x80"), + SemanticDebugPointerExpression::wordSize()); + + SemanticDebugVariableUpdate dropped; + dropped.variableASTID = 42; + dropped.phase = SemanticDebugVariablePhase::OptimizedOut; + + SemanticDebugScope scope; + scope.variableUpdates = {spilled, dropped}; + SemanticDebugDataTable table; + table.set(7, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + Json const& updates = serialized["scopes"]["7"]["0"]["variableUpdates"]; + BOOST_REQUIRE_EQUAL(updates.size(), 2); + // A pointer-only update leaves the phase alone. + BOOST_CHECK(!updates[0].contains("phase")); + // A change to optimized-out clears the previous pointer. + BOOST_CHECK_EQUAL(updates[1]["phase"].get(), "optimized-out"); + BOOST_CHECK(!updates[1].contains("pointer")); + + // An update that changes nothing, or claims a pointer for an unavailable + // value, is malformed. + Json malformed = serialized; + malformed["scopes"]["7"]["0"]["variableUpdates"][1]["pointer"] = updates[0]["pointer"]; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + malformed = serialized; + malformed["scopes"]["7"]["0"]["variableUpdates"][0].erase("pointer"); + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), 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 + // records must coexist under the same AST ID and round-trip. + SemanticDebugDataTable table; + table.set({9, 0}, std::make_shared()); + table.set({9, 2}, std::make_shared()); + + Json serialized = semanticDebugDataToJson(table); + BOOST_REQUIRE_EQUAL(serialized["scopes"].size(), 1); + BOOST_CHECK_EQUAL(serialized["scopes"]["9"].size(), 2); + // Instance 0 is written explicitly. + BOOST_CHECK(serialized["scopes"]["9"].contains("0")); + BOOST_CHECK(serialized["scopes"]["9"].contains("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); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_phases_roundtrip) +{ + // A materialized variable has a pointer; the other phases do not. + SemanticDebugScope scope; + SemanticDebugVariable materialized; + materialized.phase = SemanticDebugVariablePhase::Materialized; + materialized.pointer = storageRegion("value", "0x00"); + scope.variableDefinitions.emplace_back(std::move(materialized)); + SemanticDebugVariable computed; + computed.phase = SemanticDebugVariablePhase::Computed; + scope.variableDefinitions.emplace_back(std::move(computed)); + SemanticDebugVariable optimizedOut; + optimizedOut.phase = SemanticDebugVariablePhase::OptimizedOut; + scope.variableDefinitions.emplace_back(std::move(optimizedOut)); + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + Json const serialized = semanticDebugDataToJson(table); + Json const& variables = serialized["scopes"]["1"]["0"]["variableDefinitions"]; + BOOST_REQUIRE_EQUAL(variables.size(), 3); + BOOST_CHECK_EQUAL(variables[0]["phase"].get(), "materialized"); + BOOST_CHECK_EQUAL(variables[1]["phase"].get(), "computed"); + BOOST_CHECK_EQUAL(variables[2]["phase"].get(), "optimized-out"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + + // The invariant is enforced in both directions. + SemanticDebugVariable inconsistent; + inconsistent.phase = SemanticDebugVariablePhase::Materialized; + SemanticDebugScope inconsistentScope; + inconsistentScope.variableDefinitions.emplace_back(std::move(inconsistent)); + SemanticDebugDataTable inconsistentTable; + inconsistentTable.set(1, std::make_shared(std::move(inconsistentScope))); + BOOST_CHECK_THROW(semanticDebugDataToJson(inconsistentTable), SemanticDebugDataSerializationError); + + Json malformed = serialized; + malformed["scopes"]["1"]["0"]["variableDefinitions"][1]["pointer"] = variables[0]["pointer"]; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(yul_locals_use_the_internal_expression_prefix) +{ + SemanticDebugVariable variable; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "x", + SemanticDebugPointerExpression::yulLocal("var_x_1")); + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + + Json const serialized = semanticDebugDataToJson(table); + Json const& slot = serialized["scopes"]["1"]["0"]["variableDefinitions"][0]["pointer"]["slot"]; + BOOST_CHECK((slot == Json{{"$$yulLocal", "var_x_1"}})); + SemanticDebugDataTable const read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.find(1)); + BOOST_REQUIRE(read.find(1)->variableDefinitions.front().pointer->slot); + BOOST_CHECK( + read.find(1)->variableDefinitions.front().pointer->slot->kind == + SemanticDebugPointerExpression::Kind::YulLocal); + + // Any other internal expression is unknown to the reader. + Json malformed = serialized; + malformed["scopes"]["1"]["0"]["variableDefinitions"][0]["pointer"]["slot"] = Json{{"$$other", "x"}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(template_references_must_resolve) +{ + SemanticDebugVariable variable; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = SemanticDebugPointer::templateReference("storage_1_2"); + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + + // Nothing defines the template: rejected on deserialization. + BOOST_CHECK_THROW(semanticDebugDataFromJson(semanticDebugDataToJson(table)), SemanticDebugDataSerializationError); + + // The sidecar's pointer table defines it. + table.setPointerTemplate("storage_1_2", storageRegion("value", "0x02")); + Json const serialized = semanticDebugDataToJson(table); + BOOST_CHECK(serialized["resources"]["pointers"]["storage_1_2"]["expect"] == Json::array()); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + + // A local templates block defines it for references inside its target. + SemanticDebugPointer local = SemanticDebugPointer::withTemplates( + {{"local-slot", storageRegion("local", "0x03")}}, + SemanticDebugPointer::templateReference("local-slot")); + SemanticDebugVariable localVariable; + localVariable.phase = SemanticDebugVariablePhase::Materialized; + localVariable.pointer = std::move(local); + SemanticDebugScope localScope; + localScope.variableDefinitions.emplace_back(std::move(localVariable)); + SemanticDebugDataTable localTable; + localTable.set(2, std::make_shared(std::move(localScope))); + Json const localSerialized = semanticDebugDataToJson(localTable); + BOOST_CHECK(localSerialized["scopes"]["2"]["0"]["variableDefinitions"][0]["pointer"].contains("templates")); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(localSerialized)) == localSerialized); +} + +BOOST_AUTO_TEST_CASE(number_literals_deserialize_as_hex_literals) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["resources"]["pointers"]["slot-one"] = Json{ + {"expect", Json::array()}, + {"for", Json{{"location", "storage"}, {"name", "value"}, {"slot", 1}, {"offset", 0}, {"length", 32}}} + }; + SemanticDebugDataTable table = semanticDebugDataFromJson(serialized); + SemanticDebugPointer const* pointer = table.findPointerTemplate("slot-one"); + BOOST_REQUIRE(pointer); + BOOST_REQUIRE(pointer->slot && pointer->slot->value); + BOOST_CHECK_EQUAL(*pointer->slot->value, "0x01"); + // The schema allows numbers; the compiler carries and re-emits them as hex literals. + Json const reserialized = semanticDebugDataToJson(table)["resources"]["pointers"]["slot-one"]["for"]; + BOOST_CHECK_EQUAL(reserialized["slot"].get(), "0x01"); + BOOST_CHECK_EQUAL(reserialized["offset"].get(), "0x00"); + BOOST_CHECK_EQUAL(reserialized["length"].get(), "0x20"); +} + +BOOST_AUTO_TEST_CASE(pointer_resources_reject_internal_expressions) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["resources"]["pointers"]["local"] = Json{ + {"expect", Json::array()}, + {"for", Json{{"location", "stack"}, {"name", "value"}, {"slot", Json{{"$$yulLocal", "var_value"}}}}} + }; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(malformed_pointer_documents_are_rejected) +{ + auto sidecarWithPointer = [](Json _pointer) { + Json serialized = semanticDebugDataToJson({}); + Json const variable{{"phase", "materialized"}, {"pointer", std::move(_pointer)}}; + serialized["scopes"]["1"] = Json{{"0", Json{{"variableDefinitions", Json::array({variable})}}}}; + return serialized; + }; + auto rejects = [&](Json _pointer) { + BOOST_CHECK_THROW(semanticDebugDataFromJson(sidecarWithPointer(std::move(_pointer))), SemanticDebugDataSerializationError); + }; + + BOOST_CHECK_NO_THROW(semanticDebugDataFromJson(sidecarWithPointer(Json{{"location", "storage"}, {"slot", "0x0"}}))); + // Only the schema's locations. + rejects(Json{{"location", "unknown"}, {"slot", "0x0"}}); + // A segment location needs a slot, a slice location an offset and a length. + rejects(Json{{"location", "storage"}, {"offset", "0x0"}}); + rejects(Json{{"location", "memory"}, {"offset", "0x0"}}); + // $sized takes a positive size. + rejects(Json{{"location", "storage"}, {"slot", Json{{"$sized0", "0x0"}}}}); + // No members outside the schema. + rejects(Json{{"location", "storage"}, {"slot", "0x0"}, {"extra", true}}); + // A group has at least one member. + rejects(Json{{"group", Json::array()}}); + // A template is not a pointer; it lives in the resources or a templates block. + rejects(Json{{"expect", Json::array()}, {"for", Json{{"location", "storage"}, {"slot", "0x0"}}}}); + // Nesting is bounded. + Json nested{{"location", "storage"}, {"slot", "0x0"}}; + for (size_t depth = 0; depth < 300; ++depth) + nested = Json{{"group", Json::array({nested})}}; + rejects(nested); +} + +BOOST_AUTO_TEST_CASE(free_names_resolve_through_templates_and_terminate_on_cycles) +{ + SemanticDebugDataTable table; + table.setPointerTemplate("a", SemanticDebugPointer::templateReference("b")); + table.setPointerTemplate("b", SemanticDebugPointer::templateReference("a")); + // A cyclic template pair resolves to no names rather than recursing forever. + BOOST_CHECK(semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("a"), table).empty()); + + SemanticDebugPointer open = storageRegion("value", "0x0"); + open.slot = SemanticDebugPointerExpression::variable("key"); + table.setPointerTemplate("open", open); + BOOST_CHECK((semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("open"), table) == std::set{"key"})); + + // The expected parameters of a template are bound by its instantiation. + open.expectedParameters = {"key"}; + table.setPointerTemplate("closed", open); + BOOST_CHECK(semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("closed"), table).empty()); + + // A Yul local is a name only the Yul stage can supply. + SemanticDebugPointer local = storageRegion("value", "0x0"); + local.slot = SemanticDebugPointerExpression::yulLocal("var_value"); + BOOST_CHECK((semanticDebugPointerFreeNames(local, table) == std::set{"var_value"})); + BOOST_CHECK(semanticDebugPointerHasInternalExpression(local)); + BOOST_CHECK(!semanticDebugPointerHasInternalExpression(open)); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::langutil::test diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index c13704cc061a..11d1ee23c819 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; @@ -1973,53 +1985,53 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { - 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") && @@ -2118,21 +2129,21 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, 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({"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({"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") && @@ -2140,19 +2151,24 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.deployedBytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, + // Selecting an ethdebug output does not enable 'ethdebug' in the debug info selection implicitly. + { + generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug"})), + std::nullopt + }, { generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebugs"})), std::nullopt @@ -2162,7 +2178,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2170,18 +2186,18 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "ir"}), YulCode()), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["object"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.deployedBytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug", "ir"}), YulCode()), std::nullopt }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"}), YulCode()), std::nullopt }, { @@ -2200,7 +2216,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) }, { generateExperimentalStandardJson( - true, {}, { + true, Json::array({"ast-id", "ethdebug"}), { {"fileA", {{"contractA", Json::array({"evm.deployedBytecode.ethdebug"})}}}, {"fileB", {{"contractB", Json::array({"evm.bytecode.ethdebug"})}}} }, @@ -2229,7 +2245,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::data::make({"deployedBytecode", "bytecode"}), bytecodeType) { frontend::StandardCompiler compiler; - Json result = compiler.compile(generateExperimentalStandardJson(true, {}, Json::array({std::string("evm.") + bytecodeType + ".ethdebug"}))); + Json result = compiler.compile(generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({std::string("evm.") + bytecodeType + ".ethdebug"}))); BOOST_REQUIRE(result["contracts"]["fileA"]["C"]["evm"][bytecodeType].contains("ethdebug")); bool creation = std::string(bytecodeType) == "bytecode"; Json ethdebugInstructionsToCheck = result["contracts"]["fileA"]["C"]["evm"][bytecodeType]["ethdebug"]; @@ -2269,6 +2285,22 @@ BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::d } } +BOOST_AUTO_TEST_CASE(ethdebug_rejects_yul_optimization) +{ + frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; + Json input = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({"ir"}), + YulCode(std::move(yulSources)) + ); + 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/libsolidity/ethdebugTests/basic_contract.sol b/test/libsolidity/ethdebugTests/basic_contract.sol index 7b5fd3174258..0099cc6cedc3 100644 --- a/test/libsolidity/ethdebugTests/basic_contract.sol +++ b/test/libsolidity/ethdebugTests/basic_contract.sol @@ -19,6 +19,9 @@ 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 +// .resources.pointers | length: 1 // // C.contract.name: C // C.creation.environment: create diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 69a6d344f880..bc801d528574 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -1419,25 +1419,25 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_incompatible_outputs) // has been removed. These combinations are now valid as long as other requirements are met. static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program-runtime", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program-runtime", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--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"}, }, }; @@ -1551,6 +1555,10 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) { {"solc", "--experimental", "--ethdebug-program", tempDir.path().string() + "/input.sol"}, }, + // Selecting an ethdebug output does not enable --debug-info ethdebug implicitly. + { + {"solc", "--experimental", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + }, { {"solc", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", tempDir.path().string() + "/input.sol"}, }, @@ -1560,28 +1568,28 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) }; static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--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", "--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", "--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", "--ethdebug-program", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program-runtime", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, }, { {"solc", "--experimental", "--ethdebug-resources", tempDir.path().string() + "/input.sol"}, diff --git a/test/solc/CommandLineParser.cpp b/test/solc/CommandLineParser.cpp index 5dd00e05aab8..cad7c5cd36e3 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -650,37 +650,45 @@ 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"}); - 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); - BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); - // --ethdebug-program implicitly enables debug-info ethdebug - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--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 implicitly enables debug-info ethdebug - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--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); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // Selecting an ethdebug output does not modify the debug-info selection: without + // --debug-info ethdebug the program outputs are an error. + auto expectsExplicitEthdebug = [](CommandLineValidationError const& _exception) { + return std::string(_exception.what()).find("--debug-info must contain ethdebug") != std::string::npos; + }; + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program-runtime", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "location", "--ethdebug-program", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); // both --ethdebug-program and --ethdebug-program-runtime - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); 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);