From 19a7db35fd83e4ebde7b3f368a6b2b9621e8fbf3 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 31 Aug 2026 10:37:38 +0200 Subject: [PATCH 1/2] ethdebug: Require an explicit debug info selection and ast-id for ethdebug outputs Selecting an ethdebug output no longer implicitly enables the ethdebug and ast-id components of the debug info selection, just as --ir-optimized does not imply --optimize: requesting a program ethdebug output without ethdebug in --debug-info or settings.debug.debugInfo is now an error, in both the CLI and Standard JSON. Semantic debug info transfer depends on @ast-id comments, so ethdebug now also depends on the ast-id component: selecting ethdebug without ast-id is an error. The ethdebug.resources and ethdebug.compilation outputs are derived from analysis results alone and still need no selection. The optimizer and SSA CFG gates now apply whenever ethdebug is selected, not only when a program output is requested. --- Changelog.md | 2 + docs/using-the-compiler.rst | 7 +- libsolidity/interface/CompilerStack.cpp | 4 + libsolidity/interface/StandardCompiler.cpp | 33 +++--- solc/CommandLineParser.cpp | 47 ++++---- .../ethdebug_debuginfo_ssa_cfg/args | 2 +- test/cmdlineTests/ethdebug_on_abstract/args | 2 +- test/cmdlineTests/ethdebug_on_interface/args | 2 +- .../ethdebug_program_without_debug_info/args | 1 + .../ethdebug_program_without_debug_info/err | 1 + .../ethdebug_program_without_debug_info/exit | 1 + .../input.sol | 6 + test/cmdlineTests/metadata_experimental/args | 2 +- .../standard_metadata_experimental/input.json | 1 + .../input.json | 1 + .../output.json | 12 +- .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 27 +++++ .../output.json | 11 ++ .../input.json | 2 +- .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../standard_via_ssa_cfg_ethdebug/input.json | 1 + .../input.json | 2 +- .../input.json | 2 +- .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../standard_yul_ethdebug_ir/input.json | 1 + .../input.json | 1 + .../input.json | 1 + .../input.json | 1 + test/cmdlineTests/via_ssa_cfg_ethdebug/args | 2 +- test/ethdebugSchemaTests/input_file.json | 1 + test/libsolidity/StandardCompiler.cpp | 104 ++++++++++++------ test/solc/CommandLineInterface.cpp | 52 +++++---- test/solc/CommandLineParser.cpp | 40 ++++--- 51 files changed, 269 insertions(+), 123 deletions(-) create mode 100644 test/cmdlineTests/ethdebug_program_without_debug_info/args create mode 100644 test/cmdlineTests/ethdebug_program_without_debug_info/err create mode 100644 test/cmdlineTests/ethdebug_program_without_debug_info/exit create mode 100644 test/cmdlineTests/ethdebug_program_without_debug_info/input.sol create mode 100644 test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json create mode 100644 test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json diff --git a/Changelog.md b/Changelog.md index d05d6fcd3717..890cf719bc22 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,11 +8,13 @@ Language Features: Compiler Features: * Commandline Interface: Remove support for the experimental Language Server Protocol (LSP) mode. +* Commandline Interface: Selecting an ethdebug output no longer implicitly enables the `ethdebug` and `ast-id` components of `--debug-info`; they have to be selected explicitly. * EVM-ASM Optimizer: Improve performance of block deduplicator. * General: Improve performance throughout the compiler using Boost's flat versions of unordered set and map. * General: Remove support for the experimental Generic Solidity prototype (`pragma experimental solidity`). * SMTChecker: Emit a deprecation warning for the BMC engine. * SMTChecker: Support `block.slotnum`. +* Standard JSON Interface: Selecting an ethdebug output no longer implicitly enables the `ethdebug` and `ast-id` components of `settings.debug.debugInfo`; they have to be selected explicitly. * Standard JSON Interface: Fix the entire output being replaced by a `JSONError` ("Error writing output JSON.") when an error message quotes a long source line and truncating it splits a multi-byte character. Bugfixes: diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index e7a3ccda380d..fbe84c254e7b 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -392,7 +392,9 @@ Input Description // The snippet is quoted and follows the corresponding `@src` annotation. // - `ast-id`: Annotations of the form `@ast-id ` over elements that can be mapped back to a definition in the original Solidity file. // `` is a node ID in the Solidity AST ('ast' output). - // - `ethdebug`: Ethdebug annotations (experimental). Automatically enabled when any ethdebug output is requested. + // - `ethdebug`: Ethdebug annotations (experimental). Depends on `ast-id`; selecting + // `ethdebug` without `ast-id` is an error. Requesting a program ethdebug output + // does not change this selection, which must then contain `ethdebug`. // - `*`: Wildcard value that can be used to request all non-experimental components. "debugInfo": ["location", "snippet", "ast-id", "ethdebug"] }, @@ -770,7 +772,8 @@ The table below details all currently available experimental features. +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | Non-mainnet EVMs | ``evm`` | yes | ``--evm-version `` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ -| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, ``--debug-info ethdebug`` | +| Ethdebug | ``ethdebug`` | no | ``--ethdebug-resources``, ``--ethdebug-compilation``, ``--ethdebug-program``, ``--ethdebug-program-runtime``, | +| | | | ``--debug-info ast-id,ethdebug`` | +-----------------------+--------------------------+------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ | | | no | ``--yul-cfg-json`` | | SSA CFG + ``ssa-cfg`` +------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b2b6f95625ae..26aa8e865df8 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -295,6 +295,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; } diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index c8cb87504624..2c7840e36212 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -962,7 +962,6 @@ std::variant StandardCompiler::parseI Error::Type::JSONError, "To use 'snippet' with settings.debug.debugInfo you must select also 'location'." ); - ret.debugInfoSelection = debugInfoSelection.value(); } } @@ -1257,33 +1256,40 @@ std::variant StandardCompiler::parseI return formatFatalError(Error::Type::FatalError, "'settings.debug.debugInfo' 'ethdebug' is only supported for languages 'Solidity' and 'Yul'."); } + // Selecting an ethdebug output does not modify the debug info selection, just + // as 'irOptimized' does not imply the optimizer: the selection must contain + // 'ethdebug' explicitly. + bool const ethdebugSelected = ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug; + if (isEthdebugProgramRequested(ret.outputSelection)) { if (ret.language == "Solidity" && !ret.viaIR) return formatFatalError(Error::Type::FatalError, "'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' can only be selected as output, if 'viaIR' was set."); - if (!ret.debugInfoSelection.has_value()) - { - ret.debugInfoSelection = DebugInfoSelection::Default(); - ret.debugInfoSelection->enable("ethdebug"); - } - else - { - if (!ret.debugInfoSelection->ethdebug && ret.language == "Solidity") - return formatFatalError(Error::Type::FatalError, "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output."); - } + if (!ethdebugSelected) + return formatFatalError(Error::Type::FatalError, "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output."); } if (ret.debugInfoSelection.has_value() && ret.debugInfoSelection->ethdebug) { if (!ret.experimental) return formatFatalError(Error::Type::FatalError, "Ethdebug annotations are experimental and can only be included in 'settings.debug.debugInfo' by enabling the 'settings.experimental' option."); + if (!ret.debugInfoSelection->astID) + return formatFatalError( + Error::Type::JSONError, + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + ); } + if ( + ret.debugInfoSelection.has_value() && + ret.debugInfoSelection->ethdebug && + ret.optimiserSettings.runYulOptimiser + ) + solUnimplemented("Optimization is not yet supported with ethdebug."); + if (isEthdebugProgramRequested(ret.outputSelection)) { - if (ret.optimiserSettings.runYulOptimiser) - solUnimplemented("Optimization is not yet supported with ethdebug."); if (ret.viaSSACFG) solUnimplemented("SSA CFG codegen does not yet support ethdebug."); } @@ -1802,7 +1808,6 @@ Json StandardCompiler::compileYul(InputsAndSettings _inputsAndSettings) contractName = stack.parserResult()->name; if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ir", wildcardMatchesExperimental)) output["contracts"][sourceName][contractName]["ir"] = stack.print(); - if (isArtifactRequested(_inputsAndSettings.outputSelection, sourceName, contractName, "ast", wildcardMatchesExperimental)) { Json sourceResult; diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 54df68667b1a..ca56a04a8ae3 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -642,7 +642,7 @@ General Information)").c_str(), ("Debug info components to be included in the produced EVM assembly and Yul code. " "Value can be 'all', 'none' or a comma-separated list containing one or more of the " "following components: " + util::joinHumanReadable(DebugInfoSelection::AllExceptExperimental().selectedNames()) + - ", or ethdebug (experimental). " + ", or ethdebug (experimental, requires ast-id). " "Note that 'all' does not include experimental components.").c_str() ) ( @@ -1194,6 +1194,8 @@ void CommandLineParser::processArgs() if (m_options.output.debugInfoSelection->snippet && !m_options.output.debugInfoSelection->location) solThrow(CommandLineValidationError, "To use 'snippet' with --" + g_strDebugInfo + " you must select also 'location'."); + if (m_options.output.debugInfoSelection->ethdebug && !m_options.output.debugInfoSelection->astID) + solThrow(CommandLineValidationError, "To use 'ethdebug' with --" + g_strDebugInfo + " you must select also 'ast-id'."); } parseCombinedJsonOption(); @@ -1355,7 +1357,21 @@ void CommandLineParser::processArgs() m_options.output.viaSSACFG = m_args.contains(g_strViaSSACFG); - if (m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) + // Selecting an ethdebug output does not modify the debug info selection, + // just as --ir-optimized does not imply --optimize: the selection must + // contain ethdebug explicitly. + bool const ethdebugSelected = + m_options.output.debugInfoSelection.has_value() && m_options.output.debugInfoSelection->ethdebug; + if ((m_options.compiler.outputs.ethdebugProgram || m_options.compiler.outputs.ethdebugProgramRuntime) && !ethdebugSelected) + solThrow( + CommandLineValidationError, + fmt::format( + "--debug-info must contain ethdebug when using --{} / --{}.", + CompilerOutputs::componentName(&CompilerOutputs::ethdebugProgram), + CompilerOutputs::componentName(&CompilerOutputs::ethdebugProgramRuntime) + ) + ); + if (ethdebugSelected) { if (m_options.output.viaSSACFG) solUnimplemented("ethdebug is not yet supported with --" + g_strViaSSACFG + "."); @@ -1363,12 +1379,6 @@ void CommandLineParser::processArgs() solUnimplemented( "Optimization (using --" + g_strOptimize + ") is not yet supported with ethdebug." ); - - if (!m_options.output.debugInfoSelection.has_value()) - { - m_options.output.debugInfoSelection = DebugInfoSelection::Default(); - m_options.output.debugInfoSelection->enable("ethdebug"); - } } return; } @@ -1525,19 +1535,14 @@ void CommandLineParser::processArgs() enableEthdebugProgramMessage + " output can only be selected, if --via-ir was specified." ); - if (!m_options.output.debugInfoSelection.has_value()) - { - m_options.output.debugInfoSelection = DebugInfoSelection::Default(); - m_options.output.debugInfoSelection->enable("ethdebug"); - } - else - { - if (!m_options.output.debugInfoSelection->ethdebug) - solThrow( - CommandLineValidationError, - "--debug-info must contain ethdebug, when compiling with " + enableEthdebugProgramMessage + "." - ); - } + // Selecting an ethdebug output does not modify the debug info selection, + // just as --ir-optimized does not imply --optimize: the selection must + // contain ethdebug explicitly. + if (!m_options.output.debugInfoSelection.has_value() || !m_options.output.debugInfoSelection->ethdebug) + solThrow( + CommandLineValidationError, + "--debug-info must contain ethdebug when using " + enableEthdebugProgramMessage + "." + ); } if ( diff --git a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args index 8dd599e3d575..ccd11791ab21 100644 --- a/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args +++ b/test/cmdlineTests/ethdebug_debuginfo_ssa_cfg/args @@ -1 +1 @@ ---experimental --debug-info ethdebug --via-ssa-cfg +--experimental --debug-info ast-id,ethdebug --via-ssa-cfg diff --git a/test/cmdlineTests/ethdebug_on_abstract/args b/test/cmdlineTests/ethdebug_on_abstract/args index 7a58d286f809..c98066148481 100644 --- a/test/cmdlineTests/ethdebug_on_abstract/args +++ b/test/cmdlineTests/ethdebug_on_abstract/args @@ -1 +1 @@ ---experimental --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 +--experimental --debug-info location,snippet,ast-id,ethdebug --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/ethdebug_on_interface/args b/test/cmdlineTests/ethdebug_on_interface/args index 7a58d286f809..c98066148481 100644 --- a/test/cmdlineTests/ethdebug_on_interface/args +++ b/test/cmdlineTests/ethdebug_on_interface/args @@ -1 +1 @@ ---experimental --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 +--experimental --debug-info location,snippet,ast-id,ethdebug --ethdebug-compilation --ethdebug-program --ethdebug-program-runtime --via-ir --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/args b/test/cmdlineTests/ethdebug_program_without_debug_info/args new file mode 100644 index 000000000000..eda454f50546 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/args @@ -0,0 +1 @@ +--experimental --ethdebug-program --via-ir diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/err b/test/cmdlineTests/ethdebug_program_without_debug_info/err new file mode 100644 index 000000000000..53d9566da3c4 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/err @@ -0,0 +1 @@ +Error: --debug-info must contain ethdebug when using --ethdebug-program / --ethdebug-program-runtime. diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/exit b/test/cmdlineTests/ethdebug_program_without_debug_info/exit new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/exit @@ -0,0 +1 @@ +1 diff --git a/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol b/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol new file mode 100644 index 000000000000..cd008c45b465 --- /dev/null +++ b/test/cmdlineTests/ethdebug_program_without_debug_info/input.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity >=0.0; + +interface C { + function f() external; +} diff --git a/test/cmdlineTests/metadata_experimental/args b/test/cmdlineTests/metadata_experimental/args index fa925f16ed8f..714483671a52 100644 --- a/test/cmdlineTests/metadata_experimental/args +++ b/test/cmdlineTests/metadata_experimental/args @@ -1 +1 @@ ---metadata --experimental --via-ir --ethdebug-program --ethdebug-compilation --no-cbor-metadata --pretty-json --json-indent 4 +--metadata --experimental --debug-info location,snippet,ast-id,ethdebug --via-ir --ethdebug-program --ethdebug-compilation --no-cbor-metadata --pretty-json --json-indent 4 diff --git a/test/cmdlineTests/standard_metadata_experimental/input.json b/test/cmdlineTests/standard_metadata_experimental/input.json index fddbca92eb2f..73922713c00c 100644 --- a/test/cmdlineTests/standard_metadata_experimental/input.json +++ b/test/cmdlineTests/standard_metadata_experimental/input.json @@ -8,6 +8,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json index 878b2b41f77a..89d120cdd9c3 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/input.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json index 8f198f1f3adb..ea3e07cbc99f 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_compatible/output.json @@ -155,6 +155,7 @@ object \"A1_14\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 13 /// @src 0:72:121 function fun_a_13(var_x_3) { @@ -276,7 +277,7 @@ object \"A1_14\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 0:72:121 + /// @ast-id 13 @src 0:72:121 function fun_a(var_x) { /// @src 0:112:113 @@ -449,6 +450,7 @@ object \"A2_27\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 26 /// @src 0:138:187 function fun_a_26(var_x_16) { @@ -570,7 +572,7 @@ object \"A2_27\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 0:138:187 + /// @ast-id 26 @src 0:138:187 function fun_a(var_x) { /// @src 0:178:179 @@ -745,6 +747,7 @@ object \"A1_42\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 41 /// @src 1:72:121 function fun_b_41(var_x_31) { @@ -866,7 +869,7 @@ object \"A1_42\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 1:72:121 + /// @ast-id 41 @src 1:72:121 function fun_b(var_x) { /// @src 1:112:113 @@ -1039,6 +1042,7 @@ object \"B2_55\" { if iszero(condition) { panic_error_0x01() } } + /// @ast-id 54 /// @src 1:138:187 function fun_b_54(var_x_44) { @@ -1160,7 +1164,7 @@ object \"B2_55\" { { if iszero(condition) { panic_error_0x01() } } - /// @src 1:138:187 + /// @ast-id 54 @src 1:138:187 function fun_b(var_x) { /// @src 1:178:179 diff --git a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json index e1104f5b300c..fc5be60b592f 100644 --- a/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json +++ b/test/cmdlineTests/standard_output_debuginfo_ethdebug_with_interfaces_and_abstracts/input.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json b/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json index 17afe568db11..974e7d4dc0d5 100644 --- a/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json +++ b/test/cmdlineTests/standard_output_ethdebug_parse_only_incompatible/input.json @@ -6,6 +6,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "stopAfter": "parsing", "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json index b83cf6508863..c9a29bb95183 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json index ff8dd714040e..0e136310fb02 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json index 514012ee7c20..68a55cda99c6 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json index 514012ee7c20..68a55cda99c6 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_and_deployedbytecode_optimizer_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json index 9bab955c8985..45c72569d7e4 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json index 1a3fb90988e2..ac4f27d71e8a 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_bytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json index 23698d7c12a5..0f5907d3b05c 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json index ce2ae6f33e60..1b06b73f5a45 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_deployedbytecode_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json index 1034d8d37f05..d2b5feaf8481 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json index ba4046a64848..1b5cd2785737 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "outputSelection": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json new file mode 100644 index 000000000000..a4dc2be0f83f --- /dev/null +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/input.json @@ -0,0 +1,27 @@ +{ + "language": "Solidity", + "sources": { + "a.sol": { + "content": "//SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.0;\ncontract A1 { function a(uint x) public pure { assert(x > 0); } } contract A2 { function a(uint x) public pure { assert(x > 0); } }" + }, + "b.sol": { + "content": "//SPDX-License-Identifier: GPL-3.0\npragma solidity >=0.0;\ncontract A1 { function b(uint x) public pure { assert(x > 0); } } contract B2 { function b(uint x) public pure { assert(x > 0); } }" + } + }, + "settings": { + "experimental": true, + "viaIR": true, + "optimizer": { + "enabled": false + }, + "outputSelection": { + "*": { + "*": [ + "evm.bytecode.ethdebug", + "ir", + "ethdebug.resources" + ] + } + } + } +} diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json new file mode 100644 index 000000000000..eb6ccad4bbd0 --- /dev/null +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_debug_info/output.json @@ -0,0 +1,11 @@ +{ + "errors": [ + { + "component": "general", + "formattedMessage": "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output.", + "message": "'ethdebug' needs to be enabled in 'settings.debug.debugInfo', if 'evm.bytecode.ethdebug' or 'evm.deployedBytecode.ethdebug' was selected as output.", + "severity": "error", + "type": "FatalError" + } + ] +} diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json index 5fca11155a92..ecdb964bd0a3 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_experimental/input.json @@ -8,7 +8,7 @@ "settings": { "viaIR": true, "debug": { - "debugInfo": ["ethdebug"] + "debugInfo": ["ast-id", "ethdebug"] }, "outputSelection": { "A.sol": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json index 574a8b1c43cd..270e8fdd1a7e 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_no_viair/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "outputSelection": { "*": { "*": [ diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json index e1084799b98f..a733f6b9bc09 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_ir/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json index 235e3c855fe6..be68769dda4f 100644 --- a/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json +++ b/test/cmdlineTests/standard_output_selection_ethdebug_optimize_iroptimized/input.json @@ -9,6 +9,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaIR": true, "optimizer": { diff --git a/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json b/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json index d5ad9153e9c9..16d99b03fb19 100644 --- a/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json +++ b/test/cmdlineTests/standard_via_ssa_cfg_ethdebug/input.json @@ -6,6 +6,7 @@ } }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "viaSSACFG": true, "outputSelection": { diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json index c459aadef6f2..5b9f3afce007 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_compatible_output/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["ir", "irOptimized", "evm.bytecode.ethdebug", "ethdebug.resources"]} } diff --git a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json index eac38e3db63d..6d936e39bdd7 100644 --- a/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json +++ b/test/cmdlineTests/standard_yul_debug_info_ethdebug_verbatim_unimplemented/input.json @@ -9,7 +9,7 @@ }, "settings": { "experimental": true, - "debug": {"debugInfo": ["ethdebug"]}, + "debug": {"debugInfo": ["ast-id", "ethdebug"]}, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug"]} } diff --git a/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json b/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json index c0ae2d50fedb..2fc0385db13b 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_assign_immutable/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json index 617c93f688a7..835adb066024 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_bytecode_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ir", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json b/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json index 5abd87a0db38..4d08c9ef3cd5 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_bytecode_iroptimized/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "irOptimized", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json index ff39d837e10f..6306f980b983 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.deployedBytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json index 713c5c84231a..720f12391d01 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_deployed_bytecode_and_bytecode/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.deployedBytecode.ethdebug", "evm.bytecode.ethdebug", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_ir/input.json index 617c93f688a7..835adb066024 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "ir", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json b/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json index abcb5e96dfcd..44646f574ba9 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_iroptimize/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "outputSelection": { "*": {"*": ["evm.bytecode.ethdebug", "irOptimize", "ethdebug.resources"]} diff --git a/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json b/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json index 8074135b4f3b..6a35ee35c102 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_optimize_ir/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "optimizer": { "enabled": true diff --git a/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json b/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json index 417a42a31177..683ca199c41e 100644 --- a/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json +++ b/test/cmdlineTests/standard_yul_ethdebug_optimize_iroptimized/input.json @@ -4,6 +4,7 @@ "C": {"urls": ["in.yul"]} }, "settings": { + "debug": {"debugInfo": ["location", "snippet", "ast-id", "ethdebug"]}, "experimental": true, "optimizer": { "enabled": true diff --git a/test/cmdlineTests/via_ssa_cfg_ethdebug/args b/test/cmdlineTests/via_ssa_cfg_ethdebug/args index 7c85efbc7f3c..bd920fce0d5e 100644 --- a/test/cmdlineTests/via_ssa_cfg_ethdebug/args +++ b/test/cmdlineTests/via_ssa_cfg_ethdebug/args @@ -1 +1 @@ ---experimental --via-ssa-cfg --ethdebug-program --ethdebug-compilation +--experimental --debug-info location,snippet,ast-id,ethdebug --via-ssa-cfg --ethdebug-program --ethdebug-compilation diff --git a/test/ethdebugSchemaTests/input_file.json b/test/ethdebugSchemaTests/input_file.json index dc7038e6d02e..b67fc8a40b62 100644 --- a/test/ethdebugSchemaTests/input_file.json +++ b/test/ethdebugSchemaTests/input_file.json @@ -13,6 +13,7 @@ "viaIR": true, "debug": { "debugInfo": [ + "ast-id", "ethdebug" ] }, diff --git a/test/libsolidity/StandardCompiler.cpp b/test/libsolidity/StandardCompiler.cpp index c13704cc061a..11d1ee23c819 100644 --- a/test/libsolidity/StandardCompiler.cpp +++ b/test/libsolidity/StandardCompiler.cpp @@ -1934,36 +1934,48 @@ BOOST_AUTO_TEST_CASE(ethdebug_excluded_from_wildcards) BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) { + frontend::StandardCompiler compiler; + Json missingAstID = compiler.compile(generateExperimentalStandardJson( + true, + Json::array({"ethdebug"}), + Json::array({"ir"}) + )); + BOOST_CHECK(containsError( + missingAstID, + "JSONError", + "To use 'ethdebug' with settings.debug.debugInfo you must select also 'ast-id'." + )); + static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt, }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"irOptimized"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; @@ -1973,53 +1985,53 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { generateExperimentalStandardJson(true, {}, Json::array({"irOptimized", "evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { - return result.dump().find("/// ethdebug: enabled") != std::string::npos; + return result.dump().find("'ethdebug' needs to be enabled in 'settings.debug.debugInfo'") != std::string::npos; } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos; } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"irOptimized"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"irOptimized"}), YulCode()), {} }, { @@ -2027,7 +2039,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) }, { generateExperimentalStandardJson( - true, Json::array({"ethdebug"}), { + true, Json::array({"ast-id", "ethdebug"}), { {"fileA", {{"contractA", Json::array({"evm.deployedBytecode.bin"})}}}, {"fileB", {{"contractB", Json::array({"evm.bytecode.bin"})}}} }, @@ -2039,15 +2051,14 @@ BOOST_AUTO_TEST_CASE(ethdebug_debug_info_ethdebug) std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), EvmAssemblyCode()), std::nullopt, }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"*"}), SolidityAstCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"*"}), SolidityAstCode()), std::nullopt, }, }; - frontend::StandardCompiler compiler; for (auto const& test: tests) { Json result = compiler.compile(std::get<0>(test)); @@ -2060,7 +2071,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) { static std::vector>>> tests{ { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), std::nullopt }, { @@ -2068,7 +2079,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2076,7 +2087,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(false, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(false, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), std::nullopt }, { @@ -2096,21 +2107,21 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, Json::array({"ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2118,21 +2129,21 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug"})), [](const Json& result) { return result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2140,19 +2151,24 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.deployedBytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug"); } }, + // Selecting an ethdebug output does not enable 'ethdebug' in the debug info selection implicitly. + { + generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug"})), + std::nullopt + }, { generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebugs"})), std::nullopt @@ -2162,7 +2178,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) std::nullopt }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"})), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"})), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["C"]["evm"]["deployedBytecode"].contains("ethdebug") && @@ -2170,18 +2186,18 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "ir"}), YulCode()), [](const Json& result) { return result.dump().find("/// ethdebug: enabled") != std::string::npos && result["contracts"]["fileA"]["object"]["evm"]["bytecode"].contains("ethdebug"); } }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.deployedBytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.deployedBytecode.ethdebug", "ir"}), YulCode()), std::nullopt }, { - generateExperimentalStandardJson(true, {}, Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"}), YulCode()), + generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({"evm.bytecode.ethdebug", "evm.deployedBytecode.ethdebug", "ir"}), YulCode()), std::nullopt }, { @@ -2200,7 +2216,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) }, { generateExperimentalStandardJson( - true, {}, { + true, Json::array({"ast-id", "ethdebug"}), { {"fileA", {{"contractA", Json::array({"evm.deployedBytecode.ethdebug"})}}}, {"fileB", {{"contractB", Json::array({"evm.bytecode.ethdebug"})}}} }, @@ -2229,7 +2245,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_ethdebug_output) BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::data::make({"deployedBytecode", "bytecode"}), bytecodeType) { frontend::StandardCompiler compiler; - Json result = compiler.compile(generateExperimentalStandardJson(true, {}, Json::array({std::string("evm.") + bytecodeType + ".ethdebug"}))); + Json result = compiler.compile(generateExperimentalStandardJson(true, Json::array({"ast-id", "ethdebug"}), Json::array({std::string("evm.") + bytecodeType + ".ethdebug"}))); BOOST_REQUIRE(result["contracts"]["fileA"]["C"]["evm"][bytecodeType].contains("ethdebug")); bool creation = std::string(bytecodeType) == "bytecode"; Json ethdebugInstructionsToCheck = result["contracts"]["fileA"]["C"]["evm"][bytecodeType]["ethdebug"]; @@ -2269,6 +2285,22 @@ BOOST_DATA_TEST_CASE(ethdebug_output_instructions_smoketest, boost::unit_test::d } } +BOOST_AUTO_TEST_CASE(ethdebug_rejects_yul_optimization) +{ + frontend::StandardCompiler compiler; + std::map yulSources; + yulSources["fileA.yul"] = "object \"C\" { code { stop() } }"; + Json input = generateExperimentalStandardJson( + false, + Json::array({"ast-id", "ethdebug"}), + Json::array({"ir"}), + YulCode(std::move(yulSources)) + ); + input["settings"]["optimizer"] = {{"enabled", true}}; + Json result = compiler.compile(input); + BOOST_CHECK(containsError(result, "UnimplementedFeatureError", "Optimization is not yet supported with ethdebug.")); +} + BOOST_AUTO_TEST_CASE(no_experimental_import_ast_solidity_evmasm) { frontend::StandardCompiler compiler; diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 69a6d344f880..bc801d528574 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -1419,25 +1419,25 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_incompatible_outputs) // has been removed. These combinations are now valid as long as other requirements are met. static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program-runtime", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program-runtime", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--via-ir", "--ethdebug-program", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--via-ir", "--ethdebug-program", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--asm-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-ast-json", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir-optimized-ast-json", tempDir.path().string() + "/input.sol"}, }, }; for (auto const& test: supportedCLIFlagCombinations) @@ -1487,9 +1487,13 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) createFilesWithParentDirs({tempDir.path() / "input.sol"}, "pragma solidity >=0.0; contract C { function f() public pure {} }"); createFilesWithParentDirs({tempDir.path() / "input.yul"}, "{}"); static std::vector> erroneousCLIFlagCombinations{ + // ethdebug depends on ast-id. + { + {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + }, // --debug-info ethdebug with --optimize is not supported { - {"solc", "--experimental", "--debug-info", "ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--optimize", "--ir", tempDir.path().string() + "/input.sol"}, }, { {"solc", "--experimental", "--debug-info", "location", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, @@ -1503,22 +1507,22 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_debug_info_ethdebug) }; static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--debug-info", "ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--strict-assembly", tempDir.path().string() + "/input.yul"}, }, }; @@ -1551,6 +1555,10 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) { {"solc", "--experimental", "--ethdebug-program", tempDir.path().string() + "/input.sol"}, }, + // Selecting an ethdebug output does not enable --debug-info ethdebug implicitly. + { + {"solc", "--experimental", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + }, { {"solc", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", tempDir.path().string() + "/input.sol"}, }, @@ -1560,28 +1568,28 @@ BOOST_AUTO_TEST_CASE(cli_ethdebug_ethdebug_output) }; static std::vector> supportedCLIFlagCombinations{ { - {"solc", "--experimental", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir", "--ir", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, }, { - {"solc", "--experimental", "--ethdebug-program-runtime", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, + {"solc", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir", "--ir-optimized", tempDir.path().string() + "/input.sol"}, }, { {"solc", "--experimental", "--ethdebug-resources", tempDir.path().string() + "/input.sol"}, diff --git a/test/solc/CommandLineParser.cpp b/test/solc/CommandLineParser.cpp index 5dd00e05aab8..cad7c5cd36e3 100644 --- a/test/solc/CommandLineParser.cpp +++ b/test/solc/CommandLineParser.cpp @@ -650,37 +650,45 @@ BOOST_AUTO_TEST_CASE(invalid_optimizer_sequence_without_optimize) BOOST_AUTO_TEST_CASE(ethdebug) { // --ethdebug-program with explicit debug-info - CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program", "--via-ir"}); + CommandLineOptions commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --ethdebug-program-runtime with explicit debug-info - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ethdebug-program-runtime", "--via-ir"}); - BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); - BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); - BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); - BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); - // --ethdebug-program implicitly enables debug-info ethdebug - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program", "--via-ir"}); - BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); - BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); - BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); - BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); - // --ethdebug-program-runtime implicitly enables debug-info ethdebug - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program-runtime", "--via-ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program-runtime", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); + // Selecting an ethdebug output does not modify the debug-info selection: without + // --debug-info ethdebug the program outputs are an error. + auto expectsExplicitEthdebug = [](CommandLineValidationError const& _exception) { + return std::string(_exception.what()).find("--debug-info must contain ethdebug") != std::string::npos; + }; + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program-runtime", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); + BOOST_CHECK_EXCEPTION( + parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "location", "--ethdebug-program", "--via-ir"}), + CommandLineValidationError, + expectsExplicitEthdebug + ); // both --ethdebug-program and --ethdebug-program-runtime - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ethdebug-program", "--ethdebug-program-runtime", "--via-ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, true); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection.has_value(), true); BOOST_CHECK_EQUAL(commandLineOptions.output.debugInfoSelection->ethdebug, true); // --debug-info ethdebug with --ir only (no program output) - commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ethdebug", "--ir"}); + commandLineOptions = parseCommandLine({"solc", "contract.sol", "--experimental", "--debug-info", "ast-id,ethdebug", "--ir"}); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgram, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ethdebugProgramRuntime, false); BOOST_CHECK_EQUAL(commandLineOptions.compiler.outputs.ir, true); From bdfb8267665146fe0f01703bda95f2a47300a3e8 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 31 Aug 2026 10:40:40 +0200 Subject: [PATCH 2/2] ethdebug: Emit type documents, state variable pointers and the program-level context Populate ethdebug/format/info/resources from analysis results: every referenced type as an ethdebug/format/type document keyed by the compiler's type identifier, and the pointer of every state variable in storage and transient storage as a reusable template, derived from the storage layout (packing, mappings, dynamic arrays, bytes/string encodings, structs). Mapping keys become template parameters the consumer binds. The internal model behind this is the semantic debug info side table specified in docs/internals/ethdebug_internal_debug_info.rst: scope records keyed by (AST ID, instance) with variables carrying identity, declaration range, type reference, phase and an ethdebug pointer, plus a versioned serialization of the table checked strictly against the public pointer schema. Numeric source IDs match the ethdebug.compilation record. State variables whose pointer is closed, i.e. reads no template parameter and no generated Yul local, are also published in the program-level context of the ethdebug program outputs, with their declaration and type reference. --- Changelog.md | 1 + libevmasm/Ethdebug.cpp | 15 +- libevmasm/Ethdebug.h | 18 +- libevmasm/EthdebugSchema.cpp | 8 +- libevmasm/EthdebugSchema.h | 6 +- liblangutil/CMakeLists.txt | 6 + liblangutil/SemanticDebugData.h | 542 +++++++++ liblangutil/SemanticDebugDataAnalysis.cpp | 224 ++++ liblangutil/SemanticDebugDataAnalysis.h | 50 + .../SemanticDebugDataSerialization.cpp | 1042 +++++++++++++++++ liblangutil/SemanticDebugDataSerialization.h | 66 ++ liblangutil/SemanticDebugDataTable.h | 119 ++ libsolidity/CMakeLists.txt | 4 + .../codegen/ir/SemanticDebugDataBuilder.cpp | 874 ++++++++++++++ .../codegen/ir/SemanticDebugDataBuilder.h | 44 + libsolidity/interface/CompilerStack.cpp | 34 +- libsolidity/interface/Ethdebug.cpp | 137 +++ libsolidity/interface/Ethdebug.h | 61 + test/CMakeLists.txt | 1 + .../output.json | 7 +- test/ethdebugSchemaTests/sources/a.sol | 6 + .../test_ethdebug_schema_conformity.py | 121 +- test/liblangutil/DebugData.cpp | 539 +++++++++ .../ethdebugTests/basic_contract.sol | 3 + 24 files changed, 3911 insertions(+), 17 deletions(-) create mode 100644 liblangutil/SemanticDebugData.h create mode 100644 liblangutil/SemanticDebugDataAnalysis.cpp create mode 100644 liblangutil/SemanticDebugDataAnalysis.h create mode 100644 liblangutil/SemanticDebugDataSerialization.cpp create mode 100644 liblangutil/SemanticDebugDataSerialization.h create mode 100644 liblangutil/SemanticDebugDataTable.h create mode 100644 libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp create mode 100644 libsolidity/codegen/ir/SemanticDebugDataBuilder.h create mode 100644 libsolidity/interface/Ethdebug.cpp create mode 100644 libsolidity/interface/Ethdebug.h create mode 100644 test/liblangutil/DebugData.cpp diff --git a/Changelog.md b/Changelog.md index 890cf719bc22..8e2fed7d17c4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@ Compiler Features: * SMTChecker: Emit a deprecation warning for the BMC engine. * SMTChecker: Support `block.slotnum`. * Standard JSON Interface: Selecting an ethdebug output no longer implicitly enables the `ethdebug` and `ast-id` components of `settings.debug.debugInfo`; they have to be selected explicitly. +* ethdebug: Populate `ethdebug/format/info/resources` with the type documents and the pointer templates of the state variables in storage and transient storage, and publish the state variables in a program-level context (experimental). * Standard JSON Interface: Fix the entire output being replaced by a `JSONError` ("Error writing output JSON.") when an error message quotes a long source line and truncating it splits a multi-byte character. Bugfixes: diff --git a/libevmasm/Ethdebug.cpp b/libevmasm/Ethdebug.cpp index 0b341d7e8df2..35992dbac1bf 100644 --- a/libevmasm/Ethdebug.cpp +++ b/libevmasm/Ethdebug.cpp @@ -171,7 +171,7 @@ schema::materials::Compilation materialCompilation(std::vector const& _s } // anonymous namespace -Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject) +Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject, std::optional _programContext) { return schema::Program{ .compilation = std::nullopt, @@ -186,17 +186,22 @@ Json ethdebug::program(std::string_view _name, unsigned _sourceID, Assembly cons } }, .environment = _assembly.isCreation() ? schema::Program::Environment::CREATE : schema::Program::Environment::CALL, - .context = std::nullopt, + .context = std::move(_programContext), .instructions = programInstructions(_assembly, _linkerObject, _sourceID) }; } -Json ethdebug::resources(std::vector const& _sources, std::string_view _version) +Json ethdebug::resources( + std::vector const& _sources, + std::string_view _version, + Json _types, + Json _pointers +) { schema::info::Resources result; result.compilation = materialCompilation(_sources, _version); - result.types = Json::object(); - result.pointers = Json::object(); + result.types = std::move(_types); + result.pointers = std::move(_pointers); return result; } diff --git a/libevmasm/Ethdebug.h b/libevmasm/Ethdebug.h index dc67a9ffc812..3af9c463e6b8 100644 --- a/libevmasm/Ethdebug.h +++ b/libevmasm/Ethdebug.h @@ -21,8 +21,11 @@ #include #include +#include #include +#include + namespace solidity::evmasm::ethdebug { @@ -35,10 +38,21 @@ struct Source }; // returns ethdebug/format/program. -Json program(std::string_view _name, unsigned _sourceID, Assembly const& _assembly, LinkerObject const& _linkerObject); +Json program( + std::string_view _name, + unsigned _sourceID, + Assembly const& _assembly, + LinkerObject const& _linkerObject, + std::optional _programContext = std::nullopt +); // returns ethdebug/format/info/resources -Json resources(std::vector const& _sources, std::string_view _version); +Json resources( + std::vector const& _sources, + std::string_view _version, + Json _types = Json::object(), + Json _pointers = Json::object() +); // returns the 'compilation' object from ethdebug/format/info/resources Json compilation(std::vector const& _sources, std::string_view _version); diff --git a/libevmasm/EthdebugSchema.cpp b/libevmasm/EthdebugSchema.cpp index d67878449b14..188bb883863b 100644 --- a/libevmasm/EthdebugSchema.cpp +++ b/libevmasm/EthdebugSchema.cpp @@ -102,7 +102,9 @@ void schema::program::to_json(Json& _json, Context::Variable const& _contextVari { auto const numProperties = _contextVariable.identifier.has_value() + - _contextVariable.declaration.has_value(); + _contextVariable.declaration.has_value() + + _contextVariable.type.has_value() + + _contextVariable.pointer.has_value(); solRequire(numProperties >= 1, EthdebugException, "Context variable has no properties."); if (_contextVariable.identifier) { @@ -111,6 +113,10 @@ void schema::program::to_json(Json& _json, Context::Variable const& _contextVari } if (_contextVariable.declaration) _json["declaration"] = *_contextVariable.declaration; + if (_contextVariable.type) + _json["type"] = *_contextVariable.type; + if (_contextVariable.pointer) + _json["pointer"] = *_contextVariable.pointer; } void schema::program::to_json(Json& _json, Context const& _context) diff --git a/libevmasm/EthdebugSchema.h b/libevmasm/EthdebugSchema.h index 57f21a550f51..d0f4fe31d2e9 100644 --- a/libevmasm/EthdebugSchema.h +++ b/libevmasm/EthdebugSchema.h @@ -123,8 +123,10 @@ struct Context { std::optional identifier; std::optional declaration; - // TODO: type - // TODO: pointer according to ethdebug/format/spec/pointer + // ethdebug/format/type/specifier: a full type representation or an { "id": ... } reference. + std::optional type; + // ethdebug/format/pointer: a region or collection describing where the value lives. + std::optional pointer; }; std::optional code; diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 054d01d91881..f590d95b2aa9 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -17,6 +17,12 @@ set(sources Scanner.cpp Scanner.h CharStreamProvider.h + SemanticDebugData.h + SemanticDebugDataAnalysis.cpp + SemanticDebugDataAnalysis.h + SemanticDebugDataSerialization.cpp + SemanticDebugDataSerialization.h + SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h new file mode 100644 index 000000000000..22457a830859 --- /dev/null +++ b/liblangutil/SemanticDebugData.h @@ -0,0 +1,542 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * The compiler structures of the semantic debug info specified in + * docs/internals/ethdebug_internal_debug_info.rst: scope records with their + * variables, phases and ethdebug pointers. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +// The SemanticDebugData prefix names the compiler structures of the semantic +// debug info specified in docs/internals/ethdebug_internal_debug_info.rst: +// the scope records attached to Yul nodes, their variables and pointers, and +// the side table that carries them across a textual Yul boundary. + +/// Whether a variable's value can be recovered at a program point. The region +/// a materialized value lives in is not duplicated here: the variable's pointer +/// already records it. +enum class SemanticDebugVariablePhase +{ + /// The value has a concrete representation described by the variable's pointer. + Materialized, + /// The value has no region because the program recomputes it where it is + /// used. No recovery recipe is carried; the phase only records that + /// recomputation would be possible in principle, unlike OptimizedOut. + Computed, + /// No recoverable representation is available at this program point. + OptimizedOut +}; + +/// Source range in the shape of the ethdebug source range schema. The source +/// is identified by the same ID the accompanying ethdebug.compilation record +/// uses; a path is never used as an ID, and the Yul stage transports the ID +/// unchanged without interpreting it. +struct SemanticDebugSourceRange +{ + uint64_t sourceID = 0; + int offset = 0; + int length = 0; + + /// The range of @a _location in the source with ID @a _sourceID, or + /// nothing when the location is empty or the source has no ID. + static std::optional fromLocation( + SourceLocation const& _location, + std::optional _sourceID + ) + { + if (!_sourceID || _location.start < 0 || _location.end < _location.start) + return std::nullopt; + return SemanticDebugSourceRange{*_sourceID, _location.start, _location.end - _location.start}; + } + + bool operator==(SemanticDebugSourceRange const&) const = default; +}; + +/// Node in an ethdebug pointer expression tree. Expressions evaluate to +/// unsigned values and may reference named regions and variables bound by +/// scope definitions, list indices or template parameters. +struct SemanticDebugPointerExpression +{ + enum class Kind + { + Unknown, + /// Literal unsigned value. @a value holds the canonical `0x`-prefixed + /// hex form. + Literal, + /// The EVM word size in bytes (`$wordsize`). + WordSize, + /// Reference to a variable bound by a scope definition, a list index + /// name or a template parameter, emitted as written. A variable that + /// nothing binds is a dependency on the surrounding Yul code, like a + /// Yul local, and is treated as one by the survival check. + Variable, + /// A generated Yul local standing in for a stack depth that is not + /// known until the Yul-to-EVM transform has run. + /// + /// Nothing binds the name, so a consumer cannot resolve it, and emitting + /// it would put a Yul identifier where the public format expects a + /// slot. It is internal only, serialized as `{"$$yulLocal": ...}` with + /// the compiler's `$$` prefix that cannot clash with the schema's own + /// `$` forms, and must be resolved or removed before public emission, + /// which publishes no pointer still carrying it. + YulLocal, + /// `{".slot": }` — the slot defined for the referenced region. + LookupSlot, + /// `{".offset": }` — the offset defined for the referenced region. + LookupOffset, + /// `{".length": }` — the length defined for the referenced region. + LookupLength, + /// `{"$read": }` — the raw machine-state bytes in the referenced region. + Read, + /// `{"$sum": [...]}` over any number of operands. + Sum, + /// `{"$product": [...]}` over any number of operands. + Product, + /// `{"$difference": [a, b]}` — clamped at zero. + Difference, + /// `{"$quotient": [a, b]}` — integer division. + Quotient, + /// `{"$remainder": [a, b]}` — modular remainder. + Remainder, + /// `{"$keccak256": [...]}` — hash of the tightly packed operand bytes. + Keccak256, + /// `{"$concat": [...]}` — byte concatenation of the operands. + Concat, + /// `{"$sized": x}` when @a value holds the decimal byte width N, + /// `{"$wordsized": x}` when @a value is unset. + Resize + }; + + Kind kind = Kind::Unknown; + /// Payload interpreted according to @a kind: literal value, variable identifier, + /// referenced region name (or `$this`) or resize width. + std::optional value; + /// Sub-expressions for arithmetic, hashing, concatenation and resize kinds. + std::vector operands; + + static SemanticDebugPointerExpression literal(std::string _value) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Literal; + result.value = std::move(_value); + return result; + } + + static SemanticDebugPointerExpression wordSize() + { + SemanticDebugPointerExpression result; + result.kind = Kind::WordSize; + return result; + } + + static SemanticDebugPointerExpression variable(std::string _identifier) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Variable; + result.value = std::move(_identifier); + return result; + } + + static SemanticDebugPointerExpression yulLocal(std::string _yulName) + { + SemanticDebugPointerExpression result; + result.kind = Kind::YulLocal; + result.value = std::move(_yulName); + return result; + } + + static SemanticDebugPointerExpression lookupSlot(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupSlot; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression lookupOffset(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupOffset; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression lookupLength(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::LookupLength; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression read(std::string _region) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Read; + result.value = std::move(_region); + return result; + } + + static SemanticDebugPointerExpression sum(std::vector _operands) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Sum; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression product( + std::vector _operands + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Product; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression difference( + SemanticDebugPointerExpression _minuend, + SemanticDebugPointerExpression _subtrahend + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Difference; + result.operands.emplace_back(std::move(_minuend)); + result.operands.emplace_back(std::move(_subtrahend)); + return result; + } + + static SemanticDebugPointerExpression quotient( + SemanticDebugPointerExpression _dividend, + SemanticDebugPointerExpression _divisor + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Quotient; + result.operands.emplace_back(std::move(_dividend)); + result.operands.emplace_back(std::move(_divisor)); + return result; + } + + static SemanticDebugPointerExpression remainder( + SemanticDebugPointerExpression _dividend, + SemanticDebugPointerExpression _divisor + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Remainder; + result.operands.emplace_back(std::move(_dividend)); + result.operands.emplace_back(std::move(_divisor)); + return result; + } + + static SemanticDebugPointerExpression keccak256( + std::vector _operands + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Keccak256; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression concat( + std::vector _operands + ) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Concat; + result.operands = std::move(_operands); + return result; + } + + static SemanticDebugPointerExpression wordSized(SemanticDebugPointerExpression _operand) + { + SemanticDebugPointerExpression result; + result.kind = Kind::Resize; + result.operands.emplace_back(std::move(_operand)); + return result; + } + + static SemanticDebugPointerExpression sized( + unsigned _bytes, + SemanticDebugPointerExpression _operand + ) + { + // The ethdebug $sized expression requires a positive byte width; + // use wordSized() for word-sized resizing. + solAssert(_bytes > 0); + SemanticDebugPointerExpression result; + result.kind = Kind::Resize; + result.value = std::to_string(_bytes); + result.operands.emplace_back(std::move(_operand)); + return result; + } +}; + + +/// Internal representation of an ethdebug pointer: a single region of EVM data +/// or a structured collection of sub-pointers. Which fields are meaningful +/// depends on @a pointerClass. +struct SemanticDebugPointer +{ + enum class Class + { + /// A single addressed range of data. Uses @a location, @a name, @a slot, + /// @a offset and @a length. + Region, + /// An ordered composition of sub-pointers in @a group. + Group, + /// A dynamically sized repetition: @a count elements, the index bound to + /// @a indexName inside @a listElement. + List, + /// A pointer chosen by the non-zero-ness of @a condition: @a thenPointer, + /// otherwise the optional @a elsePointer. + Conditional, + /// A pointer with auxiliary variables: ordered @a definitions bound inside + /// @a scopeTarget. Later definitions may reference earlier ones. + Scope, + /// A reference to a pointer template defined elsewhere: @a templateName, + /// with produced region names optionally renamed through @a yields. + /// The template is defined either by an enclosing Templates pointer or + /// by the pointer template table of the sidecar. + TemplateReference, + /// A pointer with locally defined templates: @a templates are available + /// by name to template references inside @a scopeTarget. + Templates, + Unknown + }; + + enum class Location + { + Stack, + Storage, + Transient, + Memory, + Calldata, + Returndata, + Code, + Unknown + }; + + Class pointerClass = Class::Unknown; + + /// Template parameters (the pointer template's `expect` list) that must be + /// bound externally before this pointer can be evaluated, e.g. mapping + /// keys. Meaningful on a root pointer and on template definitions, in the + /// sidecar's pointer table as well as in local `templates` blocks. + std::vector expectedParameters; + + // Class::Region + std::optional location; + std::optional name; + /// Word-oriented locations (stack, storage, transient) address by slot. + std::optional slot; + /// Byte offset: within the slot for word-oriented locations, absolute for + /// byte-oriented ones (memory, calldata, returndata, code). + std::optional offset; + /// Byte length of the region. + std::optional length; + + // Class::Group + std::vector group; + + // Class::List + std::optional count; + std::optional indexName; + std::shared_ptr listElement; + + // Class::Conditional + std::optional condition; + std::shared_ptr thenPointer; + std::shared_ptr elsePointer; + + // Class::Scope and Class::Templates share the target: the `in` pointer. + std::vector> definitions; + std::shared_ptr scopeTarget; + + // Class::TemplateReference + std::optional templateName; + std::vector> yields; + + // Class::Templates + std::vector>> templates; + + static SemanticDebugPointer region( + Location _location, + std::optional _name, + std::optional _slot, + std::optional _offset = std::nullopt, + std::optional _length = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Region; + result.location = _location; + result.name = std::move(_name); + result.slot = std::move(_slot); + result.offset = std::move(_offset); + result.length = std::move(_length); + return result; + } + + static SemanticDebugPointer makeGroup(std::vector _members) + { + SemanticDebugPointer result; + result.pointerClass = Class::Group; + result.group = std::move(_members); + return result; + } + + static SemanticDebugPointer list( + SemanticDebugPointerExpression _count, + std::string _indexName, + SemanticDebugPointer _element + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::List; + result.count = std::move(_count); + result.indexName = std::move(_indexName); + result.listElement = std::make_shared(std::move(_element)); + return result; + } + + static SemanticDebugPointer conditional( + SemanticDebugPointerExpression _condition, + SemanticDebugPointer _then, + std::optional _else = std::nullopt + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Conditional; + result.condition = std::move(_condition); + result.thenPointer = std::make_shared(std::move(_then)); + if (_else) + result.elsePointer = std::make_shared(std::move(*_else)); + return result; + } + + static SemanticDebugPointer scope( + std::vector> _definitions, + SemanticDebugPointer _target + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Scope; + result.definitions = std::move(_definitions); + result.scopeTarget = std::make_shared(std::move(_target)); + return result; + } + + static SemanticDebugPointer templateReference( + std::string _templateName, + std::vector> _yields = {} + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::TemplateReference; + result.templateName = std::move(_templateName); + result.yields = std::move(_yields); + return result; + } + + static SemanticDebugPointer withTemplates( + std::vector> _templates, + SemanticDebugPointer _target + ) + { + SemanticDebugPointer result; + result.pointerClass = Class::Templates; + for (auto& [templateName, definition]: _templates) + result.templates.emplace_back( + std::move(templateName), + std::make_shared(std::move(definition)) + ); + result.scopeTarget = std::make_shared(std::move(_target)); + return result; + } +}; + +/// One binding introduced by a scope record: the source declaration, its type +/// and, when the value is materialized, the pointer that resolves it. +struct SemanticDebugVariable +{ + /// Source-language identifier, if the variable has one. Unnamed return + /// parameters are still represented and leave this unset. + std::optional identifier; + std::optional declarationASTID; + std::optional declarationSourceRange; + std::optional typeID; + /// Whether the value is materialized, computable or unavailable. A + /// materialized variable has a pointer; the other phases do not. + SemanticDebugVariablePhase phase = SemanticDebugVariablePhase::OptimizedOut; + std::optional pointer; +}; + +/// Changes a variable's phase or pointer from the carrying Yul node onward, +/// within the variable's scope, until the next update - the analogue of an +/// `llvm.dbg.value` intrinsic. A single per-scope phase and pointer cannot +/// describe a pass that gives one source variable different generated names +/// over its lifetime (SSATransform) or replaces stored values with +/// recomputation (Rematerialiser); this record can, and a Drop is an update to +/// OptimizedOut. +struct SemanticDebugVariableUpdate +{ + /// Declaration AST ID of the variable being updated. + int64_t variableASTID = 0; + /// Replacement phase valid from the carrying node onward. Omitted when + /// only the pointer changes. + std::optional phase; + /// Replacement pointer for a materialized value. A change to Computed or + /// OptimizedOut clears the previous pointer. + std::optional pointer; +}; + +/// The semantic scope described by an attached Yul node. On a node that +/// introduces a source scope it defines the scope's variables; on a +/// finer-grained node it carries only variable updates. A variable appears in +/// exactly one scope record; nested records do not repeat enclosing bindings. +struct SemanticDebugScope +{ + using ConstPtr = std::shared_ptr; + + /// Bindings introduced by the scope, in source order. + std::vector variableDefinitions = {}; + /// Phase or pointer changes taking effect at the node this payload is + /// attached to. Scope records define variables; statement-level records + /// update them. + std::vector variableUpdates = {}; +}; + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataAnalysis.cpp b/liblangutil/SemanticDebugDataAnalysis.cpp new file mode 100644 index 000000000000..806a6086fbf4 --- /dev/null +++ b/liblangutil/SemanticDebugDataAnalysis.cpp @@ -0,0 +1,224 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include +#include + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +/// Collects the free names of pointers, resolving template references. The +/// free names of a template are computed once per template, with the +/// template's own parameters bound, and cached; a template that references +/// itself, directly or through other templates, contributes nothing to its +/// own free names. +class FreeNameCollector +{ +public: + explicit FreeNameCollector(SemanticDebugDataTable const& _table): m_table(_table) {} + + std::set collect(SemanticDebugPointer const& _pointer) + { + std::set free; + std::set bound; + LocalTemplates localTemplates; + visit(_pointer, bound, localTemplates, free); + return free; + } + +private: + using LocalTemplates = std::map>; + + void visit( + SemanticDebugPointerExpression const& _expression, + std::set const& _bound, + std::set& _free + ) + { + using Kind = SemanticDebugPointerExpression::Kind; + // A Yul local is a dependency on the current Yul code by definition; + // nothing binds it. An unbound variable is a dependency as well. + if (_expression.kind == Kind::YulLocal && _expression.value) + _free.insert(*_expression.value); + else if (_expression.kind == Kind::Variable && _expression.value) + { + if (!_bound.count(*_expression.value)) + _free.insert(*_expression.value); + } + else + // Lookups and reads name regions, which live in a separate namespace. + for (SemanticDebugPointerExpression const& operand: _expression.operands) + visit(operand, _bound, _free); + } + + void visit( + SemanticDebugPointer const& _pointer, + std::set& _bound, + LocalTemplates& _localTemplates, + std::set& _free + ) + { + ScopedNames parameters{_bound, _pointer.expectedParameters}; + + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value()) + visit(**expression, _bound, _free); + + // Scope definitions are ordered: each may reference the earlier ones. + std::vector definedNames; + for (auto const& [definedName, definedValue]: _pointer.definitions) + { + visit(definedValue, _bound, _free); + if (_bound.insert(definedName).second) + definedNames.emplace_back(definedName); + } + + for (SemanticDebugPointer const& member: _pointer.group) + visit(member, _bound, _localTemplates, _free); + + if (_pointer.listElement) + { + ScopedNames index{_bound, _pointer.indexName ? std::vector{*_pointer.indexName} : std::vector{}}; + visit(*_pointer.listElement, _bound, _localTemplates, _free); + } + + // Locally defined templates are visible to references inside the target. + std::vector addedTemplates; + if (_pointer.pointerClass == SemanticDebugPointer::Class::Templates) + for (auto const& [templateName, definition]: _pointer.templates) + if (definition && _localTemplates.emplace(templateName, definition).second) + addedTemplates.emplace_back(templateName); + + if (_pointer.pointerClass == SemanticDebugPointer::Class::TemplateReference && _pointer.templateName) + for (std::string const& name: freeNamesOfTemplate(*_pointer.templateName, _localTemplates)) + if (!_bound.count(name)) + _free.insert(name); + + for (auto const* subPointer: {&_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer) + visit(**subPointer, _bound, _localTemplates, _free); + + for (std::string const& templateName: addedTemplates) + _localTemplates.erase(templateName); + for (std::string const& definedName: definedNames) + _bound.erase(definedName); + } + + /// The free names of a template, with its own parameters bound. Local + /// templates shadow the table's; results for the table's templates are + /// cached, results for local ones are recomputed per reference. + std::set freeNamesOfTemplate(std::string const& _name, LocalTemplates& _localTemplates) + { + if (m_resolving.count(_name)) + return {}; + + SemanticDebugPointer const* definition = nullptr; + bool fromTable = false; + if (auto const it = _localTemplates.find(_name); it != _localTemplates.end()) + definition = it->second.get(); + else if ((definition = m_table.findPointerTemplate(_name))) + { + fromTable = true; + if (auto const cached = m_tableTemplateFreeNames.find(_name); cached != m_tableTemplateFreeNames.end()) + return cached->second; + } + if (!definition) + return {}; + + m_resolving.insert(_name); + std::set free; + std::set bound; + visit(*definition, bound, _localTemplates, free); + m_resolving.erase(_name); + + if (fromTable) + m_tableTemplateFreeNames.emplace(_name, free); + return free; + } + + /// Binds names for the lifetime of the object, restoring the previous + /// state afterwards. + class ScopedNames + { + public: + ScopedNames(std::set& _bound, std::vector const& _names): m_bound(_bound) + { + for (std::string const& name: _names) + if (m_bound.insert(name).second) + m_added.emplace_back(name); + } + ~ScopedNames() + { + for (std::string const& name: m_added) + m_bound.erase(name); + } + + private: + std::set& m_bound; + std::vector m_added; + }; + + SemanticDebugDataTable const& m_table; + std::set m_resolving; + std::map> m_tableTemplateFreeNames; +}; + +bool hasInternalExpression(SemanticDebugPointerExpression const& _expression) +{ + if (_expression.kind == SemanticDebugPointerExpression::Kind::YulLocal) + return true; + for (SemanticDebugPointerExpression const& operand: _expression.operands) + if (hasInternalExpression(operand)) + return true; + return false; +} + +} + +std::set langutil::semanticDebugPointerFreeNames( + SemanticDebugPointer const& _pointer, + SemanticDebugDataTable const& _table +) +{ + return FreeNameCollector{_table}.collect(_pointer); +} + +bool langutil::semanticDebugPointerHasInternalExpression(SemanticDebugPointer const& _pointer) +{ + for (auto const* expression: {&_pointer.slot, &_pointer.offset, &_pointer.length, &_pointer.count, &_pointer.condition}) + if (expression->has_value() && hasInternalExpression(**expression)) + return true; + for (auto const& [definedName, definedValue]: _pointer.definitions) + if (hasInternalExpression(definedValue)) + return true; + for (SemanticDebugPointer const& member: _pointer.group) + if (semanticDebugPointerHasInternalExpression(member)) + return true; + for (auto const& [templateName, definition]: _pointer.templates) + if (definition && semanticDebugPointerHasInternalExpression(*definition)) + return true; + for (auto const* subPointer: {&_pointer.listElement, &_pointer.thenPointer, &_pointer.elsePointer, &_pointer.scopeTarget}) + if (*subPointer && semanticDebugPointerHasInternalExpression(**subPointer)) + return true; + return false; +} diff --git a/liblangutil/SemanticDebugDataAnalysis.h b/liblangutil/SemanticDebugDataAnalysis.h new file mode 100644 index 000000000000..44ca653b9cb6 --- /dev/null +++ b/liblangutil/SemanticDebugDataAnalysis.h @@ -0,0 +1,50 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Analyses of ethdebug pointers shared by the Yul side table transfer and the + * public emission. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace solidity::langutil +{ + +/// The names that @a _pointer reads and nothing inside it binds: generated Yul +/// locals (`$$yulLocal`) and variables not bound by a `define`, a list index or +/// a template parameter. Template references are followed through the local +/// templates blocks enclosing them and through the pointer templates of +/// @a _table; a reference to an unknown template contributes nothing. +std::set semanticDebugPointerFreeNames( + SemanticDebugPointer const& _pointer, + SemanticDebugDataTable const& _table +); + +/// Whether @a _pointer, including the templates it defines locally, still +/// contains a compiler-internal expression such as `$$yulLocal`. Templates in +/// the sidecar's table are not followed: they are rejected on deserialization +/// when they contain one. +bool semanticDebugPointerHasInternalExpression(SemanticDebugPointer const& _pointer); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp new file mode 100644 index 000000000000..b0cb212c5088 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -0,0 +1,1042 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::langutil; + +namespace +{ + +/// Sidecars nest pointers and expressions recursively; a document deeper than +/// this is rejected rather than parsed with unbounded recursion. +constexpr size_t maximumNestingDepth = 256; + +/// Expressions internal to the compiler use this prefix, which cannot clash +/// with the schema's own `$` forms. +constexpr std::string_view internalExpressionPrefix = "$$"; +constexpr std::string_view yulLocalKey = "$$yulLocal"; + +void require(bool _condition, std::string const& _message) +{ + solRequire(_condition, SemanticDebugDataSerializationError, _message); +} + +void requireObject(Json const& _json, std::string const& _path) +{ + require(_json.is_object(), _path + " must be an object."); +} + +void requireArray(Json const& _json, std::string const& _path) +{ + require(_json.is_array(), _path + " must be an array."); +} + +/// Rejects members outside @a _allowed: the format is versioned, so an +/// unknown member is a mistake rather than an extension. +void requireOnlyMembers(Json const& _json, std::set const& _allowed, std::string const& _path) +{ + requireObject(_json, _path); + for (auto const& [name, value]: _json.items()) + require(_allowed.count(name), _path + " has an unknown member \"" + name + "\"."); +} + +Json const& requiredMember(Json const& _json, std::string const& _name, std::string const& _path) +{ + requireObject(_json, _path); + require(_json.contains(_name), _path + "." + _name + " is required."); + return _json.at(_name); +} + +std::string requiredString(Json const& _json, std::string const& _name, std::string const& _path) +{ + Json const& value = requiredMember(_json, _name, _path); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +template +T integerValue(Json const& _value, std::string const& _path) +{ + require(_value.is_number_integer(), _path + " must be an integer."); + if constexpr (std::is_unsigned_v) + { + require( + _value.is_number_unsigned() || _value.get() >= 0, + _path + " must not be negative." + ); + require(_value.get() <= std::numeric_limits::max(), _path + " is too large."); + } + else if (_value.is_number_unsigned()) + require( + _value.get() <= static_cast(std::numeric_limits::max()), + _path + " is too large." + ); + else + { + Json::number_integer_t const rawValue = _value.get(); + require(rawValue >= std::numeric_limits::min(), _path + " is too small."); + require(rawValue <= std::numeric_limits::max(), _path + " is too large."); + } + return _value.get(); +} + +template +T requiredInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + return integerValue(requiredMember(_json, _name, _path), _path + "." + _name); +} + +template +std::optional optionalInteger(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + return integerValue(_json.at(_name), _path + "." + _name); +} + +std::optional optionalString(Json const& _json, std::string const& _name, std::string const& _path) +{ + if (!_json.contains(_name)) + return std::nullopt; + Json const& value = _json.at(_name); + require(value.is_string(), _path + "." + _name + " must be a string."); + return value.get(); +} + +template +void setOptional(Json& _json, std::string const& _name, std::optional const& _value) +{ + if (_value) + _json[_name] = *_value; +} + +/// Phase values use the compiler's dash-separated convention for enumeration +/// values, like `ast-id` in the debug info selection. +using Phase = SemanticDebugVariablePhase; +constexpr std::pair phaseNames[]{ + {Phase::Materialized, "materialized"}, + {Phase::Computed, "computed"}, + {Phase::OptimizedOut, "optimized-out"} +}; + +using PointerLocation = SemanticDebugPointer::Location; +constexpr std::pair pointerLocationNames[]{ + {PointerLocation::Stack, "stack"}, + {PointerLocation::Storage, "storage"}, + {PointerLocation::Transient, "transient"}, + {PointerLocation::Memory, "memory"}, + {PointerLocation::Calldata, "calldata"}, + {PointerLocation::Returndata, "returndata"}, + {PointerLocation::Code, "code"} +}; + +template +std::string enumToString(Enum _value, Names const& _names) +{ + for (auto const& [value, name]: _names) + if (_value == value) + return std::string(name); + solAssert(false, "Unhandled semantic debug data enum value."); +} + +template +auto enumFromString(std::string const& _name, Names const& _names, std::string const& _path) + -> std::decay_tfirst)> +{ + for (auto const& [value, name]: _names) + if (_name == name) + return value; + solThrow(SemanticDebugDataSerializationError, _path + " has unknown value \"" + _name + "\"."); +} + +/// The pointer schema's identifier grammar: `^[a-zA-Z_\-]+[a-zA-Z0-9$_\-]*$`. +bool isIdentifier(std::string_view _text) +{ + auto const isStart = [](char _c) { return std::isalpha(static_cast(_c)) || _c == '_' || _c == '-'; }; + auto const isRest = [&](char _c) { return isStart(_c) || std::isdigit(static_cast(_c)) || _c == '$'; }; + if (_text.empty() || !isStart(_text.front())) + return false; + for (char const c: _text) + if (!isRest(c)) + return false; + return true; +} + +/// A literal in its canonical form: `0x` followed by at least one hex digit. +bool isHexLiteral(std::string_view _text) +{ + if (_text.size() < 3 || _text.substr(0, 2) != "0x") + return false; + for (char const c: _text.substr(2)) + if (!std::isxdigit(static_cast(c))) + return false; + return true; +} + +/// A positive decimal number without leading zeros, as `$sized` requires. +bool isPositiveDecimal(std::string_view _text) +{ + if (_text.empty() || _text.front() == '0') + return false; + for (char const c: _text) + if (!std::isdigit(static_cast(c))) + return false; + return true; +} + +/// Word-oriented locations address by slot, byte-oriented ones by offset and +/// length, as the schema's segment and slice addressing schemes require. +bool isWordOriented(PointerLocation _location) +{ + return _location == PointerLocation::Stack || _location == PointerLocation::Storage || _location == PointerLocation::Transient; +} + +void requireRegionFields(SemanticDebugPointer const& _region, std::string const& _path) +{ + require(_region.location.has_value() && *_region.location != PointerLocation::Unknown, _path + " must state a location."); + require(!_region.name || isIdentifier(*_region.name), _path + ".name must be an identifier."); + if (isWordOriented(*_region.location)) + require(_region.slot.has_value(), _path + " must address its slot."); + else + require(_region.offset.has_value() && _region.length.has_value(), _path + " must address its offset and length."); +} + +/// Source ranges are serialized in the shape of the schema's +/// materials/source-range. The source ID is the one the accompanying +/// ethdebug.compilation record uses, transported as an opaque number. +Json sourceRangeToJson(SemanticDebugSourceRange const& _range) +{ + require(_range.offset >= 0 && _range.length >= 0, "Cannot serialize an invalid source range."); + return Json{ + {"source", Json{{"id", _range.sourceID}}}, + {"range", Json{{"length", _range.length}, {"offset", _range.offset}}} + }; +} + +SemanticDebugSourceRange sourceRangeFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"source", "range"}, _path); + SemanticDebugSourceRange result; + Json const& source = requiredMember(_json, "source", _path); + requireOnlyMembers(source, {"id"}, _path + ".source"); + result.sourceID = requiredInteger(source, "id", _path + ".source"); + Json const& range = requiredMember(_json, "range", _path); + requireOnlyMembers(range, {"offset", "length"}, _path + ".range"); + result.offset = requiredInteger(range, "offset", _path + ".range"); + result.length = requiredInteger(range, "length", _path + ".range"); + require(result.offset >= 0 && result.length >= 0, _path + ".range must not be negative."); + return result; +} + +Json expressionToJson(SemanticDebugPointerExpression const& _expression); +Json pointerBodyToJson(SemanticDebugPointer const& _pointer); +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path, size_t _depth); +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path, size_t _depth); +SemanticDebugPointer templateFromJson(Json const& _json, std::string const& _path, size_t _depth); + +/// Pointer expressions are serialized in the ethdebug pointer expression +/// grammar itself: `0x` strings for literals, identifiers for bound variables, +/// `"$wordsize"`, and single-key objects for lookups, reads, arithmetic and +/// resizing. The one internal form, `{"$$yulLocal": ...}`, has no schema +/// counterpart: it names a stack depth that is not known yet. +Json expressionToJson(SemanticDebugPointerExpression const& _expression) +{ + using Kind = SemanticDebugPointerExpression::Kind; + + auto const operands = [&](size_t _arity = 0) { + require( + _arity == 0 ? !_expression.operands.empty() : _expression.operands.size() == _arity, + "Pointer expression has the wrong number of operands." + ); + Json result = Json::array(); + for (SemanticDebugPointerExpression const& operand: _expression.operands) + result.emplace_back(expressionToJson(operand)); + return result; + }; + auto const value = [&]() -> std::string const& { + require(_expression.value.has_value(), "Pointer expression lacks its value."); + return *_expression.value; + }; + + switch (_expression.kind) + { + case Kind::Literal: + require(isHexLiteral(value()), "Pointer expression literal \"" + value() + "\" is not a hex literal."); + return Json(value()); + case Kind::Variable: + require(isIdentifier(value()), "Pointer expression variable \"" + value() + "\" is not an identifier."); + return Json(value()); + case Kind::YulLocal: + return Json{{std::string(yulLocalKey), value()}}; + case Kind::WordSize: + return Json("$wordsize"); + case Kind::LookupSlot: + return Json{{".slot", value()}}; + case Kind::LookupOffset: + return Json{{".offset", value()}}; + case Kind::LookupLength: + return Json{{".length", value()}}; + case Kind::Read: + return Json{{"$read", value()}}; + case Kind::Sum: + return Json{{"$sum", operands()}}; + case Kind::Product: + return Json{{"$product", operands()}}; + case Kind::Difference: + return Json{{"$difference", operands(2)}}; + case Kind::Quotient: + return Json{{"$quotient", operands(2)}}; + case Kind::Remainder: + return Json{{"$remainder", operands(2)}}; + case Kind::Keccak256: + return Json{{"$keccak256", operands()}}; + case Kind::Concat: + return Json{{"$concat", operands()}}; + case Kind::Resize: + require(_expression.operands.size() == 1, "A resize expression takes exactly one operand."); + if (!_expression.value) + return Json{{"$wordsized", expressionToJson(_expression.operands.front())}}; + require(isPositiveDecimal(*_expression.value), "A sized resize expression needs a positive byte width."); + return Json{{"$sized" + *_expression.value, expressionToJson(_expression.operands.front())}}; + case Kind::Unknown: + break; + } + solThrow(SemanticDebugDataSerializationError, "Cannot serialize an unknown pointer expression."); +} + +SemanticDebugPointerExpression expressionFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + using Kind = SemanticDebugPointerExpression::Kind; + require(_depth < maximumNestingDepth, _path + " is nested too deeply."); + SemanticDebugPointerExpression result; + + if (_json.is_number_integer()) + { + require(_json.is_number_unsigned() || _json.get() >= 0, _path + " must not be negative."); + return SemanticDebugPointerExpression::literal(toCompactHexWithPrefix(u256(_json.get()))); + } + if (_json.is_string()) + { + std::string text = _json.get(); + if (text == "$wordsize") + return SemanticDebugPointerExpression::wordSize(); + if (isHexLiteral(text)) + return SemanticDebugPointerExpression::literal(std::move(text)); + require(isIdentifier(text), _path + " is neither a hex literal nor an identifier."); + return SemanticDebugPointerExpression::variable(std::move(text)); + } + + requireObject(_json, _path); + require(_json.size() == 1, _path + " must have exactly one member."); + std::string const key = _json.begin().key(); + Json const& value = _json.begin().value(); + std::string const valuePath = _path + "." + key; + + auto const regionName = [&](Kind _kind) { + require(value.is_string() && isIdentifier(value.get()), valuePath + " must name a region."); + result.kind = _kind; + result.value = value.get(); + }; + auto const operands = [&](Kind _kind, size_t _arity = 0) { + requireArray(value, valuePath); + require( + _arity == 0 ? !value.empty() : value.size() == _arity, + valuePath + " has the wrong number of operands." + ); + result.kind = _kind; + for (size_t index = 0; index < value.size(); ++index) + result.operands.emplace_back( + expressionFromJson(value.at(index), valuePath + "[" + std::to_string(index) + "]", _depth + 1) + ); + }; + + if (key == ".slot") + regionName(Kind::LookupSlot); + else if (key == ".offset") + regionName(Kind::LookupOffset); + else if (key == ".length") + regionName(Kind::LookupLength); + else if (key == "$read") + regionName(Kind::Read); + else if (key == yulLocalKey) + { + require(value.is_string() && !value.get().empty(), valuePath + " must name a Yul variable."); + result.kind = Kind::YulLocal; + result.value = value.get(); + } + else if (key == "$sum") + operands(Kind::Sum); + else if (key == "$product") + operands(Kind::Product); + else if (key == "$difference") + operands(Kind::Difference, 2); + else if (key == "$quotient") + operands(Kind::Quotient, 2); + else if (key == "$remainder") + operands(Kind::Remainder, 2); + else if (key == "$keccak256") + operands(Kind::Keccak256); + else if (key == "$concat") + operands(Kind::Concat); + else if (key == "$wordsized") + { + result.kind = Kind::Resize; + result.operands.emplace_back(expressionFromJson(value, valuePath, _depth + 1)); + } + else if (key.starts_with("$sized")) + { + std::string width = key.substr(6); + require(isPositiveDecimal(width), _path + ": malformed resize key \"" + key + "\"."); + result.kind = Kind::Resize; + result.value = std::move(width); + result.operands.emplace_back(expressionFromJson(value, valuePath, _depth + 1)); + } + else if (key.starts_with(internalExpressionPrefix)) + solThrow(SemanticDebugDataSerializationError, _path + ": unknown internal pointer expression \"" + key + "\"."); + else + solThrow(SemanticDebugDataSerializationError, _path + ": unknown pointer expression key \"" + key + "\"."); + return result; +} + +/// Pointers are serialized as ethdebug pointer documents. The class is +/// structural, exactly as in the schema: a region has a `location`, a group +/// has `group`, a list has `list`, a conditional has `if`, a scope is a +/// `define`/`in` chain, a template reference has `template`, and locally +/// defined templates are a `templates`/`in` pair. +Json pointerBodyToJson(SemanticDebugPointer const& _pointer) +{ + using Class = SemanticDebugPointer::Class; + + switch (_pointer.pointerClass) + { + case Class::Region: + { + requireRegionFields(_pointer, "Region pointer"); + Json result = Json::object(); + setOptional(result, "name", _pointer.name); + result["location"] = enumToString(*_pointer.location, pointerLocationNames); + for (auto const& [name, expression]: { + std::pair{"slot", &_pointer.slot}, std::pair{"offset", &_pointer.offset}, std::pair{"length", &_pointer.length} + }) + if (expression->has_value()) + result[name] = expressionToJson(**expression); + return result; + } + case Class::Group: + { + require(!_pointer.group.empty(), "A group pointer must have at least one member."); + Json members = Json::array(); + for (SemanticDebugPointer const& member: _pointer.group) + members.emplace_back(semanticDebugPointerToJson(member)); + return Json{{"group", std::move(members)}}; + } + case Class::List: + { + require( + _pointer.indexName.has_value() && isIdentifier(*_pointer.indexName) && _pointer.listElement != nullptr, + "A list pointer must bind an index name and an element." + ); + Json list = Json::object(); + if (_pointer.count) + list["count"] = expressionToJson(*_pointer.count); + list["each"] = *_pointer.indexName; + list["is"] = semanticDebugPointerToJson(*_pointer.listElement); + return Json{{"list", std::move(list)}}; + } + case Class::Conditional: + { + require( + _pointer.condition.has_value() && _pointer.thenPointer != nullptr, + "A conditional pointer must have a condition and a consequent." + ); + Json result{ + {"if", expressionToJson(*_pointer.condition)}, + {"then", semanticDebugPointerToJson(*_pointer.thenPointer)} + }; + if (_pointer.elsePointer) + result["else"] = semanticDebugPointerToJson(*_pointer.elsePointer); + return result; + } + case Class::Scope: + { + require( + !_pointer.definitions.empty() && _pointer.scopeTarget != nullptr, + "A scope pointer must define at least one name for its target." + ); + // Scope definitions are ordered while JSON object members are not, + // so each definition becomes its own define/in level. + Json inner = semanticDebugPointerToJson(*_pointer.scopeTarget); + for (auto definition = _pointer.definitions.rbegin(); definition != _pointer.definitions.rend(); ++definition) + { + require(isIdentifier(definition->first), "A scope definition must bind an identifier."); + inner = Json{ + {"define", Json{{definition->first, expressionToJson(definition->second)}}}, + {"in", std::move(inner)} + }; + } + return inner; + } + case Class::TemplateReference: + { + require( + _pointer.templateName.has_value() && isIdentifier(*_pointer.templateName), + "A template reference must name its template." + ); + Json result{{"template", *_pointer.templateName}}; + if (!_pointer.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _pointer.yields) + { + require(isIdentifier(producedName) && isIdentifier(newName), "A template reference must yield identifiers."); + yields[producedName] = newName; + } + result["yields"] = std::move(yields); + } + return result; + } + case Class::Templates: + { + require(_pointer.scopeTarget != nullptr, "A templates pointer must have a target."); + Json templates = Json::object(); + for (auto const& [templateName, definition]: _pointer.templates) + { + require(isIdentifier(templateName) && definition != nullptr, "A local pointer template must have a name and a definition."); + templates[templateName] = semanticDebugPointerTemplateToJson(*definition); + } + return Json{{"templates", std::move(templates)}, {"in", semanticDebugPointerToJson(*_pointer.scopeTarget)}}; + } + case Class::Unknown: + break; + } + solThrow(SemanticDebugDataSerializationError, "Cannot serialize an unknown pointer class."); +} + +SemanticDebugPointer pointerFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + using Class = SemanticDebugPointer::Class; + + require(_depth < maximumNestingDepth, _path + " is nested too deeply."); + requireObject(_json, _path); + + SemanticDebugPointer result; + if (_json.contains("location")) + { + requireOnlyMembers(_json, {"name", "location", "slot", "offset", "length"}, _path); + result.pointerClass = Class::Region; + result.name = optionalString(_json, "name", _path); + result.location = enumFromString(requiredString(_json, "location", _path), pointerLocationNames, _path + ".location"); + for (auto const& [name, expression]: { + std::pair{"slot", &result.slot}, std::pair{"offset", &result.offset}, std::pair{"length", &result.length} + }) + if (_json.contains(name)) + *expression = expressionFromJson(_json.at(name), _path + "." + name, _depth + 1); + requireRegionFields(result, _path); + } + else if (_json.contains("group")) + { + requireOnlyMembers(_json, {"group"}, _path); + result.pointerClass = Class::Group; + Json const& members = _json.at("group"); + requireArray(members, _path + ".group"); + require(!members.empty(), _path + ".group must have at least one member."); + for (size_t index = 0; index < members.size(); ++index) + result.group.emplace_back( + pointerFromJson(members.at(index), _path + ".group[" + std::to_string(index) + "]", _depth + 1) + ); + } + else if (_json.contains("list")) + { + requireOnlyMembers(_json, {"list"}, _path); + result.pointerClass = Class::List; + Json const& list = _json.at("list"); + requireOnlyMembers(list, {"count", "each", "is"}, _path + ".list"); + if (list.contains("count")) + result.count = expressionFromJson(list.at("count"), _path + ".list.count", _depth + 1); + result.indexName = requiredString(list, "each", _path + ".list"); + require(isIdentifier(*result.indexName), _path + ".list.each must be an identifier."); + result.listElement = std::make_shared( + pointerFromJson(requiredMember(list, "is", _path + ".list"), _path + ".list.is", _depth + 1) + ); + } + else if (_json.contains("if")) + { + requireOnlyMembers(_json, {"if", "then", "else"}, _path); + result.pointerClass = Class::Conditional; + result.condition = expressionFromJson(_json.at("if"), _path + ".if", _depth + 1); + result.thenPointer = std::make_shared( + pointerFromJson(requiredMember(_json, "then", _path), _path + ".then", _depth + 1) + ); + if (_json.contains("else")) + result.elsePointer = std::make_shared( + pointerFromJson(_json.at("else"), _path + ".else", _depth + 1) + ); + } + else if (_json.contains("define")) + { + // A define/in chain is folded back into one ordered definition list. + // The members of one define object cannot reference each other, so + // their order within that object does not matter. + result.pointerClass = Class::Scope; + Json const* current = &_json; + std::string path = _path; + size_t depth = _depth; + while (current->contains("define")) + { + require(depth < maximumNestingDepth, path + " is nested too deeply."); + requireOnlyMembers(*current, {"define", "in"}, path); + Json const& define = current->at("define"); + requireObject(define, path + ".define"); + require(!define.empty(), path + ".define must define at least one name."); + for (auto const& [name, value]: define.items()) + { + require(isIdentifier(name), path + ".define." + name + " is not an identifier."); + result.definitions.emplace_back(name, expressionFromJson(value, path + ".define." + name, depth + 1)); + } + current = &requiredMember(*current, "in", path); + path += ".in"; + ++depth; + } + result.scopeTarget = std::make_shared(pointerFromJson(*current, path, depth)); + } + else if (_json.contains("template")) + { + requireOnlyMembers(_json, {"template", "yields"}, _path); + result.pointerClass = Class::TemplateReference; + result.templateName = requiredString(_json, "template", _path); + require(isIdentifier(*result.templateName), _path + ".template must be an identifier."); + if (_json.contains("yields")) + { + Json const& yields = _json.at("yields"); + requireObject(yields, _path + ".yields"); + for (auto const& [producedName, newName]: yields.items()) + { + require( + isIdentifier(producedName) && newName.is_string() && isIdentifier(newName.get()), + _path + ".yields." + producedName + " must map an identifier to an identifier." + ); + result.yields.emplace_back(producedName, newName.get()); + } + } + } + else if (_json.contains("templates")) + { + requireOnlyMembers(_json, {"templates", "in"}, _path); + result.pointerClass = Class::Templates; + Json const& templates = _json.at("templates"); + requireObject(templates, _path + ".templates"); + for (auto const& [templateName, definition]: templates.items()) + { + require(isIdentifier(templateName), _path + ".templates." + templateName + " is not an identifier."); + result.templates.emplace_back( + templateName, + std::make_shared( + templateFromJson(definition, _path + ".templates." + templateName, _depth + 1) + ) + ); + } + result.scopeTarget = std::make_shared( + pointerFromJson(requiredMember(_json, "in", _path), _path + ".in", _depth + 1) + ); + } + else + solThrow( + SemanticDebugDataSerializationError, + _path + " is not a pointer: expected one of location, group, list, if, define, template or templates." + ); + return result; +} + +SemanticDebugPointer templateFromJson(Json const& _json, std::string const& _path, size_t _depth) +{ + requireOnlyMembers(_json, {"expect", "for"}, _path); + Json const& expect = requiredMember(_json, "expect", _path); + requireArray(expect, _path + ".expect"); + // Templates occur only in the table and in local templates blocks; a + // template in a pointer position, including this one's body, is rejected. + SemanticDebugPointer result = pointerFromJson(requiredMember(_json, "for", _path), _path + ".for", _depth + 1); + for (size_t index = 0; index < expect.size(); ++index) + { + std::string const parameterPath = _path + ".expect[" + std::to_string(index) + "]"; + require(expect.at(index).is_string(), parameterPath + " must be a string."); + result.expectedParameters.emplace_back(expect.at(index).get()); + require(isIdentifier(result.expectedParameters.back()), parameterPath + " must be an identifier."); + } + return result; +} + +/// A materialized variable has a pointer; a computed or optimized-out +/// variable does not. +void requirePhaseConsistency(std::optional _phase, bool _hasPointer, std::string const& _path) +{ + if (!_phase) + return; + if (*_phase == Phase::Materialized) + require(_hasPointer, _path + ": a materialized variable must have a pointer."); + else + require(!_hasPointer, _path + ": a " + enumToString(*_phase, phaseNames) + " variable must not have a pointer."); +} + +Json variableToJson(SemanticDebugVariable const& _variable) +{ + require(!_variable.identifier || !_variable.identifier->empty(), "Variable identifier must not be empty."); + requirePhaseConsistency(_variable.phase, _variable.pointer.has_value(), "variable"); + Json result = Json::object(); + setOptional(result, "identifier", _variable.identifier); + setOptional(result, "declarationASTID", _variable.declarationASTID); + if (_variable.declarationSourceRange) + result["declarationSourceRange"] = sourceRangeToJson(*_variable.declarationSourceRange); + setOptional(result, "typeID", _variable.typeID); + result["phase"] = enumToString(_variable.phase, phaseNames); + if (_variable.pointer) + result["pointer"] = semanticDebugPointerToJson(*_variable.pointer); + return result; +} + +SemanticDebugVariable variableFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers( + _json, {"identifier", "declarationASTID", "declarationSourceRange", "typeID", "phase", "pointer"}, _path + ); + SemanticDebugVariable result; + result.identifier = optionalString(_json, "identifier", _path); + require(!result.identifier || !result.identifier->empty(), _path + ".identifier must not be empty."); + result.declarationASTID = optionalInteger(_json, "declarationASTID", _path); + if (_json.contains("declarationSourceRange")) + result.declarationSourceRange = sourceRangeFromJson(_json.at("declarationSourceRange"), _path + ".declarationSourceRange"); + result.typeID = optionalString(_json, "typeID", _path); + result.phase = enumFromString(requiredString(_json, "phase", _path), phaseNames, _path + ".phase"); + if (_json.contains("pointer")) + result.pointer = pointerFromJson(_json.at("pointer"), _path + ".pointer", 0); + requirePhaseConsistency(result.phase, result.pointer.has_value(), _path); + return result; +} + +Json variableUpdateToJson(SemanticDebugVariableUpdate const& _update) +{ + require(_update.phase.has_value() || _update.pointer.has_value(), "A variable update must change the phase or the pointer."); + requirePhaseConsistency(_update.phase, _update.pointer.has_value(), "variable update"); + Json result{{"variableASTID", _update.variableASTID}}; + if (_update.phase) + result["phase"] = enumToString(*_update.phase, phaseNames); + if (_update.pointer) + result["pointer"] = semanticDebugPointerToJson(*_update.pointer); + return result; +} + +SemanticDebugVariableUpdate variableUpdateFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"variableASTID", "phase", "pointer"}, _path); + SemanticDebugVariableUpdate result; + result.variableASTID = requiredInteger(_json, "variableASTID", _path); + if (_json.contains("phase")) + result.phase = enumFromString(requiredString(_json, "phase", _path), phaseNames, _path + ".phase"); + if (_json.contains("pointer")) + result.pointer = pointerFromJson(_json.at("pointer"), _path + ".pointer", 0); + require(result.phase.has_value() || result.pointer.has_value(), _path + " must change the phase or the pointer."); + requirePhaseConsistency(result.phase, result.pointer.has_value(), _path); + return result; +} + +Json scopeToJson(SemanticDebugScope const& _scope) +{ + Json result = Json::object(); + result["variableDefinitions"] = Json::array(); + for (auto const& variable: _scope.variableDefinitions) + result["variableDefinitions"].emplace_back(variableToJson(variable)); + // Omitted while empty, which is all code generation produces: only + // optimizer passes emit updates. + if (!_scope.variableUpdates.empty()) + { + result["variableUpdates"] = Json::array(); + for (auto const& update: _scope.variableUpdates) + result["variableUpdates"].emplace_back(variableUpdateToJson(update)); + } + return result; +} + +SemanticDebugScope scopeFromJson(Json const& _json, std::string const& _path) +{ + requireOnlyMembers(_json, {"variableDefinitions", "variableUpdates"}, _path); + SemanticDebugScope result; + Json const& variables = requiredMember(_json, "variableDefinitions", _path); + requireArray(variables, _path + ".variableDefinitions"); + for (size_t index = 0; index < variables.size(); ++index) + result.variableDefinitions.emplace_back( + variableFromJson(variables.at(index), _path + ".variableDefinitions[" + std::to_string(index) + "]") + ); + if (_json.contains("variableUpdates")) + { + Json const& updates = _json.at("variableUpdates"); + requireArray(updates, _path + ".variableUpdates"); + for (size_t index = 0; index < updates.size(); ++index) + result.variableUpdates.emplace_back( + variableUpdateFromJson(updates.at(index), _path + ".variableUpdates[" + std::to_string(index) + "]") + ); + } + return result; +} + +/// The keys of the two-level scope map are the canonical decimal forms of the +/// AST ID and the instance: an optional sign for the AST ID, digits without +/// leading zeros, within the 64-bit range. +int64_t decimalKey(std::string const& _text, std::string const& _path) +{ + std::string_view digits = _text; + bool const negative = !digits.empty() && digits.front() == '-'; + if (negative) + digits.remove_prefix(1); + bool canonical = !digits.empty() && (digits == "0" ? !negative : digits.front() != '0'); + for (char const digit: digits) + if (!std::isdigit(static_cast(digit))) + canonical = false; + require(canonical, _path + " key \"" + _text + "\" is not a decimal integer."); + + int64_t value = 0; + auto const [end, error] = std::from_chars(_text.data(), _text.data() + _text.size(), value); + require(error == std::errc{} && end == _text.data() + _text.size(), _path + " key \"" + _text + "\" is out of range."); + return value; +} + +int64_t astIDKey(std::string const& _text, std::string const& _path) +{ + return decimalKey(_text, _path); +} + +int64_t instanceKey(std::string const& _text, std::string const& _path) +{ + int64_t const instance = decimalKey(_text, _path); + require(instance >= 0, _path + " key \"" + _text + "\" must not be negative."); + return instance; +} + +/// A template reference must resolve against a local templates block in the +/// same pointer document or against the sidecar's pointer template table. +void requireResolvableTemplateReferences( + SemanticDebugPointer const& _pointer, + std::set const& _localTemplates, + SemanticDebugDataTable const& _table, + std::string const& _path +) +{ + using Class = SemanticDebugPointer::Class; + auto const recurse = [&](std::shared_ptr const& _sub, std::string const& _subPath) { + if (_sub) + requireResolvableTemplateReferences(*_sub, _localTemplates, _table, _subPath); + }; + + switch (_pointer.pointerClass) + { + case Class::TemplateReference: + require( + _pointer.templateName.has_value() && + (_localTemplates.count(*_pointer.templateName) || _table.findPointerTemplate(*_pointer.templateName)), + _path + " references unknown pointer template \"" + _pointer.templateName.value_or("") + "\"." + ); + return; + case Class::Templates: + { + std::set localTemplates = _localTemplates; + for (auto const& [templateName, definition]: _pointer.templates) + { + recurse(definition, _path + ".templates." + templateName); + localTemplates.insert(templateName); + } + if (_pointer.scopeTarget) + requireResolvableTemplateReferences(*_pointer.scopeTarget, localTemplates, _table, _path + ".in"); + return; + } + case Class::Group: + for (size_t index = 0; index < _pointer.group.size(); ++index) + requireResolvableTemplateReferences( + _pointer.group[index], _localTemplates, _table, _path + ".group[" + std::to_string(index) + "]" + ); + return; + case Class::List: + recurse(_pointer.listElement, _path + ".list.is"); + return; + case Class::Conditional: + recurse(_pointer.thenPointer, _path + ".then"); + recurse(_pointer.elsePointer, _path + ".else"); + return; + case Class::Scope: + recurse(_pointer.scopeTarget, _path + ".in"); + return; + case Class::Region: + case Class::Unknown: + return; + } +} + +void requireResolvableTemplateReferences(SemanticDebugDataTable const& _table) +{ + for (auto const& [templateName, definition]: _table.pointerTemplates()) + requireResolvableTemplateReferences(definition, {}, _table, "sidecar.resources.pointers." + templateName); + for (auto const& [key, scope]: _table.scopes()) + { + if (!scope) + continue; + std::string const path = "sidecar.scopes." + std::to_string(key.first) + "." + std::to_string(key.second); + for (size_t index = 0; index < scope->variableDefinitions.size(); ++index) + if (auto const& pointer = scope->variableDefinitions[index].pointer) + requireResolvableTemplateReferences( + *pointer, {}, _table, path + ".variableDefinitions[" + std::to_string(index) + "].pointer" + ); + for (size_t index = 0; index < scope->variableUpdates.size(); ++index) + if (auto const& pointer = scope->variableUpdates[index].pointer) + requireResolvableTemplateReferences( + *pointer, {}, _table, path + ".variableUpdates[" + std::to_string(index) + "].pointer" + ); + } +} + +} // namespace + +Json langutil::semanticDebugPointerToJson(SemanticDebugPointer const& _pointer) +{ + if (_pointer.expectedParameters.empty()) + return pointerBodyToJson(_pointer); + return semanticDebugPointerTemplateToJson(_pointer); +} + +Json langutil::semanticDebugPointerTemplateToJson(SemanticDebugPointer const& _template) +{ + Json expect = Json::array(); + for (std::string const& parameter: _template.expectedParameters) + { + require(isIdentifier(parameter), "A template parameter must be an identifier."); + expect.emplace_back(parameter); + } + return Json{{"expect", std::move(expect)}, {"for", pointerBodyToJson(_template)}}; +} + +Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) +{ + Json result{ + {"format", std::string(SemanticDebugDataFormat)}, + {"version", SemanticDebugDataFormatVersion} + }; + setOptional(result, "contractName", _table.contractName()); + + // The resource tables mirror the public ethdebug.resources object: type + // documents are written verbatim, they already are the public type + // schema, and pointer templates in the schema's template form. + if (!_table.types().empty() || !_table.pointerTemplates().empty()) + { + Json types = Json::object(); + for (auto const& [id, document]: _table.types()) + types[id] = document; + Json pointers = Json::object(); + for (auto const& [name, definition]: _table.pointerTemplates()) + pointers[name] = semanticDebugPointerTemplateToJson(definition); + result["resources"] = Json{{"types", std::move(types)}, {"pointers", std::move(pointers)}}; + } + + // Scope records form a two-level map from the decimal AST ID to the + // decimal instance, with instance 0 written explicitly. The JSON library + // orders object members by key, which keeps the output deterministic. + Json scopes = Json::object(); + for (auto const& [key, scope]: _table.scopes()) + { + require(scope != nullptr, "Semantic debug data table contains a null scope record."); + scopes[std::to_string(key.first)][std::to_string(key.second)] = scopeToJson(*scope); + } + result["scopes"] = std::move(scopes); + return result; +} + +SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) +{ + std::string const root = "sidecar"; + requireOnlyMembers(_json, {"format", "version", "contractName", "resources", "scopes"}, root); + std::string const format = requiredString(_json, "format", root); + require(format == SemanticDebugDataFormat, "Unsupported semantic debug data format \"" + format + "\"."); + unsigned const version = requiredInteger(_json, "version", root); + require(version == SemanticDebugDataFormatVersion, "Unsupported semantic debug data format version " + std::to_string(version) + "."); + + SemanticDebugDataTable result; + if (std::optional contractName = optionalString(_json, "contractName", root)) + result.setContractName(std::move(*contractName)); + + if (_json.contains("resources")) + { + Json const& resources = _json.at("resources"); + requireOnlyMembers(resources, {"types", "pointers"}, root + ".resources"); + if (resources.contains("types")) + { + Json const& types = resources.at("types"); + requireObject(types, root + ".resources.types"); + for (auto const& [typeID, document]: types.items()) + { + requireObject(document, root + ".resources.types." + typeID); + result.setType(typeID, document); + } + } + if (resources.contains("pointers")) + { + Json const& pointers = resources.at("pointers"); + requireObject(pointers, root + ".resources.pointers"); + for (auto const& [name, definition]: pointers.items()) + { + std::string const path = root + ".resources.pointers." + name; + require(isIdentifier(name), path + " is not an identifier."); + SemanticDebugPointer pointerTemplate = templateFromJson(definition, path, 0); + // Pointer templates are public resources, published verbatim: + // an expression internal to the compiler has no place in them. + require( + !semanticDebugPointerHasInternalExpression(pointerTemplate), + path + " must not contain an internal expression." + ); + result.setPointerTemplate(name, std::move(pointerTemplate)); + } + } + } + + Json const& scopes = requiredMember(_json, "scopes", root); + requireObject(scopes, root + ".scopes"); + for (auto const& [astIDText, instances]: scopes.items()) + { + std::string const astIDPath = root + ".scopes." + astIDText; + int64_t const astID = astIDKey(astIDText, root + ".scopes"); + requireObject(instances, astIDPath); + for (auto const& [instanceText, scope]: instances.items()) + result.set( + SemanticDebugDataTable::Key{astID, instanceKey(instanceText, astIDPath)}, + std::make_shared(scopeFromJson(scope, astIDPath + "." + instanceText)) + ); + } + + requireResolvableTemplateReferences(result); + return result; +} diff --git a/liblangutil/SemanticDebugDataSerialization.h b/liblangutil/SemanticDebugDataSerialization.h new file mode 100644 index 000000000000..42128d83f30e --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.h @@ -0,0 +1,66 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * JSON serialization of the semantic debug info side table: the ethdebug + * sidecar that crosses a textual Yul boundary. + * + * The envelope and the variable records are compiler-specific and versioned. + * The type and pointer documents inside them follow the public ethdebug + * schemas, with the single internal `$$yulLocal` expression for a generated + * Yul local whose stack depth is not known yet. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace solidity::langutil +{ + +inline constexpr std::string_view SemanticDebugDataFormat = "solidity-ethdebug-semantic-data"; +inline constexpr unsigned SemanticDebugDataFormatVersion = 1; + +/// Raised for a sidecar that is malformed and for a pointer that cannot be +/// represented in the ethdebug pointer schema. +struct SemanticDebugDataSerializationError: virtual util::Exception {}; + +/// Serializes the side table: the resource tables and every scope record, +/// attached to a Yul node or not. +Json semanticDebugDataToJson(SemanticDebugDataTable const& _table); + +/// Deserializes a sidecar, rejecting an unknown format, an unsupported +/// version, malformed fields and template references that resolve neither to +/// a local templates block nor to the sidecar's pointer table. +SemanticDebugDataTable semanticDebugDataFromJson(Json const& _json); + +/// Serializes @a _pointer as an ethdebug pointer document; a root pointer that +/// expects parameters takes the schema's template form. Public emission uses +/// the same writer, after checking that no internal expression remains. +Json semanticDebugPointerToJson(SemanticDebugPointer const& _pointer); + +/// Serializes @a _template as an ethdebug pointer template document, i.e. in +/// the `expect`/`for` form also when it expects no parameters. +Json semanticDebugPointerTemplateToJson(SemanticDebugPointer const& _template); + +} // namespace solidity::langutil diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h new file mode 100644 index 000000000000..b7627e716b93 --- /dev/null +++ b/liblangutil/SemanticDebugDataTable.h @@ -0,0 +1,119 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * The semantic debug info side table: scope records keyed by source AST + * origin, together with the type and pointer template resources. + */ + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +/// The semantic debug info side table: scope records keyed by source-language +/// AST origin and generated-scope instance, together with the resource tables +/// (types and pointer templates) that the records reference by ID. +class SemanticDebugDataTable +{ +public: + /// A scope record is identified by the source AST ID together with a scope + /// instance discriminator. Code generation produces instance 0 only; + /// passes that clone code give each copy its own instance, so the copies + /// do not overwrite each other. In Yul text the instance travels in the + /// `@ast-id-instance` annotation next to `@ast-id`. + using Key = std::pair; + + void setContractName(std::string _contractName) { m_contractName = std::move(_contractName); } + std::optional const& contractName() const { return m_contractName; } + + void set(Key _key, SemanticDebugScope::ConstPtr _scope) { m_scopes[_key] = std::move(_scope); } + void set(int64_t _astID, SemanticDebugScope::ConstPtr _scope) { set(Key{_astID, 0}, std::move(_scope)); } + + SemanticDebugScope::ConstPtr find(Key _key) const + { + auto const it = m_scopes.find(_key); + return it == m_scopes.end() ? nullptr : it->second; + } + SemanticDebugScope::ConstPtr find(int64_t _astID, int64_t _instance = 0) const + { + return find(Key{_astID, _instance}); + } + + std::map const& scopes() const { return m_scopes; } + + /// Type documents in the shape of the public ethdebug type schema, keyed + /// by the compiler's type identifier. This is the only type store: scope + /// records reference into it by ID, and both the sidecar's resources and + /// the public ethdebug.resources.types output are this map, verbatim. + void setType(std::string _id, Json _document) { m_types[std::move(_id)] = std::move(_document); } + Json const* findType(std::string const& _id) const + { + auto const it = m_types.find(_id); + return it == m_types.end() ? nullptr : &it->second; + } + std::map const& types() const { return m_types; } + + /// Pointer templates keyed by producer-defined name, mirroring the public + /// ethdebug.resources.pointers table. Variables reference an entry with the + /// pointer schema's `template` reference, the same way they reference types. + void setPointerTemplate(std::string _name, SemanticDebugPointer _template) + { + m_pointerTemplates[std::move(_name)] = std::move(_template); + } + SemanticDebugPointer const* findPointerTemplate(std::string const& _name) const + { + auto const it = m_pointerTemplates.find(_name); + return it == m_pointerTemplates.end() ? nullptr : &it->second; + } + std::map const& pointerTemplates() const { return m_pointerTemplates; } + + /// Adds the content of @a _other: its contract name if it has one, and its + /// scope records, types and pointer templates, replacing entries with the + /// same key. + void merge(SemanticDebugDataTable const& _other) + { + if (_other.m_contractName) + m_contractName = _other.m_contractName; + for (auto const& [key, scope]: _other.m_scopes) + m_scopes[key] = scope; + for (auto const& [id, document]: _other.m_types) + m_types[id] = document; + for (auto const& [name, definition]: _other.m_pointerTemplates) + m_pointerTemplates[name] = definition; + } + + bool empty() const { return m_scopes.empty() && m_types.empty() && m_pointerTemplates.empty(); } + +private: + std::optional m_contractName; + std::map m_scopes; + std::map m_types; + std::map m_pointerTemplates; +}; + +} // namespace solidity::langutil diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 49bdb3d0932a..a8ae465b18ed 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -101,6 +101,8 @@ set(sources codegen/ir/IRLValue.h codegen/ir/IRVariable.cpp codegen/ir/IRVariable.h + codegen/ir/SemanticDebugDataBuilder.cpp + codegen/ir/SemanticDebugDataBuilder.h formal/ArraySlicePredicate.cpp formal/ArraySlicePredicate.h formal/BMC.cpp @@ -144,6 +146,8 @@ set(sources interface/CompilerStack.cpp interface/CompilerStack.h interface/DebugSettings.h + interface/Ethdebug.cpp + interface/Ethdebug.h interface/FileReader.cpp interface/FileReader.h interface/ImportRemapper.cpp diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp new file mode 100644 index 000000000000..03ea100a8256 --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.cpp @@ -0,0 +1,874 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::frontend; +using namespace solidity::langutil; + +namespace +{ + +using PointerExpression = SemanticDebugPointerExpression; + +/// Type documents under construction, together with the compilation's source +/// indices: declaration ranges are published with the same numeric source IDs +/// the ethdebug.compilation record uses, never with paths. +struct TypeRegistry +{ + std::map documents; + std::map const* sourceIndices = nullptr; +}; + +std::optional sourceID(TypeRegistry const& _types, SourceLocation const& _location) +{ + if (!_types.sourceIndices || !_location.sourceName) + return std::nullopt; + auto const it = _types.sourceIndices->find(*_location.sourceName); + if (it == _types.sourceIndices->end()) + return std::nullopt; + return it->second; +} + +/// An ethdebug identifier must not start with `$`, which a Solidity identifier +/// may. Every name the producer publishes passes through here. +std::string identifier(std::string const& _name) +{ + solAssert(!_name.empty()); + return _name.front() == '$' ? "_" + _name : _name; +} + +/// Registers the ethdebug/format/type document for @a _type - and every type +/// it composes - into @a _types under the compiler's type identifiers. There +/// is no intermediate representation: what this builds is what the sidecar +/// serializes and what ethdebug.resources.types publishes. Wrappers reference +/// composed types by `{"id": ...}`, and the placeholder inserted before +/// descending is what terminates recursive types. +/// @returns false for a type the schema has no document for, such as a +/// literal or a magic type; such a type composes into no document either. +bool registerTypeDocument(TypeRegistry& _types, Type const& _type); + +std::optional typeWrapper(TypeRegistry& _types, std::optional _name, Type const& _type) +{ + if (!registerTypeDocument(_types, _type)) + return std::nullopt; + Json wrapper = Json::object(); + if (_name) + wrapper["name"] = std::move(*_name); + wrapper["type"] = Json{{"id", _type.identifier()}}; + return wrapper; +} + +/// The wrappers of @a _types in order, or nothing if any of them has no document. +std::optional typeWrappers(TypeRegistry& _types, std::vector const& _componentTypes) +{ + Json wrappers = Json::array(); + for (Type const* componentType: _componentTypes) + { + if (!componentType) + continue; + std::optional wrapper = typeWrapper(_types, std::nullopt, *componentType); + if (!wrapper) + return std::nullopt; + wrappers.emplace_back(std::move(*wrapper)); + } + return wrappers; +} + +Json typeDefinition(TypeRegistry const& _types, Declaration const& _declaration) +{ + Json definition = Json::object(); + if (!_declaration.name().empty()) + definition["name"] = _declaration.name(); + SourceLocation const& location = _declaration.location(); + if (std::optional source = sourceID(_types, location); source && location.hasText()) + definition["location"] = Json{ + {"range", Json{{"length", location.end - location.start}, {"offset", location.start}}}, + {"source", Json{{"id", *source}}} + }; + return definition; +} + +std::optional typeDocument(TypeRegistry& _types, Type const& _type) +{ + Json result = Json::object(); + auto attachDefinition = [&](Declaration const& _declaration) { + Json definition = typeDefinition(_types, _declaration); + if (!definition.empty()) + result["definition"] = std::move(definition); + }; + auto contains = [&](Type const& _componentType) -> bool { + std::optional wrapper = typeWrapper(_types, std::nullopt, _componentType); + if (wrapper) + result["contains"] = std::move(*wrapper); + return wrapper.has_value(); + }; + + switch (_type.category()) + { + case Type::Category::Address: + { + auto const& addressType = dynamic_cast(_type); + result["kind"] = "address"; + result["payable"] = addressType.stateMutability() == StateMutability::Payable; + break; + } + case Type::Category::Integer: + { + auto const& integerType = dynamic_cast(_type); + result["kind"] = integerType.isSigned() ? "int" : "uint"; + result["bits"] = integerType.numBits(); + break; + } + case Type::Category::FixedPoint: + { + auto const& fixedPointType = dynamic_cast(_type); + result["kind"] = fixedPointType.isSigned() ? "fixed" : "ufixed"; + result["bits"] = fixedPointType.numBits(); + result["places"] = fixedPointType.fractionalDigits(); + break; + } + case Type::Category::Bool: + result["kind"] = "bool"; + break; + case Type::Category::FixedBytes: + { + auto const& bytesType = dynamic_cast(_type); + result["kind"] = "bytes"; + result["size"] = bytesType.numBytes(); + break; + } + case Type::Category::Array: + { + auto const& arrayType = dynamic_cast(_type); + if (arrayType.isByteArrayOrString()) + { + result["kind"] = arrayType.isString() ? "string" : "bytes"; + break; + } + result["kind"] = "array"; + if (!arrayType.isDynamicallySized()) + result["count"] = toCompactHexWithPrefix(arrayType.length()); + if (!contains(*arrayType.baseType())) + return std::nullopt; + break; + } + case Type::Category::ArraySlice: + { + // A slice's representation is the dynamic array it views, and no + // consumer distinguishes the two, so no separate kind is carried. + auto const& sliceType = dynamic_cast(_type); + result["kind"] = "array"; + if (!contains(*sliceType.arrayType().baseType())) + return std::nullopt; + break; + } + case Type::Category::Contract: + { + auto const& contractType = dynamic_cast(_type); + result["kind"] = "contract"; + result["payable"] = contractType.isPayable(); + if (contractType.contractDefinition().isLibrary()) + result["library"] = true; + else if (contractType.contractDefinition().isInterface()) + result["interface"] = true; + attachDefinition(contractType.contractDefinition()); + break; + } + case Type::Category::Struct: + { + auto const& structType = dynamic_cast(_type); + Json members = Json::array(); + for (ASTPointer const& member: structType.structDefinition().members()) + { + if (!member->annotation().type) + continue; + std::optional wrapper = typeWrapper(_types, member->name(), *member->annotation().type); + if (!wrapper) + return std::nullopt; + members.emplace_back(std::move(*wrapper)); + } + result["kind"] = "struct"; + result["contains"] = std::move(members); + attachDefinition(structType.structDefinition()); + break; + } + case Type::Category::Enum: + { + auto const& enumType = dynamic_cast(_type); + Json values = Json::array(); + for (ASTPointer const& member: enumType.enumDefinition().members()) + values.emplace_back(member->name()); + result["kind"] = "enum"; + result["values"] = std::move(values); + attachDefinition(enumType.enumDefinition()); + break; + } + case Type::Category::UserDefinedValueType: + { + auto const& aliasType = dynamic_cast(_type); + result["kind"] = "alias"; + if (!contains(aliasType.underlyingType())) + return std::nullopt; + attachDefinition(aliasType.definition()); + break; + } + case Type::Category::Tuple: + { + auto const& tupleType = dynamic_cast(_type); + std::optional members = typeWrappers(_types, tupleType.components()); + if (!members) + return std::nullopt; + result["kind"] = "tuple"; + result["contains"] = std::move(*members); + break; + } + case Type::Category::Mapping: + { + auto const& mappingType = dynamic_cast(_type); + std::optional key = typeWrapper(_types, std::nullopt, *mappingType.keyType()); + std::optional value = typeWrapper(_types, std::nullopt, *mappingType.valueType()); + if (!key || !value) + return std::nullopt; + result["kind"] = "mapping"; + result["contains"] = Json{{"key", std::move(*key)}, {"value", std::move(*value)}}; + break; + } + case Type::Category::Function: + { + auto const& functionType = dynamic_cast(_type); + result["kind"] = "function"; + if (functionType.kind() == FunctionType::Kind::Internal) + result["internal"] = true; + else if (functionType.kind() == FunctionType::Kind::External) + result["external"] = true; + + auto wrappedTuple = [&](std::vector const& _memberTypes) -> std::optional { + std::optional wrappers = typeWrappers(_types, _memberTypes); + if (!wrappers) + return std::nullopt; + return Json{{"type", Json{{"kind", "tuple"}, {"contains", std::move(*wrappers)}}}}; + }; + std::optional parameters = wrappedTuple(functionType.parameterTypes()); + if (!parameters) + return std::nullopt; + Json contained{{"parameters", std::move(*parameters)}}; + if (!functionType.returnParameterTypes().empty()) + { + std::optional returns = wrappedTuple(functionType.returnParameterTypes()); + if (!returns) + return std::nullopt; + contained["returns"] = std::move(*returns); + } + result["contains"] = std::move(contained); + + if (functionType.hasDeclaration()) + attachDefinition(functionType.declaration()); + break; + } + case Type::Category::RationalNumber: + case Type::Category::StringLiteral: + case Type::Category::TypeType: + case Type::Category::Modifier: + case Type::Category::Magic: + case Type::Category::Module: + case Type::Category::InaccessibleDynamic: + // Compile-time-only types have no runtime representation to describe. + return std::nullopt; + } + return result; +} + +bool registerTypeDocument(TypeRegistry& _types, Type const& _type) +{ + std::string const id = _type.identifier(); + if (_types.documents.count(id)) + return true; + // Present before descending, so a recursive type terminates. + _types.documents.emplace(id, Json::object()); + if (std::optional document = typeDocument(_types, _type)) + { + _types.documents[id] = std::move(*document); + return true; + } + _types.documents.erase(id); + return false; +} + +/// The region of one stack slot. A Yul name stands in for a stack depth that +/// is only known after the Yul-to-EVM transform; it is not a variable a +/// consumer can bind, so it is kept as its own expression kind and never +/// published as though it were a slot. +SemanticDebugPointer stackRegionPointer(std::string _name, std::string const& _yulVariable) +{ + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + std::move(_name), + PointerExpression::yulLocal(_yulVariable) + ); +} + +std::optional sourceIdentifier(VariableDeclaration const& _variable) +{ + return _variable.name().empty() ? std::nullopt : std::make_optional(identifier(_variable.name())); +} + +PointerExpression literalExpression(u256 const& _value) +{ + return PointerExpression::literal(toCompactHexWithPrefix(_value)); +} + +/// @returns @a _base advanced by @a _slots storage slots, folding the addition +/// into the literal when possible to keep emitted pointers readable. +PointerExpression advanceSlots(PointerExpression _base, u256 const& _slots) +{ + if (_slots == 0) + return _base; + if (_base.kind == PointerExpression::Kind::Literal && _base.value) + return literalExpression(u256(*_base.value) + _slots); + return PointerExpression::sum({std::move(_base), literalExpression(_slots)}); +} + +/// The name the state variable's pointer template is published under: the +/// location and the AST IDs of the most derived contract and the variable, so +/// that an inherited variable at a different slot in another contract of the +/// same compilation does not clash. +std::string stateVariablePointerTemplateName( + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + SemanticDebugPointer::Location _location +) +{ + solAssert( + _location == SemanticDebugPointer::Location::Storage || + _location == SemanticDebugPointer::Location::Transient + ); + std::string const prefix = _location == SemanticDebugPointer::Location::Storage ? "storage_" : "transient_"; + return prefix + std::to_string(_contract.id()) + "_" + std::to_string(_variable.id()); +} + +std::optional stackPointer(VariableDeclaration const& _variable) +{ + if (!_variable.annotation().type) + return std::nullopt; + + std::vector stackSlots = IRVariable(_variable).stackSlots(); + if (stackSlots.empty()) + return std::nullopt; + + if (stackSlots.size() == 1) + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + sourceIdentifier(_variable), + PointerExpression::yulLocal(stackSlots.front()) + ); + + SemanticDebugPointer result; + result.pointerClass = SemanticDebugPointer::Class::Group; + result.name = sourceIdentifier(_variable); + for (std::string const& stackSlot: stackSlots) + result.group.emplace_back(stackRegionPointer(stackSlot, stackSlot)); + return result; +} + +/// Builds the pointer of a state variable in storage or transient storage. One +/// builder instance describes one root pointer; mapping keys encountered +/// anywhere in it become template parameters collected in @a expectedParameters. +class StateVariablePointerBuilder +{ +public: + explicit StateVariablePointerBuilder(SemanticDebugPointer::Location _location): + m_location(_location) + {} + + SemanticDebugPointer build( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + if (auto const* mappingType = dynamic_cast(&_type)) + return buildMapping(*mappingType, std::move(_slot), _name); + + if (auto const* arrayType = dynamic_cast(&_type)) + { + if (arrayType->isByteArrayOrString()) + return buildBytesOrString(std::move(_slot), _name); + if (arrayType->isDynamicallySized()) + return buildDynamicArray(*arrayType, std::move(_slot), _name); + return buildStaticArray(*arrayType, std::move(_slot), _name); + } + + if (auto const* structType = dynamic_cast(&_type)) + return buildStruct(*structType, std::move(_slot), _name); + + return wholeRegion(_type, std::move(_slot), std::move(_offset), _name); + } + + std::vector takeExpectedParameters() + { + return std::move(m_expectedParameters); + } + +private: + /// A single region covering the value as laid out from its base slot. Used + /// for value types and as the fallback for compositions that are not (or + /// cannot be) decomposed further. + SemanticDebugPointer wholeRegion( + Type const& _type, + PointerExpression _slot, + std::optional _offset, + std::string const& _name + ) + { + u256 const byteLength = u256(_type.storageBytes()) * _type.storageSize(); + std::optional length; + if (_offset.has_value() || byteLength != 32) + length = literalExpression(byteLength); + return SemanticDebugPointer::region( + m_location, + _name, + std::move(_slot), + std::move(_offset), + std::move(length) + ); + } + + /// The mapping value lives at `keccak256(pad(key) . slot)`. The key is not + /// stored anywhere; it becomes a template parameter the debugger must bind. + SemanticDebugPointer buildMapping( + MappingType const& _mappingType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const keyParameter = m_expectedParameters.empty() + ? "key" + : "key" + std::to_string(m_expectedParameters.size()); + m_expectedParameters.emplace_back(keyParameter); + + PointerExpression keyExpression = PointerExpression::variable(keyParameter); + // Value-type keys are hashed as full words; bytes and string keys are + // hashed as their raw bytes. + if (_mappingType.keyType()->isValueType()) + keyExpression = PointerExpression::wordSized(std::move(keyExpression)); + + PointerExpression valueSlot = PointerExpression::keccak256({ + std::move(keyExpression), + PointerExpression::wordSized(std::move(_slot)) + }); + return build(*_mappingType.valueType(), std::move(valueSlot), std::nullopt, _name); + } + + /// Dynamic arrays store their element count in the base slot and their data + /// starting at `keccak256(slot)`. + SemanticDebugPointer buildDynamicArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + std::string const lengthName = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthRegion = SemanticDebugPointer::region(m_location, lengthName, _slot); + SemanticDebugPointer elements = SemanticDebugPointer::scope( + {{dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})}}, + elementList( + _arrayType, + PointerExpression::variable(dataVariable), + PointerExpression::read(lengthName), + _name + ) + ); + + std::vector members; + members.emplace_back(std::move(lengthRegion)); + members.emplace_back(std::move(elements)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStaticArray( + ArrayType const& _arrayType, + PointerExpression _slot, + std::string const& _name + ) + { + return elementList(_arrayType, std::move(_slot), literalExpression(_arrayType.length()), _name); + } + + /// A list of element pointers laid out from @a _dataStart. Value-type + /// elements narrower than a word are packed multiple to a slot; everything + /// else advances in whole slots. + SemanticDebugPointer elementList( + ArrayType const& _arrayType, + PointerExpression _dataStart, + PointerExpression _count, + std::string const& _name + ) + { + Type const& elementType = *_arrayType.baseType(); + std::string const indexName = _name + "-index"; + std::string const elementName = _name + "-item"; + PointerExpression index = PointerExpression::variable(indexName); + + SemanticDebugPointer element; + if (elementType.storageBytes() < 32) + { + solAssert(elementType.isValueType(), "Only value types can be packed."); + u256 const elementBytes = elementType.storageBytes(); + u256 const elementsPerSlot = 32 / elementBytes; + element = SemanticDebugPointer::region( + m_location, + elementName, + PointerExpression::sum({ + std::move(_dataStart), + PointerExpression::quotient(index, literalExpression(elementsPerSlot)) + }), + PointerExpression::product({ + PointerExpression::remainder(index, literalExpression(elementsPerSlot)), + literalExpression(elementBytes) + }), + literalExpression(elementBytes) + ); + } + else + { + u256 const slotsPerElement = elementType.storageSize(); + PointerExpression stride = slotsPerElement == 1 + ? index + : PointerExpression::product({index, literalExpression(slotsPerElement)}); + PointerExpression elementSlot = PointerExpression::sum({std::move(_dataStart), std::move(stride)}); + element = build(elementType, std::move(elementSlot), std::nullopt, elementName); + } + + return SemanticDebugPointer::list(std::move(_count), indexName, std::move(element)); + } + + /// `bytes` and `string` use the compact encoding: short values keep their + /// data in the base slot with the doubled length in the last byte; long + /// values keep `2 * length + 1` in the base slot and their data starting at + /// `keccak256(slot)`. + SemanticDebugPointer buildBytesOrString(PointerExpression _slot, std::string const& _name) + { + std::string const lengthFlagName = _name + "-length-flag"; + std::string const longLengthName = _name + "-long-length"; + std::string const lengthVariable = _name + "-length"; + std::string const dataVariable = _name + "-data"; + + SemanticDebugPointer lengthFlagRegion = SemanticDebugPointer::region( + m_location, + lengthFlagName, + _slot, + PointerExpression::difference(PointerExpression::wordSize(), literalExpression(1)), + literalExpression(1) + ); + + SemanticDebugPointer shortValue = SemanticDebugPointer::scope( + {{lengthVariable, PointerExpression::quotient(PointerExpression::read(lengthFlagName), literalExpression(2))}}, + SemanticDebugPointer::region( + m_location, + _name, + _slot, + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + + SemanticDebugPointer longLengthRegion = SemanticDebugPointer::region(m_location, longLengthName, _slot); + SemanticDebugPointer longData = SemanticDebugPointer::scope( + { + { + lengthVariable, + PointerExpression::quotient( + PointerExpression::difference(PointerExpression::read(longLengthName), literalExpression(1)), + literalExpression(2) + ) + }, + {dataVariable, PointerExpression::keccak256({PointerExpression::wordSized(std::move(_slot))})} + }, + SemanticDebugPointer::region( + m_location, + _name, + PointerExpression::variable(dataVariable), + std::nullopt, + PointerExpression::variable(lengthVariable) + ) + ); + std::vector longMembers; + longMembers.emplace_back(std::move(longLengthRegion)); + longMembers.emplace_back(std::move(longData)); + + // The flag byte is even (2 * length) for short values and odd + // (2 * length + 1) for long ones, so `(flag + 1) % 2` selects short. + SemanticDebugPointer value = SemanticDebugPointer::conditional( + PointerExpression::remainder( + PointerExpression::sum({PointerExpression::read(lengthFlagName), literalExpression(1)}), + literalExpression(2) + ), + std::move(shortValue), + SemanticDebugPointer::makeGroup(std::move(longMembers)) + ); + + std::vector members; + members.emplace_back(std::move(lengthFlagRegion)); + members.emplace_back(std::move(value)); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + SemanticDebugPointer buildStruct( + StructType const& _structType, + PointerExpression _slot, + std::string const& _name + ) + { + // Recursive structs and pathological nesting fall back to a region + // covering the struct's slots. + if (m_depth >= maxCompositionDepth || m_structsOnPath.count(_structType.identifier())) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + + m_structsOnPath.insert(_structType.identifier()); + ++m_depth; + + std::vector members; + for (ASTPointer const& member: _structType.structDefinition().members()) + { + if (!member->annotation().type) + continue; + auto const& [slotOffset, byteOffset] = _structType.storageOffsetsOfMember(member->name()); + std::optional offset; + if (byteOffset != 0) + offset = literalExpression(byteOffset); + members.emplace_back(build( + *member->annotation().type, + advanceSlots(_slot, slotOffset), + std::move(offset), + _name + "-" + member->name() + )); + } + + --m_depth; + m_structsOnPath.erase(_structType.identifier()); + + if (members.empty()) + return wholeRegion(_structType, std::move(_slot), std::nullopt, _name); + return SemanticDebugPointer::makeGroup(std::move(members)); + } + + static constexpr unsigned maxCompositionDepth = 16; + + SemanticDebugPointer::Location m_location; + std::vector m_expectedParameters; + std::set m_structsOnPath; + unsigned m_depth = 0; +}; + +SemanticDebugPointer stateVariablePointer( + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _location +) +{ + solAssert(_variable.annotation().type, "State variable type expected."); + + StateVariablePointerBuilder builder{_location}; + std::optional offset; + if (_offset != 0) + offset = literalExpression(_offset); + SemanticDebugPointer result = builder.build( + *_variable.annotation().type, + literalExpression(_slot), + std::move(offset), + identifier(_variable.name()) + ); + result.expectedParameters = builder.takeExpectedParameters(); + return result; +} + +/// The record of @a _variable without its phase and pointer: identity, source +/// range and, when the type has a document, the type reference. +SemanticDebugVariable variableRecord(TypeRegistry& _types, VariableDeclaration const& _variable) +{ + SemanticDebugVariable result; + result.identifier = sourceIdentifier(_variable); + result.declarationASTID = _variable.id(); + result.declarationSourceRange = + SemanticDebugSourceRange::fromLocation(_variable.location(), sourceID(_types, _variable.location())); + if (_variable.annotation().type && registerTypeDocument(_types, *_variable.annotation().type)) + result.typeID = _variable.annotation().type->identifier(); + return result; +} + +/// The record of a parameter or return variable, materialized in the Yul locals +/// of its stack slots. +SemanticDebugVariable stackVariableRecord(TypeRegistry& _types, VariableDeclaration const& _variable) +{ + SemanticDebugVariable result = variableRecord(_types, _variable); + // A value without stack slots has nothing to materialize. + if (std::optional pointer = stackPointer(_variable)) + { + result.phase = SemanticDebugVariablePhase::Materialized; + result.pointer = std::move(*pointer); + } + else + result.phase = SemanticDebugVariablePhase::OptimizedOut; + return result; +} + +/// The record of a state variable. Its pointer is a reusable template published +/// in the pointer resources under a producer-defined name; the record +/// references it the same way it references its type. +SemanticDebugVariable stateVariableRecord( + TypeRegistry& _types, + SemanticDebugDataTable& _table, + ContractDefinition const& _contract, + VariableDeclaration const& _variable, + u256 const& _slot, + unsigned _offset, + SemanticDebugPointer::Location _location +) +{ + SemanticDebugVariable result = variableRecord(_types, _variable); + std::string const templateName = stateVariablePointerTemplateName(_contract, _variable, _location); + _table.setPointerTemplate(templateName, stateVariablePointer(_variable, _slot, _offset, _location)); + result.phase = SemanticDebugVariablePhase::Materialized; + result.pointer = SemanticDebugPointer::templateReference(templateName); + return result; +} + +void appendStackVariables( + TypeRegistry& _types, + std::vector& _variables, + std::vector> const& _declarations +) +{ + for (ASTPointer const& declaration: _declarations) + _variables.emplace_back(stackVariableRecord(_types, *declaration)); +} + +void setScope(SemanticDebugDataTable& _table, int64_t _astID, std::vector _variables) +{ + if (_variables.empty()) + return; + _table.set(_astID, std::make_shared(SemanticDebugScope{ + .variableDefinitions = std::move(_variables) + })); +} + +/// The scope of a function: its parameters and return variables, in that order. +void addFunction(TypeRegistry& _types, SemanticDebugDataTable& _table, FunctionDefinition const& _function) +{ + std::vector variables; + appendStackVariables(_types, variables, _function.parameters()); + appendStackVariables(_types, variables, _function.returnParameters()); + setScope(_table, _function.id(), std::move(variables)); +} + +/// The scope of a modifier: its parameters. +void addModifier(TypeRegistry& _types, SemanticDebugDataTable& _table, ModifierDefinition const& _modifier) +{ + std::vector variables; + appendStackVariables(_types, variables, _modifier.parameters()); + setScope(_table, _modifier.id(), std::move(variables)); +} + +/// The scope of the contract: its state variables in storage and in transient +/// storage, each with a pointer template in the resources. +void addStateVariables(TypeRegistry& _types, SemanticDebugDataTable& _table, ContractDefinition const& _contract) +{ + auto const* typeType = dynamic_cast(_contract.type()); + solAssert(typeType, "Contract TypeType expected."); + auto const* contractType = dynamic_cast(typeType->actualType()); + solAssert(contractType, "Contract type expected."); + + std::vector variables; + auto append = [&](DataLocation _dataLocation, SemanticDebugPointer::Location _location) { + for (auto const& [variable, slot, offset]: contractType->linearizedStateVariables(_dataLocation)) + if (!variable->name().empty()) + variables.emplace_back(stateVariableRecord(_types, _table, _contract, *variable, slot, offset, _location)); + }; + append(DataLocation::Storage, SemanticDebugPointer::Location::Storage); + append(DataLocation::Transient, SemanticDebugPointer::Location::Transient); + setScope(_table, _contract.id(), std::move(variables)); +} + +} + +SemanticDebugDataTable solidity::frontend::buildSemanticDebugDataTable( + ContractDefinition const& _contract, + std::map const& _sourceIndices +) +{ + SemanticDebugDataTable table; + table.setContractName(_contract.name()); + + TypeRegistry types{{}, &_sourceIndices}; + addStateVariables(types, table, _contract); + + // Inherited functions and modifiers are compiled into the most derived + // contract's IR with the AST IDs of their original definitions, so every + // linearized base contract contributes its scopes. + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + { + for (FunctionDefinition const* function: contract->definedFunctions()) + addFunction(types, table, *function); + for (ModifierDefinition const* modifier: contract->functionModifiers()) + addModifier(types, table, *modifier); + } + + // Free functions and internal library functions reachable through imports + // are compiled into the contract's IR as well. A library's external + // functions live in the library's own IR and are covered by its own table. + SourceUnit const& sourceUnit = _contract.sourceUnit(); + std::set sourceUnits = sourceUnit.referencedSourceUnits(true); + sourceUnits.insert(&sourceUnit); + for (SourceUnit const* unit: sourceUnits) + for (ASTPointer const& node: unit->nodes()) + { + if (auto const* freeFunction = dynamic_cast(node.get())) + addFunction(types, table, *freeFunction); + else if (auto const* library = dynamic_cast(node.get()); library && library->isLibrary()) + for (FunctionDefinition const* function: library->definedFunctions()) + if (function->visibility() <= Visibility::Internal) + addFunction(types, table, *function); + } + + for (auto& [id, document]: types.documents) + table.setType(id, std::move(document)); + + return table; +} diff --git a/libsolidity/codegen/ir/SemanticDebugDataBuilder.h b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h new file mode 100644 index 000000000000..402e9c30f600 --- /dev/null +++ b/libsolidity/codegen/ir/SemanticDebugDataBuilder.h @@ -0,0 +1,44 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Builds the semantic debug info of a contract from analysis results. + */ + +#pragma once + +#include + +#include +#include + +namespace solidity::frontend +{ + +class ContractDefinition; + +/// Builds the semantic debug info table of @a _contract from analysis results: +/// type documents, storage pointer templates and the scope records of the +/// contract's state variables, functions and modifiers. @a _sourceIndices maps +/// source unit names to the numeric source IDs of the ethdebug.compilation +/// record, which declaration ranges are published with. +langutil::SemanticDebugDataTable buildSemanticDebugDataTable( + ContractDefinition const& _contract, + std::map const& _sourceIndices +); + +} // namespace solidity::frontend diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 26aa8e865df8..30356422ab6b 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include +#include #include #include @@ -79,6 +81,7 @@ #include #include +#include #include @@ -1165,7 +1168,24 @@ Json CompilerStack::interfaceSymbols(std::string const& _contractName) const Json CompilerStack::ethdebug() const { solAssert(m_stackState >= AnalysisSuccessful, "Analysis was not successful."); - return evmasm::ethdebug::resources(ethdebugSources(), VersionString); + // The resource tables are derived from analysis results alone, so + // ethdebug.resources is available right after analysis. + ethdebug::Resources resources; + std::map const sourceIndexMap = sourceIndices(); + for (auto const& contractEntry: m_contracts) + { + Contract const& compiledContract = contractEntry.second; + if (!compiledContract.contract) + continue; + resources.merge(ethdebug::resources(buildSemanticDebugDataTable(*compiledContract.contract, sourceIndexMap))); + } + + return evmasm::ethdebug::resources( + ethdebugSources(), + VersionString, + std::move(resources.types), + std::move(resources.pointers) + ); } Json CompilerStack::ethdebugCompilation() const @@ -1208,8 +1228,16 @@ Json CompilerStack::ethdebug(Contract const& _contract, bool _runtime) const if (!assembly) return {}; - solAssert(sourceIndices().contains(_contract.contract->sourceUnitName())); - return evmasm::ethdebug::program(_contract.contract->name(), sourceIndices()[_contract.contract->sourceUnitName()], *assembly, object); + std::map const sourceIndexMap = sourceIndices(); + solAssert(sourceIndexMap.contains(_contract.contract->sourceUnitName())); + + return evmasm::ethdebug::program( + _contract.contract->name(), + sourceIndexMap.at(_contract.contract->sourceUnitName()), + *assembly, + object, + ethdebug::programContext(buildSemanticDebugDataTable(*_contract.contract, sourceIndexMap)) + ); } bytes CompilerStack::cborMetadata(std::string const& _contractName, bool _forIR) const diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp new file mode 100644 index 000000000000..387d4af5ad9e --- /dev/null +++ b/libsolidity/interface/Ethdebug.cpp @@ -0,0 +1,137 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include +#include + +#include +#include +#include +#include + +using namespace solidity; +using namespace solidity::langutil; +using namespace solidity::frontend; + +namespace schema = solidity::evmasm::ethdebug::schema; + +namespace +{ + +/// The declaration range already carries the source ID of the compilation +/// record, so it is published as is. +schema::materials::SourceRange declarationRange(SemanticDebugSourceRange const& _range) +{ + schema::materials::Reference reference; + reference.id = schema::materials::ID{static_cast(_range.sourceID)}; + reference.type = std::nullopt; + + schema::materials::SourceRange sourceRange; + sourceRange.source = std::move(reference); + sourceRange.range = schema::materials::SourceRange::Range{ + .length = schema::data::Unsigned{_range.length}, + .offset = schema::data::Unsigned{_range.offset} + }; + return sourceRange; +} + +} + +void ethdebug::Resources::merge(Resources const& _other) +{ + for (auto const& [id, document]: _other.types.items()) + types[id] = document; + for (auto const& [name, definition]: _other.pointers.items()) + pointers[name] = definition; +} + +ethdebug::Resources ethdebug::resources(SemanticDebugDataTable const& _table) +{ + Resources result; + for (auto const& [id, document]: _table.types()) + result.types[id] = document; + // Deserialization rejects templates carrying an internal expression, and + // the producer never puts one into a template, so the writer publishes + // exactly the sidecar's table. + for (auto const& [name, definition]: _table.pointerTemplates()) + result.pointers[name] = semanticDebugPointerTemplateToJson(definition); + return result; +} + +std::optional ethdebug::programContext(SemanticDebugDataTable const& _table) +{ + using Class = SemanticDebugPointer::Class; + + std::vector variables; + for (auto const& [key, scope]: _table.scopes()) + { + if (!scope) + continue; + + for (SemanticDebugVariable const& variable: scope->variableDefinitions) + { + if (variable.phase != SemanticDebugVariablePhase::Materialized || !variable.pointer) + continue; + + // A pointer that reads a generated Yul local, or a variable that + // nothing binds, is only meaningful once the Yul-to-EVM transform + // has resolved it; the program-level context carries the closed + // pointers, i.e. those of the state variables. + if (!semanticDebugPointerFreeNames(*variable.pointer, _table).empty()) + continue; + + // A template reference is resolved against the table; the template + // body is inlined, so the reference's renaming of regions does not + // apply, and the context variable's own identifier names the value. + SemanticDebugPointer const* pointer = &*variable.pointer; + if (pointer->pointerClass == Class::TemplateReference) + { + pointer = pointer->templateName ? _table.findPointerTemplate(*pointer->templateName) : nullptr; + if (!pointer) + continue; + } + + schema::program::Context::Variable contextVariable; + contextVariable.identifier = variable.identifier; + if (variable.declarationSourceRange) + contextVariable.declaration = declarationRange(*variable.declarationSourceRange); + // The type resources carry the document; the context references it. + if (variable.typeID) + contextVariable.type = Json{{"id", *variable.typeID}}; + if (pointer->expectedParameters.empty()) + { + Json lowered = semanticDebugPointerToJson(*pointer); + // The context variable's identifier already names it; a bare + // top-level region does not need an extra "name" property. + if (lowered.contains("location")) + lowered.erase("name"); + contextVariable.pointer = std::move(lowered); + } + variables.emplace_back(std::move(contextVariable)); + } + } + + if (variables.empty()) + return std::nullopt; + + schema::program::Context context; + context.variables = std::move(variables); + return context; +} diff --git a/libsolidity/interface/Ethdebug.h b/libsolidity/interface/Ethdebug.h new file mode 100644 index 000000000000..9e82a0429b1d --- /dev/null +++ b/libsolidity/interface/Ethdebug.h @@ -0,0 +1,61 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Lowers the internal semantic debug info to the public ethdebug resource + * tables and program context. + */ + +#pragma once + +#include + +#include + +#include + +#include + +namespace solidity::frontend::ethdebug +{ + +/// The public ethdebug resource tables of a side table: the type documents +/// verbatim and the pointer templates in the schema's template form, under +/// the producer-defined names the sidecar keys them by. +struct Resources +{ + Json types = Json::object(); + Json pointers = Json::object(); + + /// Adds the tables of @a _other, replacing entries with the same key. + void merge(Resources const& _other); +}; + +Resources resources(langutil::SemanticDebugDataTable const& _table); + +/// The program-level ethdebug context: every materialized variable whose +/// pointer is closed, i.e. reads neither a generated Yul local nor an unbound +/// variable, with its declaration range, its type reference and, when its +/// pointer template expects no parameters, the pointer itself. A template that +/// expects parameters, such as mapping keys, stays in the resources for the +/// consumer to instantiate, and the variable is listed without a pointer. +/// @returns nothing when no variable qualifies. +std::optional programContext( + langutil::SemanticDebugDataTable const& _table +); + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b14bccfe95a4..e0ca687263ba 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,7 @@ detect_stray_source_files("${libevmasm_sources}" "libevmasm/") set(liblangutil_sources liblangutil/CharStream.cpp + liblangutil/DebugData.cpp liblangutil/Scanner.cpp liblangutil/SourceLocation.cpp ) diff --git a/test/cmdlineTests/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/ethdebugSchemaTests/sources/a.sol b/test/ethdebugSchemaTests/sources/a.sol index feada97f0246..fe84d71eee16 100644 --- a/test/ethdebugSchemaTests/sources/a.sol +++ b/test/ethdebugSchemaTests/sources/a.sol @@ -2,6 +2,12 @@ pragma solidity >=0.0; contract A1 { + uint128 stored; + bool enabled; + mapping(address => uint256) balances; + uint256[] values; + string label; + function a(uint x) public pure { assert(x > 0); } diff --git a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py index bc176072a0fe..c57d909f79e2 100755 --- a/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py +++ b/test/ethdebugSchemaTests/test_ethdebug_schema_conformity.py @@ -104,6 +104,77 @@ def test_program_sanity(output_selection, environment, solc_output): assert all(instruction["operation"]["mnemonic"] for instruction in instructions) +def test_program_context_includes_state_variables(solc_output): + programs = { + (source_name, contract_name): program + for source_name, contract_name, program + in ethdebug_programs(solc_output, "evm.deployedBytecode.ethdebug") + } + + variables = { + variable["identifier"]: variable + for variable in programs[("a.sol", "A1")]["context"]["variables"] + } + types = solc_output["ethdebug"]["resources"]["types"] + + def referenced_type(variable): + reference = variable["type"] + assert set(reference) == {"id"} + return types[reference["id"]] + + assert referenced_type(variables["stored"]) == {"kind": "uint", "bits": 128} + assert variables["stored"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "length": "0x10", + } + assert referenced_type(variables["enabled"]) == {"kind": "bool"} + assert variables["enabled"]["pointer"] == { + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } + + # Complex type resources reference their component resources by ID. + assert referenced_type(variables["balances"]) == { + "kind": "mapping", + "contains": { + "key": {"type": {"id": "t_address"}}, + "value": {"type": {"id": "t_uint256"}}, + }, + } + # A mapping pointer expects the key as a template parameter, so it is not a + # closed expression and only appears as a template in the pointer resources. + assert "pointer" not in variables["balances"] + + assert referenced_type(variables["values"]) == { + "kind": "array", + "contains": {"type": {"id": "t_uint256"}}, + } + values_pointer = variables["values"]["pointer"] + assert values_pointer["group"][0]["name"] == "values-length" + assert values_pointer["group"][0]["location"] == "storage" + assert values_pointer["group"][0]["slot"] == "0x02" + assert values_pointer["group"][1]["define"] == { + "values-data": {"$keccak256": [{"$wordsized": "0x02"}]} + } + values_list = values_pointer["group"][1]["in"]["list"] + assert values_list["count"] == {"$read": "values-length"} + assert values_list["each"] == "values-index" + assert values_list["is"]["slot"] == {"$sum": ["values-data", "values-index"]} + + assert referenced_type(variables["label"]) == {"kind": "string"} + label_pointer = variables["label"]["pointer"] + assert label_pointer["group"][0]["name"] == "label-length-flag" + assert label_pointer["group"][0]["offset"] == {"$difference": ["$wordsize", "0x01"]} + conditional = label_pointer["group"][1] + assert "if" in conditional and "then" in conditional and "else" in conditional + + # Contracts without state variables emit no program-level context. + assert "context" not in programs[("a.sol", "A2")] + + def test_resources_match_standard_json_sources(solc_output): standard_json_sources = {source_name: source["id"] for source_name, source in solc_output["sources"].items()} ethdebug_sources = { @@ -125,9 +196,53 @@ def test_resources_include_standard_json_source_contents(standard_json_input, so assert ethdebug_sources[source_name]["language"] == "Solidity" -def test_resources_include_empty_type_and_pointer_tables(solc_output): - assert solc_output["ethdebug"]["resources"]["types"] == {} - assert solc_output["ethdebug"]["resources"]["pointers"] == {} +def test_resources_include_type_and_pointer_tables(solc_output): + types = solc_output["ethdebug"]["resources"]["types"] + assert types["t_uint256"] == { + "kind": "uint", + "bits": 256, + } + assert types["t_address"] == {"kind": "address", "payable": False} + + # Composed types reference their component types by ID into this table. + mapping_types = [entry for entry in types.values() if entry.get("kind") == "mapping"] + assert mapping_types == [{ + "kind": "mapping", + "contains": { + "key": {"type": {"id": "t_address"}}, + "value": {"type": {"id": "t_uint256"}}, + }, + }] + array_types = [entry for entry in types.values() if entry.get("kind") == "array"] + assert {"kind": "array", "contains": {"type": {"id": "t_uint256"}}} in array_types + assert {"kind": "string"} in types.values() + + pointers = solc_output["ethdebug"]["resources"]["pointers"] + pointer_targets = [pointer["for"] for pointer in pointers.values()] + assert { + "name": "stored", + "location": "storage", + "slot": "0x00", + "length": "0x10", + } in pointer_targets + assert { + "name": "enabled", + "location": "storage", + "slot": "0x00", + "offset": "0x10", + "length": "0x01", + } in pointer_targets + + # Mapping pointers are exported as templates over their expected keys. + templates_with_parameters = [pointer for pointer in pointers.values() if pointer["expect"]] + assert templates_with_parameters == [{ + "expect": ["key"], + "for": { + "name": "balances", + "location": "storage", + "slot": {"$keccak256": [{"$wordsized": "key"}, {"$wordsized": "0x01"}]}, + }, + }] def test_resources_and_compilation_share_compilation(solc_output): diff --git a/test/liblangutil/DebugData.cpp b/test/liblangutil/DebugData.cpp new file mode 100644 index 000000000000..a559b984de3b --- /dev/null +++ b/test/liblangutil/DebugData.cpp @@ -0,0 +1,539 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace solidity::langutil::test +{ + +namespace +{ + +SemanticDebugPointer storageRegion(std::string _name, std::string _slot) +{ + return SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + std::move(_name), + SemanticDebugPointerExpression::literal(std::move(_slot)) + ); +} + +} + +BOOST_AUTO_TEST_SUITE(DebugDataTest) + +BOOST_AUTO_TEST_CASE(pointer_expressions_compose) +{ + // keccak256($wordsized(key), $wordsized(0x02)) — the storage slot of a + // mapping value, parameterized by the template variable "key". + SemanticDebugPointerExpression slot = SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x02")) + }); + + BOOST_CHECK(slot.kind == SemanticDebugPointerExpression::Kind::Keccak256); + BOOST_REQUIRE_EQUAL(slot.operands.size(), 2); + BOOST_CHECK(slot.operands.at(0).kind == SemanticDebugPointerExpression::Kind::Resize); + BOOST_CHECK(!slot.operands.at(0).value); + BOOST_REQUIRE_EQUAL(slot.operands.at(0).operands.size(), 1); + BOOST_CHECK(slot.operands.at(0).operands.front().kind == SemanticDebugPointerExpression::Kind::Variable); + BOOST_REQUIRE_EQUAL(slot.operands.at(1).operands.size(), 1); + BOOST_CHECK(slot.operands.at(1).operands.front().kind == SemanticDebugPointerExpression::Kind::Literal); + BOOST_REQUIRE(slot.operands.at(1).operands.front().value); + BOOST_CHECK_EQUAL(*slot.operands.at(1).operands.front().value, "0x02"); +} + +BOOST_AUTO_TEST_CASE(pointer_collections_compose) +{ + // group [ length region; define data := keccak256($wordsized(0x00)) in + // list over $read(length) ] — the storage layout of a dynamic array. + SemanticDebugPointer element = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "values-item", + SemanticDebugPointerExpression::sum({ + SemanticDebugPointerExpression::variable("values-data"), + SemanticDebugPointerExpression::variable("values-index") + }) + ); + SemanticDebugPointer pointer = SemanticDebugPointer::makeGroup({ + storageRegion("values-length", "0x00"), + SemanticDebugPointer::scope( + {{"values-data", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x00")) + })}}, + SemanticDebugPointer::list( + SemanticDebugPointerExpression::read("values-length"), + "values-index", + std::move(element) + ) + ) + }); + + BOOST_CHECK(pointer.pointerClass == SemanticDebugPointer::Class::Group); + BOOST_REQUIRE_EQUAL(pointer.group.size(), 2); + BOOST_CHECK(pointer.group.at(0).pointerClass == SemanticDebugPointer::Class::Region); + BOOST_CHECK(pointer.group.at(1).pointerClass == SemanticDebugPointer::Class::Scope); + BOOST_REQUIRE_EQUAL(pointer.group.at(1).definitions.size(), 1); + BOOST_CHECK_EQUAL(pointer.group.at(1).definitions.front().first, "values-data"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget); + BOOST_CHECK(pointer.group.at(1).scopeTarget->pointerClass == SemanticDebugPointer::Class::List); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->indexName); + BOOST_CHECK_EQUAL(*pointer.group.at(1).scopeTarget->indexName, "values-index"); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->count); + BOOST_CHECK(pointer.group.at(1).scopeTarget->count->kind == SemanticDebugPointerExpression::Kind::Read); + BOOST_REQUIRE(pointer.group.at(1).scopeTarget->listElement); + BOOST_CHECK(pointer.group.at(1).scopeTarget->listElement->pointerClass == SemanticDebugPointer::Class::Region); +} + +BOOST_AUTO_TEST_CASE(recursive_types_reference_by_id) +{ + // struct Node { uint256 value; Node[] children; } - the table is + // normalized, so the cycle is closed by an `{"id": ...}` reference and the + // documents pass through serialization untouched. + SemanticDebugDataTable table; + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.setType("t_array$_t_struct$_Node", Json{ + {"kind", "array"}, + {"contains", Json{{"type", Json{{"id", "t_struct$_Node"}}}}}}); + table.setType("t_struct$_Node", Json{ + {"kind", "struct"}, + {"contains", Json::array({ + Json{{"name", "value"}, {"type", Json{{"id", "t_uint256"}}}}, + Json{{"name", "children"}, {"type", Json{{"id", "t_array$_t_struct$_Node"}}}}})}, + {"definition", Json{{"name", "Node"}}}}); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["resources"]["types"].size(), 3); + BOOST_CHECK_EQUAL( + serialized["resources"]["types"]["t_array$_t_struct$_Node"]["contains"]["type"]["id"].get(), + "t_struct$_Node"); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.findType("t_struct$_Node") != nullptr); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_uses_ast_id) +{ + SemanticDebugScope scope; + auto semanticDebugScope = std::make_shared(std::move(scope)); + + SemanticDebugDataTable table; + BOOST_CHECK(table.empty()); + + table.set(23, semanticDebugScope); + + BOOST_CHECK(!table.empty()); + BOOST_CHECK(table.find(23) == semanticDebugScope); + BOOST_CHECK(!table.find(24)); + BOOST_CHECK(!table.find(23, 1)); + BOOST_CHECK(table.find(SemanticDebugDataTable::Key{23, 0}) == semanticDebugScope); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_json_roundtrip) +{ + Json containerType{ + {"kind", "struct"}, + {"contains", Json::array({Json{{"name", "member"}, {"type", Json{{"id", "t_uint256"}}}}})}, + {"definition", Json{ + {"name", "Container"}, + {"location", Json{ + {"range", Json{{"length", 16}, {"offset", 4}}}, + {"source", Json{{"id", 0}}}}}}}}; + + // A mapping value template, published in the pointer resources and + // instantiated by the consumer with the key. + SemanticDebugPointer mappingValue = storageRegion("value", "0x00"); + mappingValue.slot = SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::variable("key")), + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + }); + mappingValue.expectedParameters = {"key"}; + + SemanticDebugPointer pointer = SemanticDebugPointer::scope( + {{"slot", SemanticDebugPointerExpression::keccak256({ + SemanticDebugPointerExpression::wordSized(SemanticDebugPointerExpression::literal("0x01")) + })}}, + SemanticDebugPointer::conditional( + SemanticDebugPointerExpression::read("condition"), + SemanticDebugPointer::templateReference("mapping-value", {{"value", "renamed-value"}}), + SemanticDebugPointer::region( + SemanticDebugPointer::Location::Storage, + "fallback", + SemanticDebugPointerExpression::variable("slot"), + SemanticDebugPointerExpression::literal("0x00"), + SemanticDebugPointerExpression::wordSize() + ) + ) + ); + + SemanticDebugVariable variable; + variable.identifier = "value"; + variable.declarationASTID = 9; + variable.declarationSourceRange = SemanticDebugSourceRange{0, 21, 5}; + variable.typeID = "t_struct$_Container"; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = std::move(pointer); + + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + + SemanticDebugDataTable table; + table.setContractName("ContainerContract"); + table.setType("t_struct$_Container", std::move(containerType)); + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.setPointerTemplate("mapping-value", std::move(mappingValue)); + table.set(42, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["format"].get(), SemanticDebugDataFormat); + BOOST_CHECK_EQUAL(serialized["version"].get(), SemanticDebugDataFormatVersion); + BOOST_CHECK_EQUAL(serialized["contractName"], "ContainerContract"); + // The pointer template table mirrors ethdebug.resources.pointers. + BOOST_CHECK(serialized["resources"]["pointers"]["mapping-value"]["expect"] == Json::array({"key"})); + // Field names are identical in the JSON form and the compiler structures. + Json const& serializedVariable = serialized["scopes"]["42"]["0"]["variableDefinitions"][0]; + BOOST_CHECK_EQUAL(serializedVariable["declarationASTID"].get(), 9); + BOOST_CHECK_EQUAL(serializedVariable["declarationSourceRange"]["source"]["id"].get(), 0); + BOOST_CHECK_EQUAL(serializedVariable["declarationSourceRange"]["range"]["offset"].get(), 21); + BOOST_CHECK_EQUAL(serializedVariable["typeID"].get(), "t_struct$_Container"); + BOOST_CHECK_EQUAL(serializedVariable["phase"].get(), "materialized"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_json_rejects_unknown_version_and_malformed_keys) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["version"] = SemanticDebugDataFormatVersion + 1; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); + serialized["version"] = SemanticDebugDataFormatVersion; + + Json const emptyScope{{"variableDefinitions", Json::array()}}; + for (std::string const& astIDKey: {"abc", "01", "1.5", "", "-0"}) + { + Json malformed = serialized; + malformed["scopes"][astIDKey] = Json{{"0", emptyScope}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + } + for (std::string const& instanceKey: {"-1", "x", "00"}) + { + Json malformed = serialized; + malformed["scopes"]["1"] = Json{{instanceKey, emptyScope}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + } + + // A negative AST ID is unusual but within the documented int64 range. + serialized["scopes"]["-1"] = Json{{"0", emptyScope}}; + BOOST_CHECK(semanticDebugDataFromJson(serialized).find(-1) != nullptr); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_types_are_shared_by_id) +{ + auto makeVariable = [&](std::string _name) { + SemanticDebugVariable variable; + variable.identifier = std::move(_name); + variable.typeID = "t_uint256"; + return variable; + }; + SemanticDebugScope scope; + scope.variableDefinitions = {makeVariable("a"), makeVariable("b")}; + + SemanticDebugDataTable table; + table.setType("t_uint256", Json{{"kind", "uint"}, {"bits", 256}}); + table.set(1, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK_EQUAL(serialized["resources"]["types"].size(), 1); + Json const& variables = serialized["scopes"]["1"]["0"]["variableDefinitions"]; + // Variables carry only the ID; the document lives in the table. + BOOST_CHECK(!variables[0].contains("type")); + BOOST_CHECK(!variables[1].contains("type")); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.findType("t_uint256") != nullptr); + BOOST_CHECK_EQUAL((*read.findType("t_uint256"))["bits"].get(), 256); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_updates_roundtrip) +{ + // The dbg.value analogue: a statement-level rebinding of a variable, here + // a spill to memory that keeps the phase and a drop to optimized-out. + SemanticDebugVariableUpdate spilled; + spilled.variableASTID = 42; + spilled.pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Memory, + "spill", + std::nullopt, + SemanticDebugPointerExpression::literal("0x80"), + SemanticDebugPointerExpression::wordSize()); + + SemanticDebugVariableUpdate dropped; + dropped.variableASTID = 42; + dropped.phase = SemanticDebugVariablePhase::OptimizedOut; + + SemanticDebugScope scope; + scope.variableUpdates = {spilled, dropped}; + SemanticDebugDataTable table; + table.set(7, std::make_shared(std::move(scope))); + + Json serialized = semanticDebugDataToJson(table); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + Json const& updates = serialized["scopes"]["7"]["0"]["variableUpdates"]; + BOOST_REQUIRE_EQUAL(updates.size(), 2); + // A pointer-only update leaves the phase alone. + BOOST_CHECK(!updates[0].contains("phase")); + // A change to optimized-out clears the previous pointer. + BOOST_CHECK_EQUAL(updates[1]["phase"].get(), "optimized-out"); + BOOST_CHECK(!updates[1].contains("pointer")); + + // An update that changes nothing, or claims a pointer for an unavailable + // value, is malformed. + Json malformed = serialized; + malformed["scopes"]["7"]["0"]["variableUpdates"][1]["pointer"] = updates[0]["pointer"]; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); + malformed = serialized; + malformed["scopes"]["7"]["0"]["variableUpdates"][0].erase("pointer"); + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_table_distinguishes_scope_instances) +{ + // A cloned scope keeps its AST ID and gets its own instance; the two + // records must coexist under the same AST ID and round-trip. + SemanticDebugDataTable table; + table.set({9, 0}, std::make_shared()); + table.set({9, 2}, std::make_shared()); + + Json serialized = semanticDebugDataToJson(table); + BOOST_REQUIRE_EQUAL(serialized["scopes"].size(), 1); + BOOST_CHECK_EQUAL(serialized["scopes"]["9"].size(), 2); + // Instance 0 is written explicitly. + BOOST_CHECK(serialized["scopes"]["9"].contains("0")); + BOOST_CHECK(serialized["scopes"]["9"].contains("2")); + + SemanticDebugDataTable read = semanticDebugDataFromJson(serialized); + BOOST_CHECK(read.find({9, 0}) != nullptr); + BOOST_CHECK(read.find({9, 2}) != nullptr); + BOOST_CHECK(read.find({9, 1}) == nullptr); + BOOST_CHECK(semanticDebugDataToJson(read) == serialized); +} + +BOOST_AUTO_TEST_CASE(semantic_debug_data_variable_phases_roundtrip) +{ + // A materialized variable has a pointer; the other phases do not. + SemanticDebugScope scope; + SemanticDebugVariable materialized; + materialized.phase = SemanticDebugVariablePhase::Materialized; + materialized.pointer = storageRegion("value", "0x00"); + scope.variableDefinitions.emplace_back(std::move(materialized)); + SemanticDebugVariable computed; + computed.phase = SemanticDebugVariablePhase::Computed; + scope.variableDefinitions.emplace_back(std::move(computed)); + SemanticDebugVariable optimizedOut; + optimizedOut.phase = SemanticDebugVariablePhase::OptimizedOut; + scope.variableDefinitions.emplace_back(std::move(optimizedOut)); + + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + Json const serialized = semanticDebugDataToJson(table); + Json const& variables = serialized["scopes"]["1"]["0"]["variableDefinitions"]; + BOOST_REQUIRE_EQUAL(variables.size(), 3); + BOOST_CHECK_EQUAL(variables[0]["phase"].get(), "materialized"); + BOOST_CHECK_EQUAL(variables[1]["phase"].get(), "computed"); + BOOST_CHECK_EQUAL(variables[2]["phase"].get(), "optimized-out"); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + + // The invariant is enforced in both directions. + SemanticDebugVariable inconsistent; + inconsistent.phase = SemanticDebugVariablePhase::Materialized; + SemanticDebugScope inconsistentScope; + inconsistentScope.variableDefinitions.emplace_back(std::move(inconsistent)); + SemanticDebugDataTable inconsistentTable; + inconsistentTable.set(1, std::make_shared(std::move(inconsistentScope))); + BOOST_CHECK_THROW(semanticDebugDataToJson(inconsistentTable), SemanticDebugDataSerializationError); + + Json malformed = serialized; + malformed["scopes"]["1"]["0"]["variableDefinitions"][1]["pointer"] = variables[0]["pointer"]; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(yul_locals_use_the_internal_expression_prefix) +{ + SemanticDebugVariable variable; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = SemanticDebugPointer::region( + SemanticDebugPointer::Location::Stack, + "x", + SemanticDebugPointerExpression::yulLocal("var_x_1")); + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + + Json const serialized = semanticDebugDataToJson(table); + Json const& slot = serialized["scopes"]["1"]["0"]["variableDefinitions"][0]["pointer"]["slot"]; + BOOST_CHECK((slot == Json{{"$$yulLocal", "var_x_1"}})); + SemanticDebugDataTable const read = semanticDebugDataFromJson(serialized); + BOOST_REQUIRE(read.find(1)); + BOOST_REQUIRE(read.find(1)->variableDefinitions.front().pointer->slot); + BOOST_CHECK( + read.find(1)->variableDefinitions.front().pointer->slot->kind == + SemanticDebugPointerExpression::Kind::YulLocal); + + // Any other internal expression is unknown to the reader. + Json malformed = serialized; + malformed["scopes"]["1"]["0"]["variableDefinitions"][0]["pointer"]["slot"] = Json{{"$$other", "x"}}; + BOOST_CHECK_THROW(semanticDebugDataFromJson(malformed), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(template_references_must_resolve) +{ + SemanticDebugVariable variable; + variable.phase = SemanticDebugVariablePhase::Materialized; + variable.pointer = SemanticDebugPointer::templateReference("storage_1_2"); + SemanticDebugScope scope; + scope.variableDefinitions.emplace_back(std::move(variable)); + SemanticDebugDataTable table; + table.set(1, std::make_shared(std::move(scope))); + + // Nothing defines the template: rejected on deserialization. + BOOST_CHECK_THROW(semanticDebugDataFromJson(semanticDebugDataToJson(table)), SemanticDebugDataSerializationError); + + // The sidecar's pointer table defines it. + table.setPointerTemplate("storage_1_2", storageRegion("value", "0x02")); + Json const serialized = semanticDebugDataToJson(table); + BOOST_CHECK(serialized["resources"]["pointers"]["storage_1_2"]["expect"] == Json::array()); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(serialized)) == serialized); + + // A local templates block defines it for references inside its target. + SemanticDebugPointer local = SemanticDebugPointer::withTemplates( + {{"local-slot", storageRegion("local", "0x03")}}, + SemanticDebugPointer::templateReference("local-slot")); + SemanticDebugVariable localVariable; + localVariable.phase = SemanticDebugVariablePhase::Materialized; + localVariable.pointer = std::move(local); + SemanticDebugScope localScope; + localScope.variableDefinitions.emplace_back(std::move(localVariable)); + SemanticDebugDataTable localTable; + localTable.set(2, std::make_shared(std::move(localScope))); + Json const localSerialized = semanticDebugDataToJson(localTable); + BOOST_CHECK(localSerialized["scopes"]["2"]["0"]["variableDefinitions"][0]["pointer"].contains("templates")); + BOOST_CHECK(semanticDebugDataToJson(semanticDebugDataFromJson(localSerialized)) == localSerialized); +} + +BOOST_AUTO_TEST_CASE(number_literals_deserialize_as_hex_literals) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["resources"]["pointers"]["slot-one"] = Json{ + {"expect", Json::array()}, + {"for", Json{{"location", "storage"}, {"name", "value"}, {"slot", 1}, {"offset", 0}, {"length", 32}}} + }; + SemanticDebugDataTable table = semanticDebugDataFromJson(serialized); + SemanticDebugPointer const* pointer = table.findPointerTemplate("slot-one"); + BOOST_REQUIRE(pointer); + BOOST_REQUIRE(pointer->slot && pointer->slot->value); + BOOST_CHECK_EQUAL(*pointer->slot->value, "0x01"); + // The schema allows numbers; the compiler carries and re-emits them as hex literals. + Json const reserialized = semanticDebugDataToJson(table)["resources"]["pointers"]["slot-one"]["for"]; + BOOST_CHECK_EQUAL(reserialized["slot"].get(), "0x01"); + BOOST_CHECK_EQUAL(reserialized["offset"].get(), "0x00"); + BOOST_CHECK_EQUAL(reserialized["length"].get(), "0x20"); +} + +BOOST_AUTO_TEST_CASE(pointer_resources_reject_internal_expressions) +{ + Json serialized = semanticDebugDataToJson({}); + serialized["resources"]["pointers"]["local"] = Json{ + {"expect", Json::array()}, + {"for", Json{{"location", "stack"}, {"name", "value"}, {"slot", Json{{"$$yulLocal", "var_value"}}}}} + }; + BOOST_CHECK_THROW(semanticDebugDataFromJson(serialized), SemanticDebugDataSerializationError); +} + +BOOST_AUTO_TEST_CASE(malformed_pointer_documents_are_rejected) +{ + auto sidecarWithPointer = [](Json _pointer) { + Json serialized = semanticDebugDataToJson({}); + Json const variable{{"phase", "materialized"}, {"pointer", std::move(_pointer)}}; + serialized["scopes"]["1"] = Json{{"0", Json{{"variableDefinitions", Json::array({variable})}}}}; + return serialized; + }; + auto rejects = [&](Json _pointer) { + BOOST_CHECK_THROW(semanticDebugDataFromJson(sidecarWithPointer(std::move(_pointer))), SemanticDebugDataSerializationError); + }; + + BOOST_CHECK_NO_THROW(semanticDebugDataFromJson(sidecarWithPointer(Json{{"location", "storage"}, {"slot", "0x0"}}))); + // Only the schema's locations. + rejects(Json{{"location", "unknown"}, {"slot", "0x0"}}); + // A segment location needs a slot, a slice location an offset and a length. + rejects(Json{{"location", "storage"}, {"offset", "0x0"}}); + rejects(Json{{"location", "memory"}, {"offset", "0x0"}}); + // $sized takes a positive size. + rejects(Json{{"location", "storage"}, {"slot", Json{{"$sized0", "0x0"}}}}); + // No members outside the schema. + rejects(Json{{"location", "storage"}, {"slot", "0x0"}, {"extra", true}}); + // A group has at least one member. + rejects(Json{{"group", Json::array()}}); + // A template is not a pointer; it lives in the resources or a templates block. + rejects(Json{{"expect", Json::array()}, {"for", Json{{"location", "storage"}, {"slot", "0x0"}}}}); + // Nesting is bounded. + Json nested{{"location", "storage"}, {"slot", "0x0"}}; + for (size_t depth = 0; depth < 300; ++depth) + nested = Json{{"group", Json::array({nested})}}; + rejects(nested); +} + +BOOST_AUTO_TEST_CASE(free_names_resolve_through_templates_and_terminate_on_cycles) +{ + SemanticDebugDataTable table; + table.setPointerTemplate("a", SemanticDebugPointer::templateReference("b")); + table.setPointerTemplate("b", SemanticDebugPointer::templateReference("a")); + // A cyclic template pair resolves to no names rather than recursing forever. + BOOST_CHECK(semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("a"), table).empty()); + + SemanticDebugPointer open = storageRegion("value", "0x0"); + open.slot = SemanticDebugPointerExpression::variable("key"); + table.setPointerTemplate("open", open); + BOOST_CHECK((semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("open"), table) == std::set{"key"})); + + // The expected parameters of a template are bound by its instantiation. + open.expectedParameters = {"key"}; + table.setPointerTemplate("closed", open); + BOOST_CHECK(semanticDebugPointerFreeNames(SemanticDebugPointer::templateReference("closed"), table).empty()); + + // A Yul local is a name only the Yul stage can supply. + SemanticDebugPointer local = storageRegion("value", "0x0"); + local.slot = SemanticDebugPointerExpression::yulLocal("var_value"); + BOOST_CHECK((semanticDebugPointerFreeNames(local, table) == std::set{"var_value"})); + BOOST_CHECK(semanticDebugPointerHasInternalExpression(local)); + BOOST_CHECK(!semanticDebugPointerHasInternalExpression(open)); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::langutil::test diff --git a/test/libsolidity/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