From d3fcae4d9c05799742bb499c8a83870a1dd0bf35 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 5 Oct 2026 08:46:49 +0200 Subject: [PATCH] ethdebug: Carry the semantic debug info sidecar across the textual Yul boundary Serialize the side table as its versioned JSON sidecar and accept it back, so a two-stage Solidity -> Yul and Yul -> bytecode compilation produces the same debug output as a one-stage one, and other Yul-based frontends can supply their own semantic debug info: - Standard JSON: the irEthdebug output emits the sidecar of a contract's ir output; auxiliaryInput.ethdebug attaches one to Yul input. - CLI: --ir-ethdebug emits it; --ethdebug-input attaches one in strict assembly mode. Attached records flow into the ethdebug outputs of the Yul pipeline: resources are published from the attached table, and the program-level context of the assembled objects is derived from it, under the sidecar's contract name. --- Changelog.md | 2 + .../ethdebug_internal_debug_info.rst | 14 +- docs/using-the-compiler.rst | 22 +- libsolidity/interface/CompilerStack.cpp | 8 +- libsolidity/interface/Ethdebug.cpp | 20 + libsolidity/interface/Ethdebug.h | 11 + libsolidity/interface/StandardCompiler.cpp | 83 ++- libsolidity/interface/StandardCompiler.h | 1 + libyul/YulStack.cpp | 26 +- libyul/YulStack.h | 15 + solc/CommandLineInterface.cpp | 79 ++- solc/CommandLineInterface.h | 1 + solc/CommandLineParser.cpp | 32 +- solc/CommandLineParser.h | 4 + .../ethdebug_input_invalid_json/args | 1 + .../ethdebug_input_invalid_json/err | 1 + .../ethdebug_input_invalid_json/exit | 1 + .../ethdebug_input_invalid_json/in.yul | 5 + .../ethdebug_input_invalid_json/sidecar.json | 1 + .../ethdebug_input_missing_file/args | 1 + .../ethdebug_input_missing_file/err | 1 + .../ethdebug_input_missing_file/exit | 1 + .../ethdebug_input_missing_file/in.yul | 5 + .../ethdebug_input_two_yul_sources/a.yul | 5 + .../ethdebug_input_two_yul_sources/args | 1 + .../ethdebug_input_two_yul_sources/b.yul | 5 + .../ethdebug_input_two_yul_sources/err | 1 + .../ethdebug_input_two_yul_sources/exit | 1 + .../sidecar.json | 1 + test/cmdlineTests/ir_ethdebug/args | 1 + test/cmdlineTests/ir_ethdebug/input.sol | 12 + test/cmdlineTests/ir_ethdebug/output | 149 ++++++ .../args | 1 + .../in.yul | 18 + .../input.json | 69 +++ .../output.json | 477 ++++++++++++++++++ test/libsolidity/StandardCompiler.cpp | 157 ++++++ test/solc/CommandLineInterface.cpp | 51 ++ test/solc/CommandLineParser.cpp | 50 +- 39 files changed, 1304 insertions(+), 30 deletions(-) create mode 100644 test/cmdlineTests/ethdebug_input_invalid_json/args create mode 100644 test/cmdlineTests/ethdebug_input_invalid_json/err create mode 100644 test/cmdlineTests/ethdebug_input_invalid_json/exit create mode 100644 test/cmdlineTests/ethdebug_input_invalid_json/in.yul create mode 100644 test/cmdlineTests/ethdebug_input_invalid_json/sidecar.json create mode 100644 test/cmdlineTests/ethdebug_input_missing_file/args create mode 100644 test/cmdlineTests/ethdebug_input_missing_file/err create mode 100644 test/cmdlineTests/ethdebug_input_missing_file/exit create mode 100644 test/cmdlineTests/ethdebug_input_missing_file/in.yul create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/a.yul create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/args create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/b.yul create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/err create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/exit create mode 100644 test/cmdlineTests/ethdebug_input_two_yul_sources/sidecar.json create mode 100644 test/cmdlineTests/ir_ethdebug/args create mode 100644 test/cmdlineTests/ir_ethdebug/input.sol create mode 100644 test/cmdlineTests/ir_ethdebug/output create mode 100644 test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/args create mode 100644 test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/in.yul create mode 100644 test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/input.json create mode 100644 test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/output.json diff --git a/Changelog.md b/Changelog.md index d5a4e6aa02e7..fb7199d8957a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -34,6 +34,7 @@ Language Features: Compiler Features: * Commandline Interface: Remove support for the experimental Language Server Protocol (LSP) mode. +* Commandline Interface: Add the experimental `--ir-ethdebug` output and the assembler-mode `--ethdebug-input` option, which carry the semantic debug info of the Yul IR across a textual Yul boundary. * EVM: Deprecate support for "constantinople", "petersburg", "istanbul" and "berlin" EVM versions. * Evmasm Optimizer: Improve performance of block deduplicator. * General: Improve performance throughout the compiler using Boost's flat versions of unordered set and map. @@ -41,6 +42,7 @@ Compiler Features: * General: Replace the current greedy stack shuffler in the experimental SSA CFG pipeline with a planning one. * SMTChecker: Emit a deprecation warning for the BMC engine. * SMTChecker: Support `block.slotnum`. +* Standard JSON Interface: Add the experimental `irEthdebug` output and `auxiliaryInput.ethdebug` input, which carry the semantic debug info of the Yul IR across a textual Yul boundary. * Yul: Add the `@ast-id-instance` debug info annotation, which distinguishes Yul scopes cloned from the same Solidity definition. * Yul Optimizer: `LoopInvariantCodeMotion` can now move expressions depending on function parameters out of loops. * Yul Optimizer: `UnusedStoreEliminator` can now recognize redundant memory and storage operations whose start offset or length is a function parameter. diff --git a/docs/compiler-guide/ethdebug_internal_debug_info.rst b/docs/compiler-guide/ethdebug_internal_debug_info.rst index 53f151019ae4..abc526aef422 100644 --- a/docs/compiler-guide/ethdebug_internal_debug_info.rst +++ b/docs/compiler-guide/ethdebug_internal_debug_info.rst @@ -77,15 +77,11 @@ Accordingly, ``ethdebug`` depends on the ``ast-id`` ``--debug-info`` component. CLI and Standard JSON input reject a debug-info selection containing ``ethdebug`` without ``ast-id``. Selecting an ethdebug output does not modify the debug-info selection, just as ``--ir-optimized`` does not imply ``--optimize``. Without ``ethdebug`` in the selection the program outputs are still produced from the assembly, but carry none of the semantic debug info the component adds; the resources and compilation outputs do not depend on it. +The sidecar carries the semantic debug info itself, so requesting the ``irEthdebug`` output or attaching ``auxiliaryInput.ethdebug`` without ``ethdebug`` in the selection is an error. Core Structures =============== -.. note:: - - Of the types named below, only ``langutil::DebugData`` exists in the compiler today. - The ``SemanticDebugScope`` family and the ``semanticDebugScope`` field on ``DebugData`` are planned additions and will arrive with the implementation pull requests. - ``langutil::DebugData`` is the debug payload carried by Yul AST nodes. The sidecar envelope and variable records are compiler-specific. Type documents and pointer documents follow the public `type `_ and `pointer `_ schemas, with one internal pointer expression for unresolved Yul locals. @@ -398,7 +394,8 @@ Each source record in ``ethdebug.compilation`` maps its ID to the source path an Instance ``0`` is written explicitly. Readers reject an unknown format, an unsupported version, malformed sidecar fields, and keys that do not parse as decimal integers in the documented ranges. -Writers emit deterministic output by ordering both key levels in ascending numeric order. +Writers emit deterministic output: the compiler's JSON writer orders object members lexicographically by key, so the decimal keys of both levels appear in that order rather than in numeric order. +Readers do not depend on key order. Compiler Interfaces ------------------- @@ -406,7 +403,8 @@ Compiler Interfaces Standard JSON uses these fields: - Solidity output ``contracts[][].ir`` contains the Yul text. -- Solidity output ``contracts[][].irEthdebug`` contains its semantic sidecar. +- Solidity output ``contracts[][].irEthdebug`` contains the semantic sidecar of its ``ir`` output. + The optimized IR is not a supported sidecar input yet: the Yul pipeline renames generated locals, and optimizer passes do not maintain the records (see the staging note above). - Yul input ``auxiliaryInput.ethdebug`` supplies the sidecar for the Yul source. - Yul output ``irEthdebug`` emits the updated sidecar again. @@ -420,7 +418,7 @@ The sidecar is both compiler input and compiler output, allowing the same debug .. note:: - For implementation testing, ``EthdebugTest`` will add ``Contract.semantic`` as a direct view of the serialized sidecar. + For implementation testing, ``EthdebugTest`` exposes ``Contract.semantic`` as a direct view of the serialized sidecar. Required Variable Coverage ========================== diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index e13cb145c54e..25c7bcc7dcb6 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -276,6 +276,20 @@ Input Description } } }, + // Optional auxiliary inputs. + "auxiliaryInput": { + // Semantic debug info for the Yul input (experimental): the object the Solidity + // `irEthdebug` output emits for the `ir` output, attached to the Yul by its `@ast-id` + // annotations. Requires `ethdebug` in `settings.debug.debugInfo`. Yul optimization is + // not yet supported together with it. + "ethdebug": { + "format": "solidity-ethdebug-semantic-data", + "version": 1, + "contractName": "ContractName", + "resources": {/* ... */}, + "scopes": {/* ... */} + } + }, // Optional "settings": { @@ -398,7 +412,7 @@ Input Description // `ethdebug` without `ast-id` is an error. Requesting an ethdebug output does not // change this selection; without `ethdebug` in it the `evm.bytecode.ethdebug` and // `evm.deployedBytecode.ethdebug` outputs carry none of the semantic debug info - // this component adds. + // this component adds, and `irEthdebug` and `auxiliaryInput.ethdebug` are an error. // - `*`: Wildcard value that can be used to request all non-experimental components. "debugInfo": ["location", "snippet", "ast-id", "ethdebug"] }, @@ -451,6 +465,7 @@ Input Description // userdoc - User documentation (natspec) // metadata - Metadata // ir - Yul intermediate representation of the code before optimization + // irEthdebug - Semantic debug info of the `ir` output: type documents, pointer templates and the variable scopes keyed by AST ID. Requires `ethdebug` in `settings.debug.debugInfo`. (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) @@ -608,6 +623,8 @@ Output Description "devdoc": {}, // Intermediate representation before optimization (string) "ir": "", + // Semantic debug info of the Yul IR (experimental) + "irEthdebug": {/* ... */}, // AST of intermediate representation before optimization "irAst": {/* ... */}, // Intermediate representation after optimization (string) @@ -773,7 +790,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 ethdebug`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | SSA CFG | ``ssa-cfg`` | yes | ``--via-ssa-cfg`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 5552c0ab5d35..196d71e0d2d3 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -1166,8 +1166,6 @@ Json CompilerStack::interfaceSymbols(std::string const& _contractName) const Json CompilerStack::ethdebug() const { solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); - // The resource tables are derived from analysis results alone, so - // ethdebug.resources is available right after analysis. ethdebug::Resources resources; std::map const sourceIndexMap = sourceIndices(); for (auto const& [name, compiledContract]: m_contracts) @@ -1610,9 +1608,9 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti *compiledContract.yulIR, compiledContract.yulSemanticDebugData ? &*compiledContract.yulSemanticDebugData : nullptr ); - // The side table pairs with the unoptimized IR: attaching checks every - // pointer against it, and the table is kept as attached. The optimized IR, - // whose variable names differ, is not a supported pairing yet. + // The sidecar pairs with the unoptimized IR: attaching checks every pointer + // against it, and the table is kept as attached. The optimized IR, whose + // variable names differ, is not a supported sidecar input yet. if (compiledContract.yulSemanticDebugData) compiledContract.yulSemanticDebugData = stack.semanticDebugData(); if (!_unoptimizedOnly) diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp index 67dce65aef42..5f52ddb49676 100644 --- a/libsolidity/interface/Ethdebug.cpp +++ b/libsolidity/interface/Ethdebug.cpp @@ -25,8 +25,11 @@ #include +#include #include +#include + #include #include #include @@ -1019,6 +1022,23 @@ ethdebug::Resources ethdebug::resources(ContractDefinition const& _contract, std return result; } +ethdebug::Resources ethdebug::resources(langutil::SemanticDebugDataTable const& _table) +{ + Resources result; + result.types = _table.types(); + result.pointers = _table.pointerTemplates(); + return result; +} + +std::optional ethdebug::programContext(langutil::SemanticDebugDataTable const& _table) +{ + langutil::SemanticDebugScope variables; + for (auto const& scope: _table.scopes() | ranges::views::values) + if (scope) + variables.variableDefinitions += scope->variableDefinitions; + return programContext(variables, resources(_table)); +} + langutil::SemanticDebugDataTable ethdebug::semanticDebugDataTable( ContractDefinition const& _contract, std::map const& _sourceIndices diff --git a/libsolidity/interface/Ethdebug.h b/libsolidity/interface/Ethdebug.h index b8254228dd18..458c4c2b89dd 100644 --- a/libsolidity/interface/Ethdebug.h +++ b/libsolidity/interface/Ethdebug.h @@ -85,6 +85,17 @@ std::optional programContext( Resources const& _resources ); +/// The resources @a _table carries: its type documents and pointer templates. +Resources resources(langutil::SemanticDebugDataTable const& _table); + +/// The program-level context of the variables of all scope records of +/// @a _table, as programContext() selects them: those whose pointer is closed, +/// which are the contract-level ones. For Yul input, whose side table comes +/// from a sidecar rather than from the contract it was generated from. +std::optional programContext( + langutil::SemanticDebugDataTable const& _table +); + /// The semantic debug info side table of @a _contract, as specified in /// docs/compiler-guide/ethdebug_internal_debug_info.rst, together with its /// resources(). It holds the scope record of the contract, with the state diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index 60b55acf4474..99c6c7d1d3f5 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", "ethdebug"}; + static std::set experimental{"ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "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; @@ -185,7 +187,7 @@ bool isArtifactRequested(Json const& _outputSelection, std::string const& _artif else if (selectedArtifact == "*") { // 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) @@ -272,7 +274,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", + "ir", "irAst", "irEthdebug", "irOptimized", "irOptimizedAst", "evm.gasEstimates", "evm.legacyAssembly", "evm.assembly" } + evmObjectComponents("bytecode") + evmObjectComponents("deployedBytecode"); @@ -321,7 +323,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" }; @@ -388,7 +390,8 @@ CompilerStack::ContractSelection pipelineConfig( pipelineForContract.irCodegen || pipelineForContract.irOptimization || request == "ir" || - request == "irAst"; + request == "irAst" || + request == "irEthdebug"; pipelineForContract.bytecode = isEvmBytecodeRequested(_jsonOutputSelection); } std::string key = (sourceUnitName == "*") ? "" : sourceUnitName; @@ -475,7 +478,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"); } @@ -848,6 +851,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 (util::JsonValidationError const& _exception) + { + return formatFatalError( + Error::Type::JSONError, + "Invalid 'auxiliaryInput.ethdebug': " + stringOrDefault(_exception.comment()) + ); + } + } } Json const& settings = _input.value("settings", Json::object()); @@ -1256,12 +1279,30 @@ 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'."); } + // The sidecar carries the semantic debug info itself: requesting it or + // attaching one without the ethdebug debug info selection is an error, + // unlike the program outputs, which are simply empty. + static std::array constexpr semanticSidecarArtifacts{"irEthdebug"}; + if ( + (ret.semanticDebugData || areArtifactsRequested(ret.outputSelection, semanticSidecarArtifacts)) && + !(ret.debugInfoSelection.has_value() && 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) && ret.language == "Solidity" && !ret.viaIR) return formatFatalError(Error::Type::FatalError, "'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' can only be selected as output, if 'viaIR' was set."); @@ -1611,6 +1652,14 @@ 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)) + { + // The output requires ethdebug in the debug info selection, and the + // selection makes IR generation store the table on the contract. + auto const& semanticDebugData = compilerStack.yulSemanticDebugData(contractName); + solAssert(semanticDebugData, "Semantic debug info missing for contract " + contractName + "."); + contractData["irEthdebug"] = semanticDebugDataToJson(*semanticDebugData); + } 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)) @@ -1783,8 +1832,19 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) else { contractName = stack.parserResult()->name; + if (_inputsAndSettings.semanticDebugData) + { + stack.attachSemanticDebugData(*_inputsAndSettings.semanticDebugData); + stack.setEthdebugProgramContext( + _inputsAndSettings.semanticDebugData->contractName().value_or(contractName), + ethdebug::programContext(stack.semanticDebugData()) + ); + } 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)) { Json sourceResult; @@ -1867,9 +1927,14 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.resources")) { solAssert(_inputsAndSettings.experimental, ""); + ethdebug::Resources resources; + if (_inputsAndSettings.semanticDebugData) + resources = ethdebug::resources(stack.semanticDebugData()); output["ethdebug"]["resources"] = evmasm::ethdebug::resources( {{.id = 0, .path = sourceName, .contents = sourceContents, .language = "Yul"}}, - VersionString + VersionString, + std::move(resources.types), + std::move(resources.pointers) ); } if (isEthdebugGlobalOutputRequested(_inputsAndSettings.outputSelection, "ethdebug.compilation")) diff --git a/libsolidity/interface/StandardCompiler.h b/libsolidity/interface/StandardCompiler.h index cf2200d8cf12..0a9440aca201 100644 --- a/libsolidity/interface/StandardCompiler.h +++ b/libsolidity/interface/StandardCompiler.h @@ -82,6 +82,7 @@ class StandardCompiler OptimiserSettings optimiserSettings; std::optional debugInfoSelection; std::map libraries; + std::optional semanticDebugData; bool metadataLiteralSources = false; CompilerStack::MetadataFormat metadataFormat = CompilerStack::defaultMetadataFormat(); CompilerStack::MetadataHash metadataHash = CompilerStack::MetadataHash::IPFS; diff --git a/libyul/YulStack.cpp b/libyul/YulStack.cpp index e806828f5d63..8d93ee97ef6a 100644 --- a/libyul/YulStack.cpp +++ b/libyul/YulStack.cpp @@ -275,14 +275,14 @@ YulStack::assembleWithDeployed(std::optional _deployName, bool {{m_charStream->name(), 0}} ); if (debugInfoSelection().ethdebug) - creationObject.ethdebug = evmasm::ethdebug::program(creationObject.assembly->name(), 0, *creationObject.assembly, *creationObject.bytecode); + creationObject.ethdebug = ethdebugProgram(creationObject); if (deployedAssembly) { deployedObject.bytecode = std::make_shared(deployedAssembly->assemble()); deployedObject.assembly = deployedAssembly; if (debugInfoSelection().ethdebug) - deployedObject.ethdebug = evmasm::ethdebug::program(deployedObject.assembly->name(), 0, *deployedObject.assembly, *deployedObject.bytecode); + deployedObject.ethdebug = ethdebugProgram(deployedObject); deployedObject.sourceMappings = std::make_unique( evmasm::AssemblyItem::computeSourceMapping( deployedAssembly->items(), @@ -408,6 +408,28 @@ void YulStack::attachSemanticDebugData(SemanticDebugDataTable const& _table) applySemanticDebugData(*m_parserResult, m_semanticDebugData); } +void YulStack::setEthdebugProgramContext( + std::string _contractName, + std::optional _context +) +{ + m_ethdebugContractName = std::move(_contractName); + m_ethdebugProgramContext = std::move(_context); +} + +Json YulStack::ethdebugProgram(MachineAssemblyObject const& _object) const +{ + yulAssert(_object.assembly && _object.bytecode); + // Yul input has a single source, with ID 0 in the ethdebug compilation record. + return evmasm::ethdebug::program( + m_ethdebugContractName.value_or(_object.assembly->name()), + 0, + *_object.assembly, + *_object.bytecode, + m_ethdebugProgramContext + ); +} + Dialect const& YulStack::dialect() const { yulAssert(m_stackState >= AnalysisSuccessful); diff --git a/libyul/YulStack.h b/libyul/YulStack.h index 1843cce210f8..a827bc276a30 100644 --- a/libyul/YulStack.h +++ b/libyul/YulStack.h @@ -37,6 +37,8 @@ #include +#include + #include #include @@ -158,6 +160,14 @@ class YulStack: public langutil::CharStreamProvider /// The side table as attached to the current Yul AST: the resource tables /// and every scope record, whether or not a Yul node carries it. langutil::SemanticDebugDataTable const& semanticDebugData() const { return m_semanticDebugData; } + /// Publishes @a _context as the program-level ethdebug context of the + /// assembled objects, under @a _contractName. The context is lowered from + /// the attached semantic debug info by the caller, which knows the source + /// language the object was generated from. + void setEthdebugProgramContext( + std::string _contractName, + std::optional _context + ); Dialect const& dialect() const; @@ -179,6 +189,9 @@ class YulStack: public langutil::CharStreamProvider void reportUnimplementedFeatureError(langutil::UnimplementedFeatureError const& _error); + /// The ethdebug program output of an assembled object, with the program-level context if one was set. + Json ethdebugProgram(MachineAssemblyObject const& _object) const; + langutil::EVMVersion m_evmVersion; solidity::frontend::OptimiserSettings m_optimiserSettings; langutil::DebugInfoSelection m_debugInfoSelection{}; @@ -195,6 +208,8 @@ class YulStack: public langutil::CharStreamProvider /// Semantic debug info keyed by (AST ID, instance). Retained across reparses /// so that scope records no Yul node carries survive the Yul text boundary. langutil::SemanticDebugDataTable m_semanticDebugData; + std::optional m_ethdebugContractName; + std::optional m_ethdebugProgramContext; langutil::ErrorList m_errors; langutil::ErrorReporter m_errorReporter; diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index 6075e9ce697b..4220ae45e065 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 @@ -267,6 +269,24 @@ 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; + + // The output requires ethdebug in the debug info selection, and the + // selection makes IR generation store the table on the contract. + auto const& semanticDebugData = m_compiler->yulSemanticDebugData(_contractName); + solAssert(semanticDebugData, "Semantic debug info missing for contract " + _contractName + "."); + std::string serialized = jsonPrint(semanticDebugDataToJson(*semanticDebugData), m_options.formatting.json); + if (!m_options.output.dir.empty()) + createFile(m_compiler->filesystemFriendlyName(_contractName) + "_ir_ethdebug.json", serialized); + else + sout() << "IR ethdebug sidecar:" << std::endl << serialized << std::endl; +} + void CommandLineInterface::handleIRAst(std::string const& _contractName) { solAssert(CompilerInputModes.count(m_options.input.mode) == 1); @@ -922,6 +942,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 || @@ -1252,6 +1273,39 @@ std::string CommandLineInterface::objectWithLinkRefsHex(evmasm::LinkerObject con void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) { solAssert(m_options.input.mode == InputMode::Assembler); + std::optional semanticDebugData; + if (m_options.input.ethdebugInput) + { + // The sidecar belongs to the one Yul input; no syntax pairs sidecars + // with multiple inputs. + if (m_fileReader.sourceUnits().size() != 1) + solThrow(CommandLineExecutionError, "--ethdebug-input requires exactly one strict assembly input."); + + Json json; + std::string parseError; + std::string contents; + try + { + contents = readFileAsString(*m_options.input.ethdebugInput); + } + 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 + { + semanticDebugData = semanticDebugDataFromJson(json); + } + catch (util::JsonValidationError const& _exception) + { + solThrow( + CommandLineExecutionError, + "Invalid --ethdebug-input: " + stringOrDefault(_exception.comment()) + ); + } + } bool successful = true; std::map yulStacks; @@ -1271,6 +1325,15 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) solAssert(stack.hasErrors(), "No error reported, but parsing/analysis failed."); else { + if (semanticDebugData.has_value()) + { + stack.attachSemanticDebugData(*semanticDebugData); + stack.setEthdebugProgramContext( + semanticDebugData->contractName().value_or(stack.parserResult()->name), + ethdebug::programContext(stack.semanticDebugData()) + ); + } + if ( m_options.compiler.outputs.asmJson && stack.parserResult() && @@ -1320,11 +1383,16 @@ void CommandLineInterface::assembleYul(yul::YulStack::Machine _targetMachine) if (m_options.compiler.outputs.ethdebugResources) { + ethdebug::Resources resources; + if (semanticDebugData.has_value()) + resources = ethdebug::resources(stack.semanticDebugData()); 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(resources.types), + std::move(resources.pointers) ), m_options.formatting.json ) << std::endl; @@ -1351,6 +1419,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 << "IR ethdebug sidecar:" << std::endl; + sout() << jsonPrint( + semanticDebugDataToJson(stack.semanticDebugData()), + m_options.formatting.json + ) << std::endl; + } if (m_options.compiler.outputs.binary) { @@ -1427,6 +1503,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 7d23ec6c3fb1..885b6b5514b5 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 1272ec7e2f83..3cc560d41b74 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", "ethdebug-resources", @@ -467,6 +470,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), @@ -698,6 +702,11 @@ 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()->value_name("file"), + "(experimental) Attach the serialized ethdebug semantic debug info sidecar to the strict assembly input." + ) ; desc.add(assemblyModeOptions); @@ -749,6 +758,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.") @@ -1065,7 +1075,8 @@ void CommandLineParser::processArgs() {g_strModelCheckerTimeout, {InputMode::Compiler, InputMode::CompilerWithASTImport}}, {g_strModelCheckerBMCLoopIterations, {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) @@ -1348,6 +1359,8 @@ void CommandLineParser::processArgs() if (dialect != g_strEVM) solThrow(CommandLineValidationError, "Invalid option for --" + g_strYulDialect + ": " + dialect); } + if (m_args.contains(g_strEthdebugInput)) + m_options.input.ethdebugInput = m_args[g_strEthdebugInput].as(); m_options.output.viaSSACFG = m_args.contains(g_strViaSSACFG); @@ -1356,6 +1369,11 @@ void CommandLineParser::processArgs() // contain ethdebug explicitly. bool const ethdebugSelected = m_options.output.debugInfoSelection.has_value() && m_options.output.debugInfoSelection->ethdebug; + if ((m_options.input.ethdebugInput || m_options.compiler.outputs.irEthdebug) && !ethdebugSelected) + solThrow( + CommandLineValidationError, + fmt::format("--debug-info must contain ethdebug when using --{} or --ir-ethdebug.", g_strEthdebugInput) + ); bool const ethdebugProgramRequested = m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime; if ((ethdebugSelected || ethdebugProgramRequested) && m_options.output.viaSSACFG) @@ -1517,6 +1535,18 @@ void CommandLineParser::processArgs() enableEthdebugProgramMessage + " output can only be selected, if --via-ir was specified." ); + if ( + m_options.compiler.outputs.irEthdebug && + (!m_options.output.debugInfoSelection.has_value() || !m_options.output.debugInfoSelection->ethdebug) + ) + solThrow( + CommandLineValidationError, + fmt::format( + "--debug-info must contain ethdebug when using --{}.", + CompilerOutputs::componentName(&CompilerOutputs::irEthdebug) + ) + ); + 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 199a27b233dc..2b562a443df6 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}, @@ -103,6 +104,7 @@ struct CompilerOutputs bool binaryRuntime = false; bool abi = false; bool ir = false; + bool irEthdebug = false; bool irAstJson = false; bool irOptimized = false; bool irOptimizedAstJson = false; @@ -191,6 +193,8 @@ struct CommandLineOptions FileReader::FileSystemPathSet allowedDirectories; bool ignoreMissingFiles = false; bool noImportCallback = false; + /// Serialized ethdebug semantic debug info sidecar to attach to the strict assembly input. + std::optional ethdebugInput; } input; struct Output diff --git a/test/cmdlineTests/ethdebug_input_invalid_json/args b/test/cmdlineTests/ethdebug_input_invalid_json/args new file mode 100644 index 000000000000..9b2b1552a77c --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_invalid_json/args @@ -0,0 +1 @@ +--strict-assembly --experimental --debug-info ast-id,ethdebug --ethdebug-input sidecar.json in.yul diff --git a/test/cmdlineTests/ethdebug_input_invalid_json/err b/test/cmdlineTests/ethdebug_input_invalid_json/err new file mode 100644 index 000000000000..f8057ca4a737 --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_invalid_json/err @@ -0,0 +1 @@ +Error: Could not parse --ethdebug-input: parse error at line 1, column 2: syntax error while parsing value - invalid literal; last read: 'th' diff --git a/test/cmdlineTests/ethdebug_input_invalid_json/exit b/test/cmdlineTests/ethdebug_input_invalid_json/exit new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_invalid_json/exit @@ -0,0 +1 @@ +1 diff --git a/test/cmdlineTests/ethdebug_input_invalid_json/in.yul b/test/cmdlineTests/ethdebug_input_invalid_json/in.yul new file mode 100644 index 000000000000..31547e10e6bf --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_invalid_json/in.yul @@ -0,0 +1,5 @@ +object "C" { + code { + stop() + } +} diff --git a/test/cmdlineTests/ethdebug_input_invalid_json/sidecar.json b/test/cmdlineTests/ethdebug_input_invalid_json/sidecar.json new file mode 100644 index 000000000000..ede72e79952b --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_invalid_json/sidecar.json @@ -0,0 +1 @@ +this is not json diff --git a/test/cmdlineTests/ethdebug_input_missing_file/args b/test/cmdlineTests/ethdebug_input_missing_file/args new file mode 100644 index 000000000000..f7817d466cbf --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_missing_file/args @@ -0,0 +1 @@ +--strict-assembly --experimental --debug-info ast-id,ethdebug --ethdebug-input missing.json in.yul diff --git a/test/cmdlineTests/ethdebug_input_missing_file/err b/test/cmdlineTests/ethdebug_input_missing_file/err new file mode 100644 index 000000000000..3003e5a2e316 --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_missing_file/err @@ -0,0 +1 @@ +Error: Could not read --ethdebug-input: FileNotFound diff --git a/test/cmdlineTests/ethdebug_input_missing_file/exit b/test/cmdlineTests/ethdebug_input_missing_file/exit new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_missing_file/exit @@ -0,0 +1 @@ +1 diff --git a/test/cmdlineTests/ethdebug_input_missing_file/in.yul b/test/cmdlineTests/ethdebug_input_missing_file/in.yul new file mode 100644 index 000000000000..31547e10e6bf --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_missing_file/in.yul @@ -0,0 +1,5 @@ +object "C" { + code { + stop() + } +} diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/a.yul b/test/cmdlineTests/ethdebug_input_two_yul_sources/a.yul new file mode 100644 index 000000000000..31547e10e6bf --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/a.yul @@ -0,0 +1,5 @@ +object "C" { + code { + stop() + } +} diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/args b/test/cmdlineTests/ethdebug_input_two_yul_sources/args new file mode 100644 index 000000000000..3594bb3465b7 --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/args @@ -0,0 +1 @@ +--strict-assembly --experimental --debug-info ast-id,ethdebug --ethdebug-input sidecar.json a.yul b.yul diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/b.yul b/test/cmdlineTests/ethdebug_input_two_yul_sources/b.yul new file mode 100644 index 000000000000..31547e10e6bf --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/b.yul @@ -0,0 +1,5 @@ +object "C" { + code { + stop() + } +} diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/err b/test/cmdlineTests/ethdebug_input_two_yul_sources/err new file mode 100644 index 000000000000..f66413d5452f --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/err @@ -0,0 +1 @@ +Error: --ethdebug-input requires exactly one strict assembly input. diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/exit b/test/cmdlineTests/ethdebug_input_two_yul_sources/exit new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/exit @@ -0,0 +1 @@ +1 diff --git a/test/cmdlineTests/ethdebug_input_two_yul_sources/sidecar.json b/test/cmdlineTests/ethdebug_input_two_yul_sources/sidecar.json new file mode 100644 index 000000000000..3e15fbcb6697 --- /dev/null +++ b/test/cmdlineTests/ethdebug_input_two_yul_sources/sidecar.json @@ -0,0 +1 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "resources": {"types": {}, "pointers": {}}, "scopes": {}} diff --git a/test/cmdlineTests/ir_ethdebug/args b/test/cmdlineTests/ir_ethdebug/args new file mode 100644 index 000000000000..aa993728de9e --- /dev/null +++ b/test/cmdlineTests/ir_ethdebug/args @@ -0,0 +1 @@ +--experimental --debug-info ast-id,ethdebug --ir-ethdebug --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/ir_ethdebug/input.sol b/test/cmdlineTests/ir_ethdebug/input.sol new file mode 100644 index 000000000000..eb12e7042d58 --- /dev/null +++ b/test/cmdlineTests/ir_ethdebug/input.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.0; + +contract C { + uint256 counter; + mapping(address => uint256) balances; + + function f(uint256 argument) public returns (uint256 result) { + counter += argument; + result = balances[msg.sender] + counter; + } +} diff --git a/test/cmdlineTests/ir_ethdebug/output b/test/cmdlineTests/ir_ethdebug/output new file mode 100644 index 000000000000..9019746deaa3 --- /dev/null +++ b/test/cmdlineTests/ir_ethdebug/output @@ -0,0 +1,149 @@ +IR ethdebug sidecar: +{ + "contractName": "C", + "format": "solidity-ethdebug-semantic-data", + "resources": { + "pointers": { + "t_mapping$_t_address_$_t_uint256_$": { + "expect": [ + "slot", + "key" + ], + "for": { + "location": "storage", + "name": "value", + "slot": { + "~keccak256": [ + { + "~wordsized": "key" + }, + { + "~wordsized": "slot" + } + ] + } + } + } + }, + "types": { + "t_address": { + "kind": "address", + "payable": false + }, + "t_mapping$_t_address_$_t_uint256_$": { + "contains": { + "key": { + "type": { + "id": "t_address" + } + }, + "value": { + "type": { + "id": "t_uint256" + } + } + }, + "kind": "mapping" + }, + "t_uint256": { + "bits": 256, + "kind": "uint" + } + } + }, + "scopes": { + "28": { + "0": { + "variableDefinitions": [ + { + "declarationASTID": 9, + "declarationSourceRange": { + "range": { + "length": 16, + "offset": 152 + }, + "source": { + "id": 0 + } + }, + "identifier": "argument", + "phase": "materialized", + "pointer": { + "location": "stack", + "slot": { + "$$yulLocal": "var_argument_9" + } + }, + "typeID": "t_uint256" + }, + { + "declarationASTID": 12, + "declarationSourceRange": { + "range": { + "length": 14, + "offset": 186 + }, + "source": { + "id": 0 + } + }, + "identifier": "result", + "phase": "materialized", + "pointer": { + "location": "stack", + "slot": { + "$$yulLocal": "var_result_12" + } + }, + "typeID": "t_uint256" + } + ] + } + }, + "29": { + "0": { + "variableDefinitions": [ + { + "declarationASTID": 3, + "declarationSourceRange": { + "range": { + "length": 15, + "offset": 77 + }, + "source": { + "id": 0 + } + }, + "identifier": "counter", + "phase": "materialized", + "pointer": { + "location": "storage", + "slot": "0x00" + }, + "typeID": "t_uint256" + }, + { + "declarationASTID": 7, + "declarationSourceRange": { + "range": { + "length": 36, + "offset": 98 + }, + "source": { + "id": 0 + } + }, + "identifier": "balances", + "phase": "materialized", + "pointer": { + "location": "storage", + "slot": "0x01" + }, + "typeID": "t_mapping$_t_address_$_t_uint256_$" + } + ] + } + } + }, + "version": 1 +} diff --git a/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/args b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/args new file mode 100644 index 000000000000..18532c5a6d3f --- /dev/null +++ b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/args @@ -0,0 +1 @@ +--allow-paths . diff --git a/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/in.yul b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/in.yul new file mode 100644 index 000000000000..567f638686ca --- /dev/null +++ b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/in.yul @@ -0,0 +1,18 @@ +/// @use-src 0:"input.sol" +object "C_12_deployed" { + /// @ast-id 12 + code { + /// @src 0:60:160 "contract C {..." + mstore(64, 128) + + pop(fun_f_11(7)) + + /// @src 0:99:158 "function f(uint256 argument) public returns (uint256 result) {..." + /// @ast-id 11 + function fun_f_11(var_argument_4) -> var_result_7 { + sstore(0, var_argument_4) + var_result_7 := var_argument_4 + } + /// @src 0:60:160 "contract C {..." + } +} diff --git a/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/input.json b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/input.json new file mode 100644 index 000000000000..7c3a8e5b38f3 --- /dev/null +++ b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/input.json @@ -0,0 +1,69 @@ +{ + "language": "Yul", + "sources": { + "C": {"urls": ["in.yul"]} + }, + "auxiliaryInput": { + "ethdebug": { + "format": "solidity-ethdebug-semantic-data", + "version": 1, + "contractName": "C", + "resources": { + "types": { + "t_uint256": {"kind": "uint", "bits": 256} + }, + "pointers": { + "storage_12_3": { + "expect": [], + "for": {"location": "storage", "name": "counter", "slot": "0x0"} + } + } + }, + "scopes": { + "11": { + "0": { + "variableDefinitions": [ + { + "identifier": "argument", + "declarationASTID": 4, + "declarationSourceRange": {"source": {"id": 0}, "range": {"offset": 110, "length": 16}}, + "typeID": "t_uint256", + "phase": "materialized", + "pointer": {"location": "stack", "name": "argument", "slot": {"$$yulLocal": "var_argument_4"}} + }, + { + "identifier": "result", + "declarationASTID": 7, + "declarationSourceRange": {"source": {"id": 0}, "range": {"offset": 144, "length": 14}}, + "typeID": "t_uint256", + "phase": "materialized", + "pointer": {"location": "stack", "name": "result", "slot": {"$$yulLocal": "var_result_7"}} + } + ] + } + }, + "12": { + "0": { + "variableDefinitions": [ + { + "identifier": "counter", + "declarationASTID": 3, + "declarationSourceRange": {"source": {"id": 0}, "range": {"offset": 77, "length": 15}}, + "typeID": "t_uint256", + "phase": "materialized", + "pointer": {"template": "storage_12_3"} + } + ] + } + } + } + } + }, + "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, + "experimental": true, + "outputSelection": { + "*": {"*": ["irEthdebug", "evm.bytecode.ethdebug", "ethdebug.resources"]} + } + } +} diff --git a/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/output.json b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/output.json new file mode 100644 index 000000000000..004da59e9997 --- /dev/null +++ b/test/cmdlineTests/standard_yul_ethdebug_auxiliary_input/output.json @@ -0,0 +1,477 @@ +{ + "contracts": { + "C": { + "C_12_deployed": { + "evm": { + "bytecode": { + "ethdebug": { + "context": { + "variables": [ + { + "declaration": { + "range": { + "length": 15, + "offset": 77 + }, + "source": { + "id": 0 + } + }, + "identifier": "counter", + "pointer": { + "template": "storage_12_3" + }, + "type": { + "id": "t_uint256" + } + } + ] + }, + "contract": { + "definition": { + "source": { + "id": 0 + } + }, + "name": "C" + }, + "environment": "create", + "ethdebug": { + "schema": "ethdebug/format/program", + "version": "0.1.0-draft.2" + }, + "instructions": [ + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 0, + "operation": { + "arguments": [ + "0x80" + ], + "mnemonic": "PUSH1" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 2, + "operation": { + "arguments": [ + "0x40" + ], + "mnemonic": "PUSH1" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 4, + "operation": { + "mnemonic": "MSTORE" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 5, + "operation": { + "arguments": [ + "0x0c" + ], + "mnemonic": "PUSH1" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 7, + "operation": { + "arguments": [ + "0x07" + ], + "mnemonic": "PUSH1" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 9, + "operation": { + "arguments": [ + "0x0e" + ], + "mnemonic": "PUSH1" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 11, + "operation": { + "mnemonic": "JUMP" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 12, + "operation": { + "mnemonic": "JUMPDEST" + } + }, + { + "context": { + "code": { + "range": { + "length": 100, + "offset": 60 + }, + "source": { + "id": 0 + } + } + }, + "offset": 13, + "operation": { + "mnemonic": "STOP" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 14, + "operation": { + "mnemonic": "JUMPDEST" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 15, + "operation": { + "mnemonic": "DUP1" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 16, + "operation": { + "mnemonic": "PUSH0" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 17, + "operation": { + "mnemonic": "SSTORE" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 18, + "operation": { + "mnemonic": "SWAP1" + } + }, + { + "context": { + "code": { + "range": { + "length": 59, + "offset": 99 + }, + "source": { + "id": 0 + } + } + }, + "offset": 19, + "operation": { + "mnemonic": "JUMP" + } + } + ] + } + } + }, + "irEthdebug": { + "contractName": "C", + "format": "solidity-ethdebug-semantic-data", + "resources": { + "pointers": { + "storage_12_3": { + "expect": [], + "for": { + "location": "storage", + "name": "counter", + "slot": "0x00" + } + } + }, + "types": { + "t_uint256": { + "bits": 256, + "kind": "uint" + } + } + }, + "scopes": { + "11": { + "0": { + "variableDefinitions": [ + { + "declarationASTID": 4, + "declarationSourceRange": { + "range": { + "length": 16, + "offset": 110 + }, + "source": { + "id": 0 + } + }, + "identifier": "argument", + "phase": "materialized", + "pointer": { + "location": "stack", + "name": "argument", + "slot": { + "$$yulLocal": "var_argument_4" + } + }, + "typeID": "t_uint256" + }, + { + "declarationASTID": 7, + "declarationSourceRange": { + "range": { + "length": 14, + "offset": 144 + }, + "source": { + "id": 0 + } + }, + "identifier": "result", + "phase": "materialized", + "pointer": { + "location": "stack", + "name": "result", + "slot": { + "$$yulLocal": "var_result_7" + } + }, + "typeID": "t_uint256" + } + ] + } + }, + "12": { + "0": { + "variableDefinitions": [ + { + "declarationASTID": 3, + "declarationSourceRange": { + "range": { + "length": 15, + "offset": 77 + }, + "source": { + "id": 0 + } + }, + "identifier": "counter", + "phase": "materialized", + "pointer": { + "template": "storage_12_3" + }, + "typeID": "t_uint256" + } + ] + } + } + }, + "version": 1 + } + } + } + }, + "ethdebug": { + "resources": { + "compilation": { + "compiler": { + "name": "solc", + "version": "" + }, + "id": "solc-5f35992b0837672fa1e5d1bde7633a1c08fa4684fb0656e7a03daf0b66a3b962", + "sources": [ + { + "contents": "/// @use-src 0:\"input.sol\" +object \"C_12_deployed\" { + /// @ast-id 12 + code { + /// @src 0:60:160 \"contract C {...\" + mstore(64, 128) + + pop(fun_f_11(7)) + + /// @src 0:99:158 \"function f(uint256 argument) public returns (uint256 result) {...\" + /// @ast-id 11 + function fun_f_11(var_argument_4) -> var_result_7 { + sstore(0, var_argument_4) + var_result_7 := var_argument_4 + } + /// @src 0:60:160 \"contract C {...\" + } +} +", + "id": 0, + "language": "Yul", + "path": "C" + } + ] + }, + "ethdebug": { + "schema": "ethdebug/format/info/resources", + "version": "0.1.0-draft.2" + }, + "pointers": { + "storage_12_3": { + "expect": [], + "for": { + "location": "storage", + "name": "counter", + "slot": "0x00" + } + } + }, + "types": { + "t_uint256": { + "bits": 256, + "kind": "uint" + } + } + } + } +} diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index 35f632142269..5c7f05ed4f99 100644 --- a/test/libsolidity/StandardCompiler.cpp +++ b/test/libsolidity/StandardCompiler.cpp @@ -2367,6 +2367,163 @@ BOOST_AUTO_TEST_CASE(ethdebug_program_output_matches_two_stage_compilation) } } +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({"ast-id", "ethdebug"}), + 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"].contains("scopes")); + BOOST_REQUIRE(!firstContract["irEthdebug"]["scopes"].empty()); + BOOST_REQUIRE(firstContract["irEthdebug"].contains("resources")); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("argument") != std::string::npos); + BOOST_CHECK(firstContract["irEthdebug"].dump().find("value") != std::string::npos); + // The state variable's closed pointer is published in the program-level context. + Json const& firstContext = firstContract["evm"]["deployedBytecode"]["ethdebug"]["context"]; + BOOST_REQUIRE(firstContext.is_object()); + BOOST_REQUIRE_EQUAL(firstContext["variables"].size(), 1); + BOOST_CHECK_EQUAL(firstContext["variables"][0]["identifier"], "value"); + BOOST_CHECK_EQUAL(firstContext["variables"][0]["pointer"]["location"], "storage"); + + 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_ir_ethdebug_output_alone_generates_ir) +{ + frontend::StandardCompiler compiler; + std::map sources; + sources["fileA"] = "contract C { uint256[] values; function f(uint256 argument) public { values.push(argument); } }"; + Json input = generateExperimentalStandardJson( + true, + Json::array({"ast-id", "ethdebug"}), + Json::array({"irEthdebug"}), + SolidityCode(std::move(sources)) + ); + Json result = compiler.compile(input); + BOOST_REQUIRE(containsAtMostWarnings(result)); + Json const& sidecar = result["contracts"]["fileA"]["C"]["irEthdebug"]; + BOOST_REQUIRE(sidecar.is_object()); + BOOST_CHECK_EQUAL(sidecar["contractName"], "C"); + BOOST_CHECK(!sidecar["scopes"].empty()); + BOOST_CHECK(sidecar["resources"]["types"].contains("t_uint256")); + BOOST_CHECK(sidecar["resources"]["pointers"].contains("t_array$_t_uint256_$dyn_storage")); +} + +BOOST_AUTO_TEST_CASE(ethdebug_semantic_sidecar_rejects_malformed_input) +{ + frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; + Json input = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({"irEthdebug"}), + YulCode(std::move(yulSources)) + ); + input["auxiliaryInput"]["ethdebug"] = Json{ + {"format", "solidity-ethdebug-semantic-data"}, + {"version", 1}, + {"resources", Json{{"types", Json::object()}, {"pointers", Json::object()}}}, + {"scopes", Json{{"abc", Json::object()}}} + }; + Json result = compiler.compile(input); + bool rejected = false; + for (Json const& error: result["errors"]) + if ( + error["type"] == "JSONError" && + error["message"].get().starts_with("Invalid 'auxiliaryInput.ethdebug': ") + ) + rejected = true; + BOOST_CHECK(rejected); +} + +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}, + {"scopes", Json::object()} + }; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "JSONError", "'auxiliaryInput.ethdebug' can only be used for Yul input.")); +} + +BOOST_AUTO_TEST_CASE(ethdebug_rejects_yul_optimization) +{ + frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; + Json input = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({"ir"}), + YulCode(std::move(yulSources)) + ); + input["settings"]["optimizer"] = {{"enabled", true}}; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "UnimplementedFeatureError", "Optimization is not yet supported with ethdebug.")); +} + BOOST_AUTO_TEST_CASE(no_experimental_import_ast_solidity_evmasm) { frontend::StandardCompiler compiler; diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 43da1f342516..720b3d5c77db 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -1635,6 +1635,57 @@ 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", + "--debug-info", + "ast-id,ethdebug", + "--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", + "--debug-info", + "ast-id,ethdebug", + "--ethdebug-input", + sidecarPath.string(), + "--ir-ethdebug", + "--ethdebug-program", + yulPath.string(), + }); + BOOST_REQUIRE(secondStage.success); + BOOST_CHECK(secondStage.stderrContent.empty()); + BOOST_CHECK(secondStage.stdoutContent.find("IR ethdebug 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); + // The program-level context of the state variable is derived from the sidecar again. + BOOST_CHECK(secondStage.stdoutContent.find("\"identifier\":\"value\"") != 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 868c4367bfa9..1376e569f36d 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -197,9 +197,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, }; expectedOptions.compiler.estimateGas = true; expectedOptions.compiler.combinedJsonRequests = { @@ -698,6 +698,42 @@ BOOST_AUTO_TEST_CASE(ethdebug) 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; unlike the program outputs, it carries the + // semantic debug info itself and errors without --debug-info ethdebug. + auto expectsExplicitEthdebug = [](CommandLineValidationError const& _exception) { + return std::string(_exception.what()).find("--debug-info must contain ethdebug") != std::string::npos; + }; + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--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); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--ir-ethdebug"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + // --ethdebug-input attaches the sidecar in strict assembly mode + commandLineOptions = parseCommandLine({ + "solc", "contract.yul", "--strict-assembly", "--experimental", "--debug-info", "ast-id,ethdebug", + "--ethdebug-input", "debug.json" + }); + BOOST_REQUIRE(commandLineOptions.input.ethdebugInput.has_value()); + BOOST_CHECK_EQUAL(*commandLineOptions.input.ethdebugInput, "debug.json"); + BOOST_REQUIRE(commandLineOptions.output.debugInfoSelection.has_value()); + BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.yul", "--strict-assembly", "--experimental", "--ethdebug-input", "debug.json"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + // Only one sidecar can be attached: there is no syntax pairing sidecars with multiple inputs. + BOOST_CHECK_THROW( + parseCommandLine({ + "solc", "a.yul", "b.yul", "--strict-assembly", "--experimental", "--debug-info", "ast-id,ethdebug", + "--ethdebug-input", "a.json", "--ethdebug-input", "b.json" + }), + CommandLineValidationError + ); // --ethdebug-resources does not require --via-ir commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-resources"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugResources, true); @@ -717,6 +753,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", "--ethdebug-resources", @@ -741,6 +778,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)