diff --git a/docs/index.rst b/docs/index.rst index 77d06cd6b31c..9e4b70041510 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,6 +149,7 @@ Contents internals/layout_in_calldata.rst internals/variable_cleanup.rst internals/source_mappings.rst + internals/ethdebug_internal_metadata.rst internals/optimizer.rst metadata.rst abi-spec.rst diff --git a/docs/internals/ethdebug_internal_metadata.rst b/docs/internals/ethdebug_internal_metadata.rst new file mode 100644 index 000000000000..758944b7e820 --- /dev/null +++ b/docs/internals/ethdebug_internal_metadata.rst @@ -0,0 +1,538 @@ +.. index:: ethdebug, debug info, metadata + +************************** +ETHDebug Internal Metadata +************************** + +.. warning:: + + ETHDebug support and the interchange format described here are experimental. + They may change before ETHDebug output is stabilized. + +The compiler emits public debug information using the +`ethdebug format `_. +The structures on this page are not a second public ETHDebug format. +They are the compiler interchange model used to carry source-language semantics through Yul and later lower them to public ETHDebug types, pointers, and instruction contexts. + +Design Requirements +=================== + +The metadata pipeline has to preserve these compiler properties: + +- A Solidity-to-Yul invocation followed by a Yul-to-bytecode invocation must be able to produce the same bytecode and debug metadata as a one-stage invocation. +- A language frontend that targets Yul must be able to supply its own semantic debug metadata without linking against in-memory Solidity compiler objects. +- Every value crossing the Solidity/Yul boundary must have a serialization format. +- Source-language declaration identity and generated-Yul instance identity must not be conflated. +- Location categories must describe EVM machine state rather than Solidity-only language constructs. + +For these reasons, semantic metadata is a versioned JSON sidecar paired with Yul text. +The Yul text carries ``@ast-id`` comments that identify source-language origins. +The sidecar carries the semantic records associated with those origins. +Both artifacts are required at a compiler-to-compiler text boundary. + +Pipeline and Generation Time +============================ + +The current Solidity pipeline performs the following steps: + +1. Solidity analysis assigns AST IDs and populates the existing type annotations on declarations. +2. IR generation emits Yul and, when requested, ``@ast-id`` comments. +3. After IR generation, ``frontend::buildSemanticDebugDataTable()`` builds semantic records on demand from the analyzed AST and from the same ``IRVariable`` naming rules used by code generation. +4. ``CompilerStack`` attaches the table after parsing generated Yul into a ``YulStack``. +5. ``YulStack`` carries semantic records on Yul ``DebugData`` objects and retains unattached records in its side table. +6. Before a requested Yul reparse, the stack collects attached records into the retained table and reattaches them after parsing the printed Yul. +7. The EVM code generator lowers surviving records into public ETHDebug resources and contexts. + +Semantic type descriptors are not new analysis annotations. +They are derived on demand from ``VariableDeclaration::annotation().type`` after analysis succeeds. +This avoids eagerly constructing ETHDebug-specific types when ETHDebug was not requested. +The builder also queries storage layout and ``IRVariable`` during IR code generation because those details are code-generation properties rather than type-analysis results. + +No semantic table is carried through Yul when neither an ETHDebug artifact nor the ``ethdebug`` debug-info component is requested. + +Debug-Info Dependency +===================== + +Semantic metadata transfer currently depends on ``@ast-id`` comments. +Accordingly, ``ethdebug`` depends on the ``ast-id`` debug-info component. +This is an explicit dependency. +CLI and Standard JSON input reject an explicit selection containing ``ethdebug`` without ``ast-id``, just as ``snippet`` without ``location`` is rejected. +An output option that selects ETHDebug implicitly uses the complete required selection because the user did not provide a partial debug-info list. + +Core Structures +=============== + +``langutil::DebugData`` is the debug payload carried by Yul AST nodes. +The public format this feeds is the `ethdebug/format specification `_; the structures below mirror its `program `_, `type `_, and `pointer `_ schemas, and differ from them only where the compiler needs information that the public format does not carry. + +.. list-table:: ``DebugData`` fields relevant to ETHDebug + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``nativeLocation`` + - ``SourceLocation`` + - Location in the current Yul text. + * - ``originLocation`` + - ``SourceLocation`` + - Location in the source language. + * - ``astID`` + - optional integer + - Source-language AST origin copied from ``@ast-id``. + * - ``semanticDebugData`` + - optional ``SemanticDebugData`` pointer + - Semantic scope payload attached to this Yul node. + +``langutil::SemanticDebugData`` describes one semantic scope. + +.. list-table:: ``SemanticDebugData`` + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``lexicalScopeID`` + - optional integer + - Source-language AST identity of the scope origin. + * - ``variableDefinitions`` + - array of ``SemanticDebugVariable`` + - Bindings introduced by or visible through the scope record, in source order. + +``SemanticDebugVariable`` separates source identity, source position, static type, and current EVM data location. + +.. list-table:: ``SemanticDebugVariable`` + :header-rows: 1 + :widths: 27 25 48 + + * - Field + - Type + - Meaning + * - ``identifier`` + - optional string + - Source-language identifier. + It is absent for unnamed variables such as unnamed Solidity return parameters. + * - ``declarationAstID`` + - optional integer + - Identity of the source-language declaration. + Synthetic bindings may omit it. + * - ``declarationSourceLocation`` + - optional ``SourceLocation`` + - Source range of the declaration. + * - ``typeID`` + - optional string + - Compiler type identifier used as the exported type-resource key. + This is the ``id`` of the spec's `type reference mechanism `_ - producer-defined by the spec, and this producer uses its native type identifier as the value. + * - ``ethdebugType`` + - optional ``SemanticDebugType`` + - ETHDebug-oriented static type descriptor. + It is the value that gets registered in the public ``ethdebug.resources.types`` output under ``typeID``, not a second copy of it: variables sharing a type share the ID, and the descriptor is written once per ID. + In the serialized sidecar the ``types`` table holds these in the shape of the public type schema itself - the sidecar does not define a second type format. + It is carried through the sidecar rather than resolved at analysis time because the Yul boundary has to be crossable by a producer that is not Solidity, and such a producer has no analysis output to refer to. + * - ``dataLocation`` + - optional ``SemanticDebugVariableLocation`` + - Current abstract EVM location of the value. + * - ``ethdebugPointer`` + - optional ``SemanticDebugPointer`` + - Pointer expression resolving the value in that location. + +The pointer expression is the mapping from a source-language variable to where its value can be found. +How it refers to that place depends on whether the address is known while generating code, and the distinction is what makes the information survive optimisation. + +**Statically addressed values** - storage, transient storage, and code - are resolved during Yul code generation. +A state variable's pointer is its slot and offset, written as literals or as an expression over externally bound parameters such as mapping keys. +No Yul variable name appears in it, so no optimiser pass can invalidate it: there is nothing to rewrite. + +**Stack- and memory-backed values** cannot be resolved that early, because the stack slot does not exist until the Yul-to-EVM transform has run. +For these the pointer is not the carrier. +The variable's identity travels on the ``DebugData`` of the Yul node that produces the value, which the optimiser already propagates as it rewrites code, and the pointer is completed at emission time, once a slot has been assigned. + +**Yul variable names are not a durable handle, and pointer expressions do not use them as one.** +Consider a storage read that code generation emits as its own local:: + + let _1 := read_from_storage_split_offset_0_t_uint256(0x00) + let _2 := foo(_1) + +An optimiser pass may fold this to ``let _2 := foo(read_from_storage_split_offset_0_t_uint256(0x00))``. +A pointer that said "read the stack slot named ``_1``" would then name a variable that no longer exists. +Under the rule above it never said that: the variable is in storage, so its pointer is ``{"location": "storage", "slot": "0x00"}``, which the fold does not touch. + +This costs one thing worth stating. +The debug info no longer records that ``_1`` held the value at that moment - only that the source variable exists in the enclosing scope and where its value can be read from. +That is a deliberate trade: a consumer can always compute the value from the resolved pointer, and nothing else in the format depends on knowing which generated local happened to carry it. + +**Pointer expressions never contain arbitrary Yul.** +Embedding the generating expression would make the sidecar a second copy of the program: every optimiser pass would have to rewrite the embedded code as well as the code itself, and the expressions would be as large and as complex as whatever the user wrote. +The vocabulary above is closed for that reason. + +Scope Attachment +---------------- + +Semantic scope data belongs to the Yul node that introduces the corresponding scope. + +- Function and modifier records attach to the generated Yul ``FunctionDefinition``, not to its body block. +- A source block lowered to a distinct Yul block attaches to that ``Block``. +- A conditional or loop that introduces no separate source scope does not receive a scope payload merely because it contains a block. +- A conditional, loop clause, or catch clause that does introduce a source scope attaches its payload to the generated block representing that scope. +- Contract and file scopes with no corresponding Yul node remain side-table-only records and are still serialized. + +The transfer visitor can carry ``DebugData`` on all Yul node kinds, including names in parameter and return lists. +The producer is responsible for selecting the node that semantically owns a scope. + +Type Descriptors +================ + +``SemanticDebugType`` mirrors the public ETHDebug type vocabulary while retaining a small amount of compiler-only information needed during lowering. + +.. list-table:: Common ``SemanticDebugType`` fields + :header-rows: 1 + :widths: 24 24 52 + + * - Field + - Type + - Meaning + * - ``typeClass`` + - enum + - Elementary, complex, or unknown representation. + * - ``kind`` + - enum + - Integer, bytes, string, address, contract, enum, alias, tuple, array, mapping, slice, struct, function, or unknown kind. + * - ``bits``, ``places``, ``bytes`` + - optional integers + - Width information for numeric and fixed-bytes types. + ``bytes`` is also the representation width of address-carrying kinds: ``address`` and ``contract`` set it to ``20`` today, and a wider address is the same kind with a different width rather than a new kind. + * - ``payable``, ``isLibrary``, ``isInterface`` + - optional booleans + - Address and contract properties. + * - ``enumValues`` + - array of strings + - Enum members in declaration order. + * - ``count`` + - optional hexadecimal string + - Fixed array length. + * - ``components`` + - array of ``SemanticDebugTypeComponent`` + - Recursive element, key, value, member, parameter, return, underlying, or contract components. + * - ``definitionName`` + - optional string + - Name of a user-defined type declaration. + * - ``definitionLocation`` + - optional ``SourceLocation`` + - Source range of that declaration. + +The ``kind`` enumeration follows `ethdebug/format types `_, which does not name every Solidity type directly. +Three cases are worth stating explicitly. + +**User-defined value types** have no separate kind. +They are represented as ``alias``, carrying the declaration's name and source range and a single ``Underlying`` component for the wrapped type. +A consumer that does not care about the distinction can follow the component and treat the value as its underlying type. + +**Array slices** are the ``slice`` kind, with the element type as an ``Element`` component and dynamic sizing. +Their representation is identical to an ordinary calldata array - offset and length both on stack - and the public format has no slice kind, so they are published as dynamic arrays; the sidecar keeps the distinction for the compiler's own use. + +**Address width is data, not a kind.** +``address`` and ``contract`` carry their representation width in ``bytes``, today always ``20``. +A migration to 32-byte addresses changes the value of that field and nothing else in this format; the contract and external function types that contain an address inherit the same rule. + +**The vocabulary is extended through the format version.** +New kinds are introduced by incrementing the serialization version, and readers already reject a version they do not support, so an old reader fails loudly rather than misreading a new kind. +``unknown`` remains reserved for types the producer genuinely cannot describe, not as an escape hatch for types the vocabulary has yet to name. + +Every type component contains a role, an optional member name, an optional reference ID, and an optional inline type. +When recursively describing a type already present on the current recursion path, the inline type is omitted and the reference ID terminates the cycle. +This represents recursive structs without infinitely expanding them. + +Data Locations and Pointers +=========================== + +``SemanticDebugVariableLocation`` describes the EVM-level representation of a value. +It does not encode Solidity concepts such as ``constant`` or ``immutable`` as location kinds. + +.. list-table:: Variable data-location kinds + :header-rows: 1 + :widths: 25 75 + + * - Kind + - Meaning + * - ``Stack`` + - One or more EVM stack slots, initially represented by symbolic Yul variable names. + * - ``Storage`` + - Persistent storage. + * - ``TransientStorage`` + - Transaction-scoped transient storage. + * - ``Memory`` + - EVM memory. + * - ``Calldata`` + - Call input data. + * - ``Returndata`` + - Return data exposed by the EVM return-data buffer. + * - ``Code`` + - Bytes embedded in creation or runtime bytecode. + * - ``Computed`` + - A value with no persistent allocation that must be recomputed or rematerialized. + * - ``OptimizedOut`` + - No recoverable representation is available at this program point. + +Solidity immutables therefore have a memory location while creation code initializes them and a code location when read from deployed code. +A constant folded to an immediate may be represented by code bytes, while a constant expression that must execute is computed. +This distinction also works for future reference-type constants and for non-Solidity languages. + +``SemanticDebugPointer`` represents either one EVM region or a structured composition. + +.. list-table:: Pointer classes + :header-rows: 1 + :widths: 24 76 + + * - Class + - Meaning + * - ``Region`` + - A stack, storage, transient, memory, calldata, returndata, or code range. + * - ``Group`` + - An ordered group of pointers. + * - ``List`` + - A repeated element pointer with a count and bound index name. + * - ``Conditional`` + - A pointer selected by a condition. + * - ``Scope`` + - Ordered auxiliary definitions evaluated within a target pointer. + * - ``TemplateReference`` + - A reference to a separately exported pointer template, by ``templateName``, together with the ``yields`` bindings that name its results. + +Slots, offsets, lengths, counts, and conditions are ``SemanticDebugPointerExpression`` trees. +The vocabulary is the one defined by `ethdebug/format pointer expressions `_, restricted to the subset the compiler currently produces: + +.. list-table:: Expression kinds + :header-rows: 1 + :widths: 32 68 + + * - Kind + - Meaning + * - ``Literal`` + - A constant, written as a hexadecimal string. + * - ``WordSize`` + - The machine word size, ``$wordsize``. + * - ``Variable`` + - A name bound by an enclosing ``Scope``, ``List`` index, or ``expectedParameters``, and therefore resolvable by the consumer. + * - ``YulLocal`` + - A generated Yul local standing in for a stack depth not yet known. + Internal only: nothing binds the name, so a pointer still holding one when ETHDebug is emitted is dropped rather than published as though the name were a slot. + * - ``RegionLookup`` + - A named property of another region, such as its offset or length. + * - ``RegionRead`` + - The value stored in another region, ``$read``. + * - ``Arithmetic`` + - ``$sum``, ``$difference``, ``$product``, ``$quotient``, ``$remainder``. + * - ``Keccak256`` + - ``$keccak256`` of a concatenation, used for mapping and dynamic array slots. + * - ``Concat`` + - ``$concat``, the byte concatenation the hash is taken over. + * - ``Resize`` + - ``$resize``, adjusting a value to a given byte width. + +The remaining expression kinds in the ethdebug specification are accepted by the reader but not emitted by the Solidity producer. +Root pointers list externally bound ``expectedParameters`` such as mapping keys. +Pointers with expected parameters are exported as templates rather than closed program-context pointers. + +Templates are not a second store. +They are exactly the entries of the public `ethdebug.resources.pointers `_ output, keyed by the ``pointerID`` recorded on a variable's data location, each holding the parameters it expects and the pointer they parameterise. +A ``TemplateReference`` in the sidecar names one of those keys. +The sidecar carries the reference so that a variable can point at a shared template without repeating it; the template itself lives in the resource output, which is where a consumer reads it from. + +Location Changes +---------------- + +``dataLocation`` is the location valid for the program point represented by the containing debug context. +It is not permanently the declaration's initial location. +A value may move, split into multiple regions, be rematerialized, or become unavailable as optimization and code generation proceed. + +Optimizer transformations that preserve a value must rewrite its pointer expressions to the new Yul names or machine regions. +Transformations that clone or specialize code must clone the scope instance metadata as well. +If the compiler cannot describe a surviving value soundly, it must use ``OptimizedOut`` rather than retain a stale pointer. + +The implemented conservative rule checks all free Yul variable names used by a stack pointer after attachment or reparse. +If any required name is absent from the current Yul object, the location becomes ``OptimizedOut`` and the pointer is removed. +Bound names from pointer scopes, list indices, and template parameters are not mistaken for Yul dependencies. + +Location Updates +~~~~~~~~~~~~~~~~ + +A single per-scope location cannot describe a value that moves during its lifetime. +``SSATransform`` gives one source variable a different generated name at each assignment; ``Rematerialiser`` replaces stored values with recomputation at individual uses. +For these, an entry carries ``locationUpdates`` - the analogue of an ``llvm.dbg.value`` intrinsic: + +.. list-table:: ``SemanticDebugLocationUpdate`` fields + :header-rows: 1 + :widths: 28 24 48 + + * - Field + - Type + - Meaning + * - ``variableAstId`` + - integer + - Declaration AST ID of the variable being rebound. + * - ``dataLocation`` + - ``SemanticDebugVariableLocation`` + - The location valid from the carrying node onward. + * - ``pointer`` + - optional ``SemanticDebugPointer`` + - Absent when the location kind carries no address - ``OptimizedOut`` in particular. + +An update takes effect at the Yul node its payload is attached to and holds until the next update for the same variable within the scope, or the scope's end. +Scope entries define variables; statement-level entries update them. +A ``Drop`` is an update to ``OptimizedOut``. +Code generation emits no updates, so the field is absent from un-optimized output; only passes whose effect a static location cannot express produce them. + +Optimizer Update Rules +---------------------- + +Every transformation that rewrites Yul declares how it maintains the semantic side table. +Only the transformation knows whether a value survived unchanged, moved, merged with another value, or disappeared, so the obligation cannot be discharged by the code that runs it. +A pass with no declared strategy is treated as ``Drop``, so an undeclared pass degrades debug info instead of producing stale pointers. + +.. list-table:: Debug-info update strategies + :header-rows: 1 + :widths: 14 48 38 + + * - Strategy + - Obligation + - Passes + * - ``Preserve`` + - Copy the debug entry unchanged. + Valid only when the pass changes neither the names a pointer reads nor the region a value lives in. + - ``ForLoopInitRewriter``, ``ForLoopConditionIntoBody``, ``ForLoopConditionOutOfBody``, ``VarDeclInitializer``, ``FunctionHoister``, ``FunctionGrouper``, ``ConditionalUnsimplifier`` + * - ``Merge`` + - Keep the surviving value's entry when two values or two blocks collapse into one. + Record the discarded declarations against the same program point so both source names remain inspectable. + - ``CommonSubexpressionEliminator``, ``ExpressionJoiner``, ``ExpressionSimplifier``, ``ControlFlowSimplifier``, ``StructuralSimplifier``, ``BlockFlattener``, ``EquivalentFunctionCombiner``, ``LoadResolver`` + * - ``Remap`` + - Rewrite what the entry refers to when the pass moves it. + Renaming passes rewrite the Yul names an entry is attached to; because a statically addressed pointer holds no Yul name, renaming cannot reach one. + Spilling passes move where the value is *found*, so the pointer changes from a ``Stack`` to a ``Memory`` region; the data itself is not relocated by the debug info. + - ``Disambiguator``, ``NameSimplifier``, ``VarNameCleaner``, ``SSATransform``, ``SSAReverser``, ``ExpressionSplitter``, ``LoopInvariantCodeMotion``, ``StackToMemoryMover``, ``StackCompressor``, ``StackLimitEvader`` + * - ``Clone`` + - Duplicate the debug entry for each generated copy and give each copy its own ``scopeInstanceID``. + Without the instance discriminator the copies overwrite each other in the table. + - ``FullInliner``, ``FunctionSpecializer``, ``ExpressionInliner`` + * - ``Drop`` + - Set the location to ``OptimizedOut`` and remove the pointer. + The declaration, type, and source location are retained so the variable is still reported as belonging to the scope. + - ``DeadCodeEliminator``, ``UnusedPruner``, ``UnusedAssignEliminator``, ``UnusedStoreEliminator``, ``EqualStoreEliminator``, ``CircularReferencesPruner``, ``UnusedFunctionParameterPruner`` + +``SSATransform`` and ``Rematerialiser`` cannot be expressed as a whole-scope ``Remap``, because the answer to "where is the variable" changes between program points. +They emit location updates instead - see `Location Updates`_. +Substituting a variable use by its defining expression can leave the variable itself unused and later pruned. +The value is then still recoverable by evaluating that expression, so its location becomes ``Computed`` rather than ``OptimizedOut``. + +A pass that cannot meet its obligation for a particular value falls back to ``Drop`` for that value alone, not for the whole scope. +Dropping a location is always sound, while keeping a pointer that no longer describes the value is not. + +Identity and Optimizer Cloning +============================== + +A source-language AST ID is an origin identity, not a unique generated-code identity. +The same source function can produce multiple specialized or cloned Yul functions. +Those instances share declaration and type information but can have different current locations. + +The complete model therefore requires two identities: + +- ``originAstID`` identifies the source-language node and is preserved by ``@ast-id``. +- ``scopeInstanceID`` uniquely identifies one generated Yul scope instance and is serialized as the entry's ``instance`` field, defaulting to ``0``. + The table key is the ``(astId, instance)`` pair, so admitting cloned code is not a format change: code generation emits instance ``0`` only, and cloning passes assign fresh instances to the copies they make. + +On the un-cloned IR path currently supported by ETHDebug, where Yul optimization is rejected, every entry has instance ``0`` and the AST ID alone is in practice unique. +It must not be treated as sufficient for optimizer passes that clone or specialize functions. +Before those passes are enabled with ETHDebug, the table and Yul annotations must gain the instance discriminator so per-instance location updates cannot overwrite each other. + +Serialization +============= + +``liblangutil/SemanticDebugDataSerialization.h`` provides both serialization and deserialization. +The complete table, including recursive types, pointer expressions, source names, ordered definitions, and unattached scope records, is serialized. + +.. list-table:: Top-level JSON object + :header-rows: 1 + :widths: 22 22 56 + + * - Field + - Type + - Meaning + * - ``format`` + - string + - ``solidity-ethdebug-semantic-data``. + * - ``version`` + - integer + - Format version, currently ``1``. + * - ``contractName`` + - optional string + - Source-language contract name used by the public ETHDebug program object. + Generated Yul object names are not assumed to preserve it. + * - ``types`` + - optional object + - Type documents in the shape of the `ethdebug type schema `_, keyed by ``typeId`` and written once each. + The table is normalized: a composed type with an ID of its own is a table entry, and its containers refer to it with the spec's `type reference `_ form ``{"id": ...}`` - which is also how a recursive type closes its cycle. + A variable whose descriptor is here carries only the ID; a descriptor without an ID stays inline on its variable. + Three deviations from the public schema, all deliberate: definition locations reference sources by name, because source indices are per-invocation and the sidecar must survive into another one; the compiler's ``slice`` kind and the address-width ``size`` appear under the versioned-extension rule; and a type the producer cannot name uses the schema's own class-only form. + * - ``entries`` + - array + - Objects containing ``astId``, an optional ``instance``, and ``data``, where ``data`` is a ``SemanticDebugData`` value. + This is not ``langutil::DebugData``, and it does not repeat the AST ID: the key appears once, on the entry. + The table is an array rather than an object because the key is a pair of integers and JSON object keys are neither, and because an ordered array makes the writer's output deterministic without relying on key ordering rules. + Entries are keyed by ``(astId, instance)``. + ``instance`` defaults to ``0`` and is omitted when zero, which is the only value code generation produces - so un-cloned output carries no discriminator at all. + A pass that clones a scope gives each copy its own instance; the reader rejects a repeated pair. + +Readers reject an unknown format, an unsupported version, malformed tagged values, and duplicate table keys. +Writers emit deterministic entry order because the table is ordered by key. + +Compiler Interfaces +------------------- + +Standard JSON uses these fields: + +- Solidity output ``contracts[][].ir`` contains the Yul text. +- Solidity output ``contracts[][].irEthdebug`` contains its semantic sidecar. +- Yul input ``auxiliaryInput.ethdebug`` supplies the sidecar for the Yul source. +- Yul output ``irEthdebug`` emits the retained sidecar again. + +The command-line interface uses these options: + +- ``--ir`` and ``--ir-ethdebug`` emit the Yul text and sidecar. +- ``--strict-assembly --ethdebug-input `` supplies an unqualified sidecar when there is exactly one Yul input. +- Repeated ``--ethdebug-input =`` options pair sidecars with multiple Yul inputs. + +The sidecar is deliberately separate from public ``ethdebug/format`` JSON. +It is accepted compiler input, not merely an in-memory cache. + +Complete Variable Model +======================= + +The intended producer covers every binding visible to source-level debugging, not only the subset already emitted by the implementation. + +- Named and unnamed function parameters and return parameters are included. +- Modifier parameters and local variables in ordinary blocks are included. +- Variables introduced by ``for``, ``if``, ``try``, success, ``catch``, error, and panic clauses are included in the precise scope where they become visible. +- State variables in persistent and transient storage are included. +- File-level constants are included. +- Every visible import alias is a separate binding with its alias identifier but shares the declaration identity and value description of the imported constant. +- Synthetic language bindings such as ``this``, ``super``, and ``msg`` are included even when there is no ``VariableDeclaration`` AST node. +- Values in memory, calldata, returndata, code, and computed form are included when they are observable. + +ETHDebug variable identifiers are optional. +An unnamed Solidity return parameter is therefore emitted with declaration, type, and pointer information but without ``identifier``. +Its declaration order and AST identity still map it to the corresponding Solidity return slot and generated ``IRVariable`` stack slots. + +The current Solidity producer implements function and modifier parameters, named and unnamed function returns, inherited and free-function records, and named persistent and transient state variables. +It recursively describes mappings, arrays, ``bytes``, strings, structs, aliases, enums, contracts, and function types. +Locals, clause variables, file-level constants and aliases, synthetic bindings, and non-storage reference locations remain implementation work under the model above. + +Testing +======== + +Solidity-side producer tests that compile source live in the Ethdebug isoltest suite. +The Ethdebug isoltest suite exposes the serialized semantic sidecar as ``Contract.semantic`` and covers function, modifier, unnamed-return, inheritance, free-function, type, and storage-pointer production. +Low-level data-model and Yul-transfer tests remain focused C++ unit tests. +``DebugDataTest`` covers the data model and bidirectional JSON round trips. +``YulDebugDataTest`` covers attachment, reparse survival, and conservative location invalidation. +Standard JSON and CLI tests cover serialized two-stage compilation and sidecar pairing. diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index 04ad54a99aef..e962aa5b278a 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -275,6 +275,17 @@ Input Description } } }, + // Optional auxiliary inputs. + "auxiliaryInput": { + // Internal ethdebug semantic data sidecar for Yul input (experimental). + // This is the object emitted as the Solidity `irEthdebug` output. + "ethdebug": { + "format": "solidity-ethdebug-semantic-data", + "version": 1, + "contractName": "ContractName", + "entries": [/* ... */] + } + }, // Optional "settings": { @@ -391,7 +402,9 @@ Input Description // The snippet is quoted and follows the corresponding `@src` annotation. // - `ast-id`: Annotations of the form `@ast-id ` over elements that can be mapped back to a definition in the original Solidity file. // `` is a node ID in the Solidity AST ('ast' output). - // - `ethdebug`: Ethdebug annotations (experimental). Automatically enabled when any ethdebug output is requested. + // - `ethdebug`: Ethdebug annotations (experimental). Depends on `ast-id`; explicitly selecting + // `ethdebug` without `ast-id` is an error. The required pair is automatically enabled when + // an ethdebug output is requested without an explicit `debugInfo` selection. // - `*`: Wildcard value that can be used to request all non-experimental components. "debugInfo": ["location", "snippet", "ast-id", "ethdebug"] }, @@ -444,6 +457,7 @@ Input Description // userdoc - User documentation (natspec) // metadata - Metadata // ir - Yul intermediate representation of the code before optimization + // irEthdebug - Internal ethdebug semantic data sidecar for the Yul IR (experimental) // irAst - AST of Yul intermediate representation of the code before optimization (experimental) // irOptimized - Intermediate representation after optimization // irOptimizedAst - AST of intermediate representation after optimization (experimental) @@ -602,6 +616,8 @@ Output Description "devdoc": {}, // Intermediate representation before optimization (string) "ir": "", + // Internal ethdebug semantic data sidecar for Yul (experimental) + "irEthdebug": {/* ... */}, // AST of intermediate representation before optimization "irAst": {/* ... */}, // Intermediate representation after optimization (string) @@ -769,7 +785,8 @@ The table below details all currently available experimental features. +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | Non-mainnet EVMs | ``evm`` | yes | ``--evm-version `` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ -| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, ``--debug-info ethdebug`` | +| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, ``--ir-ethdebug``, | +| | | | ``--ethdebug-input``, ``--debug-info ast-id,ethdebug`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | | | no | ``--yul-cfg-json`` | | SSA CFG + ``ssa-cfg`` +------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/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..fdd0a26fee31 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -17,6 +17,10 @@ set(sources Scanner.cpp Scanner.h CharStreamProvider.h + SemanticDebugData.h + SemanticDebugDataSerialization.cpp + SemanticDebugDataSerialization.h + SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/DebugData.h b/liblangutil/DebugData.h index 70259bd039f3..8d1cdf940729 100644 --- a/liblangutil/DebugData.h +++ b/liblangutil/DebugData.h @@ -18,9 +18,11 @@ #pragma once +#include #include #include #include +#include namespace solidity::langutil { @@ -32,23 +34,27 @@ struct DebugData explicit DebugData( langutil::SourceLocation _nativeLocation = {}, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + SemanticDebugData::ConstPtr _semanticDebugData = {} ): nativeLocation(std::move(_nativeLocation)), originLocation(std::move(_originLocation)), - astID(_astID) + astID(_astID), + semanticDebugData(std::move(_semanticDebugData)) {} static DebugData::ConstPtr create( langutil::SourceLocation _nativeLocation, langutil::SourceLocation _originLocation = {}, - std::optional _astID = {} + std::optional _astID = {}, + SemanticDebugData::ConstPtr _semanticDebugData = {} ) { return std::make_shared( std::move(_nativeLocation), std::move(_originLocation), - _astID + _astID, + std::move(_semanticDebugData) ); } @@ -65,6 +71,8 @@ struct DebugData langutil::SourceLocation originLocation; /// ID in the (Solidity) source AST. std::optional astID; + /// Extended semantic debug data that cannot be represented in Yul comments. + SemanticDebugData::ConstPtr semanticDebugData; }; } // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h new file mode 100644 index 000000000000..0b592633b8af --- /dev/null +++ b/liblangutil/SemanticDebugData.h @@ -0,0 +1,551 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +struct SemanticDebugVariableLocation +{ + enum class Kind + { + Stack, + Storage, + TransientStorage, + Memory, + Calldata, + Returndata, + Code, + Computed, + OptimizedOut + }; + + Kind kind = Kind::OptimizedOut; + std::optional pointerID; +}; + +/// Node in an ethdebug/format/pointer/expression tree. Expressions evaluate to +/// unsigned values and may reference named regions and externally bound +/// variables (scope definitions, list indices or template parameters). +struct SemanticDebugPointerExpression +{ + enum class Kind + { + Unknown, + /// Literal unsigned value. @a value holds the canonical `0x`-prefixed hex form. + Literal, + /// The EVM word size in bytes (`$wordsize`). + WordSize, + /// Reference to a variable bound by a scope definition, a list index name or + /// a template parameter. Such a name is resolvable by the consumer, so it + /// is emitted as written. + Variable, + /// A generated Yul local standing in for a stack depth that is not known + /// until the Yul-to-EVM transform has run. + /// + /// This is not a @a Variable: 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, and a pointer still holding + /// one at emission time is dropped rather than published. + 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; + } +}; + +struct SemanticDebugType; + +/// Reference to a composed type: a stable compiler type identifier usable as an +/// `{"id": ...}` reference into the type resources table and/or an inline +/// representation. The inline representation is absent when it would recurse +/// into a type that is currently being described (recursive structs). +/// Mirrors the ethdebug/format/type wrapper and specifier schemas. +struct SemanticDebugTypeComponent +{ + enum class Role + { + /// Array element type. + Element, + /// Mapping key type. + Key, + /// Mapping value type. + Value, + /// Struct member or tuple element type. + Member, + /// Function parameter type. + Parameter, + /// Function return type. + Return, + /// Underlying type of a user defined value type. + Underlying, + /// Contract type providing an external function. + Contract + }; + + Role role = Role::Member; + /// Member, element or parameter name, if any. + std::optional name; + /// Stable compiler type identifier, e.g. `t_uint256`. + std::optional referenceID; + /// Inline type representation. Null when cut to break a recursive type cycle, + /// in which case @a referenceID identifies the type. + std::shared_ptr type; +}; + +struct SemanticDebugType +{ + enum class Class + { + Elementary, + Complex, + Unknown + }; + + enum class Kind + { + Uint, + Int, + Ufixed, + Fixed, + Bool, + Bytes, + String, + Address, + Contract, + Enum, + Alias, + Tuple, + Array, + Mapping, + /// A view over a section of an array. Same representation as the array + /// it slices; the element type is its `Element` component. + Slice, + Struct, + Function, + Unknown + }; + + Class typeClass = Class::Unknown; + Kind kind = Kind::Unknown; + + // Elementary payloads. + std::optional bits; + std::optional places; + std::optional bytes; + std::optional payable; + std::optional isLibrary; + std::optional isInterface; + /// Names of the enum members, in declaration order. + std::vector enumValues = {}; + + // Complex payloads. + /// Fixed element count of a statically sized array, as canonical + /// `0x`-prefixed hex. Unset for dynamically sized arrays. + std::optional count; + /// Function types: true for externally callable functions, false for internal + /// ones. Function types with unknown visibility leave this unset. + std::optional externalFunction; + /// Composed types: array element, mapping key/value, struct members, tuple + /// elements, alias underlying type, function parameters and returns. + std::vector components = {}; + + /// Source definition of user defined types (contract, enum, struct, alias, function). + std::optional definitionName; + std::optional definitionLocation; + +}; + +/// Internal representation of an ethdebug/format/pointer: a single region of EVM +/// data or a structured collection of sub-pointers. Which fields are meaningful +/// depends on @a pointerClass. +struct SemanticDebugPointer +{ + enum class Class + { + /// A single addressed range of data. Uses @a location, @a name, @a slot, + /// @a offset and @a length. + Region, + /// An ordered composition of sub-pointers in @a group. + Group, + /// A dynamically sized repetition: @a count elements, the index bound to + /// @a indexName inside @a listElement. + List, + /// A pointer chosen by the non-zero-ness of @a condition: @a thenPointer, + /// otherwise the optional @a elsePointer. + Conditional, + /// A pointer with auxiliary variables: ordered @a definitions bound inside + /// @a scopeTarget. Later definitions may reference earlier ones. + Scope, + /// A reference to a pointer template defined elsewhere: @a templateName, + /// with produced region names optionally renamed through @a yields. + TemplateReference, + Unknown + }; + + enum class Location + { + Stack, + Storage, + Transient, + Memory, + Calldata, + Returndata, + Code, + Unknown + }; + + Class pointerClass = Class::Unknown; + + /// Template parameters (ethdebug pointer template `expect` list) that must be + /// bound externally before this pointer can be evaluated, e.g. mapping keys. + /// Only meaningful on a root pointer. + std::vector expectedParameters = {}; + + // Class::Region + std::optional location; + std::optional name; + /// Word-oriented locations (stack, storage, transient) address by slot. + std::optional slot; + /// Byte offset: within the slot for word-oriented locations, absolute for + /// byte-oriented ones (memory, calldata, returndata, code). + std::optional offset; + /// Byte length of the region. + std::optional length; + + // Class::Group + std::vector group = {}; + + // Class::List + std::optional count; + std::optional indexName; + std::shared_ptr listElement; + + // Class::Conditional + std::optional condition; + std::shared_ptr thenPointer; + std::shared_ptr elsePointer; + + // Class::Scope + std::vector> definitions = {}; + std::shared_ptr scopeTarget; + + // Class::TemplateReference + std::optional templateName; + std::vector> yields = {}; + + static SemanticDebugPointer region( + Location _location, + std::optional _name, + std::optional _slot, + std::optional _offset = std::nullopt, + std::optional _length = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Region; + result.location = _location; + result.name = std::move(_name); + result.slot = std::move(_slot); + result.offset = std::move(_offset); + result.length = std::move(_length); + return result; + } + + static SemanticDebugPointer makeGroup(std::vector _members) + { + SemanticDebugPointer result; + result.pointerClass = Class::Group; + result.group = std::move(_members); + return result; + } + + static SemanticDebugPointer list( + SemanticDebugPointerExpression _count, + std::string _indexName, + SemanticDebugPointer _element + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::List; + result.count = std::move(_count); + result.indexName = std::move(_indexName); + result.listElement = std::make_shared(std::move(_element)); + return result; + } + + static SemanticDebugPointer conditional( + SemanticDebugPointerExpression _condition, + SemanticDebugPointer _then, + std::optional _else = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Conditional; + result.condition = std::move(_condition); + result.thenPointer = std::make_shared(std::move(_then)); + if (_else) + result.elsePointer = std::make_shared(std::move(*_else)); + return result; + } + + static SemanticDebugPointer scope( + std::vector> _definitions, + SemanticDebugPointer _target + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Scope; + result.definitions = std::move(_definitions); + result.scopeTarget = std::make_shared(std::move(_target)); + return result; + } +}; + +struct SemanticDebugVariable +{ + /// 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 declarationSourceLocation; + std::optional typeID; + std::optional ethdebugType; + std::optional dataLocation; + std::optional ethdebugPointer; +}; + +/// Rebinds a variable's current location from the carrying Yul node onward, +/// within the variable's scope, until the next update - the analogue of an +/// `llvm.dbg.value` intrinsic. A single per-scope location cannot describe a +/// pass that gives one source variable different generated names over its +/// lifetime (SSATransform) or replaces stored values with recomputation +/// (Rematerialiser); this record can, and Drop is an update to OptimizedOut. +struct SemanticDebugLocationUpdate +{ + /// Declaration AST ID of the variable being rebound. + int64_t variableAstID = 0; + SemanticDebugVariableLocation dataLocation; + /// Absent when the location kind carries no address - OptimizedOut in + /// particular. + std::optional ethdebugPointer; +}; + +struct SemanticDebugData +{ + using ConstPtr = std::shared_ptr; + + std::optional lexicalScopeID; + std::vector variableDefinitions = {}; + /// Location rebindings taking effect at the node this payload is attached + /// to. Scope entries define variables; statement-level entries update them. + std::vector locationUpdates = {}; +}; + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp new file mode 100644 index 000000000000..4ee06f506714 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -0,0 +1,1072 @@ +/* + 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 + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +void require(bool _condition, std::string const& _message) +{ + solRequire(_condition, SemanticDebugDataSerializationError, _message); +} + +void requireObject(Json const& _json, std::string const& _path) +{ + require(_json.is_object(), _path + " must be an object."); +} + +void requireArray(Json const& _json, std::string const& _path) +{ + require(_json.is_array(), _path + " must be an array."); +} + +Json const& requiredMember(Json const& _json, std::string const& _name, std::string const& _path) +{ + requireObject(_json, _path); + require(_json.contains(_name), _path + "." + _name + " is required."); + return _json.at(_name); +} + +std::string requiredString(Json const& _json, std::string const& _name, std::string const& _path) +{ + Json const& value = requiredMember(_json, _name, _path); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +template +T requiredInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + Json const& value = requiredMember(_json, _name, _path); + require(value.is_number_integer(), _path + "." + _name + " must be an integer."); + if constexpr (std::is_unsigned_v) + { + require( + value.is_number_unsigned() || value.get() >= 0, + _path + "." + _name + " must not be negative."); + Json::number_unsigned_t rawValue = value.get(); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + } + else + { + if (value.is_number_unsigned()) + require( + value.get() + <= static_cast(std::numeric_limits::max()), + _path + "." + _name + " is too large."); + else + { + Json::number_integer_t rawValue = value.get(); + require(rawValue >= std::numeric_limits::min(), _path + "." + _name + " is too small."); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + } + } + return value.get(); +} + +template +std::optional optionalValue(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + if constexpr (std::is_integral_v && std::is_unsigned_v && !std::is_same_v) + { + Json const& value = _json.at(_name); + require(value.is_number_integer(), _path + "." + _name + " must be an integer."); + require( + value.is_number_unsigned() || value.get() >= 0, + _path + "." + _name + " must not be negative."); + Json::number_unsigned_t rawValue = value.get(); + require(rawValue <= std::numeric_limits::max(), _path + "." + _name + " is too large."); + return static_cast(rawValue); + } + else if constexpr (std::is_integral_v && std::is_signed_v) + return requiredInteger(_json, _name, _path); + try + { + return _json.at(_name).get(); + } + catch (Json::exception const& _exception) + { + solThrow( + SemanticDebugDataSerializationError, + _path + "." + _name + " has an invalid value: " + std::string(_exception.what())); + } +} + +std::optional optionalString(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + Json const& value = _json.at(_name); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +using VariableLocationKind = SemanticDebugVariableLocation::Kind; +constexpr std::pair variableLocationKindNames[]{ + {VariableLocationKind::Stack, "stack"}, + {VariableLocationKind::Storage, "storage"}, + {VariableLocationKind::TransientStorage, "transientStorage"}, + {VariableLocationKind::Memory, "memory"}, + {VariableLocationKind::Calldata, "calldata"}, + {VariableLocationKind::Returndata, "returndata"}, + {VariableLocationKind::Code, "code"}, + {VariableLocationKind::Computed, "computed"}, + {VariableLocationKind::OptimizedOut, "optimizedOut"} +}; + +using ExpressionKind = SemanticDebugPointerExpression::Kind; +constexpr std::pair expressionKindNames[]{ + {ExpressionKind::Unknown, "unknown"}, + {ExpressionKind::Literal, "literal"}, + {ExpressionKind::WordSize, "wordSize"}, + {ExpressionKind::Variable, "variable"}, + {ExpressionKind::YulLocal, "yulLocal"}, + {ExpressionKind::LookupSlot, "lookupSlot"}, + {ExpressionKind::LookupOffset, "lookupOffset"}, + {ExpressionKind::LookupLength, "lookupLength"}, + {ExpressionKind::Read, "read"}, + {ExpressionKind::Sum, "sum"}, + {ExpressionKind::Product, "product"}, + {ExpressionKind::Difference, "difference"}, + {ExpressionKind::Quotient, "quotient"}, + {ExpressionKind::Remainder, "remainder"}, + {ExpressionKind::Keccak256, "keccak256"}, + {ExpressionKind::Concat, "concat"}, + {ExpressionKind::Resize, "resize"} +}; + +using TypeClass = SemanticDebugType::Class; +constexpr std::pair typeClassNames[]{ + {TypeClass::Elementary, "elementary"}, + {TypeClass::Complex, "complex"}, + {TypeClass::Unknown, "unknown"} +}; + +using TypeKind = SemanticDebugType::Kind; +constexpr std::pair typeKindNames[]{ + {TypeKind::Uint, "uint"}, + {TypeKind::Int, "int"}, + {TypeKind::Ufixed, "ufixed"}, + {TypeKind::Fixed, "fixed"}, + {TypeKind::Bool, "bool"}, + {TypeKind::Bytes, "bytes"}, + {TypeKind::String, "string"}, + {TypeKind::Address, "address"}, + {TypeKind::Contract, "contract"}, + {TypeKind::Enum, "enum"}, + {TypeKind::Alias, "alias"}, + {TypeKind::Tuple, "tuple"}, + {TypeKind::Array, "array"}, + {TypeKind::Mapping, "mapping"}, + {TypeKind::Slice, "slice"}, + {TypeKind::Struct, "struct"}, + {TypeKind::Function, "function"}, + {TypeKind::Unknown, "unknown"} +}; + +using PointerClass = SemanticDebugPointer::Class; +constexpr std::pair pointerClassNames[]{ + {PointerClass::Region, "region"}, + {PointerClass::Group, "group"}, + {PointerClass::List, "list"}, + {PointerClass::Conditional, "conditional"}, + {PointerClass::Scope, "scope"}, + {PointerClass::TemplateReference, "templateReference"}, + {PointerClass::Unknown, "unknown"} +}; + +using PointerLocation = SemanticDebugPointer::Location; +constexpr std::pair pointerLocationNames[]{ + {PointerLocation::Stack, "stack"}, + {PointerLocation::Storage, "storage"}, + {PointerLocation::Transient, "transient"}, + {PointerLocation::Memory, "memory"}, + {PointerLocation::Calldata, "calldata"}, + {PointerLocation::Returndata, "returndata"}, + {PointerLocation::Code, "code"}, + {PointerLocation::Unknown, "unknown"} +}; + +template +std::string enumToString(Enum _value, Names const& _names) +{ + for (auto const& [value, name]: _names) + if (_value == value) + return std::string(name); + solAssert(false, "Unhandled semantic debug data enum value."); +} + +template +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 + "\"."); +} + +std::string variableLocationKindToString(SemanticDebugVariableLocation::Kind _kind) +{ + return enumToString(_kind, variableLocationKindNames); +} + +SemanticDebugVariableLocation::Kind variableLocationKindFromString(std::string const& _kind, std::string const& _path) +{ + return enumFromString(_kind, variableLocationKindNames, _path); +} + +std::string expressionKindToString(SemanticDebugPointerExpression::Kind _kind) +{ + return enumToString(_kind, expressionKindNames); +} + +SemanticDebugPointerExpression::Kind expressionKindFromString(std::string const& _kind, std::string const& _path) +{ + return enumFromString(_kind, expressionKindNames, _path); +} + +std::string typeClassToString(SemanticDebugType::Class _class) +{ + return enumToString(_class, typeClassNames); +} + +SemanticDebugType::Class typeClassFromString(std::string const& _class, std::string const& _path) +{ + return enumFromString(_class, typeClassNames, _path); +} + +SemanticDebugType::Kind typeKindFromString(std::string const& _kind, std::string const& _path) +{ + return enumFromString(_kind, typeKindNames, _path); +} + +std::string pointerClassToString(SemanticDebugPointer::Class _class) +{ + return enumToString(_class, pointerClassNames); +} + +SemanticDebugPointer::Class pointerClassFromString(std::string const& _class, std::string const& _path) +{ + return enumFromString(_class, pointerClassNames, _path); +} + +std::string pointerLocationToString(SemanticDebugPointer::Location _location) +{ + return enumToString(_location, pointerLocationNames); +} + +SemanticDebugPointer::Location pointerLocationFromString(std::string const& _location, std::string const& _path) +{ + return enumFromString(_location, pointerLocationNames, _path); +} + + +template +void setOptional(Json& _json, std::string const& _name, std::optional const& _value) +{ + if (_value) + _json[_name] = *_value; +} + +Json sourceLocationToJson(SourceLocation const& _location) +{ + Json result{{"start", _location.start}, {"end", _location.end}}; + if (_location.sourceName) + result["sourceName"] = *_location.sourceName; + return result; +} + +SourceLocation sourceLocationFromJson(Json const& _json, std::string const& _path) +{ + SourceLocation result; + result.start = requiredInteger(_json, "start", _path); + result.end = requiredInteger(_json, "end", _path); + if (std::optional sourceName = optionalString(_json, "sourceName", _path)) + result.sourceName = std::make_shared(std::move(*sourceName)); + return result; +} + +Json expressionToJson(SemanticDebugPointerExpression const& _expression); +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path); +Json typeToJson(SemanticDebugType const& _type); +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path); +Json pointerToJson(SemanticDebugPointer const& _pointer); +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path); + +Json expressionToJson(SemanticDebugPointerExpression const& _expression) +{ + Json result{{"kind", expressionKindToString(_expression.kind)}}; + setOptional(result, "value", _expression.value); + if (!_expression.operands.empty()) + { + result["operands"] = Json::array(); + for (auto const& operand: _expression.operands) + result["operands"].emplace_back(expressionToJson(operand)); + } + return result; +} + +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugPointerExpression result; + result.kind = expressionKindFromString(requiredString(_json, "kind", _path), _path + ".kind"); + result.value = optionalString(_json, "value", _path); + if (_json.contains("operands")) + { + Json const& operands = _json.at("operands"); + requireArray(operands, _path + ".operands"); + for (size_t index = 0; index < operands.size(); ++index) + result.operands.emplace_back( + expressionFromJson(operands.at(index), _path + ".operands[" + std::to_string(index) + "]")); + } + return result; +} + +/// The sidecar serializes types in the shape of the public ethdebug type +/// schema: what the table holds *is* the type schema, with references by ID +/// (`{"id": ...}`) between entries, exactly as the spec's type/reference +/// mechanism defines. Two stated deviations, documented in the internals doc: +/// definition locations reference sources by name, since source indices are +/// per-invocation, and the compiler's own `slice` kind and address-width +/// `size` appear under the versioned-extension rule. +Json typeToJson(SemanticDebugType const& _type); + +/// The wrapper form: `{"name"?, "type": }`. A component +/// with a reference ID is emitted as a reference - its definition lives in the +/// shared table - and one without an ID is inlined, having nowhere else to +/// live. +Json typeWrapperToJson(SemanticDebugTypeComponent const& _component) +{ + Json wrapper = Json::object(); + if (_component.name) + wrapper["name"] = *_component.name; + if (_component.referenceID) + wrapper["type"] = Json{{"id", *_component.referenceID}}; + else if (_component.type) + wrapper["type"] = typeToJson(*_component.type); + return wrapper; +} + +Json typeWrapperArrayToJson( + SemanticDebugType const& _type, SemanticDebugTypeComponent::Role _role) +{ + Json wrappers = Json::array(); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + wrappers.emplace_back(typeWrapperToJson(component)); + return wrappers; +} + +std::optional typeSingleWrapperToJson( + SemanticDebugType const& _type, SemanticDebugTypeComponent::Role _role) +{ + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + return typeWrapperToJson(component); + return std::nullopt; +} + +Json typeToJson(SemanticDebugType const& _type) +{ + using Kind = SemanticDebugType::Kind; + using Role = SemanticDebugTypeComponent::Role; + + Json result = Json::object(); + auto attachDefinition = [&]() { + Json definition = Json::object(); + if (_type.definitionName) + definition["name"] = *_type.definitionName; + if (_type.definitionLocation) + definition["location"] = sourceLocationToJson(*_type.definitionLocation); + if (!definition.empty()) + result["definition"] = std::move(definition); + }; + auto attachSingle = [&](Role _role) { + if (std::optional wrapper = typeSingleWrapperToJson(_type, _role)) + result["contains"] = std::move(*wrapper); + }; + + switch (_type.kind) + { + case Kind::Uint: + case Kind::Int: + result["kind"] = _type.kind == Kind::Uint ? "uint" : "int"; + setOptional(result, "bits", _type.bits); + break; + case Kind::Ufixed: + case Kind::Fixed: + result["kind"] = _type.kind == Kind::Ufixed ? "ufixed" : "fixed"; + setOptional(result, "bits", _type.bits); + setOptional(result, "places", _type.places); + break; + case Kind::Bool: + result["kind"] = "bool"; + break; + case Kind::Bytes: + result["kind"] = "bytes"; + setOptional(result, "size", _type.bytes); + break; + case Kind::String: + result["kind"] = "string"; + break; + case Kind::Address: + result["kind"] = "address"; + setOptional(result, "payable", _type.payable); + setOptional(result, "size", _type.bytes); + break; + case Kind::Contract: + result["kind"] = "contract"; + setOptional(result, "payable", _type.payable); + if (_type.isLibrary && *_type.isLibrary) + result["library"] = true; + else if (_type.isInterface && *_type.isInterface) + result["interface"] = true; + setOptional(result, "size", _type.bytes); + attachDefinition(); + break; + case Kind::Enum: + { + result["kind"] = "enum"; + Json values = Json::array(); + for (std::string const& value: _type.enumValues) + values.emplace_back(value); + result["values"] = std::move(values); + attachDefinition(); + break; + } + case Kind::Alias: + result["kind"] = "alias"; + attachSingle(Role::Underlying); + attachDefinition(); + break; + case Kind::Tuple: + result["kind"] = "tuple"; + result["contains"] = typeWrapperArrayToJson(_type, Role::Member); + break; + case Kind::Array: + result["kind"] = "array"; + attachSingle(Role::Element); + setOptional(result, "count", _type.count); + break; + case Kind::Slice: + result["kind"] = "slice"; + attachSingle(Role::Element); + break; + case Kind::Mapping: + { + Json contains = Json::object(); + if (std::optional key = typeSingleWrapperToJson(_type, Role::Key)) + contains["key"] = std::move(*key); + if (std::optional value = typeSingleWrapperToJson(_type, Role::Value)) + contains["value"] = std::move(*value); + result["kind"] = "mapping"; + result["contains"] = std::move(contains); + break; + } + case Kind::Struct: + result["kind"] = "struct"; + result["contains"] = typeWrapperArrayToJson(_type, Role::Member); + attachDefinition(); + break; + case Kind::Function: + { + result["kind"] = "function"; + if (_type.externalFunction) + result[*_type.externalFunction ? "external" : "internal"] = true; + Json contains{{"parameters", + Json{{"type", Json{{"kind", "tuple"}, {"contains", typeWrapperArrayToJson(_type, Role::Parameter)}}}}}}; + bool hasReturns = false; + for (SemanticDebugTypeComponent const& component: _type.components) + hasReturns = hasReturns || component.role == Role::Return; + if (hasReturns) + contains["returns"] + = Json{{"type", Json{{"kind", "tuple"}, {"contains", typeWrapperArrayToJson(_type, Role::Return)}}}}; + result["contains"] = std::move(contains); + attachDefinition(); + break; + } + case Kind::Unknown: + // The schema's own form for a type it cannot name: the class alone. + result["class"] = typeClassToString(_type.typeClass); + break; + } + return result; +} + +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path); + +SemanticDebugTypeComponent typeWrapperFromJson( + Json const& _json, SemanticDebugTypeComponent::Role _role, std::string const& _path) +{ + SemanticDebugTypeComponent result; + result.role = _role; + result.name = optionalString(_json, "name", _path); + if (_json.contains("type")) + { + Json const& type = _json.at("type"); + requireObject(type, _path + ".type"); + // `{"id": ...}` is the spec's type reference; anything else is inline. + if (type.contains("id") && !type.contains("kind") && !type.contains("class")) + { + Json const& id = type.at("id"); + require(id.is_string(), _path + ".type.id must be a string."); + result.referenceID = id.get(); + } + else + result.type = std::make_shared(typeFromJson(type, _path + ".type")); + } + return result; +} + +void typeWrappersFromJson( + SemanticDebugType& _result, + Json const& _wrappers, + SemanticDebugTypeComponent::Role _role, + std::string const& _path) +{ + requireArray(_wrappers, _path); + for (size_t index = 0; index < _wrappers.size(); ++index) + _result.components.emplace_back( + typeWrapperFromJson(_wrappers.at(index), _role, _path + "[" + std::to_string(index) + "]")); +} + +SemanticDebugType typeFromJson(Json const& _json, std::string const& _path) +{ + using Kind = SemanticDebugType::Kind; + using Role = SemanticDebugTypeComponent::Role; + + SemanticDebugType result; + if (!_json.contains("kind")) + { + // The class-only form: a type the producer could not name. + result.kind = Kind::Unknown; + result.typeClass + = typeClassFromString(requiredString(_json, "class", _path), _path + ".class"); + return result; + } + + result.kind = typeKindFromString(requiredString(_json, "kind", _path), _path + ".kind"); + switch (result.kind) + { + case Kind::Uint: + case Kind::Int: + case Kind::Ufixed: + case Kind::Fixed: + case Kind::Bool: + case Kind::Bytes: + case Kind::String: + case Kind::Address: + case Kind::Contract: + case Kind::Enum: + result.typeClass = SemanticDebugType::Class::Elementary; + break; + default: + result.typeClass = SemanticDebugType::Class::Complex; + break; + } + + result.bits = optionalValue(_json, "bits", _path); + result.places = optionalValue(_json, "places", _path); + result.bytes = optionalValue(_json, "size", _path); + result.payable = optionalValue(_json, "payable", _path); + if (optionalValue(_json, "library", _path).value_or(false)) + result.isLibrary = true; + if (optionalValue(_json, "interface", _path).value_or(false)) + result.isInterface = true; + result.count = optionalString(_json, "count", _path); + if (_json.contains("external")) + result.externalFunction = true; + else if (_json.contains("internal")) + result.externalFunction = false; + + if (_json.contains("values")) + { + Json const& values = _json.at("values"); + requireArray(values, _path + ".values"); + for (size_t index = 0; index < values.size(); ++index) + { + require( + values.at(index).is_string(), + _path + ".values[" + std::to_string(index) + "] must be a string."); + result.enumValues.emplace_back(values.at(index).get()); + } + } + + if (_json.contains("definition")) + { + Json const& definition = _json.at("definition"); + requireObject(definition, _path + ".definition"); + result.definitionName = optionalString(definition, "name", _path + ".definition"); + if (definition.contains("location")) + result.definitionLocation + = sourceLocationFromJson(definition.at("location"), _path + ".definition.location"); + } + + if (_json.contains("contains")) + { + Json const& contains = _json.at("contains"); + std::string const path = _path + ".contains"; + switch (result.kind) + { + case Kind::Alias: + result.components.emplace_back(typeWrapperFromJson(contains, Role::Underlying, path)); + break; + case Kind::Array: + case Kind::Slice: + result.components.emplace_back(typeWrapperFromJson(contains, Role::Element, path)); + break; + case Kind::Tuple: + case Kind::Struct: + typeWrappersFromJson(result, contains, Role::Member, path); + break; + case Kind::Mapping: + requireObject(contains, path); + if (contains.contains("key")) + result.components.emplace_back(typeWrapperFromJson(contains.at("key"), Role::Key, path + ".key")); + if (contains.contains("value")) + result.components.emplace_back( + typeWrapperFromJson(contains.at("value"), Role::Value, path + ".value")); + break; + case Kind::Function: + { + requireObject(contains, path); + auto wrappedTuple = [&](std::string const& _member, Role _role) { + if (!contains.contains(_member)) + return; + Json const& wrapper = contains.at(_member); + requireObject(wrapper, path + "." + _member); + Json const& tuple = requiredMember(wrapper, "type", path + "." + _member); + typeWrappersFromJson( + result, + requiredMember(tuple, "contains", path + "." + _member + ".type"), + _role, + path + "." + _member + ".type.contains"); + }; + wrappedTuple("parameters", Role::Parameter); + wrappedTuple("returns", Role::Return); + break; + } + default: + break; + } + } + return result; +} + +Json pointerToJson(SemanticDebugPointer const& _pointer) +{ + Json result{{"class", pointerClassToString(_pointer.pointerClass)}}; + if (!_pointer.expectedParameters.empty()) + result["expectedParameters"] = _pointer.expectedParameters; + if (_pointer.location) + result["location"] = pointerLocationToString(*_pointer.location); + setOptional(result, "name", _pointer.name); + if (_pointer.slot) + result["slot"] = expressionToJson(*_pointer.slot); + if (_pointer.offset) + result["offset"] = expressionToJson(*_pointer.offset); + if (_pointer.length) + result["length"] = expressionToJson(*_pointer.length); + if (!_pointer.group.empty()) + { + result["group"] = Json::array(); + for (auto const& member: _pointer.group) + result["group"].emplace_back(pointerToJson(member)); + } + if (_pointer.count) + result["count"] = expressionToJson(*_pointer.count); + setOptional(result, "indexName", _pointer.indexName); + if (_pointer.listElement) + result["listElement"] = pointerToJson(*_pointer.listElement); + if (_pointer.condition) + result["condition"] = expressionToJson(*_pointer.condition); + if (_pointer.thenPointer) + result["thenPointer"] = pointerToJson(*_pointer.thenPointer); + if (_pointer.elsePointer) + result["elsePointer"] = pointerToJson(*_pointer.elsePointer); + if (!_pointer.definitions.empty()) + { + result["definitions"] = Json::array(); + for (auto const& [name, value]: _pointer.definitions) + result["definitions"].emplace_back(Json{{"name", name}, {"value", expressionToJson(value)}}); + } + if (_pointer.scopeTarget) + result["scopeTarget"] = pointerToJson(*_pointer.scopeTarget); + setOptional(result, "templateName", _pointer.templateName); + if (!_pointer.yields.empty()) + { + result["yields"] = Json::array(); + for (auto const& [name, value]: _pointer.yields) + result["yields"].emplace_back(Json{{"name", name}, {"value", value}}); + } + return result; +} + +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugPointer result; + result.pointerClass = pointerClassFromString(requiredString(_json, "class", _path), _path + ".class"); + if (_json.contains("expectedParameters")) + { + Json const& parameters = _json.at("expectedParameters"); + requireArray(parameters, _path + ".expectedParameters"); + for (size_t index = 0; index < parameters.size(); ++index) + { + require( + parameters.at(index).is_string(), + _path + ".expectedParameters[" + std::to_string(index) + "] must be a string."); + result.expectedParameters.emplace_back(parameters.at(index).get()); + } + } + if (std::optional location = optionalString(_json, "location", _path)) + result.location = pointerLocationFromString(*location, _path + ".location"); + result.name = optionalString(_json, "name", _path); + if (_json.contains("slot")) + result.slot = expressionFromJson(_json.at("slot"), _path + ".slot"); + if (_json.contains("offset")) + result.offset = expressionFromJson(_json.at("offset"), _path + ".offset"); + if (_json.contains("length")) + result.length = expressionFromJson(_json.at("length"), _path + ".length"); + if (_json.contains("group")) + { + Json const& group = _json.at("group"); + requireArray(group, _path + ".group"); + for (size_t index = 0; index < group.size(); ++index) + result.group.emplace_back( + pointerFromJson(group.at(index), _path + ".group[" + std::to_string(index) + "]")); + } + if (_json.contains("count")) + result.count = expressionFromJson(_json.at("count"), _path + ".count"); + result.indexName = optionalString(_json, "indexName", _path); + if (_json.contains("listElement")) + result.listElement = std::make_shared( + pointerFromJson(_json.at("listElement"), _path + ".listElement")); + if (_json.contains("condition")) + result.condition = expressionFromJson(_json.at("condition"), _path + ".condition"); + if (_json.contains("thenPointer")) + result.thenPointer = std::make_shared( + pointerFromJson(_json.at("thenPointer"), _path + ".thenPointer")); + if (_json.contains("elsePointer")) + result.elsePointer = std::make_shared( + pointerFromJson(_json.at("elsePointer"), _path + ".elsePointer")); + if (_json.contains("definitions")) + { + Json const& definitions = _json.at("definitions"); + requireArray(definitions, _path + ".definitions"); + for (size_t index = 0; index < definitions.size(); ++index) + { + std::string itemPath = _path + ".definitions[" + std::to_string(index) + "]"; + result.definitions.emplace_back( + requiredString(definitions.at(index), "name", itemPath), + expressionFromJson(requiredMember(definitions.at(index), "value", itemPath), itemPath + ".value")); + } + } + if (_json.contains("scopeTarget")) + result.scopeTarget = std::make_shared( + pointerFromJson(_json.at("scopeTarget"), _path + ".scopeTarget")); + result.templateName = optionalString(_json, "templateName", _path); + if (_json.contains("yields")) + { + Json const& yields = _json.at("yields"); + requireArray(yields, _path + ".yields"); + for (size_t index = 0; index < yields.size(); ++index) + { + std::string itemPath = _path + ".yields[" + std::to_string(index) + "]"; + result.yields.emplace_back( + requiredString(yields.at(index), "name", itemPath), + requiredString(yields.at(index), "value", itemPath)); + } + } + return result; +} + +Json variableLocationToJson(SemanticDebugVariableLocation const& _location) +{ + Json result{{"kind", variableLocationKindToString(_location.kind)}}; + setOptional(result, "pointerId", _location.pointerID); + return result; +} + +SemanticDebugVariableLocation variableLocationFromJson(Json const& _json, std::string const& _path) +{ + return { + .kind = variableLocationKindFromString(requiredString(_json, "kind", _path), _path + ".kind"), + .pointerID = optionalString(_json, "pointerId", _path)}; +} + +Json variableToJson(SemanticDebugVariable const& _variable, std::set const& _tabledTypeIDs) +{ + if (_variable.identifier && _variable.identifier->empty()) + solThrow(SemanticDebugDataSerializationError, "Variable identifier must not be empty."); + Json result = Json::object(); + setOptional(result, "identifier", _variable.identifier); + setOptional(result, "declarationAstId", _variable.declarationAstID); + if (_variable.declarationSourceLocation) + result["declarationSourceLocation"] = sourceLocationToJson(*_variable.declarationSourceLocation); + setOptional(result, "typeId", _variable.typeID); + // A descriptor whose type ID is in the shared table is not repeated per + // variable; one without an ID has nowhere else to live and stays inline. + if (_variable.ethdebugType && !(_variable.typeID && _tabledTypeIDs.count(*_variable.typeID))) + result["type"] = typeToJson(*_variable.ethdebugType); + if (_variable.dataLocation) + result["dataLocation"] = variableLocationToJson(*_variable.dataLocation); + if (_variable.ethdebugPointer) + result["pointer"] = pointerToJson(*_variable.ethdebugPointer); + return result; +} + +SemanticDebugVariable variableFromJson( + Json const& _json, + std::map const& _sharedTypes, + std::string const& _path) +{ + SemanticDebugVariable result; + result.identifier = optionalString(_json, "identifier", _path); + if (result.identifier && result.identifier->empty()) + solThrow(SemanticDebugDataSerializationError, _path + ".identifier must not be empty."); + result.declarationAstID = optionalValue(_json, "declarationAstId", _path); + if (_json.contains("declarationSourceLocation")) + result.declarationSourceLocation = sourceLocationFromJson( + _json.at("declarationSourceLocation"), + _path + ".declarationSourceLocation" + ); + result.typeID = optionalString(_json, "typeId", _path); + if (_json.contains("type")) + result.ethdebugType = typeFromJson(_json.at("type"), _path + ".type"); + else if (result.typeID) + if (auto const it = _sharedTypes.find(*result.typeID); it != _sharedTypes.end()) + result.ethdebugType = it->second; + if (_json.contains("dataLocation")) + result.dataLocation = variableLocationFromJson(_json.at("dataLocation"), _path + ".dataLocation"); + if (_json.contains("pointer")) + result.ethdebugPointer = pointerFromJson(_json.at("pointer"), _path + ".pointer"); + return result; +} + +Json locationUpdateToJson(SemanticDebugLocationUpdate const& _update) +{ + Json result{ + {"variableAstId", _update.variableAstID}, + {"dataLocation", variableLocationToJson(_update.dataLocation)}}; + if (_update.ethdebugPointer) + result["pointer"] = pointerToJson(*_update.ethdebugPointer); + return result; +} + +SemanticDebugLocationUpdate locationUpdateFromJson(Json const& _json, std::string const& _path) +{ + SemanticDebugLocationUpdate result; + result.variableAstID = requiredInteger(_json, "variableAstId", _path); + result.dataLocation + = variableLocationFromJson(requiredMember(_json, "dataLocation", _path), _path + ".dataLocation"); + if (_json.contains("pointer")) + result.ethdebugPointer = pointerFromJson(_json.at("pointer"), _path + ".pointer"); + return result; +} + +Json dataToJson(SemanticDebugData const& _data, std::set const& _tabledTypeIDs) +{ + Json result = Json::object(); + setOptional(result, "lexicalScopeId", _data.lexicalScopeID); + result["variables"] = Json::array(); + for (auto const& variable: _data.variableDefinitions) + result["variables"].emplace_back(variableToJson(variable, _tabledTypeIDs)); + // Omitted while empty, which is all code generation produces - only + // optimizer passes emit rebindings. + if (!_data.locationUpdates.empty()) + { + result["locationUpdates"] = Json::array(); + for (auto const& update: _data.locationUpdates) + result["locationUpdates"].emplace_back(locationUpdateToJson(update)); + } + return result; +} + +SemanticDebugData dataFromJson( + Json const& _json, + std::map const& _sharedTypes, + std::string const& _path) +{ + SemanticDebugData result; + result.lexicalScopeID = optionalValue(_json, "lexicalScopeId", _path); + Json const& variables = requiredMember(_json, "variables", _path); + requireArray(variables, _path + ".variables"); + for (size_t index = 0; index < variables.size(); ++index) + result.variableDefinitions.emplace_back( + variableFromJson( + variables.at(index), _sharedTypes, _path + ".variables[" + std::to_string(index) + "]")); + if (_json.contains("locationUpdates")) + { + Json const& updates = _json.at("locationUpdates"); + requireArray(updates, _path + ".locationUpdates"); + for (size_t index = 0; index < updates.size(); ++index) + result.locationUpdates.emplace_back(locationUpdateFromJson( + updates.at(index), _path + ".locationUpdates[" + std::to_string(index) + "]")); + } + return result; +} + +} // namespace + +Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) +{ + Json result{ + {"format", std::string(SemanticDebugDataFormat)}, + {"version", SemanticDebugDataFormatVersion}, + {"entries", Json::array()}}; + setOptional(result, "contractName", _table.contractName()); + + // Types are written once, keyed by type ID, and the table is normalized: + // a composed type with an ID of its own becomes a table entry, and the + // containing type references it as `{"id": ...}` - the spec's type + // reference form. Two variables with one type ID have one type by + // construction, the ID being the compiler's type identifier. The table is + // sorted by ID so the writer's output is deterministic. + std::map orderedTypes; + std::function registerType; + registerType = [&](std::string const& _id, SemanticDebugType const& _type) { + if (orderedTypes.count(_id)) + return; + // Present before descending, so a recursive type terminates. + orderedTypes.emplace(_id, Json::object()); + for (SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerType(*component.referenceID, *component.type); + orderedTypes[_id] = typeToJson(_type); + }; + for (auto const& [key, data]: _table.entries()) + if (data) + for (auto const& variable: data->variableDefinitions) + if (variable.typeID && variable.ethdebugType) + registerType(*variable.typeID, *variable.ethdebugType); + + std::set tabledTypeIDs; + if (!orderedTypes.empty()) + { + Json sharedTypes = Json::object(); + for (auto const& [id, descriptor]: orderedTypes) + { + sharedTypes[id] = descriptor; + tabledTypeIDs.insert(id); + } + result["types"] = std::move(sharedTypes); + } + + for (auto const& [key, data]: _table.entries()) + { + require(data != nullptr, "Semantic debug data table contains a null entry."); + Json entry{{"astId", key.first}, {"data", dataToJson(*data, tabledTypeIDs)}}; + // Instance 0 is the only value code generation produces, so it is + // omitted and today's output is unchanged; a cloned scope carries its + // discriminator explicitly. + if (key.second != 0) + entry["instance"] = key.second; + result["entries"].emplace_back(std::move(entry)); + } + return result; +} + +SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) +{ + requireObject(_json, "semantic debug data"); + std::string format = requiredString(_json, "format", "semantic debug data"); + require(format == SemanticDebugDataFormat, "Unsupported semantic debug data format \"" + format + "\"."); + unsigned version = requiredInteger(_json, "version", "semantic debug data"); + require( + version == SemanticDebugDataFormatVersion, + "Unsupported semantic debug data format version " + std::to_string(version) + "."); + std::optional contractName = optionalString(_json, "contractName", "semantic debug data"); + Json const& entries = requiredMember(_json, "entries", "semantic debug data"); + requireArray(entries, "semantic debug data.entries"); + + std::map protoTypes; + if (_json.contains("types")) + { + Json const& types = _json.at("types"); + requireObject(types, "semantic debug data.types"); + for (auto const& [typeID, descriptor]: types.items()) + protoTypes.emplace(typeID, typeFromJson(descriptor, "semantic debug data.types." + typeID)); + } + + // Re-attach referenced types, leaving a reference alone exactly where it + // closes a cycle - which reconstructs the shape the builder produced and + // keeps the reference IDs that public emission registers types by. + std::function&)> expand; + expand = [&](SemanticDebugType const& _proto, std::set& _path) -> SemanticDebugType { + SemanticDebugType expanded = _proto; + for (SemanticDebugTypeComponent& component: expanded.components) + { + if (component.type) + component.type = std::make_shared(expand(*component.type, _path)); + else if (component.referenceID && !_path.count(*component.referenceID)) + if (auto const it = protoTypes.find(*component.referenceID); it != protoTypes.end()) + { + _path.insert(*component.referenceID); + component.type = std::make_shared(expand(it->second, _path)); + _path.erase(*component.referenceID); + } + } + return expanded; + }; + std::map sharedTypes; + for (auto const& [typeID, proto]: protoTypes) + { + std::set path{typeID}; + sharedTypes.emplace(typeID, expand(proto, path)); + } + + SemanticDebugDataTable result; + if (contractName) + result.setContractName(std::move(*contractName)); + for (size_t index = 0; index < entries.size(); ++index) + { + std::string path = "semantic debug data.entries[" + std::to_string(index) + "]"; + Json const& entry = entries.at(index); + int64_t astID = requiredInteger(entry, "astId", path); + int64_t instance = optionalValue(entry, "instance", path).value_or(0); + SemanticDebugDataTable::Key const key{astID, instance}; + require( + !result.find(key), + path + " duplicates AST ID " + std::to_string(astID) + + (instance != 0 ? " (instance " + std::to_string(instance) + ")" : "") + "."); + result.set( + key, + std::make_shared( + dataFromJson(requiredMember(entry, "data", path), sharedTypes, path + ".data"))); + } + return result; +} diff --git a/liblangutil/SemanticDebugDataSerialization.h b/liblangutil/SemanticDebugDataSerialization.h new file mode 100644 index 000000000000..68857c0ddc8d --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.h @@ -0,0 +1,48 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include +#include + +#include + +namespace solidity::langutil +{ + +/// The serialized semantic debug data is an internal, versioned sidecar for +/// Yul. It is intentionally distinct from the public ethdebug/format schemas. +inline constexpr std::string_view SemanticDebugDataFormat = "solidity-ethdebug-semantic-data"; +inline constexpr unsigned SemanticDebugDataFormatVersion = 1; + +struct SemanticDebugDataSerializationError: virtual util::Exception +{ +}; + +/// Serializes the complete AST-ID side table into its versioned JSON format. +Json semanticDebugDataToJson(SemanticDebugDataTable const& _table); + +/// Deserializes a versioned side table. Throws +/// SemanticDebugDataSerializationError if the input is malformed or uses an +/// unsupported format version. +SemanticDebugDataTable semanticDebugDataFromJson(Json const& _json); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h new file mode 100644 index 000000000000..5da97218a15d --- /dev/null +++ b/liblangutil/SemanticDebugDataTable.h @@ -0,0 +1,94 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +/// Maps source-language AST origin IDs to semantic debug data. +/// The origin ID is sufficient for the current un-cloned Yul path. +/// A generated scope-instance discriminator must be added before semantic debug data can be +/// preserved through optimizations that clone or specialize Yul scopes with the same origin ID. +class SemanticDebugDataTable +{ +public: + void setContractName(std::string _contractName) + { + m_contractName = std::move(_contractName); + } + + std::optional const& contractName() const + { + return m_contractName; + } + + /// An entry is identified by the source AST ID together with a scope + /// instance discriminator. Code generation produces instance 0 only; + /// passes that clone code give each copy its own instance, so the copies + /// do not overwrite each other. Keying by the pair from the start means + /// admitting cloned code later is not a format change. + using Key = std::pair; + + void set(Key _key, SemanticDebugData::ConstPtr _debugData) + { + m_byKey[_key] = std::move(_debugData); + } + + void set(int64_t _astID, SemanticDebugData::ConstPtr _debugData) + { + set(Key{_astID, 0}, std::move(_debugData)); + } + + SemanticDebugData::ConstPtr find(Key _key) const + { + auto const it = m_byKey.find(_key); + return it == m_byKey.end() ? nullptr : it->second; + } + + SemanticDebugData::ConstPtr find(std::optional _astID) const + { + if (!_astID) + return nullptr; + return find(Key{*_astID, 0}); + } + + bool empty() const + { + return m_byKey.empty(); + } + + std::map const& entries() const + { + return m_byKey; + } + +private: + std::optional m_contractName; + std::map m_byKey; +}; + +} // 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..95d84b5a284a --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -0,0 +1,852 @@ +/* + 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; + +SemanticDebugType semanticType(Type const& _type, std::set& _typesOnPath); + +/// Wraps a composed type. The inline representation is cut when the composed +/// type is already being described further up the recursion path, leaving only +/// the reference ID; this terminates recursive types such as structs that +/// contain themselves through arrays or mappings. +SemanticDebugTypeComponent typeComponent( + SemanticDebugTypeComponent::Role _role, + std::optional _name, + Type const& _type, + std::set& _typesOnPath +) +{ + SemanticDebugTypeComponent result; + result.role = _role; + result.name = std::move(_name); + result.referenceID = _type.identifier(); + if (!_typesOnPath.count(_type.identifier())) + result.type = std::make_shared(semanticType(_type, _typesOnPath)); + return result; +} + +void setDefinition(SemanticDebugType& _result, Declaration const& _declaration) +{ + if (!_declaration.name().empty()) + _result.definitionName = _declaration.name(); + _result.definitionLocation = _declaration.location(); +} + +SemanticDebugType semanticType(Type const& _type, std::set& _typesOnPath) +{ + SemanticDebugType result; + bool const insertedOnPath = _typesOnPath.insert(_type.identifier()).second; + + switch (_type.category()) + { + case Type::Category::Address: + { + auto const& addressType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Address; + result.payable = addressType.stateMutability() == StateMutability::Payable; + // The representation width is data, not part of the kind: a future + // 32-byte address is the same kind with a different width. + result.bytes = 20; + break; + } + case Type::Category::Integer: + { + auto const& integerType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = integerType.isSigned() ? SemanticDebugType::Kind::Int : SemanticDebugType::Kind::Uint; + result.bits = integerType.numBits(); + break; + } + case Type::Category::FixedPoint: + { + auto const& fixedPointType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = fixedPointType.isSigned() ? SemanticDebugType::Kind::Fixed : SemanticDebugType::Kind::Ufixed; + result.bits = fixedPointType.numBits(); + result.places = fixedPointType.fractionalDigits(); + break; + } + case Type::Category::Bool: + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Bool; + break; + case Type::Category::FixedBytes: + { + auto const& bytesType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Bytes; + result.bytes = bytesType.numBytes(); + break; + } + case Type::Category::Array: + { + auto const& arrayType = dynamic_cast(_type); + if (arrayType.isByteArrayOrString()) + { + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = arrayType.isString() ? SemanticDebugType::Kind::String : SemanticDebugType::Kind::Bytes; + break; + } + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Array; + if (!arrayType.isDynamicallySized()) + result.count = toCompactHexWithPrefix(arrayType.length()); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Element, + std::nullopt, + *arrayType.baseType(), + _typesOnPath + )); + break; + } + case Type::Category::Contract: + { + auto const& contractType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Contract; + result.payable = contractType.isPayable(); + // A contract value is an address; its width travels the same way. + result.bytes = 20; + if (contractType.contractDefinition().isLibrary()) + result.isLibrary = true; + if (contractType.contractDefinition().isInterface()) + result.isInterface = true; + setDefinition(result, contractType.contractDefinition()); + break; + } + case Type::Category::Struct: + { + auto const& structType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Struct; + setDefinition(result, structType.structDefinition()); + for (ASTPointer const& member: structType.structDefinition().members()) + if (member->annotation().type) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Member, + member->name(), + *member->annotation().type, + _typesOnPath + )); + break; + } + case Type::Category::Enum: + { + auto const& enumType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Elementary; + result.kind = SemanticDebugType::Kind::Enum; + setDefinition(result, enumType.enumDefinition()); + for (ASTPointer const& member: enumType.enumDefinition().members()) + result.enumValues.emplace_back(member->name()); + break; + } + case Type::Category::UserDefinedValueType: + { + auto const& aliasType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Alias; + setDefinition(result, aliasType.definition()); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Underlying, + std::nullopt, + aliasType.underlyingType(), + _typesOnPath + )); + break; + } + case Type::Category::Tuple: + { + auto const& tupleType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Tuple; + for (Type const* component: tupleType.components()) + if (component) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Member, + std::nullopt, + *component, + _typesOnPath + )); + break; + } + case Type::Category::Mapping: + { + auto const& mappingType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Mapping; + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Key, + std::nullopt, + *mappingType.keyType(), + _typesOnPath + )); + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Value, + std::nullopt, + *mappingType.valueType(), + _typesOnPath + )); + break; + } + case Type::Category::Function: + { + auto const& functionType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Function; + if (functionType.kind() == FunctionType::Kind::Internal) + result.externalFunction = false; + else if (functionType.kind() == FunctionType::Kind::External) + result.externalFunction = true; + if (functionType.hasDeclaration()) + setDefinition(result, functionType.declaration()); + for (Type const* parameterType: functionType.parameterTypes()) + if (parameterType) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Parameter, + std::nullopt, + *parameterType, + _typesOnPath + )); + for (Type const* returnType: functionType.returnParameterTypes()) + if (returnType) + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Return, + std::nullopt, + *returnType, + _typesOnPath + )); + break; + } + case Type::Category::ArraySlice: + { + auto const& sliceType = dynamic_cast(_type); + result.typeClass = SemanticDebugType::Class::Complex; + result.kind = SemanticDebugType::Kind::Slice; + result.components.emplace_back(typeComponent( + SemanticDebugTypeComponent::Role::Element, + std::nullopt, + *sliceType.arrayType().baseType(), + _typesOnPath + )); + break; + } + case Type::Category::RationalNumber: + case Type::Category::StringLiteral: + case Type::Category::TypeType: + case Type::Category::Modifier: + case Type::Category::Magic: + case Type::Category::Module: + case Type::Category::InaccessibleDynamic: + break; + } + + if (insertedOnPath) + _typesOnPath.erase(_type.identifier()); + return result; +} + +SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _yulVariable) +{ + // A Yul name stands in for a stack depth that is only known after the + // Yul-to-EVM transform. It is not a variable a consumer can bind, so it is + // kept as its own kind and never published as though it were a slot. + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + std::move(_name), + PointerExpression::yulLocal(_yulVariable) + ); +} + +std::optional sourceIdentifier(VariableDeclaration const& _variable) +{ + return _variable.name().empty() ? std::nullopt : std::make_optional(_variable.name()); +} + +PointerExpression literalExpression(u256 const& _value) +{ + return PointerExpression::literal(toCompactHexWithPrefix(_value)); +} + +/// @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)}); +} + +std::string storagePointerID(ContractDefinition const& _contract, VariableDeclaration const& _variable) +{ + return "storage_" + std::to_string(_contract.id()) + "_" + std::to_string(_variable.id()); +} + +std::optional stackLocation(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + return SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = IRVariable(_variable).commaSeparatedList() + }; +} + +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::variable(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 ethdebug-oriented pointer descriptors for state variables. One builder +/// instance describes one root pointer; mapping keys encountered anywhere in the +/// pointer become template parameters collected in @a expectedParameters. +class StoragePointerBuilder +{ +public: + explicit StoragePointerBuilder(SemanticDebugPointer::Location _dataLocation): + m_dataLocation(_dataLocation) + {} + + SemanticDebugPointer build( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + if (auto const* mappingType = dynamic_cast(&_type)) + return buildMapping(*mappingType, std::move(_slot), _name); + + if (auto const* arrayType = dynamic_cast(&_type)) + { + if (arrayType->isByteArrayOrString()) + return buildBytesOrString(std::move(_slot), _name); + if (arrayType->isDynamicallySized()) + return buildDynamicArray(*arrayType, std::move(_slot), _name); + return buildStaticArray(*arrayType, std::move(_slot), _name); + } + + if (auto const* structType = dynamic_cast(&_type)) + return buildStruct(*structType, std::move(_slot), _name); + + return wholeRegion(_type, std::move(_slot), std::move(_offset), _name); + } + + std::vector takeExpectedParameters() + { + return std::move(m_expectedParameters); + } + +private: + /// A single region covering the value as laid out from its base slot. Used + /// for value types and as the fallback for compositions that are not (or + /// cannot be) decomposed further. + SemanticDebugPointer wholeRegion( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + u256 const byteLength = u256(_type.storageBytes()) * _type.storageSize(); + std::optional length; + if (_offset.has_value() || byteLength != 32) + length = literalExpression(byteLength); + return SemanticDebugPointer::region( + m_dataLocation, + _name, + std::move(_slot), + std::move(_offset), + std::move(length) + ); + } + + /// The mapping value lives at `keccak256(pad(key) . slot)`. The key is not + /// stored anywhere; it becomes a template parameter the debugger must bind. + SemanticDebugPointer buildMapping( + MappingType const& _mappingType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const keyParameter = m_expectedParameters.empty() + ? "key" + : "key" + std::to_string(m_expectedParameters.size()); + m_expectedParameters.emplace_back(keyParameter); + + PointerExpression keyExpression = PointerExpression::variable(keyParameter); + // Value-type keys are hashed as full words; bytes and string keys are + // hashed as their raw bytes. + if (_mappingType.keyType()->isValueType()) + keyExpression = PointerExpression::wordSized(std::move(keyExpression)); + + PointerExpression valueSlot = PointerExpression::keccak256({ + std::move(keyExpression), + PointerExpression::wordSized(std::move(_slot)) + }); + return build(*_mappingType.valueType(), std::move(valueSlot), std::nullopt, _name); + } + + /// Dynamic arrays store their element count in the base slot and their data + /// starting at `keccak256(slot)`. + SemanticDebugPointer buildDynamicArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const lengthName = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthRegion = SemanticDebugPointer::region(m_dataLocation, lengthName, _slot); + SemanticDebugPointer elements = SemanticDebugPointer::scope( + {{dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})}}, + elementList( + _arrayType, + PointerExpression::variable(dataVariable), + PointerExpression::read(lengthName), + _name + ) + ); + + std::vector members; + members.emplace_back(std::move(lengthRegion)); + members.emplace_back(std::move(elements)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStaticArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + return elementList(_arrayType, std::move(_slot), literalExpression(_arrayType.length()), _name); + } + + /// A list of element pointers laid out from @a _dataStart. Value-type + /// elements narrower than a word are packed multiple to a slot; everything + /// else advances in whole slots. + SemanticDebugPointer elementList( + ArrayType const& _arrayType, + PointerExpression _dataStart, + PointerExpression _count, + std::string const& _name + ) + { + Type const& elementType = *_arrayType.baseType(); + std::string const indexName = _name + "-index"; + std::string const elementName = _name + "-item"; + PointerExpression index = PointerExpression::variable(indexName); + + SemanticDebugPointer element; + if (elementType.storageBytes() < 32) + { + solAssert(elementType.isValueType(), "Only value types can be packed."); + u256 const elementBytes = elementType.storageBytes(); + u256 const elementsPerSlot = 32 / elementBytes; + element = SemanticDebugPointer::region( + m_dataLocation, + elementName, + PointerExpression::sum({ + std::move(_dataStart), + PointerExpression::quotient(index, literalExpression(elementsPerSlot)) + }), + PointerExpression::product({ + PointerExpression::remainder(index, literalExpression(elementsPerSlot)), + literalExpression(elementBytes) + }), + literalExpression(elementBytes) + ); + } + else + { + u256 const slotsPerElement = elementType.storageSize(); + PointerExpression stride = slotsPerElement == 1 + ? index + : PointerExpression::product({index, literalExpression(slotsPerElement)}); + PointerExpression elementSlot = PointerExpression::sum({std::move(_dataStart), std::move(stride)}); + element = build(elementType, std::move(elementSlot), std::nullopt, elementName); + } + + return SemanticDebugPointer::list(std::move(_count), indexName, std::move(element)); + } + + /// `bytes` and `string` use the compact encoding: short values keep their + /// data in the base slot with the doubled length in the last byte; long + /// values keep `2 * length + 1` in the base slot and their data starting at + /// `keccak256(slot)`. + SemanticDebugPointer buildBytesOrString(PointerExpression _slot, std::string const& _name) + { + std::string const lengthFlagName = _name + "-length-flag"; + std::string const longLengthName = _name + "-long-length"; + std::string const lengthVariable = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthFlagRegion = SemanticDebugPointer::region( + m_dataLocation, + lengthFlagName, + _slot, + PointerExpression::difference(PointerExpression::wordSize(), literalExpression(1)), + literalExpression(1) + ); + + SemanticDebugPointer shortValue = SemanticDebugPointer::scope( + {{lengthVariable, PointerExpression::quotient(PointerExpression::read(lengthFlagName), literalExpression(2))}}, + SemanticDebugPointer::region( + m_dataLocation, + _name, + _slot, + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + + SemanticDebugPointer longLengthRegion = SemanticDebugPointer::region(m_dataLocation, longLengthName, _slot); + SemanticDebugPointer longData = SemanticDebugPointer::scope( + { + { + lengthVariable, + PointerExpression::quotient( + PointerExpression::difference(PointerExpression::read(longLengthName), literalExpression(1)), + literalExpression(2) + ) + }, + {dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})} + }, + SemanticDebugPointer::region( + m_dataLocation, + _name, + PointerExpression::variable(dataVariable), + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + std::vector longMembers; + longMembers.emplace_back(std::move(longLengthRegion)); + longMembers.emplace_back(std::move(longData)); + + // The flag byte is even (2 * length) for short values and odd + // (2 * length + 1) for long ones, so `(flag + 1) % 2` selects short. + SemanticDebugPointer value = SemanticDebugPointer::conditional( + PointerExpression::remainder( + PointerExpression::sum({PointerExpression::read(lengthFlagName), literalExpression(1)}), + literalExpression(2) + ), + std::move(shortValue), + SemanticDebugPointer::makeGroup(std::move(longMembers)) + ); + + std::vector members; + members.emplace_back(std::move(lengthFlagRegion)); + members.emplace_back(std::move(value)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStruct( + StructType const& _structType, + PointerExpression _slot, + std::string const& _name + ) + { + // Recursive structs and pathological nesting fall back to a region + // covering the struct's slots. + if (m_depth >= maxCompositionDepth || m_structsOnPath.count(_structType.identifier())) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + + m_structsOnPath.insert(_structType.identifier()); + ++m_depth; + + std::vector members; + for (ASTPointer const& member: _structType.structDefinition().members()) + { + if (!member->annotation().type) + continue; + auto const& [slotOffset, byteOffset] = _structType.storageOffsetsOfMember(member->name()); + std::optional offset; + if (byteOffset != 0) + offset = literalExpression(byteOffset); + members.emplace_back(build( + *member->annotation().type, + advanceSlots(_slot, slotOffset), + std::move(offset), + _name + "-" + member->name() + )); + } + + --m_depth; + m_structsOnPath.erase(_structType.identifier()); + + if (members.empty()) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + static constexpr unsigned maxCompositionDepth = 16; + + SemanticDebugPointer::Location m_dataLocation; + std::vector m_expectedParameters; + std::set m_structsOnPath; + unsigned m_depth = 0; +}; + +SemanticDebugVariableLocation dataLocationKind(SemanticDebugPointer::Location _location) +{ + SemanticDebugVariableLocation result; + result.kind = _location == SemanticDebugPointer::Location::Transient + ? SemanticDebugVariableLocation::Kind::TransientStorage + : SemanticDebugVariableLocation::Kind::Storage; + return result; +} + +SemanticDebugVariableLocation storageLocation( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + SemanticDebugPointer::Location _dataLocation +) +{ + SemanticDebugVariableLocation result = dataLocationKind(_dataLocation); + result.pointerID = storagePointerID(_contract, _variable); + return result; +} + +SemanticDebugPointer storagePointer( + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _dataLocation +) +{ + solAssert(_variable.annotation().type, "Storage variable type expected."); + + StoragePointerBuilder builder{_dataLocation}; + std::optional offset; + if (_offset != 0) + offset = literalExpression(_offset); + SemanticDebugPointer result = builder.build( + *_variable.annotation().type, + literalExpression(_slot), + std::move(offset), + _variable.name() + ); + result.expectedParameters = builder.takeExpectedParameters(); + return result; +} + +std::optional typeID(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + return _variable.annotation().type->identifier(); +} + +std::optional ethdebugType(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + std::set typesOnPath; + return semanticType(*_variable.annotation().type, typesOnPath); +} + +SemanticDebugVariable baseSemanticVariable(VariableDeclaration const& _variable) +{ + return { + .identifier = sourceIdentifier(_variable), + .declarationAstID = _variable.id(), + .declarationSourceLocation = _variable.location(), + .typeID = typeID(_variable), + .ethdebugType = ethdebugType(_variable), + .dataLocation = std::nullopt, + .ethdebugPointer = std::nullopt + }; +} + +SemanticDebugVariable stackSemanticVariable(VariableDeclaration const& _variable) +{ + SemanticDebugVariable result = baseSemanticVariable(_variable); + result.dataLocation = stackLocation(_variable); + result.ethdebugPointer = stackPointer(_variable); + return result; +} + +SemanticDebugVariable storageSemanticVariable( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _dataLocation +) +{ + SemanticDebugVariable result = baseSemanticVariable(_variable); + result.dataLocation = storageLocation(_contract, _variable, _dataLocation); + result.ethdebugPointer = storagePointer(_variable, _slot, _offset, _dataLocation); + return result; +} + +void appendVariables( + std::vector& _variables, + std::vector> const& _declarations +) +{ + for (ASTPointer const& declaration: _declarations) + _variables.emplace_back(stackSemanticVariable(*declaration)); +} + +void appendCallableVariables(std::vector& _variables, FunctionDefinition const& _function) +{ + appendVariables(_variables, _function.parameters()); + appendVariables(_variables, _function.returnParameters()); +} + +void appendCallableVariables(std::vector& _variables, ModifierDefinition const& _modifier) +{ + appendVariables(_variables, _modifier.parameters()); +} + +template +void addCallable(SemanticDebugDataTable& _table, Callable const& _callable) +{ + std::vector variables; + appendCallableVariables(variables, _callable); + + if (variables.empty()) + return; + + _table.set(_callable.id(), std::make_shared(SemanticDebugData{ + .lexicalScopeID = _callable.id(), + .variableDefinitions = std::move(variables) + })); +} + +void addStorageVariables(SemanticDebugDataTable& _table, ContractDefinition const& _contract) +{ + auto const* typeType = dynamic_cast(_contract.type()); + solAssert(typeType, "Contract TypeType expected."); + auto const* contractType = dynamic_cast(typeType->actualType()); + solAssert(contractType, "Contract type expected."); + + std::vector variables; + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(DataLocation::Storage)) + if (!variable->name().empty()) + variables.emplace_back(storageSemanticVariable( + _contract, + *variable, + slot, + offset, + SemanticDebugPointer::Location::Storage + )); + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(DataLocation::Transient)) + if (!variable->name().empty()) + variables.emplace_back(storageSemanticVariable( + _contract, + *variable, + slot, + offset, + SemanticDebugPointer::Location::Transient + )); + + if (variables.empty()) + return; + + _table.set(_contract.id(), std::make_shared(SemanticDebugData{ + .lexicalScopeID = _contract.id(), + .variableDefinitions = std::move(variables) + })); +} + +} + +SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable(ContractDefinition const& _contract) +{ + SemanticDebugDataTable table; + table.setContractName(_contract.name()); + + addStorageVariables(table, _contract); + + // Inherited functions and modifiers are compiled into the most derived contract's IR + // with the AST IDs of their original definitions, so all linearized base contracts + // must contribute entries. + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + { + for (FunctionDefinition const* function: contract->definedFunctions()) + addCallable(table, *function); + + for (ModifierDefinition const* modifier: contract->functionModifiers()) + addCallable(table, *modifier); + } + + // Free functions reachable through imports are compiled into the contract's IR as well. + SourceUnit const& sourceUnit = _contract.sourceUnit(); + std::set sourceUnits = sourceUnit.referencedSourceUnits(true); + sourceUnits.insert(&sourceUnit); + for (SourceUnit const* unit: sourceUnits) + for (ASTPointer const& node: unit->nodes()) + if (auto const* freeFunction = dynamic_cast(node.get())) + addCallable(table, *freeFunction); + + return table; +} diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.h b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h new file mode 100644 index 000000000000..59b12fe9518a --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h @@ -0,0 +1,30 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +namespace solidity::frontend +{ + +class ContractDefinition; + +langutil::SemanticDebugDataTable buildSemanticDebugDataTable(ContractDefinition const& _contract); + +} // namespace solidity::frontend diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b2b6f95625ae..1e238ae8fdbc 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; } @@ -802,7 +809,10 @@ void CompilerStack::link() } } -YulStack CompilerStack::loadGeneratedIR(std::string const& _ir) const +YulStack CompilerStack::loadGeneratedIR( + std::string const& _ir, + SemanticDebugDataTable const* _semanticDebugData +) const { YulStack stack( m_evmVersion, @@ -823,6 +833,8 @@ YulStack CompilerStack::loadGeneratedIR(std::string const& _ir) const true // _withErrorIds ) + "\n" ); + if (_semanticDebugData) + stack.attachSemanticDebugData(*_semanticDebugData); return stack; } @@ -962,6 +974,12 @@ std::optional const& CompilerStack::yulIR(std::string const& _contr return contract(_contractName).yulIR; } +std::optional const& CompilerStack::yulSemanticDebugData(std::string const& _contractName) const +{ + solAssert(m_stackState == CompilationSuccessful, "Compilation was not successful."); + return contract(_contractName).yulSemanticDebugData; +} + std::optional CompilerStack::yulIRAst(std::string const& _contractName) const { solAssert(m_stackState == CompilationSuccessful, "Compilation was not successful."); @@ -1161,7 +1179,22 @@ Json CompilerStack::interfaceSymbols(std::string const& _contractName) const Json CompilerStack::ethdebug() const { solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); - return evmasm::ethdebug::resources(ethdebugSources(), VersionString); + Json types = Json::object(); + Json pointers = Json::object(); + std::map const sourceIndexMap = sourceIndices(); + for (auto const& contractEntry: m_contracts) + { + Contract const& compiledContract = contractEntry.second; + if (!compiledContract.contract) + continue; + + if (compiledContract.yulSemanticDebugData) + Ethdebug::collectResources(types, pointers, *compiledContract.yulSemanticDebugData, &sourceIndexMap); + else + Ethdebug::collectResources(types, pointers, buildSemanticDebugDataTable(*compiledContract.contract), &sourceIndexMap); + } + + return evmasm::ethdebug::resources(ethdebugSources(), VersionString, std::move(types), std::move(pointers)); } Json CompilerStack::ethdebugCompilation() const @@ -1204,8 +1237,21 @@ Json CompilerStack::ethdebug(Contract const& _contract, bool _runtime) const if (!assembly) return {}; - solAssert(sourceIndices().contains(_contract.contract->sourceUnitName())); - return evmasm::ethdebug::program(_contract.contract->name(), sourceIndices()[_contract.contract->sourceUnitName()], *assembly, object); + std::map const sourceIndexMap = sourceIndices(); + solAssert(sourceIndexMap.contains(_contract.contract->sourceUnitName())); + + std::optional programContext = + _contract.yulSemanticDebugData + ? Ethdebug::programContext(*_contract.yulSemanticDebugData, sourceIndexMap) + : Ethdebug::programContext(buildSemanticDebugDataTable(*_contract.contract), sourceIndexMap); + + return evmasm::ethdebug::program( + _contract.contract->name(), + sourceIndexMap.at(_contract.contract->sourceUnitName()), + *assembly, + object, + std::move(programContext) + ); } bytes CompilerStack::cborMetadata(std::string const& _contractName, bool _forIR) const @@ -1561,7 +1607,15 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti ); yulAssert(compiledContract.yulIR); - YulStack stack = loadGeneratedIR(*compiledContract.yulIR); + if (m_debugInfoSelection.ethdebug) + compiledContract.yulSemanticDebugData = buildSemanticDebugDataTable(_contract); + else + compiledContract.yulSemanticDebugData = std::nullopt; + + YulStack stack = loadGeneratedIR( + *compiledContract.yulIR, + compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr + ); if (!_unoptimizedOnly) { stack.optimize(m_viaSSACFG); @@ -1583,7 +1637,10 @@ void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract) return; // Re-parse the Yul IR in EVM dialect - YulStack stack = loadGeneratedIR(*compiledContract.yulIROptimized); + YulStack stack = loadGeneratedIR( + *compiledContract.yulIROptimized, + compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr + ); std::string deployedName = IRNames::deployedObject(_contract); solAssert(!deployedName.empty(), ""); diff --git a/libsolidity/interface/CompilerStack.h b/libsolidity/interface/CompilerStack.h index d6deb69dea1e..e3dee6a3d9e8 100644 --- a/libsolidity/interface/CompilerStack.h +++ b/libsolidity/interface/CompilerStack.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -325,6 +326,9 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac /// @returns the IR representation of a contract. std::optional const& yulIR(std::string const& _contractName) const; + /// @returns the semantic debug data sidecar associated with the contract's Yul IR. + std::optional const& yulSemanticDebugData(std::string const& _contractName) const; + /// @returns the IR representation of a contract AST in format. std::optional yulIRAst(std::string const& _contractName) const; @@ -462,6 +466,7 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac evmasm::LinkerObject runtimeObject; ///< Runtime object. std::optional yulIR; ///< Yul IR code straight from the code generator. std::optional yulIROptimized; ///< Reparsed and possibly optimized Yul IR code. + std::optional yulSemanticDebugData; util::LazyInit metadata; ///< The metadata json that will be hashed into the chain. util::LazyInit abi; util::LazyInit storageLayout; @@ -540,7 +545,10 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac /// Parses and analyzes specified Yul source and returns the YulStack that can be used to manipulate it. /// Assumes that the IR was generated from sources loaded currently into CompilerStack, which /// means that it is error-free and uses the same settings. - yul::YulStack loadGeneratedIR(std::string const& _ir) const; + yul::YulStack loadGeneratedIR( + std::string const& _ir, + langutil::SemanticDebugDataTable const* _semanticDebugData = nullptr + ) const; /// @returns the contract object for the given @a _contractName. /// Can only be called after state is CompilationSuccessful. diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp new file mode 100644 index 000000000000..cb5158bdd34e --- /dev/null +++ b/libsolidity/interface/Ethdebug.cpp @@ -0,0 +1,744 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Lowers internal semantic debug metadata to public ethdebug JSON. + */ + +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::frontend; + +namespace +{ +evmasm::ethdebug::schema::materials::SourceRange ethdebugDeclarationRange( + langutil::SourceLocation const& _location, + unsigned _sourceID +) +{ + namespace schema = evmasm::ethdebug::schema; + schema::materials::Reference reference; + reference.id = schema::materials::ID{static_cast(_sourceID)}; + reference.type = std::nullopt; + + schema::materials::SourceRange::Range range{ + .length = schema::data::Unsigned{_location.end - _location.start}, + .offset = schema::data::Unsigned{_location.start} + }; + + schema::materials::SourceRange sourceRange; + sourceRange.source = std::move(reference); + sourceRange.range = range; + return sourceRange; +} + +/// Serializes @a _location as an ethdebug source range when it points into a +/// known source unit. +std::optional ethdebugSourceRange( + langutil::SourceLocation const& _location, + std::map const* _sourceIndices +) +{ + if ( + !_sourceIndices || + !_location.hasText() || + !_location.sourceName || + !_sourceIndices->count(*_location.sourceName) + ) + return std::nullopt; + return Json(ethdebugDeclarationRange(_location, _sourceIndices->at(*_location.sourceName))); +} + +/// Lowers an internal pointer expression to the ethdebug/format/pointer/expression +/// JSON grammar. Returns nullopt for malformed expressions. +std::optional ethdebugPointerExpression(langutil::SemanticDebugPointerExpression const& _expression) +{ + using Kind = langutil::SemanticDebugPointerExpression::Kind; + + auto loweredOperands = [&]() -> std::optional { + Json operands = Json::array(); + for (langutil::SemanticDebugPointerExpression const& operand: _expression.operands) + if (std::optional lowered = ethdebugPointerExpression(operand)) + operands.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return operands; + }; + + auto arithmetic = [&](std::string const& _operation, std::optional _arity) -> std::optional { + if (_arity && _expression.operands.size() != *_arity) + return std::nullopt; + std::optional operands = loweredOperands(); + if (!operands) + return std::nullopt; + return Json{{_operation, std::move(*operands)}}; + }; + + switch (_expression.kind) + { + case Kind::Literal: + case Kind::Variable: + if (!_expression.value) + return std::nullopt; + return Json(*_expression.value); + case Kind::YulLocal: + // A Yul local names a stack depth that is not known yet. Emitting the + // name would put an identifier where a slot belongs, so the pointer + // holding it is dropped instead - the variable is still described, it + // just has no published location until stack layout is available. + return std::nullopt; + case Kind::WordSize: + return Json("$wordsize"); + case Kind::LookupSlot: + case Kind::LookupOffset: + case Kind::LookupLength: + { + if (!_expression.value) + return std::nullopt; + std::string const property = + _expression.kind == Kind::LookupSlot ? ".slot" : + _expression.kind == Kind::LookupOffset ? ".offset" : ".length"; + return Json{{property, *_expression.value}}; + } + case Kind::Read: + if (!_expression.value) + return std::nullopt; + return Json{{"$read", *_expression.value}}; + case Kind::Sum: + return arithmetic("$sum", std::nullopt); + case Kind::Product: + return arithmetic("$product", std::nullopt); + case Kind::Difference: + return arithmetic("$difference", 2); + case Kind::Quotient: + return arithmetic("$quotient", 2); + case Kind::Remainder: + return arithmetic("$remainder", 2); + case Kind::Keccak256: + return arithmetic("$keccak256", std::nullopt); + case Kind::Concat: + return arithmetic("$concat", std::nullopt); + case Kind::Resize: + { + if (_expression.operands.size() != 1) + return std::nullopt; + std::optional operand = ethdebugPointerExpression(_expression.operands.front()); + if (!operand) + return std::nullopt; + if (_expression.value) + return Json{{"$sized" + *_expression.value, std::move(*operand)}}; + return Json{{"$wordsized", std::move(*operand)}}; + } + case Kind::Unknown: + break; + } + return std::nullopt; +} + +/// Lowers an internal pointer descriptor to an ethdebug/format/pointer. Returns +/// nullopt whenever any part cannot be represented; a partial pointer would +/// mislead a consuming debugger. +std::optional ethdebugPointer(langutil::SemanticDebugPointer const& _pointer) +{ + using Class = langutil::SemanticDebugPointer::Class; + using Location = langutil::SemanticDebugPointer::Location; + + auto loweredExpression = [](std::optional const& _expression) + -> std::optional + { + if (!_expression) + return std::nullopt; + return ethdebugPointerExpression(*_expression); + }; + + switch (_pointer.pointerClass) + { + case Class::Region: + { + if (!_pointer.location) + return std::nullopt; + + std::string locationName; + // Stack, storage and transient storage address word-sized slots; the + // byte-oriented locations address byte ranges via offset and length. + bool wordOriented = false; + switch (*_pointer.location) + { + case Location::Stack: + locationName = "stack"; + wordOriented = true; + break; + case Location::Storage: + locationName = "storage"; + wordOriented = true; + break; + case Location::Transient: + locationName = "transient"; + wordOriented = true; + break; + case Location::Memory: + locationName = "memory"; + break; + case Location::Calldata: + locationName = "calldata"; + break; + case Location::Returndata: + locationName = "returndata"; + break; + case Location::Code: + locationName = "code"; + break; + case Location::Unknown: + return std::nullopt; + } + + Json result = Json::object(); + if (_pointer.name) + result["name"] = *_pointer.name; + result["location"] = locationName; + + if (wordOriented) + { + std::optional slot = loweredExpression(_pointer.slot); + if (!slot) + return std::nullopt; + result["slot"] = std::move(*slot); + } + else if (!_pointer.offset || !_pointer.length) + return std::nullopt; + + if (_pointer.offset) + { + std::optional offset = loweredExpression(_pointer.offset); + if (!offset) + return std::nullopt; + result["offset"] = std::move(*offset); + } + if (_pointer.length) + { + std::optional length = loweredExpression(_pointer.length); + if (!length) + return std::nullopt; + result["length"] = std::move(*length); + } + return result; + } + case Class::Group: + { + if (_pointer.group.empty()) + return std::nullopt; + Json members = Json::array(); + for (langutil::SemanticDebugPointer const& member: _pointer.group) + if (std::optional lowered = ethdebugPointer(member)) + members.emplace_back(std::move(*lowered)); + else + return std::nullopt; + return Json{{"group", std::move(members)}}; + } + case Class::List: + { + if (!_pointer.indexName || !_pointer.listElement) + return std::nullopt; + std::optional count = loweredExpression(_pointer.count); + std::optional element = ethdebugPointer(*_pointer.listElement); + if (!count || !element) + return std::nullopt; + return Json{{"list", Json{ + {"count", std::move(*count)}, + {"each", *_pointer.indexName}, + {"is", std::move(*element)} + }}}; + } + case Class::Conditional: + { + if (!_pointer.thenPointer) + return std::nullopt; + std::optional condition = loweredExpression(_pointer.condition); + std::optional thenPointer = ethdebugPointer(*_pointer.thenPointer); + if (!condition || !thenPointer) + return std::nullopt; + Json result{{"if", std::move(*condition)}, {"then", std::move(*thenPointer)}}; + if (_pointer.elsePointer) + { + std::optional elsePointer = ethdebugPointer(*_pointer.elsePointer); + if (!elsePointer) + return std::nullopt; + result["else"] = std::move(*elsePointer); + } + return result; + } + case Class::Scope: + { + if (_pointer.definitions.empty() || !_pointer.scopeTarget) + return std::nullopt; + std::optional inner = ethdebugPointer(*_pointer.scopeTarget); + if (!inner) + return std::nullopt; + // Scope definitions are ordered, but JSON object members are not, so + // each definition becomes its own nested scope: ordering by structure. + for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) + { + std::optional value = ethdebugPointerExpression(definition->second); + if (!value) + return std::nullopt; + inner = Json{ + {"define", Json{{definition->first, std::move(*value)}}}, + {"in", std::move(*inner)} + }; + } + return inner; + } + case Class::TemplateReference: + { + if (!_pointer.templateName) + return std::nullopt; + Json result{{"template", *_pointer.templateName}}; + if (!_pointer.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _pointer.yields) + yields[producedName] = newName; + result["yields"] = std::move(yields); + } + return result; + } + case Class::Unknown: + break; + } + return std::nullopt; +} + +std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +); + +/// Lowers a composed type to an ethdebug type wrapper: `{"type": ...}` with an +/// optional `name`. With @a _referenceComponents the type is referenced by ID +/// into the type resources; otherwise it is inlined, falling back to an ID +/// reference where inlining is impossible (recursive types). +std::optional ethdebugTypeWrapper( + langutil::SemanticDebugTypeComponent const& _component, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + Json wrapper = Json::object(); + if (_component.name) + wrapper["name"] = *_component.name; + + if ((_referenceComponents || !_component.type) && _component.referenceID) + { + wrapper["type"] = Json{{"id", *_component.referenceID}}; + return wrapper; + } + if (!_component.type) + return std::nullopt; + + if (std::optional inlined = ethdebugType(*_component.type, _referenceComponents, _sourceIndices)) + wrapper["type"] = std::move(*inlined); + else if (_component.referenceID) + wrapper["type"] = Json{{"id", *_component.referenceID}}; + else + return std::nullopt; + return wrapper; +} + +std::optional ethdebugTypeDefinition( + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + Json definition = Json::object(); + if (_type.definitionName) + definition["name"] = *_type.definitionName; + if (_type.definitionLocation) + if (std::optional location = ethdebugSourceRange(*_type.definitionLocation, _sourceIndices)) + definition["location"] = std::move(*location); + if (definition.empty()) + return std::nullopt; + return definition; +} + +/// Lowers an internal type descriptor to an ethdebug/format/type. Composed types +/// are referenced by ID or inlined according to @a _referenceComponents; see +/// ethdebugTypeWrapper. Returns nullopt for types the ethdebug vocabulary cannot +/// express yet. +std::optional ethdebugType( + langutil::SemanticDebugType const& _type, + bool _referenceComponents, + std::map const* _sourceIndices +) +{ + using TypeKind = langutil::SemanticDebugType::Kind; + using Role = langutil::SemanticDebugTypeComponent::Role; + + auto componentsWithRole = [&](Role _role) { + std::vector components; + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.role == _role) + components.emplace_back(&component); + return components; + }; + + auto singleWrapper = [&](Role _role) -> std::optional { + std::vector components = componentsWithRole(_role); + if (components.size() != 1) + return std::nullopt; + return ethdebugTypeWrapper(*components.front(), _referenceComponents, _sourceIndices); + }; + + auto wrapperArray = [&](Role _role) -> std::optional { + Json wrappers = Json::array(); + for (langutil::SemanticDebugTypeComponent const* component: componentsWithRole(_role)) + if (std::optional wrapper = ethdebugTypeWrapper(*component, _referenceComponents, _sourceIndices)) + wrappers.emplace_back(std::move(*wrapper)); + else + return std::nullopt; + return wrappers; + }; + + auto wrappedTuple = [&](Role _role) -> std::optional { + std::optional wrappers = wrapperArray(_role); + if (!wrappers) + return std::nullopt; + return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; + }; + + Json result = Json::object(); + auto attachDefinition = [&]() { + if (std::optional definition = ethdebugTypeDefinition(_type, _sourceIndices)) + result["definition"] = std::move(*definition); + }; + + switch (_type.kind) + { + case TypeKind::Uint: + case TypeKind::Int: + if (!_type.bits) + return std::nullopt; + result["kind"] = _type.kind == TypeKind::Uint ? "uint" : "int"; + result["bits"] = *_type.bits; + break; + case TypeKind::Ufixed: + case TypeKind::Fixed: + if (!_type.bits || !_type.places) + return std::nullopt; + result["kind"] = _type.kind == TypeKind::Ufixed ? "ufixed" : "fixed"; + result["bits"] = *_type.bits; + result["places"] = *_type.places; + break; + case TypeKind::Bool: + result["kind"] = "bool"; + break; + case TypeKind::Bytes: + result["kind"] = "bytes"; + if (_type.bytes) + result["size"] = *_type.bytes; + break; + case TypeKind::String: + result["kind"] = "string"; + break; + case TypeKind::Address: + result["kind"] = "address"; + if (_type.payable) + result["payable"] = *_type.payable; + break; + case TypeKind::Contract: + result["kind"] = "contract"; + if (_type.payable) + result["payable"] = *_type.payable; + if (_type.isLibrary && *_type.isLibrary) + result["library"] = true; + else if (_type.isInterface && *_type.isInterface) + result["interface"] = true; + attachDefinition(); + break; + case TypeKind::Enum: + { + result["kind"] = "enum"; + Json values = Json::array(); + for (std::string const& value: _type.enumValues) + values.emplace_back(value); + result["values"] = std::move(values); + attachDefinition(); + break; + } + case TypeKind::Alias: + { + std::optional underlying = singleWrapper(Role::Underlying); + if (!underlying) + return std::nullopt; + result["kind"] = "alias"; + result["contains"] = std::move(*underlying); + attachDefinition(); + break; + } + case TypeKind::Tuple: + { + std::optional elements = wrapperArray(Role::Member); + if (!elements) + return std::nullopt; + result["kind"] = "tuple"; + result["contains"] = std::move(*elements); + break; + } + case TypeKind::Array: + { + std::optional element = singleWrapper(Role::Element); + if (!element) + return std::nullopt; + result["kind"] = "array"; + result["contains"] = std::move(*element); + if (_type.count) + result["count"] = *_type.count; + break; + } + case TypeKind::Slice: + { + // The public format has no slice kind. A slice's representation is the + // dynamic array it views, so that is what it is published as; the + // sidecar keeps the distinction for the compiler's own use. + std::optional element = singleWrapper(Role::Element); + if (!element) + return std::nullopt; + result["kind"] = "array"; + result["contains"] = std::move(*element); + break; + } + case TypeKind::Mapping: + { + std::optional key = singleWrapper(Role::Key); + std::optional value = singleWrapper(Role::Value); + if (!key || !value) + return std::nullopt; + result["kind"] = "mapping"; + result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; + break; + } + case TypeKind::Struct: + { + std::optional members = wrapperArray(Role::Member); + if (!members) + return std::nullopt; + result["kind"] = "struct"; + result["contains"] = std::move(*members); + attachDefinition(); + break; + } + case TypeKind::Function: + { + // The schema requires knowing whether the function follows internal or + // external call semantics. + if (!_type.externalFunction) + return std::nullopt; + std::optional parameters = wrappedTuple(Role::Parameter); + if (!parameters) + return std::nullopt; + result["kind"] = "function"; + result[*_type.externalFunction ? "external" : "internal"] = true; + Json contains{{"parameters", std::move(*parameters)}}; + if (!componentsWithRole(Role::Return).empty()) + { + std::optional returns = wrappedTuple(Role::Return); + if (!returns) + return std::nullopt; + contains["returns"] = std::move(*returns); + } + result["contains"] = std::move(contains); + attachDefinition(); + break; + } + case TypeKind::Unknown: + return std::nullopt; + } + + return result; +} + +/// Registers @a _type in the type resources table under @a _id together with +/// all composed types it references. Entries are registered before descending +/// so that recursive types terminate. +void registerEthdebugType( + Json& _types, + std::string const& _id, + langutil::SemanticDebugType const& _type, + std::map const* _sourceIndices +) +{ + if (_types.contains(_id)) + return; + + std::optional lowered = ethdebugType(_type, true, _sourceIndices); + if (!lowered) + return; + _types[_id] = std::move(*lowered); + + for (langutil::SemanticDebugTypeComponent const& component: _type.components) + if (component.referenceID && component.type) + registerEthdebugType(_types, *component.referenceID, *component.type, _sourceIndices); +} + +void collectEthdebugTypes( + Json& _types, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices +) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + if (variable.typeID && variable.ethdebugType) + registerEthdebugType(_types, *variable.typeID, *variable.ethdebugType, _sourceIndices); + } +} + +bool isStorageBackedLocation(langutil::SemanticDebugVariableLocation const& _location) +{ + return + _location.kind == langutil::SemanticDebugVariableLocation::Kind::Storage || + _location.kind == langutil::SemanticDebugVariableLocation::Kind::TransientStorage; +} + +void collectEthdebugPointers(Json& _pointers, langutil::SemanticDebugDataTable const& _semanticDebugData) +{ + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + if ( + !variable.dataLocation || + !isStorageBackedLocation(*variable.dataLocation) || + !variable.dataLocation->pointerID || + !variable.ethdebugPointer || + _pointers.contains(*variable.dataLocation->pointerID) + ) + continue; + + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) + { + Json expect = Json::array(); + for (std::string const& parameter: variable.ethdebugPointer->expectedParameters) + expect.emplace_back(parameter); + _pointers[*variable.dataLocation->pointerID] = Json{ + {"expect", std::move(expect)}, + {"for", std::move(*pointer)} + }; + } + } + } +} + +} + +void Ethdebug::collectResources( + Json& _types, + Json& _pointers, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices +) +{ + collectEthdebugTypes(_types, _semanticDebugData, _sourceIndices); + collectEthdebugPointers(_pointers, _semanticDebugData); +} + +std::optional Ethdebug::programContext( + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const& _sourceIndices +) +{ + namespace schema = evmasm::ethdebug::schema; + std::vector variables; + for (auto const& entry: _semanticDebugData.entries()) + { + auto const& debugData = entry.second; + if (!debugData) + continue; + + for (langutil::SemanticDebugVariable const& variable: debugData->variableDefinitions) + { + // Program-level context currently carries named state variables, + // i.e. variables resolved to a storage-backed location. + if (!variable.dataLocation || !isStorageBackedLocation(*variable.dataLocation)) + continue; + + schema::program::Context::Variable contextVariable; + contextVariable.identifier = variable.identifier; + + if ( + variable.declarationSourceLocation && + variable.declarationSourceLocation->hasText() && + variable.declarationSourceLocation->sourceName && + _sourceIndices.count(*variable.declarationSourceLocation->sourceName) + ) + contextVariable.declaration = ethdebugDeclarationRange( + *variable.declarationSourceLocation, + _sourceIndices.at(*variable.declarationSourceLocation->sourceName) + ); + + if (variable.ethdebugType) + contextVariable.type = ethdebugType(*variable.ethdebugType, false, &_sourceIndices); + + // Pointers with expected template parameters (e.g. mapping keys) are + // not closed expressions; they are only exported as templates in the + // pointer resources. + if (variable.ethdebugPointer && variable.ethdebugPointer->expectedParameters.empty()) + if (std::optional pointer = ethdebugPointer(*variable.ethdebugPointer)) + { + // The context variable's identifier already names it; a bare + // top-level region does not need an extra "name" property. + if (pointer->is_object() && pointer->contains("location")) + pointer->erase("name"); + contextVariable.pointer = std::move(*pointer); + } + + variables.emplace_back(std::move(contextVariable)); + } + } + + if (variables.empty()) + return std::nullopt; + + schema::program::Context context; + context.variables = std::move(variables); + return context; +} diff --git a/libsolidity/interface/Ethdebug.h b/libsolidity/interface/Ethdebug.h new file mode 100644 index 000000000000..4cf59448f77c --- /dev/null +++ b/libsolidity/interface/Ethdebug.h @@ -0,0 +1,66 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Lowers internal semantic debug metadata to public ethdebug JSON. + */ + +#pragma once + +#include + +#include + +#include + +#include +#include +#include + +namespace solidity::frontend +{ + +/// Lowers the compiler-internal semantic debug metadata (see +/// liblangutil/SemanticDebugData.h) to the public ethdebug JSON vocabulary: +/// type and pointer resource tables and the program-level context. +class Ethdebug +{ +public: + /// Collects ethdebug type and pointer resources from @a _semanticDebugData + /// into @a _types and @a _pointers. Types are registered under their compiler + /// type identifiers with composed types referenced by ID and registered + /// transitively; pointers become templates over their expected parameters. + /// @a _sourceIndices maps source unit names to ethdebug source IDs and may be + /// null, in which case definition source locations are omitted. + static void collectResources( + Json& _types, + Json& _pointers, + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const* _sourceIndices + ); + + /// Builds the program-level ethdebug context: the contract's named state + /// variables, each with its declaration range, ethdebug type, and — when the + /// pointer needs no expected parameters — its storage pointer. + /// @returns nullopt when there are no such variables. + static std::optional programContext( + langutil::SemanticDebugDataTable const& _semanticDebugData, + std::map const& _sourceIndices + ); +}; + +} diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index c8cb87504624..ccf6e018d74a 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -36,6 +37,7 @@ #include #include +#include #include #include @@ -168,7 +170,7 @@ bool hashMatchesContent(std::string const& _hash, std::string const& _content) bool isArtifactRequested(Json const& _outputSelection, std::string const& _artifact, bool _wildcardMatchesExperimental) { - static std::set experimental{"ir", "irAst", "irOptimized", "irOptimizedAst", "yulCFGJson", "ethdebug"}; + static std::set experimental{"ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "yulCFGJson", "ethdebug"}; for (auto const& selectedArtifactJson: _outputSelection) { std::string const& selectedArtifact = selectedArtifactJson.get(); @@ -177,7 +179,7 @@ bool isArtifactRequested(Json const& _outputSelection, std::string const& _artif boost::algorithm::starts_with(_artifact, selectedArtifact + ".") ) { - if (_artifact.find("ethdebug") != std::string::npos) + if (_artifact == "irEthdebug" || _artifact.find("ethdebug") != std::string::npos) // only accept exact matches for ethdebug, e.g. evm.bytecode.ethdebug return selectedArtifact == _artifact; return true; @@ -188,7 +190,7 @@ bool isArtifactRequested(Json const& _outputSelection, std::string const& _artif if (_artifact == "yulCFGJson") return false; // TODO: everything ethdebug related is only experimental for now, so it should not be matched by "*". - if (_artifact.find("ethdebug") != std::string::npos) + if (_artifact == "irEthdebug" || _artifact.find("ethdebug") != std::string::npos) return false; // "ir", "irOptimized" can only be matched by "*" if activated. if (experimental.count(_artifact) == 0 || _wildcardMatchesExperimental) @@ -275,7 +277,7 @@ bool isBinaryRequested(Json const& _outputSelection) // This does not include "evm.methodIdentifiers" on purpose! static std::vector const outputsThatRequireBinaries = std::vector{ "*", - "ir", "irAst", "irOptimized", "irOptimizedAst", "yulCFGJson", + "ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "yulCFGJson", "evm.gasEstimates", "evm.legacyAssembly", "evm.assembly" } + evmObjectComponents("bytecode") + evmObjectComponents("deployedBytecode"); @@ -324,7 +326,7 @@ bool isAnyEthdebugRequested(Json const& _outputSelection) return false; static std::array constexpr ethdebugArtifacts{ - "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", + "irEthdebug", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ethdebug.resources", "ethdebug.compilation" }; @@ -479,7 +481,7 @@ std::optional checkSourceKeys(Json const& _input, std::string const& _name std::optional checkAuxiliaryInputKeys(Json const& _input) { - static std::set keys{"smtlib2responses"}; + static std::set keys{"ethdebug", "smtlib2responses"}; return checkKeys(_input, keys, "auxiliaryInput"); } @@ -852,6 +854,26 @@ std::variant StandardCompiler::parseI ret.smtLib2Responses[hash] = response.get(); } } + + if (auxInputs.contains("ethdebug")) + { + if (ret.language != "Yul") + return formatFatalError( + Error::Type::JSONError, + "\"auxiliaryInput.ethdebug\" can only be used for Yul input." + ); + try + { + ret.semanticDebugData = semanticDebugDataFromJson(auxInputs["ethdebug"]); + } + catch (SemanticDebugDataSerializationError const& _exception) + { + return formatFatalError( + Error::Type::JSONError, + "Invalid \"auxiliaryInput.ethdebug\": " + stringOrDefault(_exception.comment()) + ); + } + } } Json const& settings = _input.value("settings", Json::object()); @@ -962,7 +984,6 @@ std::variant StandardCompiler::parseI Error::Type::JSONError, "To use 'snippet' with settings.debug.debugInfo you must select also 'location'." ); - ret.debugInfoSelection = debugInfoSelection.value(); } } @@ -1251,12 +1272,32 @@ std::variant StandardCompiler::parseI ret.modelCheckerSettings.timeout = modelCheckerSettings["timeout"].get(); } - if ((ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) || isAnyEthdebugRequested(ret.outputSelection)) + if ( + (ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) || + isAnyEthdebugRequested(ret.outputSelection) || + ret.semanticDebugData.has_value() + ) { if (ret.language != "Solidity" && ret.language != "Yul") return formatFatalError(Error::Type::FatalError, "'settings.debug.debugInfo' 'ethdebug' is only supported for languages 'Solidity' and 'Yul'."); } + static std::array constexpr semanticSidecarArtifacts{"irEthdebug"}; + if (ret.semanticDebugData || areArtifactsRequested(ret.outputSelection, semanticSidecarArtifacts)) + { + if (!ret.debugInfoSelection.has_value()) + { + ret.debugInfoSelection = DebugInfoSelection::Default(); + ret.debugInfoSelection->enable("ethdebug"); + } + else if (!ret.debugInfoSelection->ethdebug) + return formatFatalError( + Error::Type::FatalError, + "'ethdebug' needs to be enabled in 'settings.debug.debugInfo' when using 'irEthdebug' or " + "'auxiliaryInput.ethdebug'." + ); + } + if (isEthdebugProgramRequested(ret.outputSelection)) { if (ret.language == "Solidity" && !ret.viaIR) @@ -1278,12 +1319,24 @@ std::variant StandardCompiler::parseI { if (!ret.experimental) return formatFatalError(Error::Type::FatalError, "Ethdebug annotations are experimental and can only be included in 'settings.debug.debugInfo' by enabling the 'settings.experimental' option."); + // Implicitly enabled selections start from the default set and always contain ast-id, so + // only an explicit partial selection can fail this check. + if (!ret.debugInfoSelection->astID) + return formatFatalError( + Error::Type::JSONError, + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + ); } + if ( + ret.debugInfoSelection.has_value() && + ret.debugInfoSelection->ethdebug && + ret.optimiserSettings.runYulOptimiser + ) + solUnimplemented("Optimization is not yet supported with ethdebug."); + if (isEthdebugProgramRequested(ret.outputSelection)) { - if (ret.optimiserSettings.runYulOptimiser) - solUnimplemented("Optimization is not yet supported with ethdebug."); if (ret.viaSSACFG) solUnimplemented("SSA CFG codegen does not yet support ethdebug."); } @@ -1626,6 +1679,13 @@ Json StandardCompiler::compileSolidity(StandardCompiler::InputsAndSettings _inpu // IR if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "ir", wildcardMatchesExperimental)) contractData["ir"] = compilerStack.yulIR(contractName).value_or(""); + if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irEthdebug", wildcardMatchesExperimental)) + { + auto const& semanticDebugData = compilerStack.yulSemanticDebugData(contractName); + contractData["irEthdebug"] = semanticDebugDataToJson( + semanticDebugData ? *semanticDebugData : SemanticDebugDataTable{} + ); + } if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irAst", wildcardMatchesExperimental)) contractData["irAst"] = compilerStack.yulIRAst(contractName).value_or(Json{}); if (compilationSuccess && isArtifactRequested(_inputsAndSettings.outputSelection, file, name, "irOptimized", wildcardMatchesExperimental)) @@ -1799,9 +1859,13 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) solAssert(stack.hasErrors(), "No error reported, but parsing/analysis failed."); else { + if (_inputsAndSettings.semanticDebugData) + stack.attachSemanticDebugData(*_inputsAndSettings.semanticDebugData); contractName = stack.parserResult()->name; if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ir", wildcardMatchesExperimental)) output["contracts"][sourceName][contractName]["ir"] = stack.print(); + if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "irEthdebug", wildcardMatchesExperimental)) + output["contracts"][sourceName][contractName]["irEthdebug"] = semanticDebugDataToJson(stack.semanticDebugData()); if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ast", wildcardMatchesExperimental)) { @@ -1816,6 +1880,28 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) object.bytecode->link(_inputsAndSettings.libraries); if (deployedObject.bytecode) deployedObject.bytecode->link(_inputsAndSettings.libraries); + + if (_inputsAndSettings.semanticDebugData && stack.debugInfoSelection().ethdebug) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + std::string const& ethdebugContractName = + _inputsAndSettings.semanticDebugData->contractName().value_or(contractName); + auto addProgramContext = [&](MachineAssemblyObject& _object) + { + if (!_object.assembly || !_object.bytecode) + return; + _object.ethdebug = evmasm::ethdebug::program( + ethdebugContractName, + 0, + *_object.assembly, + *_object.bytecode, + Ethdebug::programContext(*_inputsAndSettings.semanticDebugData, sourceIndices) + ); + }; + addProgramContext(object); + addProgramContext(deployedObject); + } } for (auto const& error: stack.errors()) @@ -1890,9 +1976,19 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.resources")) { solAssert(_inputsAndSettings.experimental, ""); + Json types = Json::object(); + Json pointers = Json::object(); + if (_inputsAndSettings.semanticDebugData) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + Ethdebug::collectResources(types, pointers, *_inputsAndSettings.semanticDebugData, &sourceIndices); + } output["ethdebug"]["resources"] = evmasm::ethdebug::resources( {{.id = 0, .path = sourceName, .contents = sourceContents, .language = "Yul"}}, - VersionString + VersionString, + std::move(types), + std::move(pointers) ); } if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.compilation")) diff --git a/libsolidity/interface/StandardCompiler.h b/libsolidity/interface/StandardCompiler.h index cf2200d8cf12..0a9440aca201 100644 --- a/libsolidity/interface/StandardCompiler.h +++ b/libsolidity/interface/StandardCompiler.h @@ -82,6 +82,7 @@ class StandardCompiler OptimiserSettings optimiserSettings; std::optional debugInfoSelection; std::map libraries; + std::optional semanticDebugData; bool metadataLiteralSources = false; CompilerStack::MetadataFormat metadataFormat = CompilerStack::defaultMetadataFormat(); CompilerStack::MetadataHash metadataHash = CompilerStack::MetadataHash::IPFS; diff --git a/libyul/CMakeLists.txt b/libyul/CMakeLists.txt index fbfadcde163e..50f95c469a84 100644 --- a/libyul/CMakeLists.txt +++ b/libyul/CMakeLists.txt @@ -40,6 +40,8 @@ add_library(yul Scope.h ScopeFiller.cpp ScopeFiller.h + SemanticDebugDataTransfer.cpp + SemanticDebugDataTransfer.h Utilities.cpp Utilities.h YulName.h diff --git a/libyul/SemanticDebugDataTransfer.cpp b/libyul/SemanticDebugDataTransfer.cpp new file mode 100644 index 000000000000..5b925780b9e2 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.cpp @@ -0,0 +1,328 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::yul; +using namespace solidity::langutil; + +namespace +{ + +/// Applies a rewriting function to every debug data pointer in a Yul AST, +/// including the ones on names in declarations, parameter and return variable lists. +class DebugDataRewriter +{ +public: + using Rewriter = std::function; + + explicit DebugDataRewriter(Rewriter _rewriter): m_rewriter(std::move(_rewriter)) {} + + void operator()(Block& _block) { visitNode(_block); } + +private: + template + void visitNode(std::vector& _nodes) + { + for (T& node: _nodes) + visitNode(node); + } + + template + void visitNode(std::unique_ptr& _node) + { + if (_node) + visitNode(*_node); + } + + template + void visitNode(std::variant& _node) + { + std::visit([this](auto& node) { this->visitNode(node); }, _node); + } + + template + void visitNode(NodeType& _node) + { + _node.debugData = m_rewriter(_node.debugData); + + if constexpr (std::is_same_v) + visitNode(_node.statements); + else if constexpr (std::is_same_v) + { + visitNode(_node.parameters); + visitNode(_node.returnVariables); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.variables); + visitNode(_node.value); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.variableNames); + visitNode(_node.value); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.functionName); + visitNode(_node.arguments); + } + else if constexpr (std::is_same_v) + visitNode(_node.expression); + else if constexpr (std::is_same_v) + { + visitNode(_node.condition); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.expression); + visitNode(_node.cases); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.value); + visitNode(_node.body); + } + else if constexpr (std::is_same_v) + { + visitNode(_node.pre); + visitNode(_node.condition); + visitNode(_node.post); + visitNode(_node.body); + } + // NameWithDebugData, Identifier, Literal, BuiltinName, Break, Continue and + // Leave carry nothing but debug data. + } + + Rewriter m_rewriter; +}; + +/// The Yul AST is exposed as const to discourage structural modification. Rewriting +/// debug data in place changes neither the AST structure nor any names, so analysis +/// info keyed by node addresses remains valid. Replacing the AST with a modified +/// copy instead would invalidate AsmAnalysisInfo. This is the single sanctioned +/// mutation point; do not const_cast the AST anywhere else. +Block& mutableCodeRoot(Object& _object) +{ + return const_cast(_object.code()->root()); +} + +void collectSemanticDebugDataFromRoot(Block& _root, SemanticDebugDataTable& _table) +{ + DebugDataRewriter collector{[&](langutil::DebugData::ConstPtr const& _debugData) { + if (_debugData && _debugData->astID && _debugData->semanticDebugData) + _table.set(*_debugData->astID, _debugData->semanticDebugData); + return _debugData; + }}; + collector(_root); +} + +std::set declaredVariableNames(Block const& _root) +{ + std::set names; + for (auto const& name: NameCollector(_root, NameCollector::OnlyVariables).names()) + names.insert(name.str()); + return names; +} + +/// Collects identifiers of Variable expressions that are not bound within the +/// pointer itself. Scope definitions, list index names and template parameters +/// bind identifiers; whatever remains free must be provided from outside. For +/// internal stack pointers the free variables are generated Yul variable names. +void collectFreeVariables( + SemanticDebugPointerExpression const& _expression, + std::set const& _bound, + std::set& _free +) +{ + if (_expression.kind == SemanticDebugPointerExpression::Kind::Variable && _expression.value) + { + if (!_bound.count(*_expression.value)) + _free.insert(*_expression.value); + return; + } + + // Lookup and Read reference region names, which live in a separate namespace. + for (SemanticDebugPointerExpression const& operand: _expression.operands) + collectFreeVariables(operand, _bound, _free); +} + +void collectFreeVariables( + SemanticDebugPointer const& _pointer, + std::set _bound, + std::set& _free +) +{ + for (std::string const& parameter: _pointer.expectedParameters) + _bound.insert(parameter); + + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value()) + collectFreeVariables(**expression, _bound, _free); + + // Scope definitions are ordered: each definition may reference the earlier ones. + for (auto const& [definedName, definedValue]: _pointer.definitions) + { + collectFreeVariables(definedValue, _bound, _free); + _bound.insert(definedName); + } + + for (SemanticDebugPointer const& member: _pointer.group) + collectFreeVariables(member, _bound, _free); + + if (_pointer.listElement) + { + std::set elementBound = _bound; + if (_pointer.indexName) + elementBound.insert(*_pointer.indexName); + collectFreeVariables(*_pointer.listElement, std::move(elementBound), _free); + } + + for (auto const* subPointer: {&_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer) + collectFreeVariables(**subPointer, _bound, _free); +} + +bool stackLocationSurvives(SemanticDebugVariable const& _variable, std::set const& _yulNames) +{ + if ( + !_variable.dataLocation || + _variable.dataLocation->kind != SemanticDebugVariableLocation::Kind::Stack || + !_variable.ethdebugPointer + ) + return true; + + std::set freeVariables; + collectFreeVariables(*_variable.ethdebugPointer, {}, freeVariables); + return std::all_of( + freeVariables.begin(), + freeVariables.end(), + [&](std::string const& _name) { return _yulNames.count(_name) != 0; } + ); +} + +SemanticDebugVariable optimizedOutVariable(SemanticDebugVariable _variable) +{ + _variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::OptimizedOut, + .pointerID = std::nullopt + }; + _variable.ethdebugPointer = std::nullopt; + return _variable; +} + +SemanticDebugData::ConstPtr updateSemanticDebugDataLocations( + SemanticDebugData::ConstPtr const& _debugData, + std::set const& _yulNames +) +{ + if (!_debugData) + return nullptr; + + SemanticDebugData result = *_debugData; + bool changed = false; + for (SemanticDebugVariable& variable: result.variableDefinitions) + if (!stackLocationSurvives(variable, _yulNames)) + { + variable = optimizedOutVariable(std::move(variable)); + changed = true; + } + + if (!changed) + return _debugData; + + return std::make_shared(std::move(result)); +} + +SemanticDebugDataTable updateSemanticDebugDataLocations( + SemanticDebugDataTable const& _table, + std::set const& _yulNames +) +{ + SemanticDebugDataTable result; + for (auto const& [key, debugData]: _table.entries()) + result.set(key, updateSemanticDebugDataLocations(debugData, _yulNames)); + return result; +} + +void applySemanticDebugDataToCode(Object& _object, SemanticDebugDataTable const& _table) +{ + SemanticDebugDataTable updated = updateSemanticDebugDataLocations( + _table, + declaredVariableNames(_object.code()->root()) + ); + DebugDataRewriter rewriter{[&](langutil::DebugData::ConstPtr const& _debugData) -> langutil::DebugData::ConstPtr { + if (!_debugData) + return _debugData; + + SemanticDebugData::ConstPtr semanticDebugData = updated.find(_debugData->astID); + if (!semanticDebugData) + return _debugData; + + return langutil::DebugData::create( + _debugData->nativeLocation, + _debugData->originLocation, + _debugData->astID, + std::move(semanticDebugData) + ); + }}; + rewriter(mutableCodeRoot(_object)); +} + +} + +void yul::collectSemanticDebugData(Object const& _object, SemanticDebugDataTable& _table) +{ + if (_object.hasCode()) + // Collection only reads debug data; the rewriter returns each pointer unchanged. + collectSemanticDebugDataFromRoot(mutableCodeRoot(const_cast(_object)), _table); + + for (auto const& subNode: _object.subObjects) + if (auto const* subObject = dynamic_cast(subNode.get())) + collectSemanticDebugData(*subObject, _table); +} + +void yul::applySemanticDebugData(Object& _object, SemanticDebugDataTable const& _table) +{ + if (_object.hasCode()) + applySemanticDebugDataToCode(_object, _table); + + for (auto const& subNode: _object.subObjects) + if (auto* subObject = dynamic_cast(subNode.get())) + applySemanticDebugData(*subObject, _table); +} diff --git a/libyul/SemanticDebugDataTransfer.h b/libyul/SemanticDebugDataTransfer.h new file mode 100644 index 000000000000..274e82fd3327 --- /dev/null +++ b/libyul/SemanticDebugDataTransfer.h @@ -0,0 +1,44 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Transfers semantic debug metadata between an AST-ID-keyed side table and Yul ASTs. + */ + +#pragma once + +#include + +namespace solidity::yul +{ + +class Object; + +/// Collects semantic debug metadata attached to the Yul AST nodes of @a _object and +/// all its sub-objects into @a _table, keyed by Solidity AST ID. +/// Existing entries in @a _table are overwritten when the AST carries newer metadata +/// for the same AST ID; entries without a corresponding Yul node are left untouched. +void collectSemanticDebugData(Object const& _object, langutil::SemanticDebugDataTable& _table); + +/// Attaches semantic debug metadata from @a _table to all Yul AST nodes of @a _object +/// and its sub-objects whose AST ID has an entry in the table. +/// Stack locations are validated separately against each object's code: a variable +/// whose referenced Yul stack slots no longer exist in that object is attached as +/// OptimizedOut there, even if the slots still exist in a sibling object. +void applySemanticDebugData(Object& _object, langutil::SemanticDebugDataTable const& _table); + +} diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index 525f0d243f54..9d5b9a4e7399 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -205,6 +206,12 @@ void YulStack::reparse() // NOTE: it is important for the source printed here to exactly match what the compiler will // eventually output to the user. In particular, debug info must be exactly the same. // Otherwise source locations will be off. + // Semantic debug metadata crosses the public Yul boundary in its separately serialized sidecar, + // rather than in the printed source. When requested, merge it into the retained side table here + // and reattach it by AST ID after the internal reparse. Entries that are not attached to any Yul + // node (e.g. contract-scope storage metadata) survive in the table itself. + if (m_debugInfoSelection.ethdebug) + collectSemanticDebugData(*m_parserResult, m_semanticDebugData); std::string source = print(); YulStack cleanStack( @@ -224,6 +231,8 @@ void YulStack::reparse() m_stackState = AnalysisSuccessful; m_parserResult = std::move(cleanStack.m_parserResult); + if (m_debugInfoSelection.ethdebug && !m_semanticDebugData.empty()) + applySemanticDebugData(*m_parserResult, m_semanticDebugData); // NOTE: We keep the char stream, and errors, even though they no longer match the object, // because it's the original source that matters to the user. Optimized code may have different @@ -434,6 +443,25 @@ std::shared_ptr YulStack::parserResult() const return m_parserResult; } +void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) +{ + yulAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); + yulAssert(m_parserResult, ""); + yulAssert(m_debugInfoSelection.ethdebug, "Semantic debug data was supplied without requesting ethdebug."); + + if (_table.contractName()) + m_semanticDebugData.setContractName(*_table.contractName()); + for (auto const& [key, debugData]: _table.entries()) + m_semanticDebugData.set(key, debugData); + + // Applying instead of blindly reattaching validates stack locations against the + // current Yul code. This matters when the table is attached to already optimized + // IR that has been reloaded from text: variables whose Yul stack slots no longer + // exist must be marked OptimizedOut instead of keeping stale locations. + if (!m_semanticDebugData.empty()) + applySemanticDebugData(*m_parserResult, m_semanticDebugData); +} + Dialect const& YulStack::dialect() const { yulAssert(m_stackState >= AnalysisSuccessful); diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 2e8f8ed30e73..b9cf38b72a2f 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -35,6 +35,8 @@ #include +#include + #include #include @@ -97,7 +99,12 @@ class YulStack: public langutil::CharStreamProvider m_soliditySourceProvider(_soliditySourceProvider), m_errorReporter(m_errors), m_objectOptimizer(_objectOptimizer ? std::move(_objectOptimizer) : std::make_shared()) - {} + { + yulAssert( + !m_debugInfoSelection.ethdebug || m_debugInfoSelection.astID, + "Ethdebug semantic data requires AST ID debug information." + ); + } /// @returns the char stream used during parsing langutil::CharStream const& charStream(std::string const& _sourceName) const override; @@ -147,6 +154,9 @@ class YulStack: public langutil::CharStreamProvider /// Return the parsed and analyzed object. std::shared_ptr parserResult() const; + void attachSemanticDebugData(langutil::SemanticDebugDataTable const& _table); + langutil::SemanticDebugDataTable const& semanticDebugData() const { return m_semanticDebugData; } + Dialect const& dialect() const; langutil::DebugInfoSelection debugInfoSelection() const { return m_debugInfoSelection; } @@ -180,6 +190,10 @@ class YulStack: public langutil::CharStreamProvider State m_stackState = Empty; std::shared_ptr m_parserResult; + /// Semantic debug metadata keyed by Solidity AST ID. Retained across reparses so + /// that entries not attached to any Yul node (e.g. contract-scope storage + /// metadata) are not lost at the Yul text boundary. + langutil::SemanticDebugDataTable m_semanticDebugData; langutil::ErrorList m_errors; langutil::ErrorReporter m_errorReporter; diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index a3afa509868a..95e7898be914 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include #include +#include #include #include @@ -270,6 +272,25 @@ void CommandLineInterface::handleIR(std::string const& _contractName) } } +void CommandLineInterface::handleIREthdebug(std::string const& _contractName) +{ + solAssert(CompilerInputModes.count(m_options.input.mode) == 1); + + if (!m_options.compiler.outputs.irEthdebug) + return; + + auto const& semanticDebugData = m_compiler->yulSemanticDebugData(_contractName); + std::string serialized = jsonPrint( + semanticDebugData ? semanticDebugDataToJson(*semanticDebugData) : + semanticDebugDataToJson(SemanticDebugDataTable{}), + m_options.formatting.json + ); + if (!m_options.output.dir.empty()) + createFile(m_compiler->filesystemFriendlyName(_contractName) + "_ir_ethdebug.json", serialized); + else + sout() << "IR ethdebug semantic data sidecar:" << std::endl << serialized << std::endl; +} + void CommandLineInterface::handleIRAst(std::string const& _contractName) { solAssert(CompilerInputModes.count(m_options.input.mode) == 1); @@ -952,6 +973,7 @@ void CommandLineInterface::compile() pipelineConfig.irCodegen = pipelineConfig.irOptimization || m_options.compiler.outputs.ir || + m_options.compiler.outputs.irEthdebug || m_options.compiler.outputs.irAstJson; pipelineConfig.bytecode = m_options.compiler.estimateGas || @@ -1282,12 +1304,74 @@ std::string CommandLineInterface::objectWithLinkRefsHex(evmasm::LinkerObject con void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) { solAssert(m_options.input.mode == InputMode::Assembler); + std::map semanticDebugDataBySource; + if (!m_options.input.ethdebugInputs.empty()) + { + for (std::string const& input: m_options.input.ethdebugInputs) + { + size_t const separator = input.find('='); + std::string sourceUnitName; + boost::filesystem::path sidecarPath; + if (separator == std::string::npos) + { + if (m_fileReader.sourceUnits().size() != 1 || m_options.input.ethdebugInputs.size() != 1) + solThrow( + CommandLineExecutionError, + "Unqualified --ethdebug-input requires exactly one strict assembly input; " + "use --ethdebug-input yul=file when compiling multiple inputs." + ); + sourceUnitName = m_fileReader.sourceUnits().begin()->first; + sidecarPath = input; + } + else + { + std::string const yulPath = input.substr(0, separator); + sidecarPath = input.substr(separator + 1); + if (yulPath.empty() || sidecarPath.empty()) + solThrow(CommandLineExecutionError, "Invalid --ethdebug-input mapping: " + input); + // The Yul file is given as a path exactly like the corresponding input argument and + // must be normalized the same way to match its source unit name. + sourceUnitName = m_fileReader.cliPathToSourceUnitName(yulPath); + if (!m_fileReader.sourceUnits().contains(sourceUnitName)) + solThrow(CommandLineExecutionError, "Unknown Yul source in --ethdebug-input mapping: " + yulPath); + } + + if (semanticDebugDataBySource.contains(sourceUnitName)) + solThrow(CommandLineExecutionError, "Duplicate --ethdebug-input for Yul source: " + sourceUnitName); + + Json json; + std::string parseError; + std::string contents; + try + { + contents = readFileAsString(sidecarPath); + } + catch (std::exception const& _exception) + { + solThrow(CommandLineExecutionError, "Could not read --ethdebug-input: "s + _exception.what()); + } + if (!jsonParseStrict(contents, json, &parseError)) + solThrow(CommandLineExecutionError, "Could not parse --ethdebug-input: " + parseError); + try + { + semanticDebugDataBySource.emplace(sourceUnitName, semanticDebugDataFromJson(json)); + } + catch (SemanticDebugDataSerializationError const& _exception) + { + solThrow( + CommandLineExecutionError, + "Invalid --ethdebug-input: " + stringOrDefault(_exception.comment()) + ); + } + } + } bool successful = true; std::map yulStacks; std::map objects; for (auto const& [sourceUnitName, yulSource]: m_fileReader.sourceUnits()) { + auto const semanticDebugData = semanticDebugDataBySource.find(sourceUnitName); auto& stack = yulStacks[sourceUnitName] = yul::YulStack( m_options.output.evmVersion, m_options.optimiserSettings(), @@ -1301,6 +1385,9 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) solAssert(stack.hasErrors(), "No error reported, but parsing/analysis failed."); else { + if (semanticDebugData != semanticDebugDataBySource.end()) + stack.attachSemanticDebugData(semanticDebugData->second); + if ( m_options.compiler.outputs.asmJson && stack.parserResult() && @@ -1317,6 +1404,20 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) yul::MachineAssemblyObject object = stack.assemble(_targetMachine, m_options.output.viaSSACFG); if (object.bytecode) object.bytecode->link(m_options.linker.libraries); + if (semanticDebugData != semanticDebugDataBySource.end() && object.assembly && object.bytecode) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + std::string const& ethdebugContractName = + semanticDebugData->second.contractName().value_or(stack.parserResult()->name); + object.ethdebug = evmasm::ethdebug::program( + ethdebugContractName, + 0, + *object.assembly, + *object.bytecode, + Ethdebug::programContext(semanticDebugData->second, sourceIndices) + ); + } objects.insert({sourceUnitName, std::move(object)}); } } @@ -1343,6 +1444,7 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) for (auto const& [sourceUnitName, yulSource]: m_fileReader.sourceUnits()) { + auto const semanticDebugData = semanticDebugDataBySource.find(sourceUnitName); solAssert(_targetMachine == yul::YulStack::Machine::EVM); yul::YulStack const& stack = yulStacks[sourceUnitName]; @@ -1350,11 +1452,21 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) if (m_options.compiler.outputs.ethdebugResources) { + Json types = Json::object(); + Json pointers = Json::object(); + if (semanticDebugData != semanticDebugDataBySource.end()) + { + std::map sourceIndices; + stack.parserResult()->collectSourceIndices(sourceIndices); + Ethdebug::collectResources(types, pointers, semanticDebugData->second, &sourceIndices); + } sout() << "======= Debug Data (ethdebug/format/info/resources) =======" << std::endl; sout() << util::jsonPrint( evmasm::ethdebug::resources( {{.id = 0, .path = sourceUnitName, .contents = yulSource, .language = "Yul"}}, - VersionString + VersionString, + std::move(types), + std::move(pointers) ), m_options.formatting.json ) << std::endl; @@ -1381,6 +1493,14 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) sout() << std::endl << "Pretty printed source:" << std::endl; sout() << stack.print() << std::endl; } + if (m_options.compiler.outputs.irEthdebug) + { + sout() << std::endl << "Ethdebug semantic data sidecar:" << std::endl; + sout() << jsonPrint( + semanticDebugDataToJson(stack.semanticDebugData()), + m_options.formatting.json + ) << std::endl; + } if (m_options.compiler.outputs.binary) { @@ -1462,6 +1582,7 @@ void CommandLineInterface::outputCompilationResults() handleBytecode(contract); handleIR(contract); + handleIREthdebug(contract); handleIRAst(contract); handleIROptimized(contract); handleIROptimizedAst(contract); diff --git a/solc/CommandLineInterface.h b/solc/CommandLineInterface.h index 3f8ddfac4631..b2018cd1c88f 100644 --- a/solc/CommandLineInterface.h +++ b/solc/CommandLineInterface.h @@ -105,6 +105,7 @@ class CommandLineInterface void handleBinary(std::string const& _contract); void handleOpcode(std::string const& _contract); void handleIR(std::string const& _contract); + void handleIREthdebug(std::string const& _contract); void handleIRAst(std::string const& _contract); void handleIROptimized(std::string const& _contract); void handleIROptimizedAst(std::string const& _contract); diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 54df68667b1a..267168d5a9a6 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -55,6 +55,7 @@ static std::string const g_strGas = "gas"; static std::string const g_strHelp = "help"; static std::string const g_strImportAst = "import-ast"; static std::string const g_strImportEvmAssemblerJson = "import-asm-json"; +static std::string const g_strEthdebugInput = "ethdebug-input"; static std::string const g_strInputFile = "input-file"; static std::string const g_strYul = "yul"; static std::string const g_strYulDialect = "yul-dialect"; @@ -162,6 +163,8 @@ std::vector const& CommandLineParser::experimentalOptionNames() static std::vector const names{ g_strImportAst, g_strImportEvmAssemblerJson, + g_strEthdebugInput, + "ir-ethdebug", "ir-ast-json", "ir-optimized-ast-json", "yul-cfg-json", @@ -468,6 +471,7 @@ void CommandLineParser::parseOutputSelection() static std::set const assemblerModeOutputs = { CompilerOutputs::componentName(&CompilerOutputs::asm_), CompilerOutputs::componentName(&CompilerOutputs::binary), + CompilerOutputs::componentName(&CompilerOutputs::irEthdebug), CompilerOutputs::componentName(&CompilerOutputs::irOptimized), CompilerOutputs::componentName(&CompilerOutputs::astCompactJson), CompilerOutputs::componentName(&CompilerOutputs::asmJson), @@ -700,6 +704,12 @@ General Information)").c_str(), po::value()->value_name(util::joinHumanReadable(g_yulDialectArgs, ",")), "Input dialect to use in assembly or yul mode." ) + ( + g_strEthdebugInput.c_str(), + po::value>()->composing()->value_name("file|yul=file"), + "(experimental) Attach a serialized ethdebug semantic data sidecar to strict assembly input. " + "Repeat yul=file for multiple Yul inputs." + ) ; desc.add(assemblyModeOptions); @@ -751,6 +761,7 @@ General Information)").c_str(), (CompilerOutputs::componentName(&CompilerOutputs::binaryRuntime).c_str(), "Binary of the runtime part of the contracts in hex.") (CompilerOutputs::componentName(&CompilerOutputs::abi).c_str(), "ABI specification of the contracts.") (CompilerOutputs::componentName(&CompilerOutputs::ir).c_str(), "Intermediate Representation (IR) of all contracts.") + (CompilerOutputs::componentName(&CompilerOutputs::irEthdebug).c_str(), "(experimental) Serialized ethdebug semantic data sidecar for the IR of all contracts.") (CompilerOutputs::componentName(&CompilerOutputs::irAstJson).c_str(), "(experimental) AST of Intermediate Representation (IR) of all contracts in a compact JSON format.") (CompilerOutputs::componentName(&CompilerOutputs::irOptimized).c_str(), "Optimized Intermediate Representation (IR) of all contracts.") (CompilerOutputs::componentName(&CompilerOutputs::irOptimizedAstJson).c_str(), "(experimental) AST of optimized Intermediate Representation (IR) of all contracts in a compact JSON format.") @@ -1071,7 +1082,8 @@ void CommandLineParser::processArgs() {g_strModelCheckerBMCLoopIterations, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, {g_strModelCheckerContracts, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, {g_strModelCheckerTargets, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, - {g_strViaSSACFG, {InputMode::Compiler, InputMode::CompilerWithASTImport, InputMode::Assembler}} + {g_strViaSSACFG, {InputMode::Compiler, InputMode::CompilerWithASTImport, InputMode::Assembler}}, + {g_strEthdebugInput, {InputMode::Assembler}} }; std::vector invalidOptionsForCurrentInputMode; for (auto const& [optionName, inputModes]: validOptionInputModeCombinations) @@ -1194,6 +1206,8 @@ void CommandLineParser::processArgs() if (m_options.output.debugInfoSelection->snippet && !m_options.output.debugInfoSelection->location) solThrow(CommandLineValidationError, "To use 'snippet' with --" + g_strDebugInfo + " you must select also 'location'."); + if (m_options.output.debugInfoSelection->ethdebug && !m_options.output.debugInfoSelection->astID) + solThrow(CommandLineValidationError, "To use 'ethdebug' with --" + g_strDebugInfo + " you must select also 'ast-id'."); } parseCombinedJsonOption(); @@ -1352,9 +1366,25 @@ void CommandLineParser::processArgs() if (dialect != g_strEVM) solThrow(CommandLineValidationError, "Invalid option for --" + g_strYulDialect + ": " + dialect); } + if (m_args.contains(g_strEthdebugInput)) + m_options.input.ethdebugInputs = m_args[g_strEthdebugInput].as>(); m_options.output.viaSSACFG = m_args.contains(g_strViaSSACFG); + if (!m_options.input.ethdebugInputs.empty() || m_options.compiler.outputs.irEthdebug) + { + if (!m_options.output.debugInfoSelection.has_value()) + { + m_options.output.debugInfoSelection = DebugInfoSelection::Default(); + m_options.output.debugInfoSelection->enable("ethdebug"); + } + else if (!m_options.output.debugInfoSelection->ethdebug) + solThrow( + CommandLineValidationError, + "--debug-info must contain ethdebug when using --ethdebug-input or --ir-ethdebug." + ); + } + if (m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) { if (m_options.output.viaSSACFG) @@ -1540,6 +1570,17 @@ void CommandLineParser::processArgs() } } + if (m_options.compiler.outputs.irEthdebug) + { + if (!m_options.output.debugInfoSelection.has_value()) + { + m_options.output.debugInfoSelection = DebugInfoSelection::Default(); + m_options.output.debugInfoSelection->enable("ethdebug"); + } + else if (!m_options.output.debugInfoSelection->ethdebug) + solThrow(CommandLineValidationError, "--debug-info must contain ethdebug when compiling with --ir-ethdebug."); + } + if ( m_options.output.debugInfoSelection.has_value() && m_options.output.debugInfoSelection->ethdebug && m_options.input.mode != InputMode::Compiler diff --git a/solc/CommandLineParser.h b/solc/CommandLineParser.h index 2593b6b2f886..62b0cc6b465a 100644 --- a/solc/CommandLineParser.h +++ b/solc/CommandLineParser.h @@ -78,6 +78,7 @@ struct CompilerOutputs {"bin-runtime", &CompilerOutputs::binaryRuntime}, {"abi", &CompilerOutputs::abi}, {"ir", &CompilerOutputs::ir}, + {"ir-ethdebug", &CompilerOutputs::irEthdebug}, {"ir-ast-json", &CompilerOutputs::irAstJson}, {"ir-optimized", &CompilerOutputs::irOptimized}, {"ir-optimized-ast-json", &CompilerOutputs::irOptimizedAstJson}, @@ -104,6 +105,7 @@ struct CompilerOutputs bool binaryRuntime = false; bool abi = false; bool ir = false; + bool irEthdebug = false; bool irAstJson = false; bool yulCFGJson = false; bool irOptimized = false; @@ -193,6 +195,7 @@ struct CommandLineOptions FileReader::FileSystemPathSet allowedDirectories; bool ignoreMissingFiles = false; bool noImportCallback = false; + std::vector ethdebugInputs; } input; struct Output diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b14bccfe95a4..2ca204140cf7 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 ) @@ -152,6 +153,7 @@ set(libyul_sources libyul/ControlFlowGraphTest.h libyul/ControlFlowSideEffectsTest.cpp libyul/ControlFlowSideEffectsTest.h + libyul/DebugData.cpp libyul/EVMCodeTransformTest.cpp libyul/EVMCodeTransformTest.h libyul/EVMDialectCompatibility.cpp diff --git a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args index 8dd599e3d575..ccd11791ab21 100644 --- a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args +++ b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args @@ -1 +1 @@ ---experimental --debug-info ethdebug --via-ssa-cfg +--experimental --debug-info ast-id,ethdebug --via-ssa-cfg diff --git a/test/cmdlineTests/standard_metadata_experimental/input.json b/test/cmdlineTests/standard_metadata_experimental/input.json index fddbca92eb2f..73922713c00c 100644 --- a/test/cmdlineTests/standard_metadata_experimental/input.json +++ b/test/cmdlineTests/standard_metadata_experimental/input.json @@ -8,6 +8,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json index 878b2b41f77a..89d120cdd9c3 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_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_selection_ethdebug_no_experimental/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json index 5fca11155a92..ecdb964bd0a3 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json @@ -8,7 +8,7 @@ "settings": { "viaIR": true, "debug": { - "debugInfo": ["ethdebug"] + "debugInfo": ["ast-id", "ethdebug"] }, "outputSelection": { "A.sol": { diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json index c459aadef6f2..5b9f3afce007 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["ir", "irOptimized", "evm.bytecode.ethdebug", "ethdebug.resources"]} } diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json index eac38e3db63d..6d936e39bdd7 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug"]} } diff --git a/test/ethdebugSchemaTests/input_file.json b/test/ethdebugSchemaTests/input_file.json index dc7038e6d02e..b67fc8a40b62 100644 --- a/test/ethdebugSchemaTests/input_file.json +++ b/test/ethdebugSchemaTests/input_file.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/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..9c7b52b10e2f 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -104,6 +104,70 @@ def test_program_sanity(output_selection, environment, solc_output): assert all(instruction["operation"]["mnemonic"] for instruction in instructions) +def test_program_context_includes_state_variables(solc_output): + programs = { + (source_name, contract_name): program + for source_name, contract_name, program + in ethdebug_programs(solc_output, "evm.deployedBytecode.ethdebug") + } + + variables = { + variable["identifier"]: variable + for variable in programs[("a.sol", "A1")]["context"]["variables"] + } + assert variables["stored"]["type"] == {"kind": "uint", "bits": 128} + assert variables["stored"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "length": "0x10", + } + assert variables["enabled"]["type"] == {"kind": "bool"} + assert variables["enabled"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } + + # Complex state variables carry recursive type representations. + assert variables["balances"]["type"] == { + "kind": "mapping", + "contains": { + "key": {"type": {"kind": "address", "payable": False}}, + "value": {"type": {"kind": "uint", "bits": 256}}, + }, + } + # A mapping pointer expects the key as a template parameter, so it is not a + # closed expression and only appears as a template in the pointer resources. + assert "pointer" not in variables["balances"] + + assert variables["values"]["type"] == { + "kind": "array", + "contains": {"type": {"kind": "uint", "bits": 256}}, + } + values_pointer = variables["values"]["pointer"] + assert values_pointer["group"][0]["name"] == "values-length" + assert values_pointer["group"][0]["location"] == "storage" + assert values_pointer["group"][0]["slot"] == "0x02" + assert values_pointer["group"][1]["define"] == { + "values-data": {"$keccak256": [{"$wordsized": "0x02"}]} + } + values_list = values_pointer["group"][1]["in"]["list"] + assert values_list["count"] == {"$read": "values-length"} + assert values_list["each"] == "values-index" + assert values_list["is"]["slot"] == {"$sum": ["values-data", "values-index"]} + + assert variables["label"]["type"] == {"kind": "string"} + label_pointer = variables["label"]["pointer"] + assert label_pointer["group"][0]["name"] == "label-length-flag" + assert label_pointer["group"][0]["offset"] == {"$difference": ["$wordsize", "0x01"]} + conditional = label_pointer["group"][1] + assert "if" in conditional and "then" in conditional and "else" in conditional + + # Contracts without state variables emit no program-level context. + assert "context" not in programs[("a.sol", "A2")] + + 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 +189,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..5fdf324aaf0a --- /dev/null +++ b/test/liblangutil/DebugData.cpp @@ -0,0 +1,455 @@ +/* + 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 + +namespace solidity::langutil::test +{ + +BOOST_AUTO_TEST_SUITE(DebugDataTest) + +BOOST_AUTO_TEST_CASE(carries_semantic_debug_data) +{ + SemanticDebugPointer ethdebugPointer; + ethdebugPointer.pointerClass = SemanticDebugPointer::Class::Region; + ethdebugPointer.location = SemanticDebugPointer::Location::Stack; + ethdebugPointer.name = "value"; + ethdebugPointer.slot = SemanticDebugPointerExpression::variable("var_value"); + + // NOTE: Built imperatively instead of with nested designated initializers, + // which crash MSVC with an internal compiler error. + SemanticDebugType ethdebugType; + ethdebugType.typeClass = SemanticDebugType::Class::Elementary; + ethdebugType.kind = SemanticDebugType::Kind::Uint; + ethdebugType.bits = 256; + + SemanticDebugVariableLocation location; + location.kind = SemanticDebugVariableLocation::Kind::Stack; + location.pointerID = "pointer:value"; + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.declarationAstID = 23; + variable.declarationSourceLocation = SourceLocation{1, 6, std::make_shared("input.sol")}; + variable.typeID = "type:uint256"; + variable.ethdebugType = ethdebugType; + variable.dataLocation = location; + variable.ethdebugPointer = ethdebugPointer; + + SemanticDebugData data; + data.lexicalScopeID = 17; + data.variableDefinitions.emplace_back(std::move(variable)); + auto semanticDebugData = std::make_shared(std::move(data)); + + auto debugData = DebugData::create( + SourceLocation{}, + SourceLocation{}, + 23, + semanticDebugData + ); + + BOOST_REQUIRE(debugData->semanticDebugData); + BOOST_REQUIRE(debugData->semanticDebugData->lexicalScopeID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->lexicalScopeID, 17); + BOOST_REQUIRE_EQUAL(debugData->semanticDebugData->variableDefinitions.size(), 1); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().identifier); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().identifier, "value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().typeID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().typeID, "type:uint256"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->typeClass == SemanticDebugType::Class::Elementary); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->kind == SemanticDebugType::Kind::Uint); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugType->bits, 256); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().dataLocation); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().dataLocation->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().dataLocation->pointerID); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().dataLocation->pointerID, "pointer:value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer); + BOOST_CHECK(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->pointerClass == SemanticDebugPointer::Class::Region); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->location); + BOOST_CHECK(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->location == SemanticDebugPointer::Location::Stack); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->name, "value"); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot); + BOOST_CHECK( + debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->kind == + SemanticDebugPointerExpression::Kind::Variable + ); + BOOST_REQUIRE(debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->value); + BOOST_CHECK_EQUAL(*debugData->semanticDebugData->variableDefinitions.front().ethdebugPointer->slot->value, "var_value"); +} + +BOOST_AUTO_TEST_CASE(pointer_expressions_compose) +{ + // keccak256($wordsized(key), $wordsized(0x02)) — the storage slot of a + // mapping value, parameterized by the template variable "key". + SemanticDebugPointerExpression slot = SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x02")) + }); + + BOOST_CHECK(slot.kind == SemanticDebugPointerExpression::Kind::Keccak256); + BOOST_REQUIRE_EQUAL(slot.operands.size(), 2); + BOOST_CHECK(slot.operands.at(0).kind == SemanticDebugPointerExpression::Kind::Resize); + BOOST_CHECK(!slot.operands.at(0).value); + BOOST_REQUIRE_EQUAL(slot.operands.at(0).operands.size(), 1); + BOOST_CHECK(slot.operands.at(0).operands.front().kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE_EQUAL(slot.operands.at(1).operands.size(), 1); + BOOST_CHECK(slot.operands.at(1).operands.front().kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(slot.operands.at(1).operands.front().value); + BOOST_CHECK_EQUAL(*slot.operands.at(1).operands.front().value, "0x02"); +} + +BOOST_AUTO_TEST_CASE(pointer_collections_compose) +{ + // group [ length region; define data := keccak256($wordsized(0x00)) in + // list over $read(length) ] — the storage layout of a dynamic array. + SemanticDebugPointer element = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "values-item", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("values-data"), + SemanticDebugPointerExpression::variable("values-index") + }) + ); + + std::vector members; + members.emplace_back(SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "values-length", + SemanticDebugPointerExpression::literal("0x00") + )); + members.emplace_back(SemanticDebugPointer::scope( + {{ + "values-data", + SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x00")) + }) + }}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::read("values-length"), + "values-index", + std::move(element) + ) + )); + SemanticDebugPointer pointer = SemanticDebugPointer::makeGroup(std::move(members)); + + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); + BOOST_CHECK(pointer.group.at(0).pointerClass == SemanticDebugPointer::Class::Region); + BOOST_CHECK(pointer.group.at(1).pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(pointer.group.at(1).definitions.size(), 1); + BOOST_CHECK_EQUAL(pointer.group.at(1).definitions.front().first, "values-data"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget); + BOOST_CHECK(pointer.group.at(1).scopeTarget->pointerClass == SemanticDebugPointer::Class::List); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->count); + BOOST_CHECK(pointer.group.at(1).scopeTarget->count->kind == SemanticDebugPointerExpression::Kind::Read); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->indexName); + BOOST_CHECK_EQUAL(*pointer.group.at(1).scopeTarget->indexName, "values-index"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->listElement); + BOOST_CHECK(pointer.group.at(1).scopeTarget->listElement->pointerClass == SemanticDebugPointer::Class::Region); +} + +BOOST_AUTO_TEST_CASE(recursive_types_reference_by_id) +{ + // struct Node { uint256 value; Node[] children; } — the array element cuts + // the recursion and keeps only the reference ID. + auto uintType = std::make_shared([]{ + SemanticDebugType type; + type.typeClass = SemanticDebugType::Class::Elementary; + type.kind = SemanticDebugType::Kind::Uint; + type.bits = 256; + return type; + }()); + + SemanticDebugTypeComponent cutElement; + cutElement.role = SemanticDebugTypeComponent::Role::Element; + cutElement.referenceID = "t_struct$_Node"; + + SemanticDebugType arrayType; + arrayType.typeClass = SemanticDebugType::Class::Complex; + arrayType.kind = SemanticDebugType::Kind::Array; + arrayType.components.emplace_back(std::move(cutElement)); + + SemanticDebugTypeComponent valueMember; + valueMember.role = SemanticDebugTypeComponent::Role::Member; + valueMember.name = "value"; + valueMember.referenceID = "t_uint256"; + valueMember.type = uintType; + + SemanticDebugTypeComponent childrenMember; + childrenMember.role = SemanticDebugTypeComponent::Role::Member; + childrenMember.name = "children"; + childrenMember.referenceID = "t_array$_t_struct$_Node"; + childrenMember.type = std::make_shared(std::move(arrayType)); + + SemanticDebugType nodeType; + nodeType.typeClass = SemanticDebugType::Class::Complex; + nodeType.kind = SemanticDebugType::Kind::Struct; + nodeType.definitionName = "Node"; + nodeType.components.emplace_back(std::move(valueMember)); + nodeType.components.emplace_back(std::move(childrenMember)); + + BOOST_REQUIRE_EQUAL(nodeType.components.size(), 2); + BOOST_REQUIRE(nodeType.components.at(1).type); + BOOST_REQUIRE_EQUAL(nodeType.components.at(1).type->components.size(), 1); + SemanticDebugTypeComponent const& element = nodeType.components.at(1).type->components.front(); + BOOST_CHECK(!element.type); + BOOST_REQUIRE(element.referenceID); + BOOST_CHECK_EQUAL(*element.referenceID, "t_struct$_Node"); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) +{ + SemanticDebugData data; + data.lexicalScopeID = 17; + auto semanticDebugData = std::make_shared(std::move(data)); + + SemanticDebugDataTable table; + BOOST_CHECK(table.empty()); + + table.set(23, semanticDebugData); + + BOOST_CHECK(!table.empty()); + BOOST_CHECK(table.find(23) == semanticDebugData); + BOOST_CHECK(!table.find(24)); + BOOST_CHECK(!table.find(std::nullopt)); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) +{ + SemanticDebugType elementType; + elementType.typeClass = SemanticDebugType::Class::Elementary; + elementType.kind = SemanticDebugType::Kind::Uint; + elementType.bits = 256; + + SemanticDebugTypeComponent component; + component.role = SemanticDebugTypeComponent::Role::Member; + component.name = "member"; + component.referenceID = "t_uint256"; + component.type = std::make_shared(elementType); + + SemanticDebugType type; + type.typeClass = SemanticDebugType::Class::Complex; + type.kind = SemanticDebugType::Kind::Struct; + type.components.emplace_back(std::move(component)); + type.definitionName = "Container"; + type.definitionLocation = SourceLocation{4, 20, std::make_shared("input.sol")}; + + SemanticDebugPointer templateReference; + templateReference.pointerClass = SemanticDebugPointer::Class::TemplateReference; + templateReference.expectedParameters = {"key"}; + templateReference.templateName = "mapping-value"; + templateReference.yields = {{"value", "renamed-value"}}; + + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"slot", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + })}}, + SemanticDebugPointer::conditional( + SemanticDebugPointerExpression::read("condition"), + std::move(templateReference), + SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "fallback", + SemanticDebugPointerExpression::variable("slot"), + SemanticDebugPointerExpression::literal("0x00"), + SemanticDebugPointerExpression::wordSize() + ) + ) + ); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.declarationAstID = 9; + variable.declarationSourceLocation = SourceLocation{21, 26, std::make_shared("input.sol")}; + variable.typeID = "t_struct$_Container"; + variable.ethdebugType = std::move(type); + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Storage, + .pointerID = "pointer:value" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 7; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.setContractName("ContainerContract"); + table.set(42, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["format"].get(), SemanticDebugDataFormat); + BOOST_CHECK_EQUAL(serialized["version"].get(), SemanticDebugDataFormatVersion); + BOOST_CHECK_EQUAL(serialized["contractName"], "ContainerContract"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_duplicate_ast_id) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["version"] = SemanticDebugDataFormatVersion + 1; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); + + serialized["version"] = SemanticDebugDataFormatVersion; + serialized["entries"] = Json::array({ + {{"astId", 1}, {"data", {{"variables", Json::array()}}}}, + {{"astId", 1}, {"data", {{"variables", Json::array()}}}} + }); + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_types_are_shared_by_id) +{ + SemanticDebugType uintType; + uintType.typeClass = SemanticDebugType::Class::Elementary; + uintType.kind = SemanticDebugType::Kind::Uint; + uintType.bits = 256; + + auto makeVariable = [&](std::string _name) { + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.typeID = "t_uint256"; + variable.ethdebugType = uintType; + return variable; + }; + SemanticDebugData data; + data.variableDefinitions = {makeVariable("a"), makeVariable("b")}; + + // A descriptor without a type ID has nowhere else to live and stays inline. + SemanticDebugVariable inlineOnly; + inlineOnly.identifier = "c"; + inlineOnly.ethdebugType = uintType; + data.variableDefinitions.emplace_back(std::move(inlineOnly)); + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["types"].size(), 1); + Json const& variables = serialized["entries"][0]["data"]["variables"]; + BOOST_CHECK(!variables[0].contains("type")); + BOOST_CHECK(!variables[1].contains("type")); + BOOST_CHECK(variables[2].contains("type")); + + // Reading re-inflates the shared descriptor onto each variable. + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + auto entry = read.find(1); + BOOST_REQUIRE(entry != nullptr); + BOOST_REQUIRE(entry->variableDefinitions[0].ethdebugType.has_value()); + BOOST_CHECK(entry->variableDefinitions[0].ethdebugType->bits == 256); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_location_updates_roundtrip) +{ + // The dbg.value analogue: a statement-level rebinding of a variable's + // location, here a spill to memory and a drop to optimized-out. + SemanticDebugLocationUpdate spilled; + spilled.variableAstID = 42; + spilled.dataLocation = {SemanticDebugVariableLocation::Kind::Memory, "pointer:spill"}; + spilled.ethdebugPointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Memory, + "spill", + SemanticDebugPointerExpression::literal("0x80")); + + SemanticDebugLocationUpdate dropped; + dropped.variableAstID = 42; + dropped.dataLocation = {SemanticDebugVariableLocation::Kind::OptimizedOut, std::nullopt}; + + SemanticDebugData data; + data.locationUpdates = {spilled, dropped}; + SemanticDebugDataTable table; + table.set(7, std::make_shared(std::move(data))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + // Absent pointer must stay absent - OptimizedOut has no address. + Json const& updates = serialized["entries"][0]["data"]["locationUpdates"]; + BOOST_CHECK_EQUAL(updates.size(), 2); + BOOST_CHECK(!updates[1].contains("pointer")); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_distinguishes_scope_instances) +{ + // A cloned scope keeps its AST ID and gets its own instance; the two + // entries must coexist, round-trip, and collide only on the full pair. + SemanticDebugDataTable table; + table.set({9, 0}, std::make_shared()); + table.set({9, 2}, std::make_shared()); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["entries"].size(), 2); + // Instance 0 is the default and stays implicit, so un-cloned output is + // unchanged by the discriminator's existence. + BOOST_CHECK(!serialized["entries"][0].contains("instance")); + BOOST_CHECK_EQUAL(serialized["entries"][1]["instance"].get(), 2); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_CHECK(read.find({9, 0}) != nullptr); + BOOST_CHECK(read.find({9, 2}) != nullptr); + BOOST_CHECK(read.find({9, 1}) == nullptr); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); + + serialized["entries"].emplace_back(Json{{"astId", 9}, {"instance", 2}, {"data", Json::object()}}); + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_location_kinds_roundtrip) +{ + using Kind = SemanticDebugVariableLocation::Kind; + std::vector const kinds{ + Kind::Stack, + Kind::Storage, + Kind::TransientStorage, + Kind::Memory, + Kind::Calldata, + Kind::Returndata, + Kind::Code, + Kind::Computed, + Kind::OptimizedOut + }; + + SemanticDebugData data; + for (Kind kind: kinds) + { + SemanticDebugVariable variable; + variable.dataLocation = SemanticDebugVariableLocation{.kind = kind, .pointerID = std::nullopt}; + data.variableDefinitions.emplace_back(std::move(variable)); + } + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(data))); + Json const serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::langutil::test diff --git a/test/libsolidity/EthdebugTest.cpp b/test/libsolidity/EthdebugTest.cpp index 8681c2d4f3c4..f84310847ec2 100644 --- a/test/libsolidity/EthdebugTest.cpp +++ b/test/libsolidity/EthdebugTest.cpp @@ -20,6 +20,7 @@ #include #include +#include #include @@ -144,6 +145,13 @@ std::optional EthdebugTest::fetchOutput( return std::nullopt; return creation["contract"]; } + if (_outputName == "semantic") + { + auto const& semanticDebugData = compiler().yulSemanticDebugData(*resolved); + if (!semanticDebugData) + return std::nullopt; + return semanticDebugDataToJson(*semanticDebugData); + } } return std::nullopt; } diff --git a/test/libsolidity/EthdebugTest.h b/test/libsolidity/EthdebugTest.h index 7a0955230f01..fa9663561b07 100644 --- a/test/libsolidity/EthdebugTest.h +++ b/test/libsolidity/EthdebugTest.h @@ -39,7 +39,8 @@ namespace solidity::frontend::test /// /// Scope keys exposed to expectations: /// - Globals: `.resources`, `.compilation`. -/// - Per contract: `Contract.creation`, `Contract.runtime`, `Contract.contract`. +/// - Per contract: `Contract.creation`, `Contract.runtime`, `Contract.contract`, +/// `Contract.semantic` (the serialized internal semantic sidecar). /// - Source-qualified per contract (when needed to disambiguate same-named /// contracts in different sources): `source.sol:Contract.creation`, etc. class EthdebugTest: public JSONExpectationTest diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index c13704cc061a..f83ad9d8f532 100644 --- a/test/libsolidity/StandardCompiler.cpp +++ b/test/libsolidity/StandardCompiler.cpp @@ -1934,36 +1934,48 @@ BOOST_AUTO_TEST_CASE(ethdebug_excluded_from_wildcards) BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) { + frontend::StandardCompiler compiler; + Json missingAstID = compiler.compile(generateExperimentalStandardJson( + true, + Json::array({"ethdebug"}), + Json::array({"ir"}) + )); + BOOST_CHECK(containsError( + missingAstID, + "JSONError", + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + )); + static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; @@ -2012,14 +2024,14 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), {} }, { @@ -2027,7 +2039,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) }, { generateExperimentalStandardJson( - true, Json::array({"ethdebug"}), { + true, Json::array({"ast-id", "ethdebug"}), { {"fileA", {{"contractA", Json::array({"evm.deployedBytecode.bin"})}}}, {"fileB", {{"contractB", Json::array({"evm.bytecode.bin"})}}} }, @@ -2039,15 +2051,14 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), SolidityAstCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), SolidityAstCode()), std::nullopt, }, }; - frontend::StandardCompiler compiler; for (auto const& test: tests) { Json result = compiler.compile(std::get<0>(test)); @@ -2060,7 +2071,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) { static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt }, { @@ -2068,7 +2079,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2076,7 +2087,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2096,21 +2107,21 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2269,6 +2280,106 @@ BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::d } } +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_supports_two_stage_compilation) +{ + frontend::StandardCompiler compiler; + std::map firstSources; + firstSources["fileA"] = + "contract C { uint256 value; function f(uint256 argument) public { value = argument; } " + "function g(uint256 argument) public pure returns (uint256) { return argument; } }"; + Json firstInput = generateExperimentalStandardJson( + true, + {}, + Json::array({ + "ir", + "irEthdebug", + "evm.bytecode.object", + "evm.bytecode.ethdebug", + "evm.deployedBytecode.object", + "evm.deployedBytecode.ethdebug" + }), + SolidityCode(std::move(firstSources)) + ); + Json firstResult = compiler.compile(firstInput); + BOOST_REQUIRE(containsAtMostWarnings(firstResult)); + Json const& firstContract = firstResult["contracts"]["fileA"]["C"]; + BOOST_REQUIRE(firstContract["ir"].is_string()); + BOOST_REQUIRE(firstContract["irEthdebug"].is_object()); + BOOST_REQUIRE(!firstContract["irEthdebug"]["entries"].empty()); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("argument") != std::string::npos); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("value") != std::string::npos); + + std::map secondSources; + secondSources["fileA.yul"] = firstContract["ir"]; + Json secondInput = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({ + "irEthdebug", + "evm.bytecode.object", + "evm.bytecode.ethdebug", + "evm.deployedBytecode.object", + "evm.deployedBytecode.ethdebug" + }), + YulCode(std::move(secondSources)) + ); + secondInput["auxiliaryInput"]["ethdebug"] = firstContract["irEthdebug"]; + Json secondResult = compiler.compile(secondInput); + BOOST_REQUIRE(containsAtMostWarnings(secondResult)); + BOOST_REQUIRE(secondResult["contracts"]["fileA.yul"].is_object()); + Json const& secondContract = secondResult["contracts"]["fileA.yul"].begin().value(); + BOOST_REQUIRE(secondContract["evm"]["bytecode"]["ethdebug"].is_object()); + BOOST_CHECK(secondContract["evm"]["bytecode"]["object"] == firstContract["evm"]["bytecode"]["object"]); + BOOST_CHECK_EQUAL( + Json::diff( + firstContract["evm"]["bytecode"]["ethdebug"], + secondContract["evm"]["bytecode"]["ethdebug"] + ).dump(), + "[]" + ); + BOOST_CHECK( + secondContract["evm"]["deployedBytecode"]["object"] == + firstContract["evm"]["deployedBytecode"]["object"] + ); + BOOST_CHECK_EQUAL( + Json::diff( + firstContract["evm"]["deployedBytecode"]["ethdebug"], + secondContract["evm"]["deployedBytecode"]["ethdebug"] + ).dump(), + "[]" + ); + BOOST_CHECK(secondContract["irEthdebug"] == firstContract["irEthdebug"]); +} + +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_is_rejected_for_non_yul_input) +{ + frontend::StandardCompiler compiler; + Json input = generateExperimentalStandardJson(false, {}, Json::array({"ir"})); + input["auxiliaryInput"]["ethdebug"] = Json{ + {"format", "solidity-ethdebug-semantic-data"}, + {"version", 1}, + {"entries", Json::array()} + }; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "JSONError", "\"auxiliaryInput.ethdebug\" can only be used for Yul input.")); +} + +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_rejects_yul_optimization) +{ + frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; + Json input = generateExperimentalStandardJson( + false, + {}, + Json::array({"irEthdebug"}), + 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/libsolidity/ethdebugTests/semantic_function_variables.sol b/test/libsolidity/ethdebugTests/semantic_function_variables.sol new file mode 100644 index 000000000000..f140dd1b3074 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_function_variables.sol @@ -0,0 +1,38 @@ +contract C { + modifier guarded(bool enabled) { + require(enabled); + _; + } + + function f(uint256 value, bytes memory payload) + public + pure + guarded(true) + returns (uint256 result, bytes memory) + { + return (value, payload); + } +} +// ---- +// C.semantic.format: solidity-ethdebug-semantic-data +// C.semantic.version: 1 +// C.semantic.entries | length: 2 +// C.semantic.entries[0].data.variables | length: 1 +// C.semantic.entries[0].data.variables[0].identifier: enabled +// C.semantic.entries[0].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[0].data.variables[0].pointer.location: stack +// C.semantic.entries[0].data.variables[0].type.kind: bool +// C.semantic.entries[1].data.variables | length: 4 +// C.semantic.entries[1].data.variables[0].identifier: value +// C.semantic.entries[1].data.variables[0].type.kind: uint +// C.semantic.entries[1].data.variables[0].type.bits: 256 +// C.semantic.entries[1].data.variables[1].identifier: payload +// C.semantic.entries[1].data.variables[1].type.kind: bytes +// C.semantic.entries[1].data.variables[1].type.dataLocation: memory +// C.semantic.entries[1].data.variables[2].identifier: result +// C.semantic.entries[1].data.variables[3].identifier: +// C.semantic.entries[1].data.variables[3].declarationAstId: +// C.semantic.entries[1].data.variables[3].dataLocation.kind: stack +// C.semantic.entries[1].data.variables[3].pointer.name: +// C.semantic.entries[1].data.variables[3].pointer.location: stack +// C.semantic.entries[1].data.variables[3].type.kind: bytes diff --git a/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol b/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol new file mode 100644 index 000000000000..e9df4ab34ac5 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_inheritance_and_free_functions.sol @@ -0,0 +1,26 @@ +function helper(uint256 input) pure returns (uint256 output) { + return input; +} + +contract Base { + function inherited(uint256 value) public pure returns (uint256 result) { + return value; + } +} + +contract C is Base { + function callHelper(uint256 value) public pure returns (uint256 result) { + return helper(value); + } +} +// ---- +// C.semantic.entries | length: 3 +// C.semantic.entries[0].data.variables[0].identifier: input +// C.semantic.entries[0].data.variables[1].identifier: output +// C.semantic.entries[1].data.variables[0].identifier: value +// C.semantic.entries[1].data.variables[1].identifier: result +// C.semantic.entries[2].data.variables[0].identifier: value +// C.semantic.entries[2].data.variables[1].identifier: result +// C.semantic.entries[0].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[1].data.variables[0].dataLocation.kind: stack +// C.semantic.entries[2].data.variables[0].dataLocation.kind: stack diff --git a/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol new file mode 100644 index 000000000000..1f98dec140b1 --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_storage_pointers.sol @@ -0,0 +1,41 @@ +contract C { + struct Item { + uint128 first; + uint64 second; + } + + mapping(address => mapping(uint256 => Item)) balances; + uint16[8] packed; + uint256[] dynamicValues; + string label; + uint128 transient transientValue; +} +// ==== +// EVMVersion: >=cancun +// ---- +// C.semantic.entries | length: 1 +// C.semantic.entries[0].data.variables | length: 5 +// C.semantic.entries[0].data.variables[0].identifier: balances +// C.semantic.entries[0].data.variables[0].dataLocation.kind: storage +// C.semantic.entries[0].data.variables[0].pointer.class: group +// C.semantic.entries[0].data.variables[0].pointer.expectedParameters: ["key", "key1"] +// C.semantic.entries[0].data.variables[0].pointer.group[0].location: storage +// C.semantic.entries[0].data.variables[0].pointer.group[0].slot.kind: keccak256 +// C.semantic.entries[0].data.variables[0].type.kind: mapping +// C.semantic.entries[0].data.variables[0].type.components[1].type.components[1].type.kind: struct +// C.semantic.entries[0].data.variables[1].identifier: packed +// C.semantic.entries[0].data.variables[1].pointer.class: list +// C.semantic.entries[0].data.variables[1].pointer.count.value: 0x08 +// C.semantic.entries[0].data.variables[1].pointer.listElement.offset.kind: product +// C.semantic.entries[0].data.variables[1].type.count: 0x08 +// C.semantic.entries[0].data.variables[2].identifier: dynamicValues +// C.semantic.entries[0].data.variables[2].pointer.class: group +// C.semantic.entries[0].data.variables[2].pointer.group[1].class: scope +// C.semantic.entries[0].data.variables[2].pointer.group[1].scopeTarget.class: list +// C.semantic.entries[0].data.variables[2].type.dynamic: true +// C.semantic.entries[0].data.variables[3].identifier: label +// C.semantic.entries[0].data.variables[3].pointer.group[1].class: conditional +// C.semantic.entries[0].data.variables[3].type.kind: string +// C.semantic.entries[0].data.variables[4].identifier: transientValue +// C.semantic.entries[0].data.variables[4].dataLocation.kind: transientStorage +// C.semantic.entries[0].data.variables[4].pointer.location: transient diff --git a/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol b/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol new file mode 100644 index 000000000000..3488cc3651ec --- /dev/null +++ b/test/libsolidity/ethdebugTests/semantic_type_descriptors.sol @@ -0,0 +1,35 @@ +type Amount is uint128; + +enum Choice { A, B, C } + +contract C { + struct Node { + uint256 value; + Node[] children; + } + + Amount amount; + Choice choice; + Node root; + C self; +} +// ---- +// C.semantic.entries[0].data.variables | length: 4 +// C.semantic.entries[0].data.variables[0].identifier: amount +// C.semantic.entries[0].data.variables[0].type.kind: alias +// C.semantic.entries[0].data.variables[0].type.definitionName: Amount +// C.semantic.entries[0].data.variables[0].type.components[0].role: underlying +// C.semantic.entries[0].data.variables[0].type.components[0].type.kind: uint +// C.semantic.entries[0].data.variables[0].type.components[0].type.bits: 128 +// C.semantic.entries[0].data.variables[1].identifier: choice +// C.semantic.entries[0].data.variables[1].type.kind: enum +// C.semantic.entries[0].data.variables[1].type.enumValues: ["A", "B", "C"] +// C.semantic.entries[0].data.variables[2].identifier: root +// C.semantic.entries[0].data.variables[2].type.kind: struct +// C.semantic.entries[0].data.variables[2].type.components[1].name: children +// C.semantic.entries[0].data.variables[2].type.components[1].type.kind: array +// C.semantic.entries[0].data.variables[2].type.components[1].type.components[0].type: +// C.semantic.entries[0].data.variables[2].type.components[1].type.components[0].referenceId: +// C.semantic.entries[0].data.variables[3].identifier: self +// C.semantic.entries[0].data.variables[3].type.kind: contract +// C.semantic.entries[0].data.variables[3].type.definitionName: C diff --git a/test/libyul/DebugData.cpp b/test/libyul/DebugData.cpp new file mode 100644 index 000000000000..fa4ad29c8b11 --- /dev/null +++ b/test/libyul/DebugData.cpp @@ -0,0 +1,438 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::langutil; + +namespace solidity::yul::test +{ + +namespace +{ + +FunctionDefinition* findFunctionDefinition(Block& _block) +{ + for (Statement& statement: _block.statements) + if (auto* functionDefinition = std::get_if(&statement)) + return functionDefinition; + return nullptr; +} + +FunctionDefinition const* findFunctionDefinition(Block const& _block) +{ + for (Statement const& statement: _block.statements) + if (auto const* functionDefinition = std::get_if(&statement)) + return functionDefinition; + return nullptr; +} + +SemanticDebugPointer stackPointer(std::string _name) +{ + SemanticDebugPointer pointer; + pointer.pointerClass = SemanticDebugPointer::Class::Region; + pointer.location = SemanticDebugPointer::Location::Stack; + pointer.name = _name; + pointer.slot = SemanticDebugPointerExpression::variable(std::move(_name)); + return pointer; +} + +SemanticDebugDataTable stackVariableTable(int64_t _astID, std::string _name, std::string _slot) +{ + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = _slot + }; + variable.ethdebugPointer = stackPointer(std::move(_slot)); + + SemanticDebugData data; + data.lexicalScopeID = _astID; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(_astID, std::make_shared(std::move(data))); + return table; +} + +void checkStackVariableSurvived(FunctionDefinition const& _function, std::string const& _slot) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugData); + SemanticDebugData const& data = *_function.debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_REQUIRE(variable.dataLocation); + BOOST_CHECK(variable.dataLocation->kind == SemanticDebugVariableLocation::Kind::Stack); + BOOST_REQUIRE(variable.dataLocation->pointerID); + BOOST_CHECK_EQUAL(*variable.dataLocation->pointerID, _slot); + BOOST_REQUIRE(variable.ethdebugPointer); +} + +void checkStackVariableOptimizedOut(FunctionDefinition const& _function) +{ + BOOST_REQUIRE(_function.debugData); + BOOST_REQUIRE(_function.debugData->semanticDebugData); + SemanticDebugData const& data = *_function.debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(data.variableDefinitions.size(), 1); + SemanticDebugVariable const& variable = data.variableDefinitions.front(); + BOOST_REQUIRE(variable.dataLocation); + BOOST_CHECK(variable.dataLocation->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!variable.dataLocation->pointerID); + BOOST_CHECK(!variable.ethdebugPointer); +} + +} + +BOOST_AUTO_TEST_SUITE(YulDebugDataTest) + +BOOST_AUTO_TEST_CASE(semantic_debug_data_survives_reparse_by_ast_id) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + auto const object = yulStack.parserResult(); + auto& root = const_cast(object->code()->root()); + auto* funDef = findFunctionDefinition(root); + BOOST_REQUIRE(funDef); + BOOST_REQUIRE(funDef->debugData); + BOOST_REQUIRE(funDef->debugData->astID); + BOOST_REQUIRE_EQUAL(*funDef->debugData->astID, 23); + + SemanticDebugData data; + data.lexicalScopeID = 17; + auto semanticDebugData = std::make_shared(std::move(data)); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + semanticDebugData + ); + + yulStack.optimize(); + + auto const& reparsedRoot = yulStack.parserResult()->code()->root(); + auto const* reparsedFunDef = findFunctionDefinition(reparsedRoot); + BOOST_REQUIRE(reparsedFunDef); + BOOST_REQUIRE(reparsedFunDef->debugData); + BOOST_REQUIRE(reparsedFunDef->debugData->astID); + BOOST_CHECK_EQUAL(*reparsedFunDef->debugData->astID, 23); + BOOST_REQUIRE(reparsedFunDef->debugData->semanticDebugData); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugData == semanticDebugData); +} + +BOOST_AUTO_TEST_CASE(reparse_marks_missing_stack_locations_optimized_out) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + auto const object = yulStack.parserResult(); + auto& root = const_cast(object->code()->root()); + auto* funDef = findFunctionDefinition(root); + BOOST_REQUIRE(funDef); + BOOST_REQUIRE(funDef->debugData); + BOOST_REQUIRE(funDef->debugData->astID); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "missing_slot" + }; + variable.ethdebugPointer = stackPointer("missing_slot"); + + SemanticDebugData data; + data.lexicalScopeID = 17; + data.variableDefinitions.emplace_back(std::move(variable)); + auto semanticDebugData = std::make_shared(std::move(data)); + funDef->debugData = DebugData::create( + funDef->debugData->nativeLocation, + funDef->debugData->originLocation, + funDef->debugData->astID, + semanticDebugData + ); + + yulStack.optimize(); + + auto const& reparsedRoot = yulStack.parserResult()->code()->root(); + auto const* reparsedFunDef = findFunctionDefinition(reparsedRoot); + BOOST_REQUIRE(reparsedFunDef); + BOOST_REQUIRE(reparsedFunDef->debugData); + BOOST_REQUIRE(reparsedFunDef->debugData->semanticDebugData); + BOOST_CHECK(reparsedFunDef->debugData->semanticDebugData != semanticDebugData); + + SemanticDebugData const& reparsedDebugData = *reparsedFunDef->debugData->semanticDebugData; + BOOST_REQUIRE_EQUAL(reparsedDebugData.variableDefinitions.size(), 1); + SemanticDebugVariable const& reparsedVariable = reparsedDebugData.variableDefinitions.front(); + BOOST_REQUIRE(reparsedVariable.dataLocation); + BOOST_CHECK(reparsedVariable.dataLocation->kind == SemanticDebugVariableLocation::Kind::OptimizedOut); + BOOST_CHECK(!reparsedVariable.dataLocation->pointerID); + BOOST_CHECK(!reparsedVariable.ethdebugPointer); +} + +BOOST_AUTO_TEST_CASE(attach_marks_missing_stack_locations_optimized_out) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + pop(1) + } + })")); + + // No optimization or reparse involved: attaching to IR whose stack slots are + // already gone must mark the variable OptimizedOut right away. + yulStack.attachSemanticDebugData(stackVariableTable(23, "value", "missing_slot")); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableOptimizedOut(*funDef); +} + +BOOST_AUTO_TEST_CASE(bound_pointer_variables_do_not_affect_survival) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + })")); + + // The pointer references var_x (a Yul variable that exists), "aux" (bound by + // a scope definition), "item" (bound as a list index) and "key" (bound as a + // template parameter). Only var_x is a free Yul dependency, so the stack + // location must survive. + SemanticDebugPointer element = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "element", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("var_x"), + SemanticDebugPointerExpression::variable("aux"), + SemanticDebugPointerExpression::variable("item"), + SemanticDebugPointerExpression::variable("key") + }) + ); + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"aux", SemanticDebugPointerExpression::literal("0x01")}}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::literal("0x02"), + "item", + std::move(element) + ) + ); + pointer.expectedParameters = {"key"}; + + SemanticDebugVariable variable; + variable.identifier = "x"; + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "var_x" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 23; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(23, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableSurvived(*funDef, "var_x"); +} + +BOOST_AUTO_TEST_CASE(free_pointer_variables_in_expressions_require_yul_names) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + })")); + + // The slot expression references a Yul variable that does not exist even + // though it is buried inside arithmetic; the location must be dropped. + SemanticDebugPointer pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "element", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("var_x"), + SemanticDebugPointerExpression::variable("var_missing") + }) + ); + + SemanticDebugVariable variable; + variable.identifier = "x"; + variable.dataLocation = SemanticDebugVariableLocation{ + .kind = SemanticDebugVariableLocation::Kind::Stack, + .pointerID = "var_x" + }; + variable.ethdebugPointer = std::move(pointer); + + SemanticDebugData data; + data.lexicalScopeID = 23; + data.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.set(23, std::make_shared(std::move(data))); + yulStack.attachSemanticDebugData(table); + + auto const* funDef = findFunctionDefinition(yulStack.parserResult()->code()->root()); + BOOST_REQUIRE(funDef); + checkStackVariableOptimizedOut(*funDef); +} + +BOOST_AUTO_TEST_CASE(stack_location_survival_is_scoped_per_object) +{ + OptimiserSettings optimiserSettings = OptimiserSettings::none(); + optimiserSettings.yulOptimiserSteps = ""; + optimiserSettings.yulOptimiserCleanupSteps = ""; + + YulStack yulStack( + solidity::test::CommonOptions::get().evmVersion(), + optimiserSettings, + DebugInfoSelection::All() + ); + + BOOST_REQUIRE(yulStack.parseAndAnalyze("source", R"(/// @use-src 0:"source" + object "a" { + code { + /** @ast-id 23 */ + function f() { + let var_x := 1 + pop(var_x) + } + f() + } + /// @use-src 0:"source" + object "a_deployed" { + code { + /** @ast-id 23 */ + function f() { + pop(1) + } + f() + } + } + })")); + + yulStack.attachSemanticDebugData(stackVariableTable(23, "x", "var_x")); + + auto const object = yulStack.parserResult(); + auto const* creationFunDef = findFunctionDefinition(object->code()->root()); + BOOST_REQUIRE(creationFunDef); + checkStackVariableSurvived(*creationFunDef, "var_x"); + + BOOST_REQUIRE_EQUAL(object->subObjects.size(), 1); + auto const* deployedObject = dynamic_cast(object->subObjects.front().get()); + BOOST_REQUIRE(deployedObject); + auto const* deployedFunDef = findFunctionDefinition(deployedObject->code()->root()); + BOOST_REQUIRE(deployedFunDef); + // The slot only survives in the creation code, so the deployed code must not + // report a stale stack location for it. + checkStackVariableOptimizedOut(*deployedFunDef); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::yul::test diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 69a6d344f880..b5b16a7a3432 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -1431,13 +1431,13 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_incompatible_outputs) {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, }; for (auto const& test: supportedCLIFlagCombinations) @@ -1487,9 +1487,13 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) createFilesWithParentDirs({tempDir.path() / "input.sol"}, "pragma solidity >=0.0; contract C { function f() public pure {} }"); createFilesWithParentDirs({tempDir.path() / "input.yul"}, "{}"); static std::vector> erroneousCLIFlagCombinations{ + // ethdebug depends on ast-id. + { + {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + }, // --debug-info ethdebug with --optimize is not supported { - {"solc", "--experimental", "--debug-info", "ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, }, { {"solc", "--experimental", "--debug-info", "location", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, @@ -1503,22 +1507,22 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) }; static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, }, }; @@ -1606,6 +1610,69 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) } } +BOOST_AUTO_TEST_CASE(cli_ethdebug_semantic_sidecar_supports_two_stage_compilation) +{ + TemporaryDirectory tempDir(TEST_CASE_NAME); + boost::filesystem::path inputPath = tempDir.path() / "input.sol"; + boost::filesystem::path outputDir = tempDir.path() / "output"; + createFilesWithParentDirs( + {inputPath}, + "contract C { uint256 value; function f(uint256 argument) public { value = argument; } }" + ); + boost::filesystem::create_directories(outputDir); + + OptionsReaderAndMessages firstStage = runCLI({ + "solc", + "--experimental", + "--ir", + "--ir-ethdebug", + "--output-dir", + outputDir.string(), + inputPath.string(), + }); + BOOST_REQUIRE(firstStage.success); + + boost::filesystem::path yulPath = outputDir / "C.yul"; + boost::filesystem::path sidecarPath = outputDir / "C_ir_ethdebug.json"; + BOOST_REQUIRE(boost::filesystem::is_regular_file(yulPath)); + BOOST_REQUIRE(boost::filesystem::is_regular_file(sidecarPath)); + BOOST_CHECK(readFileAsString(sidecarPath).find("argument") != std::string::npos); + + OptionsReaderAndMessages secondStage = runCLI({ + "solc", + "--strict-assembly", + "--experimental", + "--ethdebug-input", + sidecarPath.string(), + "--ir-ethdebug", + "--ethdebug-program", + yulPath.string(), + }); + BOOST_REQUIRE(secondStage.success); + BOOST_CHECK(secondStage.stderrContent.empty()); + BOOST_CHECK(secondStage.stdoutContent.find("Ethdebug semantic data sidecar:") != std::string::npos); + BOOST_CHECK(secondStage.stdoutContent.find("Debug Data (ethdebug/format/program):") != std::string::npos); + BOOST_CHECK(secondStage.stdoutContent.find("argument") != std::string::npos); + + boost::filesystem::path secondYulPath = outputDir / "D.yul"; + createFilesWithParentDirs({secondYulPath}, readFileAsString(yulPath)); + OptionsReaderAndMessages mappedStage = runCLI({ + "solc", + "--strict-assembly", + "--experimental", + "--ethdebug-input", + yulPath.string() + "=" + sidecarPath.string(), + "--ethdebug-input", + secondYulPath.string() + "=" + sidecarPath.string(), + "--ir-ethdebug", + yulPath.string(), + secondYulPath.string(), + }); + BOOST_REQUIRE(mappedStage.success); + BOOST_CHECK(mappedStage.stderrContent.empty()); + BOOST_CHECK(mappedStage.stdoutContent.find("argument") != std::string::npos); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::frontend::test diff --git a/test/solc/CommandLineParser.cpp b/test/solc/CommandLineParser.cpp index 5dd00e05aab8..9e9ffa809fb4 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -198,9 +198,9 @@ BOOST_AUTO_TEST_CASE(cli_mode_options) expectedOptions.formatting.withErrorIds = true; expectedOptions.compiler.outputs = { true, true, true, true, true, + true, true, true, false, true, true, true, true, true, true, - true, true, true, true, true, - true, true, true, + true, true, true, true, }; expectedOptions.compiler.estimateGas = true; expectedOptions.compiler.combinedJsonRequests = { @@ -650,13 +650,13 @@ BOOST_AUTO_TEST_CASE(invalid_optimizer_sequence_without_optimize) BOOST_AUTO_TEST_CASE(ethdebug) { // --ethdebug-program with explicit debug-info - CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir"}); + CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --ethdebug-program-runtime with explicit debug-info - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); @@ -680,12 +680,33 @@ BOOST_AUTO_TEST_CASE(ethdebug) BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --debug-info ethdebug with --ir only (no program output) - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ir, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // --ir-ethdebug emits the sidecar and implicitly enables ethdebug + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ir-ethdebug"}); + BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.irEthdebug, true); + BOOST_REQUIRE(commandLineOptions.output.debugInfoSelection.has_value()); + BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // --ethdebug-input attaches a sidecar in strict assembly mode + commandLineOptions = parseCommandLine({ + "solc", "contract.yul", "--strict-assembly", "--experimental", "--ethdebug-input", "debug.json" + }); + BOOST_REQUIRE_EQUAL(commandLineOptions.input.ethdebugInputs.size(), 1); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.front(), "debug.json"); + BOOST_REQUIRE(commandLineOptions.output.debugInfoSelection.has_value()); + BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // Repeated source=sidecar mappings pair multiple strict assembly inputs. + commandLineOptions = parseCommandLine({ + "solc", "a.yul", "b.yul", "--strict-assembly", "--experimental", + "--ethdebug-input", "a.yul=a.json", "--ethdebug-input", "b.yul=b.json" + }); + BOOST_REQUIRE_EQUAL(commandLineOptions.input.ethdebugInputs.size(), 2); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.at(0), "a.yul=a.json"); + BOOST_CHECK_EQUAL(commandLineOptions.input.ethdebugInputs.at(1), "b.yul=b.json"); // --ethdebug-resources does not require --via-ir commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-resources"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugResources, true); @@ -705,6 +726,7 @@ BOOST_AUTO_TEST_CASE(experimental_features_without_experimental_flag) std::vector const experimentalFeatures { "--import-ast", "--import-asm-json", + "--ir-ethdebug", "--ir-ast-json", "--ir-optimized-ast-json", "--yul-cfg-json", @@ -730,6 +752,15 @@ BOOST_AUTO_TEST_CASE(experimental_features_without_experimental_flag) std::vector const commandLineOptions{"solc", experimentalFeature, "contract.sol"}; BOOST_CHECK_EXCEPTION(parseCommandLine(commandLineOptions), CommandLineValidationError, hasCorrectMessage); } + + expectedErrorMessage = + "The following options are only available in experimental mode: --ethdebug-input. " + "To enable experimental mode, use the --experimental flag."; + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "--strict-assembly", "--ethdebug-input", "debug.json", "contract.yul"}), + CommandLineValidationError, + hasCorrectMessage + ); } BOOST_AUTO_TEST_CASE(via_ssa_cfg_smoke)