From f85fc95ea011291b9c8074b0731b9c14508bde26 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 9 Sep 2026 11:15:09 +0200 Subject: [PATCH 1/3] ethdebug: Move the ethdebug schema to liblangutil The semantic debug info side table lives in liblangutil, next to the DebugData of Yul nodes that will carry its records, and is expressed in the ethdebug type and pointer structures. Those therefore move down from libevmasm, which keeps producing the program and resources outputs from them under the same schema namespace. --- docs/compiler-guide/ethdebug.rst | 2 +- libevmasm/CMakeLists.txt | 2 -- libevmasm/Ethdebug.cpp | 2 +- libevmasm/Ethdebug.h | 5 ++++- liblangutil/CMakeLists.txt | 2 ++ {libevmasm => liblangutil}/EthdebugSchema.cpp | 4 ++-- {libevmasm => liblangutil}/EthdebugSchema.h | 2 +- libsolidity/interface/Ethdebug.cpp | 2 +- libsolidity/interface/Ethdebug.h | 6 +++--- 9 files changed, 15 insertions(+), 12 deletions(-) rename {libevmasm => liblangutil}/EthdebugSchema.cpp (99%) rename {libevmasm => liblangutil}/EthdebugSchema.h (99%) diff --git a/docs/compiler-guide/ethdebug.rst b/docs/compiler-guide/ethdebug.rst index 3fb80ac69425..2d911dcdec72 100644 --- a/docs/compiler-guide/ethdebug.rst +++ b/docs/compiler-guide/ethdebug.rst @@ -12,7 +12,7 @@ It describes how the compiler derives the :ref:`ethdebug output ` from .. note:: The debug info that is passed from the Solidity frontend through Yul is specified in :doc:`ethdebug_internal_debug_info`. -The documents are modelled as data structures in ``libevmasm/EthdebugSchema.h``, one per schema, and serialized to JSON from there. +The documents are modelled as data structures in ``liblangutil/EthdebugSchema.h``, one per schema, and serialized to JSON from there. The constructors of the parts assert the constraints of the schemas, so that the compiler cannot assemble a document that does not serialize. The frontend side, ``libsolidity/interface/Ethdebug.cpp``, builds the type documents and the pointer templates of a contract from its AST and types. diff --git a/libevmasm/CMakeLists.txt b/libevmasm/CMakeLists.txt index 6f3bcce8442d..e10f20922c2b 100644 --- a/libevmasm/CMakeLists.txt +++ b/libevmasm/CMakeLists.txt @@ -6,8 +6,6 @@ set(sources AssemblyItem.h Ethdebug.cpp Ethdebug.h - EthdebugSchema.cpp - EthdebugSchema.h EVMAssemblyStack.cpp EVMAssemblyStack.h BlockDeduplicator.cpp diff --git a/libevmasm/Ethdebug.cpp b/libevmasm/Ethdebug.cpp index 5dae0f42c9c0..aaffb7728ecc 100644 --- a/libevmasm/Ethdebug.cpp +++ b/libevmasm/Ethdebug.cpp @@ -18,7 +18,7 @@ #include -#include +#include #include diff --git a/libevmasm/Ethdebug.h b/libevmasm/Ethdebug.h index ff7873c6b130..eb67d8771206 100644 --- a/libevmasm/Ethdebug.h +++ b/libevmasm/Ethdebug.h @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include @@ -29,6 +29,9 @@ namespace solidity::evmasm::ethdebug { +// The schema structures live in liblangutil, next to the debug data that carries them. +namespace schema = langutil::ethdebug::schema; + struct Source { unsigned id; diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 054d01d91881..2e2c160fc29a 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -8,6 +8,8 @@ set(sources DebugInfoSelection.h ErrorReporter.cpp ErrorReporter.h + EthdebugSchema.cpp + EthdebugSchema.h EVMVersion.h EVMVersion.cpp Exceptions.cpp diff --git a/libevmasm/EthdebugSchema.cpp b/liblangutil/EthdebugSchema.cpp similarity index 99% rename from libevmasm/EthdebugSchema.cpp rename to liblangutil/EthdebugSchema.cpp index a38147c45326..eb278b1c29c3 100644 --- a/libevmasm/EthdebugSchema.cpp +++ b/liblangutil/EthdebugSchema.cpp @@ -16,7 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 -#include +#include #include @@ -27,7 +27,7 @@ #include using namespace solidity; -using namespace solidity::evmasm::ethdebug; +using namespace solidity::langutil::ethdebug; std::map const schema::materials::Reference::typeNames = { {Type::Compilation, "compilation"}, diff --git a/libevmasm/EthdebugSchema.h b/liblangutil/EthdebugSchema.h similarity index 99% rename from libevmasm/EthdebugSchema.h rename to liblangutil/EthdebugSchema.h index 3dcab0b20296..3ab617360f2f 100644 --- a/libevmasm/EthdebugSchema.h +++ b/liblangutil/EthdebugSchema.h @@ -31,7 +31,7 @@ #include #include -namespace solidity::evmasm::ethdebug::schema +namespace solidity::langutil::ethdebug::schema { struct EthdebugException: virtual util::Exception {}; diff --git a/libsolidity/interface/Ethdebug.cpp b/libsolidity/interface/Ethdebug.cpp index 1c97efe4ef25..0a4391e73c88 100644 --- a/libsolidity/interface/Ethdebug.cpp +++ b/libsolidity/interface/Ethdebug.cpp @@ -35,7 +35,7 @@ using namespace solidity; using namespace solidity::frontend; -namespace schema = solidity::evmasm::ethdebug::schema; +namespace schema = solidity::langutil::ethdebug::schema; namespace { diff --git a/libsolidity/interface/Ethdebug.h b/libsolidity/interface/Ethdebug.h index c4648903a4f8..402b234f7253 100644 --- a/libsolidity/interface/Ethdebug.h +++ b/libsolidity/interface/Ethdebug.h @@ -24,7 +24,7 @@ #include -#include +#include #include #include @@ -37,13 +37,13 @@ struct Resources { /// Type documents keyed by the compiler's type identifier. Composed types /// reference their component types by that identifier. - std::map types; + std::map types; /// The pointer template of every struct, array and mapping type the state /// variables have or are composed of, keyed by the compiler's type /// identifier: the layout of a value of the type from a base slot, which /// the template expects as `slot`. A mapping's template expects `key` as /// well. Value types have no template; they are single regions. - std::map pointers; + std::map pointers; /// Adds the tables of @a _other, replacing entries with the same key. void merge(Resources _other); From df33034028470746cea8a50aa1d907c247752e98 Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 9 Sep 2026 11:27:25 +0200 Subject: [PATCH 2/3] ethdebug: Read and validate the resources tables from JSON Add the reading direction for the ethdebug type and pointer structures: type documents, pointer expressions, pointers and pointer templates are parsed from JSON and checked against the schema, with every violation reported as a util::JsonValidationError whose message names the offending location. The generic part of that checking, i.e. object and array requirements, member presence, unknown members and typed member access, goes into libsolutil's JSON helpers next to isOfType(), so that the other JSON importers can use it as well. A new isoltest flavour, ethdebugDocumentTests, reads an ethdebug JSON document, validates it and writes it back, so that the expectation is either the document as the compiler writes it or the validation error. --- liblangutil/EthdebugSchema.cpp | 901 +++++++++++++++++- liblangutil/EthdebugSchema.h | 30 + libsolutil/JSON.cpp | 53 ++ libsolutil/JSON.h | 67 +- test/CMakeLists.txt | 2 + test/InteractiveTests.h | 2 + test/TestCase.cpp | 4 +- test/libevmasm/Assembler.cpp | 10 +- test/liblangutil/EthdebugDocumentTest.cpp | 83 ++ test/liblangutil/EthdebugDocumentTest.h | 62 ++ .../error_define_empty.ethdebugjson | 3 + .../error_expression_arity.ethdebugjson | 3 + ...r_expression_negative_literal.ethdebugjson | 3 + .../error_expression_sized_zero.ethdebugjson | 3 + .../error_expression_two_keys.ethdebugjson | 3 + .../error_expression_unknown_key.ethdebugjson | 3 + .../error_invalid_json.ethdebugjson | 3 + .../error_list_missing_each.ethdebugjson | 3 + .../error_pointer_empty_group.ethdebugjson | 3 + ...rror_pointer_memory_with_slot.ethdebugjson | 3 + ...r_pointer_name_not_identifier.ethdebugjson | 3 + ...r_pointer_region_without_slot.ethdebugjson | 3 + ...rror_pointer_unknown_location.ethdebugjson | 3 + ...error_pointer_unknown_variant.ethdebugjson | 3 + ...emplate_expect_not_identifier.ethdebugjson | 3 + .../error_template_missing_body.ethdebugjson | 3 + .../error_type_bits.ethdebugjson | 3 + .../error_type_class_mismatch.ethdebugjson | 3 + .../error_type_definition_empty.ethdebugjson | 3 + ...rror_type_function_visibility.ethdebugjson | 3 + ...ror_type_reference_with_extra.ethdebugjson | 3 + .../error_type_unknown_kind.ethdebugjson | 3 + .../error_type_unknown_member.ethdebugjson | 3 + .../error_unknown_member.ethdebugjson | 3 + .../resources_pointers.ethdebugjson | 247 +++++ .../resources_types.ethdebugjson | 222 +++++ test/tools/CMakeLists.txt | 1 + test/tools/isoltest.cpp | 2 +- 38 files changed, 1746 insertions(+), 12 deletions(-) create mode 100644 test/liblangutil/EthdebugDocumentTest.cpp create mode 100644 test/liblangutil/EthdebugDocumentTest.h create mode 100644 test/liblangutil/ethdebugDocumentTests/error_define_empty.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_expression_arity.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_expression_negative_literal.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_expression_sized_zero.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_expression_two_keys.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_expression_unknown_key.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_invalid_json.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_list_missing_each.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_empty_group.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_memory_with_slot.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_name_not_identifier.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_region_without_slot.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_location.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_variant.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_template_expect_not_identifier.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_template_missing_body.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_bits.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_class_mismatch.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_definition_empty.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_function_visibility.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_reference_with_extra.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_unknown_kind.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_type_unknown_member.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/error_unknown_member.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/resources_pointers.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/resources_types.ethdebugjson diff --git a/liblangutil/EthdebugSchema.cpp b/liblangutil/EthdebugSchema.cpp index eb278b1c29c3..9fe25101cc2f 100644 --- a/liblangutil/EthdebugSchema.cpp +++ b/liblangutil/EthdebugSchema.cpp @@ -20,9 +20,12 @@ #include +#include #include #include +#include + #include #include @@ -165,6 +168,12 @@ schema::Pointer::Resize::Resize(std::optional _size, std::shared_ptr _name, Location _location, @@ -502,7 +511,8 @@ void schema::to_json(Json& _json, Pointer::Expression const& _expression) _json = Json{{"~sized" + std::to_string(*_resize.size), *_resize.operand}}; else _json = Json{{"~wordsized", *_resize.operand}}; - } + }, + [&](Pointer::YulLocal const& _yulLocal) { _json = Json{{"$$yulLocal", _yulLocal.name}}; } }, _expression.value); } @@ -585,7 +595,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) { @@ -594,6 +606,13 @@ 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) + { + solAssert(!hasInternalExpression(*_contextVariable.pointer), "Context variable pointer contains a compiler-internal expression."); + _json["pointer"] = *_contextVariable.pointer; + } } void schema::program::to_json(Json& _json, Context const& _context) @@ -656,6 +675,884 @@ void schema::info::to_json(Json& _json, Resources const& _resources) for (auto const& [name, pointerTemplate]: _resources.pointers) { solAssert(Pointer::isIdentifier(name), "Pointer template name \"" + name + "\" is not an identifier."); + solAssert(!hasInternalExpression(*pointerTemplate.body), "Pointer template \"" + name + "\" contains a compiler-internal expression."); _json["pointers"][name] = pointerTemplate; } } + +// --------------------------------------------------------------------------- +// Reading + +namespace +{ + +using util::JsonValidationError; + +[[noreturn]] void invalid(std::string _message) +{ + BOOST_THROW_EXCEPTION(JsonValidationError() << util::errinfo_comment(std::move(_message))); +} + +/// @returns the enum value whose name in @a _names is @a _name, if any. +template +std::optional valueOfName(std::map const& _names, std::string_view _name) +{ + for (auto const& [value, name]: _names) + if (name == _name) + return value; + return std::nullopt; +} + +std::string member(std::string_view _path, std::string_view _name) +{ + return std::string(_path) + "." + std::string(_name); +} + +std::string element(std::string_view _path, size_t _index) +{ + return std::string(_path) + "[" + std::to_string(_index) + "]"; +} + +/// Documents nest pointers and expressions recursively; input deeper than +/// this is rejected rather than parsed with unbounded recursion. +constexpr size_t maximumNestingDepth = 256; + +void requireDepth(size_t _depth, std::string_view _path) +{ + if (_depth >= maximumNestingDepth) + invalid(std::string(_path) + " is nested too deeply."); +} + +std::string identifierFromJson(Json const& _json, std::string_view _path) +{ + std::string identifier = util::valueOfType(_json, _path); + if (!schema::Pointer::isIdentifier(identifier)) + invalid(std::string(_path) + " must be an identifier."); + return identifier; +} + +void requireIdentifierKey(std::string const& _key, std::string_view _path) +{ + if (!schema::Pointer::isIdentifier(_key)) + invalid(std::string(_path) + " has the member \"" + _key + "\", which is not an identifier."); +} + +/// A `0x`-prefixed string of at least one hex digit. +bool isHexString(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 decimal number without leading zeros. +bool isCanonicalDecimal(std::string_view _text) +{ + if (_text.empty() || (_text.size() > 1 && _text.front() == '0')) + return false; + for (char const c: _text) + if (!std::isdigit(static_cast(c))) + return false; + return true; +} + +schema::Type::Definition definitionFromJson(Json const& _json, std::string_view _path) +{ + util::requireOnlyMembers(_json, {"name", "location"}, _path); + std::optional name = util::optionalValue(_json, "name", _path); + std::optional location; + if (Json const* locationJson = util::optionalMember(_json, "location", _path)) + location = schema::materials::sourceRangeFromJson(*locationJson, member(_path, "location")); + if (!name && !location) + invalid(std::string(_path) + " must have a name or a location."); + return schema::Type::Definition{std::move(name), std::move(location)}; +} + +std::optional optionalDefinition(Json const& _json, std::string_view _path) +{ + if (Json const* definition = util::optionalMember(_json, "definition", _path)) + return definitionFromJson(*definition, member(_path, "definition")); + return std::nullopt; +} + +unsigned bitsFromJson(Json const& _json, std::string_view _path) +{ + unsigned const bits = util::requiredValue(_json, "bits", _path); + if (bits < 8 || bits > 256 || bits % 8 != 0) + invalid(member(_path, "bits") + " must be a multiple of 8 between 8 and 256."); + return bits; +} + +unsigned placesFromJson(Json const& _json, std::string_view _path) +{ + unsigned const places = util::requiredValue(_json, "places", _path); + if (places < 1 || places > 80) + invalid(member(_path, "places") + " must be between 1 and 80."); + return places; +} + +schema::Type typeFromJson(Json const& _json, std::string_view _path, size_t _depth); +schema::Type::Wrapper wrapperFromJson(Json const& _json, std::string_view _path, size_t _depth); + +std::vector wrappersFromJson(Json const& _json, std::string_view _path, size_t _depth) +{ + util::requireArray(_json, _path); + std::vector wrappers; + for (size_t index = 0; index < _json.size(); ++index) + wrappers.emplace_back(wrapperFromJson(_json.at(index), element(_path, index), _depth)); + return wrappers; +} + +schema::Type::Specifier specifierFromJson(Json const& _json, std::string_view _path, size_t _depth) +{ + util::requireObject(_json, _path); + // A specifier with an `id` is a reference; anything else must be a type. + if (_json.contains("id")) + { + util::requireOnlyMembers(_json, {"id"}, _path); + return schema::Type::Specifier{schema::Type::Reference{schema::materials::idFromJson(_json.at("id"), member(_path, "id"))}}; + } + return schema::Type::Specifier{std::make_shared(typeFromJson(_json, _path, _depth + 1))}; +} + +schema::Type::Wrapper wrapperFromJson(Json const& _json, std::string_view _path, size_t _depth) +{ + requireDepth(_depth, _path); + util::requireOnlyMembers(_json, {"name", "type"}, _path); + return schema::Type::Wrapper{ + util::optionalValue(_json, "name", _path), + specifierFromJson(util::requiredMember(_json, "type", _path), member(_path, "type"), _depth) + }; +} + +schema::Type typeFromJson(Json const& _json, std::string_view _path, size_t _depth) +{ + using Type = schema::Type; + requireDepth(_depth, _path); + util::requireObject(_json, _path); + std::string const kind = util::requiredValue(_json, "kind", _path); + auto const requireClass = [&](std::string_view _class) { + if (std::optional declared = util::optionalValue(_json, "class", _path)) + if (*declared != _class) + invalid(member(_path, "class") + " must be \"" + std::string(_class) + "\" for kind \"" + kind + "\"."); + }; + auto const elementary = [&](std::set _members) { + _members.insert("class"); + _members.insert("kind"); + util::requireOnlyMembers(_json, _members, _path); + requireClass("elementary"); + }; + auto const complex = [&](std::set _members) { + _members.insert("class"); + _members.insert("kind"); + _members.insert("contains"); + util::requireOnlyMembers(_json, _members, _path); + requireClass("complex"); + }; + auto const contains = [&]() -> Json const& { return util::requiredMember(_json, "contains", _path); }; + std::string const containsPath = member(_path, "contains"); + + if (kind == "uint") + { + elementary({"bits"}); + return Type{Type::UInt{bitsFromJson(_json, _path)}}; + } + if (kind == "int") + { + elementary({"bits"}); + return Type{Type::Int{bitsFromJson(_json, _path)}}; + } + if (kind == "bool") + { + elementary({}); + return Type{Type::Bool{}}; + } + if (kind == "bytes") + { + elementary({"size"}); + Type::Bytes bytes; + if (Json const* size = util::optionalMember(_json, "size", _path)) + bytes.size = schema::data::unsignedFromJson(*size, member(_path, "size")); + return Type{std::move(bytes)}; + } + if (kind == "string") + { + elementary({"encoding"}); + return Type{Type::String{util::optionalValue(_json, "encoding", _path)}}; + } + if (kind == "ufixed") + { + elementary({"bits", "places"}); + return Type{Type::UFixed{bitsFromJson(_json, _path), placesFromJson(_json, _path)}}; + } + if (kind == "fixed") + { + elementary({"bits", "places"}); + return Type{Type::Fixed{bitsFromJson(_json, _path), placesFromJson(_json, _path)}}; + } + if (kind == "address") + { + elementary({"payable"}); + return Type{Type::Address{util::optionalValue(_json, "payable", _path)}}; + } + if (kind == "contract") + { + elementary({"payable", "library", "interface", "definition"}); + Type::Contract contract; + contract.payable = util::optionalValue(_json, "payable", _path); + bool const library = util::optionalValue(_json, "library", _path).value_or(false); + bool const interface = util::optionalValue(_json, "interface", _path).value_or(false); + if (library && interface) + invalid(std::string(_path) + " cannot be both a library and an interface."); + contract.kind = library ? Type::Contract::Kind::Library : interface ? Type::Contract::Kind::Interface : Type::Contract::Kind::Contract; + contract.definition = optionalDefinition(_json, _path); + return Type{std::move(contract)}; + } + if (kind == "enum") + { + elementary({"values", "definition"}); + Type::Enum document; + Json const& values = util::requireArray(util::requiredMember(_json, "values", _path), member(_path, "values")); + for (size_t index = 0; index < values.size(); ++index) + document.values.emplace_back(util::valueOfType(values.at(index), element(member(_path, "values"), index))); + document.definition = optionalDefinition(_json, _path); + return Type{std::move(document)}; + } + if (kind == "alias") + { + complex({"definition"}); + return Type{Type::Alias{wrapperFromJson(contains(), containsPath, _depth + 1), optionalDefinition(_json, _path)}}; + } + if (kind == "array") + { + complex({"count"}); + Type::Array array{wrapperFromJson(contains(), containsPath, _depth + 1), std::nullopt}; + if (Json const* count = util::optionalMember(_json, "count", _path)) + array.count = schema::data::unsignedFromJson(*count, member(_path, "count")); + return Type{std::move(array)}; + } + if (kind == "mapping") + { + complex({}); + util::requireOnlyMembers(contains(), {"key", "value"}, containsPath); + return Type{Type::Mapping{ + wrapperFromJson(util::requiredMember(contains(), "key", containsPath), member(containsPath, "key"), _depth + 1), + wrapperFromJson(util::requiredMember(contains(), "value", containsPath), member(containsPath, "value"), _depth + 1) + }}; + } + if (kind == "struct") + { + complex({"definition"}); + return Type{Type::Struct{wrappersFromJson(contains(), containsPath, _depth + 1), optionalDefinition(_json, _path)}}; + } + if (kind == "tuple") + { + complex({}); + return Type{Type::Tuple{wrappersFromJson(contains(), containsPath, _depth + 1)}}; + } + if (kind == "function") + { + complex({"internal", "external", "definition"}); + bool const internal = util::optionalValue(_json, "internal", _path).value_or(false); + bool const external = util::optionalValue(_json, "external", _path).value_or(false); + if (internal == external) + invalid(std::string(_path) + " must be either internal or external."); + util::requireOnlyMembers(contains(), {"parameters", "returns"}, containsPath); + Type::Function function{ + internal ? Type::Function::Visibility::Internal : Type::Function::Visibility::External, + wrapperFromJson(util::requiredMember(contains(), "parameters", containsPath), member(containsPath, "parameters"), _depth + 1), + std::nullopt, + optionalDefinition(_json, _path) + }; + if (Json const* returns = util::optionalMember(contains(), "returns", containsPath)) + function.returns = wrapperFromJson(*returns, member(containsPath, "returns"), _depth + 1); + return Type{std::move(function)}; + } + invalid(member(_path, "kind") + " has the unknown value \"" + kind + "\"."); +} + +using ReadOptions = schema::Pointer::ReadOptions; +schema::Pointer::Expression expressionFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options); +schema::Pointer pointerFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options); +schema::Pointer::Template templateFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options); + +std::string regionReferenceFromJson(Json const& _json, std::string_view _path) +{ + std::string reference = util::valueOfType(_json, _path); + if (reference != "~this" && !schema::Pointer::isIdentifier(reference)) + invalid(std::string(_path) + " must name a region."); + return reference; +} + +schema::Pointer::Operands operandsFromJson(Json const& _json, std::string_view _path, size_t _depth, std::optional _arity, ReadOptions _options) +{ + util::requireArray(_json, _path); + if (_arity && _json.size() != *_arity) + invalid(std::string(_path) + " must have exactly " + std::to_string(*_arity) + " operands."); + schema::Pointer::Operands operands; + for (size_t index = 0; index < _json.size(); ++index) + operands.emplace_back(expressionFromJson(_json.at(index), element(_path, index), _depth + 1, _options)); + return operands; +} + +schema::Pointer::Expression expressionFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options) +{ + using Pointer = schema::Pointer; + using Expression = Pointer::Expression; + requireDepth(_depth, _path); + + if (_json.is_number_integer() || _json.is_string()) + { + if (_json.is_string()) + if (std::optional constant = valueOfName(Pointer::constantNames, _json.get())) + return Expression{*constant}; + if (_json.is_string() && schema::Pointer::isIdentifier(_json.get())) + return Expression{Pointer::Variable{_json.get()}}; + return Expression{Pointer::Literal{schema::data::unsignedFromJson(_json, _path)}}; + } + + util::requireObject(_json, _path); + if (_json.size() != 1) + invalid(std::string(_path) + " must have exactly one member."); + std::string const key = _json.begin().key(); + Json const& value = _json.begin().value(); + std::string const valuePath = member(_path, key); + + if (std::optional property = valueOfName(Pointer::Lookup::propertyNames, key)) + return Expression{Pointer::Lookup{*property, regionReferenceFromJson(value, valuePath)}}; + if (key == "~read") + return Expression{Pointer::Read{regionReferenceFromJson(value, valuePath)}}; + if (std::optional op = valueOfName(Pointer::Arithmetic::operatorNames, key)) + { + using Operator = Pointer::Arithmetic::Operator; + bool const binary = *op == Operator::Difference || *op == Operator::Quotient || *op == Operator::Remainder; + std::optional const arity = binary ? std::optional{2} : std::nullopt; + return Expression{Pointer::Arithmetic{*op, operandsFromJson(value, valuePath, _depth, arity, _options)}}; + } + if (key == "~keccak256") + return Expression{Pointer::Keccak256{operandsFromJson(value, valuePath, _depth, std::nullopt, _options)}}; + if (key == "~concat") + return Expression{Pointer::Concat{operandsFromJson(value, valuePath, _depth, std::nullopt, _options)}}; + if (key == "~wordsized") + return Expression{Pointer::Resize{std::nullopt, std::make_shared(expressionFromJson(value, valuePath, _depth + 1, _options))}}; + if (key.starts_with("~sized")) + { + std::string const width = key.substr(6); + if (!isCanonicalDecimal(width) || width == "0") + invalid(std::string(_path) + " has the malformed resize key \"" + key + "\"."); + unsigned size = 0; + try + { + size = static_cast(std::stoul(width)); + } + catch (std::out_of_range const&) + { + invalid(std::string(_path) + " has the malformed resize key \"" + key + "\"."); + } + return Expression{Pointer::Resize{size, std::make_shared(expressionFromJson(value, valuePath, _depth + 1, _options))}}; + } + if (key == "$$yulLocal" && _options.internalExpressions) + { + std::string name = util::valueOfType(value, valuePath); + if (name.empty()) + invalid(valuePath + " must name a Yul variable."); + return Expression{Pointer::YulLocal{std::move(name)}}; + } + if (key.starts_with("$$")) + invalid(std::string(_path) + " uses the compiler-internal expression key \"" + key + "\", which is not accepted here."); + invalid(std::string(_path) + " has the unknown expression key \"" + key + "\"."); +} + +std::optional optionalExpression(Json const& _json, std::string_view _name, std::string_view _path, size_t _depth, ReadOptions _options) +{ + if (Json const* value = util::optionalMember(_json, _name, _path)) + return expressionFromJson(*value, member(_path, _name), _depth + 1, _options); + return std::nullopt; +} + +schema::Pointer::Region regionFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options) +{ + using Location = schema::Pointer::Location; + util::requireOnlyMembers(_json, {"name", "location", "slot", "offset", "length"}, _path); + std::optional name; + if (Json const* nameJson = util::optionalMember(_json, "name", _path)) + name = identifierFromJson(*nameJson, member(_path, "name")); + std::string const locationName = util::requiredValue(_json, "location", _path); + std::optional const location = valueOfName(schema::Pointer::locationNames, locationName); + if (!location) + invalid(member(_path, "location") + " has the unknown value \"" + locationName + "\"."); + bool const wordOriented = *location == Location::Stack || *location == Location::Storage || *location == Location::Transient; + std::optional slot = optionalExpression(_json, "slot", _path, _depth, _options); + std::optional offset = optionalExpression(_json, "offset", _path, _depth, _options); + std::optional length = optionalExpression(_json, "length", _path, _depth, _options); + // Word-oriented locations address by slot, byte-oriented ones by offset and length. + if (wordOriented && !slot) + invalid(std::string(_path) + " must address its slot."); + if (!wordOriented && (slot || !offset || !length)) + invalid(std::string(_path) + " must address its offset and length, and has no slot."); + return schema::Pointer::Region{std::move(name), *location, std::move(slot), std::move(offset), std::move(length)}; +} + +schema::Pointer pointerFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options) +{ + using Pointer = schema::Pointer; + requireDepth(_depth, _path); + util::requireObject(_json, _path); + auto const subPointer = [&](Json const& _sub, std::string const& _subPath) { + return std::make_shared(pointerFromJson(_sub, _subPath, _depth + 1, _options)); + }; + + if (_json.contains("location")) + return Pointer{regionFromJson(_json, _path, _depth, _options)}; + if (_json.contains("group")) + { + util::requireOnlyMembers(_json, {"group"}, _path); + Json const& members = util::requireArray(_json.at("group"), member(_path, "group")); + if (members.empty()) + invalid(member(_path, "group") + " must have at least one member."); + std::vector groupMembers; + for (size_t index = 0; index < members.size(); ++index) + groupMembers.emplace_back(pointerFromJson(members.at(index), element(member(_path, "group"), index), _depth + 1, _options)); + return Pointer{Pointer::Group{std::move(groupMembers)}}; + } + if (_json.contains("list")) + { + util::requireOnlyMembers(_json, {"list"}, _path); + std::string const listPath = member(_path, "list"); + Json const& list = _json.at("list"); + util::requireOnlyMembers(list, {"count", "each", "is"}, listPath); + return Pointer{Pointer::List{ + expressionFromJson(util::requiredMember(list, "count", listPath), member(listPath, "count"), _depth + 1, _options), + identifierFromJson(util::requiredMember(list, "each", listPath), member(listPath, "each")), + subPointer(util::requiredMember(list, "is", listPath), member(listPath, "is")) + }}; + } + if (_json.contains("if")) + { + util::requireOnlyMembers(_json, {"if", "then", "else"}, _path); + Pointer::Conditional conditional{ + expressionFromJson(_json.at("if"), member(_path, "if"), _depth + 1, _options), + subPointer(util::requiredMember(_json, "then", _path), member(_path, "then")), + nullptr + }; + if (Json const* otherwise = util::optionalMember(_json, "else", _path)) + conditional.otherwise = subPointer(*otherwise, member(_path, "else")); + return Pointer{std::move(conditional)}; + } + if (_json.contains("define")) + { + // A define/in chain is folded into one ordered definition list. The + // members of one define object cannot reference each other, so their + // order within that object does not matter. + std::vector> definitions; + Json const* current = &_json; + std::string path(_path); + size_t depth = _depth; + while (current->contains("define")) + { + requireDepth(depth, path); + util::requireOnlyMembers(*current, {"define", "in"}, path); + Json const& define = util::requireObject(current->at("define"), member(path, "define")); + if (define.empty()) + invalid(member(path, "define") + " must define at least one variable."); + for (auto const& [name, value]: define.items()) + { + requireIdentifierKey(name, member(path, "define")); + definitions.emplace_back(name, expressionFromJson(value, member(member(path, "define"), name), depth + 1, _options)); + } + current = &util::requiredMember(*current, "in", path); + path = member(path, "in"); + ++depth; + } + return Pointer{Pointer::Scope{std::move(definitions), std::make_shared(pointerFromJson(*current, path, depth, _options))}}; + } + if (_json.contains("template")) + { + util::requireOnlyMembers(_json, {"template", "yields"}, _path); + std::string name = identifierFromJson(_json.at("template"), member(_path, "template")); + std::vector> yields; + if (Json const* yieldsJson = util::optionalMember(_json, "yields", _path)) + { + util::requireObject(*yieldsJson, member(_path, "yields")); + for (auto const& [producedName, newName]: yieldsJson->items()) + { + requireIdentifierKey(producedName, member(_path, "yields")); + yields.emplace_back(producedName, identifierFromJson(newName, member(member(_path, "yields"), producedName))); + } + } + return Pointer{Pointer::TemplateReference{std::move(name), std::move(yields)}}; + } + if (_json.contains("templates")) + { + util::requireOnlyMembers(_json, {"templates", "in"}, _path); + std::vector> templates; + Json const& definitions = util::requireObject(_json.at("templates"), member(_path, "templates")); + for (auto const& [templateName, definition]: definitions.items()) + { + requireIdentifierKey(templateName, member(_path, "templates")); + templates.emplace_back(templateName, templateFromJson(definition, member(member(_path, "templates"), templateName), _depth + 1, _options)); + } + return Pointer{Pointer::Templates{std::move(templates), subPointer(util::requiredMember(_json, "in", _path), member(_path, "in"))}}; + } + invalid(std::string(_path) + " is not a pointer: expected one of location, group, list, if, define, template or templates."); +} + +schema::Pointer::Template templateFromJson(Json const& _json, std::string_view _path, size_t _depth, ReadOptions _options) +{ + requireDepth(_depth, _path); + util::requireOnlyMembers(_json, {"expect", "for"}, _path); + std::vector expect; + Json const& expectJson = util::requireArray(util::requiredMember(_json, "expect", _path), member(_path, "expect")); + for (size_t index = 0; index < expectJson.size(); ++index) + expect.emplace_back(identifierFromJson(expectJson.at(index), element(member(_path, "expect"), index))); + return schema::Pointer::Template{ + std::move(expect), + std::make_shared(pointerFromJson(util::requiredMember(_json, "for", _path), member(_path, "for"), _depth + 1, _options)) + }; +} + +} + +schema::data::Unsigned schema::data::unsignedFromJson(Json const& _json, std::string_view _path) +{ + if (_json.is_number_unsigned()) + return Unsigned{_json.get()}; + if (_json.is_string() && isHexString(_json.get())) + return Unsigned{HexValue{util::fromHex(_json.get())}}; + invalid(std::string(_path) + " must be an unsigned number or a 0x-prefixed hex string."); +} + +schema::materials::ID schema::materials::idFromJson(Json const& _json, std::string_view _path) +{ + if (_json.is_string()) + return ID{_json.get()}; + if (_json.is_number_unsigned()) + return ID{_json.get()}; + invalid(std::string(_path) + " must be a string or an unsigned number."); +} + +schema::materials::SourceRange schema::materials::sourceRangeFromJson(Json const& _json, std::string_view _path) +{ + util::requireOnlyMembers(_json, {"source", "range"}, _path); + SourceRange sourceRange; + Json const& source = util::requiredMember(_json, "source", _path); + std::string const sourcePath = member(_path, "source"); + util::requireOnlyMembers(source, {"id", "type"}, sourcePath); + sourceRange.source.id = idFromJson(util::requiredMember(source, "id", sourcePath), member(sourcePath, "id")); + if (std::optional type = util::optionalValue(source, "type", sourcePath)) + { + sourceRange.source.type = valueOfName(Reference::typeNames, *type); + if (!sourceRange.source.type) + invalid(member(sourcePath, "type") + " has the unknown value \"" + *type + "\"."); + } + if (Json const* range = util::optionalMember(_json, "range", _path)) + { + std::string const rangePath = member(_path, "range"); + util::requireOnlyMembers(*range, {"offset", "length"}, rangePath); + sourceRange.range = SourceRange::Range{ + .length = data::unsignedFromJson(util::requiredMember(*range, "length", rangePath), member(rangePath, "length")), + .offset = data::unsignedFromJson(util::requiredMember(*range, "offset", rangePath), member(rangePath, "offset")) + }; + } + return sourceRange; +} + +schema::Type schema::typeFromJson(Json const& _json, std::string_view _path) +{ + return ::typeFromJson(_json, _path, 0); +} + +schema::Type::Wrapper schema::wrapperFromJson(Json const& _json, std::string_view _path) +{ + return ::wrapperFromJson(_json, _path, 0); +} + +schema::Pointer::Expression schema::expressionFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options) +{ + return ::expressionFromJson(_json, _path, 0, _options); +} + +schema::Pointer schema::pointerFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options) +{ + return ::pointerFromJson(_json, _path, 0, _options); +} + +schema::Pointer::Template schema::templateFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options) +{ + return ::templateFromJson(_json, _path, 0, _options); +} + +namespace +{ + +bool hasInternalExpression(schema::Pointer::Expression const& _expression) +{ + using Pointer = schema::Pointer; + auto const anyOperand = [](Pointer::Operands const& _operands) { + return ranges::any_of(_operands, [](Pointer::Expression const& _operand) { return hasInternalExpression(_operand); }); + }; + return std::visit(util::GenericVisitor{ + [](Pointer::YulLocal const&) { return true; }, + [&](Pointer::Arithmetic const& _arithmetic) { return anyOperand(_arithmetic.operands); }, + [&](Pointer::Keccak256 const& _keccak256) { return anyOperand(_keccak256.operands); }, + [&](Pointer::Concat const& _concat) { return anyOperand(_concat.operands); }, + [](Pointer::Resize const& _resize) { return hasInternalExpression(*_resize.operand); }, + [](auto const&) { return false; } + }, _expression.value); +} + +bool hasInternalExpression(std::optional const& _expression) +{ + return _expression && hasInternalExpression(*_expression); +} + +} + +bool schema::hasInternalExpression(Pointer const& _pointer) +{ + auto const inSubPointer = [](std::shared_ptr const& _sub) { return _sub && hasInternalExpression(*_sub); }; + return std::visit(util::GenericVisitor{ + [](Pointer::Region const& _region) + { + return ::hasInternalExpression(_region.slot) || ::hasInternalExpression(_region.offset) || ::hasInternalExpression(_region.length); + }, + [](Pointer::Group const& _group) { return ranges::any_of(_group.members, [](Pointer const& _member) { return hasInternalExpression(_member); }); }, + [&](Pointer::List const& _list) { return ::hasInternalExpression(_list.count) || inSubPointer(_list.is); }, + [&](Pointer::Conditional const& _conditional) + { + return ::hasInternalExpression(_conditional.condition) || inSubPointer(_conditional.then) || inSubPointer(_conditional.otherwise); + }, + [&](Pointer::Scope const& _scope) + { + return ranges::any_of(_scope.definitions, [](auto const& _definition) { return ::hasInternalExpression(_definition.second); }) || inSubPointer(_scope.in); + }, + [](Pointer::TemplateReference const&) { return false; }, + [&](Pointer::Templates const& _templates) + { + return ranges::any_of(_templates.templates, [](auto const& _template) { return hasInternalExpression(*_template.second.body); }) || inSubPointer(_templates.in); + } + }, _pointer.value); +} + +std::map schema::info::typesFromJson(Json const& _json, std::string_view _path) +{ + util::requireObject(_json, _path); + std::map types; + for (auto const& [id, document]: _json.items()) + types.emplace(id, ::typeFromJson(document, member(_path, id), 0)); + return types; +} + +std::map schema::info::pointersFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options) +{ + util::requireObject(_json, _path); + std::map pointers; + for (auto const& [name, definition]: _json.items()) + { + requireIdentifierKey(name, _path); + pointers.emplace(name, ::templateFromJson(definition, member(_path, name), 0, _options)); + } + return pointers; +} + +namespace +{ + +/// Collects the free names of pointers, resolving template references. The +/// free names of a table template are computed once, with the template's +/// own parameters bound, and cached. +class FreeNameCollector +{ +public: + explicit FreeNameCollector(std::map const& _templates): + m_templates(_templates) + {} + + std::set collect(schema::Pointer const& _pointer) + { + std::set free; + std::set bound; + LocalTemplates localTemplates; + visit(_pointer, bound, localTemplates, free); + return free; + } + +private: + using LocalTemplates = std::map; + + /// 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; + }; + + void visit(schema::Pointer::Expression const& _expression, std::set const& _bound, std::set& _free) + { + using Pointer = schema::Pointer; + auto const operands = [&](Pointer::Operands const& _operands) { + for (Pointer::Expression const& operand: _operands) + visit(operand, _bound, _free); + }; + std::visit(util::GenericVisitor{ + // A Yul local is a dependency on the current Yul code by + // definition; nothing binds it. An unbound variable is one too. + [&](Pointer::YulLocal const& _yulLocal) { _free.insert(_yulLocal.name); }, + [&](Pointer::Variable const& _variable) + { + if (!_bound.count(_variable.identifier)) + _free.insert(_variable.identifier); + }, + [&](Pointer::Arithmetic const& _arithmetic) { operands(_arithmetic.operands); }, + [&](Pointer::Keccak256 const& _keccak256) { operands(_keccak256.operands); }, + [&](Pointer::Concat const& _concat) { operands(_concat.operands); }, + [&](Pointer::Resize const& _resize) { visit(*_resize.operand, _bound, _free); }, + // Literals, constants, lookups and reads: the latter two name + // regions, which live in a separate namespace. + [](auto const&) {} + }, _expression.value); + } + + void visit( + std::optional const& _expression, + std::set const& _bound, + std::set& _free + ) + { + if (_expression) + visit(*_expression, _bound, _free); + } + + void visit( + schema::Pointer const& _pointer, + std::set& _bound, + LocalTemplates& _localTemplates, + std::set& _free + ) + { + using Pointer = schema::Pointer; + auto const sub = [&](std::shared_ptr const& _sub) { + if (_sub) + visit(*_sub, _bound, _localTemplates, _free); + }; + std::visit(util::GenericVisitor{ + [&](Pointer::Region const& _region) + { + visit(_region.slot, _bound, _free); + visit(_region.offset, _bound, _free); + visit(_region.length, _bound, _free); + }, + [&](Pointer::Group const& _group) + { + for (Pointer const& member: _group.members) + visit(member, _bound, _localTemplates, _free); + }, + [&](Pointer::List const& _list) + { + visit(_list.count, _bound, _free); + ScopedNames index{_bound, {_list.each}}; + sub(_list.is); + }, + [&](Pointer::Conditional const& _conditional) + { + visit(_conditional.condition, _bound, _free); + sub(_conditional.then); + sub(_conditional.otherwise); + }, + [&](Pointer::Scope const& _scope) + { + // Definitions are ordered: each may reference the earlier ones. + std::vector definedNames; + for (auto const& [name, value]: _scope.definitions) + { + visit(value, _bound, _free); + definedNames.emplace_back(name); + } + ScopedNames defined{_bound, definedNames}; + sub(_scope.in); + }, + [&](Pointer::TemplateReference const& _reference) + { + for (std::string const& name: freeNamesOfTemplate(_reference.name, _localTemplates)) + if (!_bound.count(name)) + _free.insert(name); + }, + [&](Pointer::Templates const& _templates) + { + // Locally defined templates are visible to references inside the target. + std::vector added; + for (auto const& [name, definition]: _templates.templates) + if (_localTemplates.emplace(name, &definition).second) + added.emplace_back(name); + sub(_templates.in); + for (std::string const& name: added) + _localTemplates.erase(name); + } + }, _pointer.value); + } + + /// 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 {}; + + schema::Pointer::Template const* definition = nullptr; + bool fromTable = false; + if (auto const local = _localTemplates.find(_name); local != _localTemplates.end()) + definition = local->second; + else if (auto const it = m_templates.find(_name); it != m_templates.end()) + { + definition = &it->second; + 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; + { + ScopedNames parameters{bound, definition->expect}; + visit(*definition->body, bound, _localTemplates, free); + } + m_resolving.erase(_name); + + if (fromTable) + m_tableTemplateFreeNames.emplace(_name, free); + return free; + } + + std::map const& m_templates; + std::set m_resolving; + std::map> m_tableTemplateFreeNames; +}; + +} + +std::set schema::freeNames(Pointer const& _pointer, std::map const& _templates) +{ + return FreeNameCollector{_templates}.collect(_pointer); +} diff --git a/liblangutil/EthdebugSchema.h b/liblangutil/EthdebugSchema.h index 3ab617360f2f..0889b6503937 100644 --- a/liblangutil/EthdebugSchema.h +++ b/liblangutil/EthdebugSchema.h @@ -678,4 +678,34 @@ namespace info void to_json(Json& _json, Resources const& _resources); } +// Reading. Every function throws util::JsonValidationError, whose message +// names the offending location, for input that does not follow the schema. + +namespace data +{ +/// ethdebug/format/data/value: an unsigned number or a `0x`-prefixed hex string. +Unsigned unsignedFromJson(Json const& _json, std::string_view _path); +} + +namespace materials +{ +ID idFromJson(Json const& _json, std::string_view _path); +SourceRange sourceRangeFromJson(Json const& _json, std::string_view _path); +} + +Type typeFromJson(Json const& _json, std::string_view _path); +Type::Wrapper wrapperFromJson(Json const& _json, std::string_view _path); + +Pointer::Expression expressionFromJson(Json const& _json, std::string_view _path); +Pointer pointerFromJson(Json const& _json, std::string_view _path); +Pointer::Template templateFromJson(Json const& _json, std::string_view _path); + +namespace info +{ +/// The type table of a resources object, keyed by type ID. +std::map typesFromJson(Json const& _json, std::string_view _path); +/// The pointer table of a resources object, keyed by template name. +std::map pointersFromJson(Json const& _json, std::string_view _path); +} + } diff --git a/libsolutil/JSON.cpp b/libsolutil/JSON.cpp index 6665fb4b9170..52cf86ccd930 100644 --- a/libsolutil/JSON.cpp +++ b/libsolutil/JSON.cpp @@ -183,3 +183,56 @@ std::optional jsonValueByPath(Json const& _node, std::string_view _jsonPat } } // namespace solidity::util + +namespace solidity::util +{ + +namespace +{ + +[[noreturn]] void throwValidationError(std::string _message) +{ + BOOST_THROW_EXCEPTION(JsonValidationError() << errinfo_comment(std::move(_message))); +} + +} + +Json const& requireObject(Json const& _json, std::string_view _path) +{ + if (!_json.is_object()) + throwValidationError(std::string(_path) + " must be an object."); + return _json; +} + +Json const& requireArray(Json const& _json, std::string_view _path) +{ + if (!_json.is_array()) + throwValidationError(std::string(_path) + " must be an array."); + return _json; +} + +void requireOnlyMembers(Json const& _json, std::set const& _allowed, std::string_view _path) +{ + requireObject(_json, _path); + for (auto const& [name, value]: _json.items()) + if (!_allowed.count(name)) + throwValidationError(std::string(_path) + " has an unknown member \"" + name + "\"."); +} + +Json const& requiredMember(Json const& _json, std::string_view _name, std::string_view _path) +{ + requireObject(_json, _path); + if (!_json.contains(_name)) + throwValidationError(std::string(_path) + "." + std::string(_name) + " is required."); + return _json.at(_name); +} + +Json const* optionalMember(Json const& _json, std::string_view _name, std::string_view _path) +{ + requireObject(_json, _path); + if (!_json.contains(_name)) + return nullptr; + return &_json.at(_name); +} + +} diff --git a/libsolutil/JSON.h b/libsolutil/JSON.h index 2cdac70c7857..7790b9bcac00 100644 --- a/libsolutil/JSON.h +++ b/libsolutil/JSON.h @@ -24,12 +24,15 @@ #pragma once #include +#include #include +#include +#include +#include #include #include -#include -#include +#include namespace solidity { @@ -184,4 +187,64 @@ T getOrDefault(Json const& _input, std::string const& _name, T _default = {}) return detail::helper::getOrDefault(_input, _name, _default); } +/// Thrown by the validation helpers below when JSON input does not have the +/// expected structure. The message names the offending location, so it can +/// be reported to the user as is. +DEV_SIMPLE_EXCEPTION(JsonValidationError); + +/// The name of the JSON type that @a T is read from, for messages. +template +constexpr std::string_view jsonTypeName() +{ + if constexpr (std::is_same_v) + return "string"; + else if constexpr (std::is_same_v) + return "boolean"; + else if constexpr (std::is_floating_point_v) + return "number"; + else if constexpr (std::is_unsigned_v) + return "unsigned integer"; + else + return "integer"; +} + +/// @returns @a _json if it is an object, throws JsonValidationError otherwise. +Json const& requireObject(Json const& _json, std::string_view _path); +/// @returns @a _json if it is an array, throws JsonValidationError otherwise. +Json const& requireArray(Json const& _json, std::string_view _path); +/// Throws JsonValidationError unless @a _json is an object whose members are all in @a _allowed. +void requireOnlyMembers(Json const& _json, std::set const& _allowed, std::string_view _path); +/// @returns the member @a _name of the object @a _json, throws JsonValidationError if there is none. +Json const& requiredMember(Json const& _json, std::string_view _name, std::string_view _path); +/// @returns the member @a _name of the object @a _json or nullptr if there is none. +Json const* optionalMember(Json const& _json, std::string_view _name, std::string_view _path); + +/// @returns @a _json read as @a T, throws JsonValidationError if it is not of that type. +template +T valueOfType(Json const& _json, std::string_view _path) +{ + if (!isOfType(_json)) + BOOST_THROW_EXCEPTION(JsonValidationError() << errinfo_comment(std::string(_path) + " must be a " + std::string(jsonTypeName()) + ".")); + return get(_json); +} + +/// @returns the member @a _name of the object @a _json read as @a T, throws JsonValidationError +/// if it is missing or not of that type. +template +T requiredValue(Json const& _json, std::string_view _name, std::string_view _path) +{ + return valueOfType(requiredMember(_json, _name, _path), std::string(_path) + "." + std::string(_name)); +} + +/// @returns the member @a _name of the object @a _json read as @a T if present, throws +/// JsonValidationError if it is present but not of that type. +template +std::optional optionalValue(Json const& _json, std::string_view _name, std::string_view _path) +{ + Json const* member = optionalMember(_json, _name, _path); + if (!member) + return std::nullopt; + return valueOfType(*member, std::string(_path) + "." + std::string(_name)); +} + } // namespace solidity::util diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b14bccfe95a4..9fbc55426d88 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,8 @@ detect_stray_source_files("${libevmasm_sources}" "libevmasm/") set(liblangutil_sources liblangutil/CharStream.cpp + liblangutil/EthdebugDocumentTest.cpp + liblangutil/EthdebugDocumentTest.h liblangutil/Scanner.cpp liblangutil/SourceLocation.cpp ) diff --git a/test/InteractiveTests.h b/test/InteractiveTests.h index 73be3bf63081..01f41bc99d47 100644 --- a/test/InteractiveTests.h +++ b/test/InteractiveTests.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,7 @@ Testsuite const g_interactiveTestsuites[] = { {"JSON Natspec", "libsolidity", "natspecJSON", false, false, &NatspecJSONTest::create}, {"SMT Checker", "libsolidity", "smtCheckerTests", true, false, &SMTCheckerTest::create}, {"Ethdebug", "libsolidity", "ethdebugTests", false, false, &EthdebugTest::create}, + {"Ethdebug Document", "liblangutil", "ethdebugDocumentTests", false, false, &langutil::test::EthdebugDocumentTest::create}, {"Gas Estimates", "libsolidity", "gasTests", false, false, &GasTest::create}, {"Memory Guard", "libsolidity", "memoryGuardTests", false, false, &MemoryGuardTest::create}, {"AST Properties", "libsolidity", "astPropertyTests", false, false, &ASTPropertyTest::create}, diff --git a/test/TestCase.cpp b/test/TestCase.cpp index 8081d7d86068..90698aa39a11 100644 --- a/test/TestCase.cpp +++ b/test/TestCase.cpp @@ -51,8 +51,8 @@ void TestCase::printUpdatedSettings(std::ostream& _stream, std::string const& _l bool TestCase::isTestFilename(boost::filesystem::path const& _filename) { std::string extension = _filename.extension().string(); - // NOTE: .asmjson rather than .json because JSON files that do not represent test cases exist in some test dirs. - return (extension == ".sol" || extension == ".yul" || extension == ".asm" || extension == ".asmjson" || extension == ".stack") && + // NOTE: .asmjson and .ethdebugjson rather than .json because JSON files that do not represent test cases exist in some test dirs. + return (extension == ".sol" || extension == ".yul" || extension == ".asm" || extension == ".asmjson" || extension == ".ethdebugjson" || extension == ".stack") && !_filename.string().starts_with('~') && !_filename.string().starts_with('.'); } diff --git a/test/libevmasm/Assembler.cpp b/test/libevmasm/Assembler.cpp index 9e63668ed04c..72c344cea54e 100644 --- a/test/libevmasm/Assembler.cpp +++ b/test/libevmasm/Assembler.cpp @@ -431,7 +431,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_program_last_instruction_with_immediate_arguments) assembly.append(AssemblyItem{0x11223344}); LinkerObject output = assembly.assemble(); - Json const program = ethdebug::program("", 0, assembly, output); + Json const program = evmasm::ethdebug::program("", 0, assembly, output); BOOST_REQUIRE(program["instructions"].size() == 1); BOOST_REQUIRE(program["instructions"][0]["operation"]["mnemonic"] == "PUSH4"); BOOST_REQUIRE(program["instructions"][0]["operation"]["arguments"][0] == "0x11223344"); @@ -442,7 +442,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_program_last_instruction_with_immediate_arguments) assembly.append(AssemblyItem{0x1122334455}); LinkerObject output = assembly.assemble(); - Json const program = ethdebug::program("", 0, assembly, output); + Json const program = evmasm::ethdebug::program("", 0, assembly, output); BOOST_REQUIRE(program["instructions"].size() == 2); BOOST_REQUIRE(program["instructions"][0]["operation"]["mnemonic"] == "PUSH0"); BOOST_REQUIRE(!program["instructions"][0]["operation"].contains("arguments")); @@ -453,7 +453,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_program_last_instruction_with_immediate_arguments) BOOST_AUTO_TEST_CASE(ethdebug_resources) { - Json const resources = ethdebug::resources( + Json const resources = evmasm::ethdebug::resources( { {.id = 0, .path = "sourceA", .contents = "contentsA", .language = "Solidity"}, {.id = 1, .path = "sourceB", .contents = "contentsB", .language = "Yul"} @@ -462,7 +462,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_resources) ); std::string const compilationID = resources["compilation"]["id"]; BOOST_REQUIRE(compilationID.substr(0, 5) == "solc-"); - std::string const repeatedCompilationID = ethdebug::resources( + std::string const repeatedCompilationID = evmasm::ethdebug::resources( { {.id = 0, .path = "sourceA", .contents = "contentsA", .language = "Solidity"}, {.id = 1, .path = "sourceB", .contents = "contentsB", .language = "Yul"} @@ -470,7 +470,7 @@ BOOST_AUTO_TEST_CASE(ethdebug_resources) "version1" )["compilation"]["id"]; BOOST_REQUIRE(compilationID == repeatedCompilationID); - std::string const changedCompilationID = ethdebug::resources( + std::string const changedCompilationID = evmasm::ethdebug::resources( { {.id = 0, .path = "sourceA", .contents = "changedContentsA", .language = "Solidity"}, {.id = 1, .path = "sourceB", .contents = "contentsB", .language = "Yul"} diff --git a/test/liblangutil/EthdebugDocumentTest.cpp b/test/liblangutil/EthdebugDocumentTest.cpp new file mode 100644 index 000000000000..92f6c23ef82a --- /dev/null +++ b/test/liblangutil/EthdebugDocumentTest.cpp @@ -0,0 +1,83 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include + +#include + +#include + +#include +#include + +using namespace solidity; +using namespace solidity::langutil; +using namespace solidity::langutil::test; + +namespace schema = solidity::langutil::ethdebug::schema; + +EthdebugDocumentTest::EthdebugDocumentTest(std::string const& _filename): + TestCase(_filename) +{ + m_source = m_reader.source(); + m_expectation = m_reader.simpleExpectations(); + m_document = m_reader.enumSetting("document", {{"resources", Document::Resources}}, "resources"); + if (!_filename.ends_with(".ethdebugjson")) + BOOST_THROW_EXCEPTION(std::runtime_error("Not an ethdebug document test: \"" + _filename + "\". Expected extension: .ethdebugjson.")); +} + +frontend::test::TestCase::TestResult EthdebugDocumentTest::run(std::ostream& _stream, std::string const& _linePrefix, bool const _formatted) +{ + Json input; + std::string parseError; + if (!util::jsonParseStrict(m_source, input, &parseError)) + { + m_obtainedResult = "Error: " + parseError + "\n"; + return checkResult(_stream, _linePrefix, _formatted); + } + + try + { + Json output; + switch (m_document) + { + case Document::Resources: + { + util::requireOnlyMembers(input, {"types", "pointers"}, "resources"); + schema::info::Resources resources; + if (input.contains("types")) + resources.types = schema::info::typesFromJson(input.at("types"), "resources.types"); + if (input.contains("pointers")) + resources.pointers = schema::info::pointersFromJson(input.at("pointers"), "resources.pointers"); + // The tables only; the compilation record is not part of the document. + Json serialized = resources; + output = Json{{"types", serialized["types"]}, {"pointers", serialized["pointers"]}}; + break; + } + } + m_obtainedResult = util::jsonPrint(output, util::JsonFormat{util::JsonFormat::Pretty, 4}) + "\n"; + } + catch (util::JsonValidationError const& _error) + { + m_obtainedResult = "Error: " + util::stringOrDefault(_error.comment()) + "\n"; + } + + return checkResult(_stream, _linePrefix, _formatted); +} diff --git a/test/liblangutil/EthdebugDocumentTest.h b/test/liblangutil/EthdebugDocumentTest.h new file mode 100644 index 000000000000..a105dca3bbe0 --- /dev/null +++ b/test/liblangutil/EthdebugDocumentTest.h @@ -0,0 +1,62 @@ +/* + 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 +/** + * Test case that reads an ethdebug JSON document, validates it and writes it back. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace solidity::langutil::test +{ + +/// Reads an ethdebug JSON document into the compiler's structures, validates it +/// and serializes it again. The expectation is either the document as the +/// compiler writes it or the validation error. +/// +/// Available settings: +/// - document: The document kind. `resources` (the default) reads the `types` +/// and `pointers` tables of ethdebug/format/info/resources; `sidecar` reads +/// the semantic debug info sidecar. +class EthdebugDocumentTest: public frontend::test::TestCase +{ +public: + static std::unique_ptr create(Config const& _config) + { + return std::make_unique(_config.filename); + } + EthdebugDocumentTest(std::string const& _filename); + + TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; + +private: + enum class Document + { + Resources, + Sidecar + }; + + Document m_document = Document::Resources; +}; + +} diff --git a/test/liblangutil/ethdebugDocumentTests/error_define_empty.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_define_empty.ethdebugjson new file mode 100644 index 000000000000..26fc34a3f7a9 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_define_empty.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"define": {}, "in": {"location": "storage", "slot": 0}}}}} +// ---- +// Error: resources.pointers.p.for.define must define at least one variable. diff --git a/test/liblangutil/ethdebugDocumentTests/error_expression_arity.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_expression_arity.ethdebugjson new file mode 100644 index 000000000000..6b8cee94321d --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_expression_arity.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "slot": {"~difference": [1]}}}}} +// ---- +// Error: resources.pointers.p.for.slot.~difference must have exactly 2 operands. diff --git a/test/liblangutil/ethdebugDocumentTests/error_expression_negative_literal.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_expression_negative_literal.ethdebugjson new file mode 100644 index 000000000000..5181a693bc22 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_expression_negative_literal.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "slot": -1}}}} +// ---- +// Error: resources.pointers.p.for.slot must be an unsigned number or a 0x-prefixed hex string. diff --git a/test/liblangutil/ethdebugDocumentTests/error_expression_sized_zero.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_expression_sized_zero.ethdebugjson new file mode 100644 index 000000000000..5e632035c259 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_expression_sized_zero.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "slot": {"~sized0": 1}}}}} +// ---- +// Error: resources.pointers.p.for.slot has the malformed resize key "~sized0". diff --git a/test/liblangutil/ethdebugDocumentTests/error_expression_two_keys.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_expression_two_keys.ethdebugjson new file mode 100644 index 000000000000..fa76708a5d47 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_expression_two_keys.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "slot": {"~sum": [1], "~product": [1]}}}}} +// ---- +// Error: resources.pointers.p.for.slot must have exactly one member. diff --git a/test/liblangutil/ethdebugDocumentTests/error_expression_unknown_key.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_expression_unknown_key.ethdebugjson new file mode 100644 index 000000000000..edc6ca3126b1 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_expression_unknown_key.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "slot": {"~hash": [1]}}}}} +// ---- +// Error: resources.pointers.p.for.slot has the unknown expression key "~hash". diff --git a/test/liblangutil/ethdebugDocumentTests/error_invalid_json.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_invalid_json.ethdebugjson new file mode 100644 index 000000000000..8b90bdb91d51 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_invalid_json.ethdebugjson @@ -0,0 +1,3 @@ +{"types": { +// ---- +// Error: parse error at line 2, column 1: syntax error while parsing object key - unexpected end of input; expected string literal diff --git a/test/liblangutil/ethdebugDocumentTests/error_list_missing_each.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_list_missing_each.ethdebugjson new file mode 100644 index 000000000000..45d346c1091e --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_list_missing_each.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"list": {"count": 1, "is": {"location": "storage", "slot": 0}}}}}} +// ---- +// Error: resources.pointers.p.for.list.each is required. diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_empty_group.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_empty_group.ethdebugjson new file mode 100644 index 000000000000..ecbc503053ba --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_empty_group.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"group": []}}}} +// ---- +// Error: resources.pointers.p.for.group must have at least one member. diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_memory_with_slot.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_memory_with_slot.ethdebugjson new file mode 100644 index 000000000000..f13eefa52524 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_memory_with_slot.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "memory", "slot": 0, "offset": 0, "length": 32}}}} +// ---- +// Error: resources.pointers.p.for must address its offset and length, and has no slot. diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_name_not_identifier.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_name_not_identifier.ethdebugjson new file mode 100644 index 000000000000..15f41b2410f6 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_name_not_identifier.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"~bad": {"expect": [], "for": {"location": "storage", "slot": 0}}}} +// ---- +// Error: resources.pointers has the member "~bad", which is not an identifier. diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_region_without_slot.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_region_without_slot.ethdebugjson new file mode 100644 index 000000000000..7c67d87f28ed --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_region_without_slot.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "storage", "offset": 0}}}} +// ---- +// Error: resources.pointers.p.for must address its slot. diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_location.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_location.ethdebugjson new file mode 100644 index 000000000000..a8123e640b81 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_location.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"location": "heap", "slot": 0}}}} +// ---- +// Error: resources.pointers.p.for.location has the unknown value "heap". diff --git a/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_variant.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_variant.ethdebugjson new file mode 100644 index 000000000000..01da2cad27ac --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_pointer_unknown_variant.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": [], "for": {"region": "storage"}}}} +// ---- +// Error: resources.pointers.p.for is not a pointer: expected one of location, group, list, if, define, template or templates. diff --git a/test/liblangutil/ethdebugDocumentTests/error_template_expect_not_identifier.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_template_expect_not_identifier.ethdebugjson new file mode 100644 index 000000000000..9eec90bd9b78 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_template_expect_not_identifier.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": ["1st"], "for": {"location": "storage", "slot": 0}}}} +// ---- +// Error: resources.pointers.p.expect[0] must be an identifier. diff --git a/test/liblangutil/ethdebugDocumentTests/error_template_missing_body.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_template_missing_body.ethdebugjson new file mode 100644 index 000000000000..8957d29b7777 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_template_missing_body.ethdebugjson @@ -0,0 +1,3 @@ +{"pointers": {"p": {"expect": []}}} +// ---- +// Error: resources.pointers.p.for is required. diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_bits.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_bits.ethdebugjson new file mode 100644 index 000000000000..78a65164f7ef --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_bits.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "uint", "bits": 12}}} +// ---- +// Error: resources.types.t.bits must be a multiple of 8 between 8 and 256. diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_class_mismatch.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_class_mismatch.ethdebugjson new file mode 100644 index 000000000000..df67ca303468 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_class_mismatch.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"class": "complex", "kind": "uint", "bits": 8}}} +// ---- +// Error: resources.types.t.class must be "elementary" for kind "uint". diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_definition_empty.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_definition_empty.ethdebugjson new file mode 100644 index 000000000000..777165eed70e --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_definition_empty.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "enum", "values": [], "definition": {}}}} +// ---- +// Error: resources.types.t.definition must have a name or a location. diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_function_visibility.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_function_visibility.ethdebugjson new file mode 100644 index 000000000000..deacde5f921a --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_function_visibility.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "function", "contains": {"parameters": {"type": {"kind": "tuple", "contains": []}}}}}} +// ---- +// Error: resources.types.t must be either internal or external. diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_reference_with_extra.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_reference_with_extra.ethdebugjson new file mode 100644 index 000000000000..8c2bd390be58 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_reference_with_extra.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "array", "contains": {"type": {"id": "x", "kind": "uint"}}}}} +// ---- +// Error: resources.types.t.contains.type has an unknown member "kind". diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_unknown_kind.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_unknown_kind.ethdebugjson new file mode 100644 index 000000000000..5e2fa15b9988 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_unknown_kind.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "matrix"}}} +// ---- +// Error: resources.types.t.kind has the unknown value "matrix". diff --git a/test/liblangutil/ethdebugDocumentTests/error_type_unknown_member.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_type_unknown_member.ethdebugjson new file mode 100644 index 000000000000..2c33a484bc12 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_type_unknown_member.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {"t": {"kind": "uint", "bits": 256, "signed": false}}} +// ---- +// Error: resources.types.t has an unknown member "signed". diff --git a/test/liblangutil/ethdebugDocumentTests/error_unknown_member.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/error_unknown_member.ethdebugjson new file mode 100644 index 000000000000..53ce8184d5b4 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/error_unknown_member.ethdebugjson @@ -0,0 +1,3 @@ +{"types": {}, "pointers": {}, "extra": 1} +// ---- +// Error: resources has an unknown member "extra". diff --git a/test/liblangutil/ethdebugDocumentTests/resources_pointers.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/resources_pointers.ethdebugjson new file mode 100644 index 000000000000..194328c71dab --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/resources_pointers.ethdebugjson @@ -0,0 +1,247 @@ +{ + "types": {}, + "pointers": { + "value": { + "expect": [], + "for": {"name": "value", "location": "storage", "slot": 0, "offset": "0x10", "length": 16} + }, + "stack-and-memory": { + "expect": ["depth"], + "for": { + "group": [ + {"name": "pointer", "location": "stack", "slot": "depth"}, + {"name": "data", "location": "memory", "offset": {"~read": "pointer"}, "length": "~wordsize"}, + {"location": "calldata", "offset": {"~sum": [4, {"~product": ["depth", "~wordsize"]}]}, "length": {"~difference": [{".length": "data"}, 1]}}, + {"location": "returndata", "offset": {"~quotient": [{".offset": "~this"}, 2]}, "length": {"~remainder": [{".slot": "pointer"}, "~wordsize"]}}, + {"location": "code", "offset": {"~sized4": {"~keccak256": [{"~wordsized": "depth"}, "0x00"]}}, "length": {"~concat": ["0xdead", "0xbeef"]}}, + {"location": "transient", "slot": {"~concat": []}} + ] + } + }, + "array": { + "expect": [], + "for": { + "define": {"array-slot": 5}, + "in": { + "define": {"data-start": {"~keccak256": [{"~wordsized": "array-slot"}]}, "count": {"~read": "array-length"}}, + "in": { + "group": [ + {"name": "array-length", "location": "storage", "slot": "array-slot"}, + { + "list": { + "count": "count", + "each": "i", + "is": { + "if": {"~remainder": ["i", 2]}, + "then": {"name": "odd", "location": "storage", "slot": {"~sum": ["data-start", "i"]}}, + "else": {"template": "even-item", "yields": {"item": "even"}} + } + } + } + ] + } + } + } + }, + "with-local-templates": { + "expect": ["base"], + "for": { + "templates": { + "word": {"expect": ["at"], "for": {"name": "word", "location": "storage", "slot": "at"}} + }, + "in": {"template": "word"} + } + } + } +} +// ---- +// { +// "pointers": { +// "array": { +// "expect": [], +// "for": { +// "define": { +// "array-slot": 5 +// }, +// "in": { +// "define": { +// "count": { +// "~read": "array-length" +// } +// }, +// "in": { +// "define": { +// "data-start": { +// "~keccak256": [ +// { +// "~wordsized": "array-slot" +// } +// ] +// } +// }, +// "in": { +// "group": [ +// { +// "location": "storage", +// "name": "array-length", +// "slot": "array-slot" +// }, +// { +// "list": { +// "count": "count", +// "each": "i", +// "is": { +// "else": { +// "template": "even-item", +// "yields": { +// "item": "even" +// } +// }, +// "if": { +// "~remainder": [ +// "i", +// 2 +// ] +// }, +// "then": { +// "location": "storage", +// "name": "odd", +// "slot": { +// "~sum": [ +// "data-start", +// "i" +// ] +// } +// } +// } +// } +// } +// ] +// } +// } +// } +// } +// }, +// "stack-and-memory": { +// "expect": [ +// "depth" +// ], +// "for": { +// "group": [ +// { +// "location": "stack", +// "name": "pointer", +// "slot": "depth" +// }, +// { +// "length": "~wordsize", +// "location": "memory", +// "name": "data", +// "offset": { +// "~read": "pointer" +// } +// }, +// { +// "length": { +// "~difference": [ +// { +// ".length": "data" +// }, +// 1 +// ] +// }, +// "location": "calldata", +// "offset": { +// "~sum": [ +// 4, +// { +// "~product": [ +// "depth", +// "~wordsize" +// ] +// } +// ] +// } +// }, +// { +// "length": { +// "~remainder": [ +// { +// ".slot": "pointer" +// }, +// "~wordsize" +// ] +// }, +// "location": "returndata", +// "offset": { +// "~quotient": [ +// { +// ".offset": "~this" +// }, +// 2 +// ] +// } +// }, +// { +// "length": { +// "~concat": [ +// "0xdead", +// "0xbeef" +// ] +// }, +// "location": "code", +// "offset": { +// "~sized4": { +// "~keccak256": [ +// { +// "~wordsized": "depth" +// }, +// "0x00" +// ] +// } +// } +// }, +// { +// "location": "transient", +// "slot": { +// "~concat": [] +// } +// } +// ] +// } +// }, +// "value": { +// "expect": [], +// "for": { +// "length": 16, +// "location": "storage", +// "name": "value", +// "offset": "0x10", +// "slot": 0 +// } +// }, +// "with-local-templates": { +// "expect": [ +// "base" +// ], +// "for": { +// "in": { +// "template": "word" +// }, +// "templates": { +// "word": { +// "expect": [ +// "at" +// ], +// "for": { +// "location": "storage", +// "name": "word", +// "slot": "at" +// } +// } +// } +// } +// } +// }, +// "types": {} +// } diff --git a/test/liblangutil/ethdebugDocumentTests/resources_types.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/resources_types.ethdebugjson new file mode 100644 index 000000000000..8b4ba6b889d6 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/resources_types.ethdebugjson @@ -0,0 +1,222 @@ +{ + "types": { + "t_uint256": {"kind": "uint", "bits": 256}, + "t_int8": {"class": "elementary", "kind": "int", "bits": 8}, + "t_bool": {"kind": "bool"}, + "t_bytes4": {"kind": "bytes", "size": 4}, + "t_bytes": {"kind": "bytes"}, + "t_string": {"kind": "string", "encoding": "utf-8"}, + "t_fixed128x18": {"kind": "fixed", "bits": 128, "places": 18}, + "t_ufixed": {"kind": "ufixed", "bits": 256, "places": 1}, + "t_address_payable": {"kind": "address", "payable": true}, + "t_library": {"kind": "contract", "library": true, "definition": {"name": "L"}}, + "t_interface": {"kind": "contract", "interface": true, "payable": false, "definition": {"location": {"source": {"id": 1}, "range": {"offset": 10, "length": 20}}}}, + "t_color": {"kind": "enum", "values": ["Red", "Green"], "definition": {"name": "Color", "location": {"source": {"id": "a.sol"}}}}, + "t_price": {"class": "complex", "kind": "alias", "contains": {"type": {"id": "t_uint256"}}, "definition": {"name": "Price"}}, + "t_static": {"kind": "array", "contains": {"type": {"id": "t_uint256"}}, "count": "0x02"}, + "t_dynamic": {"kind": "array", "contains": {"type": {"id": "t_uint256"}}, "count": 3}, + "t_mapping": {"kind": "mapping", "contains": {"key": {"type": {"id": "t_address_payable"}}, "value": {"type": {"kind": "bool"}}}}, + "t_point": {"kind": "struct", "contains": [{"name": "x", "type": {"id": "t_uint256"}}, {"type": {"id": "t_bool"}}], "definition": {"name": "Point"}}, + "t_pair": {"kind": "tuple", "contains": [{"type": {"id": "t_uint256"}}, {"name": "second", "type": {"id": "t_bool"}}]}, + "t_internal": {"kind": "function", "internal": true, "contains": {"parameters": {"type": {"kind": "tuple", "contains": [{"type": {"id": "t_uint256"}}]}}}}, + "t_external": {"kind": "function", "external": true, "contains": {"parameters": {"type": {"kind": "tuple", "contains": []}}, "returns": {"type": {"id": "t_bool"}}}, "definition": {"name": "f"}} + }, + "pointers": {} +} +// ---- +// { +// "pointers": {}, +// "types": { +// "t_address_payable": { +// "kind": "address", +// "payable": true +// }, +// "t_bool": { +// "kind": "bool" +// }, +// "t_bytes": { +// "kind": "bytes" +// }, +// "t_bytes4": { +// "kind": "bytes", +// "size": 4 +// }, +// "t_color": { +// "definition": { +// "location": { +// "source": { +// "id": "a.sol" +// } +// }, +// "name": "Color" +// }, +// "kind": "enum", +// "values": [ +// "Red", +// "Green" +// ] +// }, +// "t_dynamic": { +// "contains": { +// "type": { +// "id": "t_uint256" +// } +// }, +// "count": 3, +// "kind": "array" +// }, +// "t_external": { +// "contains": { +// "parameters": { +// "type": { +// "contains": [], +// "kind": "tuple" +// } +// }, +// "returns": { +// "type": { +// "id": "t_bool" +// } +// } +// }, +// "definition": { +// "name": "f" +// }, +// "external": true, +// "kind": "function" +// }, +// "t_fixed128x18": { +// "bits": 128, +// "kind": "fixed", +// "places": 18 +// }, +// "t_int8": { +// "bits": 8, +// "kind": "int" +// }, +// "t_interface": { +// "definition": { +// "location": { +// "range": { +// "length": 20, +// "offset": 10 +// }, +// "source": { +// "id": 1 +// } +// } +// }, +// "interface": true, +// "kind": "contract", +// "payable": false +// }, +// "t_internal": { +// "contains": { +// "parameters": { +// "type": { +// "contains": [ +// { +// "type": { +// "id": "t_uint256" +// } +// } +// ], +// "kind": "tuple" +// } +// } +// }, +// "internal": true, +// "kind": "function" +// }, +// "t_library": { +// "definition": { +// "name": "L" +// }, +// "kind": "contract", +// "library": true +// }, +// "t_mapping": { +// "contains": { +// "key": { +// "type": { +// "id": "t_address_payable" +// } +// }, +// "value": { +// "type": { +// "kind": "bool" +// } +// } +// }, +// "kind": "mapping" +// }, +// "t_pair": { +// "contains": [ +// { +// "type": { +// "id": "t_uint256" +// } +// }, +// { +// "name": "second", +// "type": { +// "id": "t_bool" +// } +// } +// ], +// "kind": "tuple" +// }, +// "t_point": { +// "contains": [ +// { +// "name": "x", +// "type": { +// "id": "t_uint256" +// } +// }, +// { +// "type": { +// "id": "t_bool" +// } +// } +// ], +// "definition": { +// "name": "Point" +// }, +// "kind": "struct" +// }, +// "t_price": { +// "contains": { +// "type": { +// "id": "t_uint256" +// } +// }, +// "definition": { +// "name": "Price" +// }, +// "kind": "alias" +// }, +// "t_static": { +// "contains": { +// "type": { +// "id": "t_uint256" +// } +// }, +// "count": "0x02", +// "kind": "array" +// }, +// "t_string": { +// "encoding": "utf-8", +// "kind": "string" +// }, +// "t_ufixed": { +// "bits": 256, +// "kind": "ufixed", +// "places": 1 +// }, +// "t_uint256": { +// "bits": 256, +// "kind": "uint" +// } +// } +// } diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index f0553628fbd6..e377c520fe0e 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(isoltest ../libsolidity/util/TestFileParser.cpp ../libsolidity/util/TestFunctionCall.cpp ../libsolidity/EthdebugTest.cpp + ../liblangutil/EthdebugDocumentTest.cpp ../libsolidity/GasTest.cpp ../libsolidity/MemoryGuardTest.cpp ../libsolidity/NatspecJSONTest.cpp diff --git a/test/tools/isoltest.cpp b/test/tools/isoltest.cpp index e845e9eccd28..86e94888e908 100644 --- a/test/tools/isoltest.cpp +++ b/test/tools/isoltest.cpp @@ -75,7 +75,7 @@ class TestFilter boost::replace_all(filter, "/", "\\/"); boost::replace_all(filter, "*", ".*"); - m_filterExpression = std::regex{"(" + filter + "(\\.sol|\\.yul|\\.asm|\\.asmjson|\\.stack))"}; + m_filterExpression = std::regex{"(" + filter + "(\\.sol|\\.yul|\\.asm|\\.asmjson|\\.ethdebugjson|\\.stack))"}; } bool matches(fs::path const& _path, std::string const& _name) const From 74b357cb6fe59b69aeb8ca2a6eb0de299e8b76db Mon Sep 17 00:00:00 2001 From: djole Date: Wed, 9 Sep 2026 11:33:39 +0200 Subject: [PATCH 3/3] ethdebug: Add the semantic debug info side table and its sidecar Add the compiler structures of the semantic debug info specified in docs/internals/ethdebug_internal_debug_info.rst: scope records keyed by a scope ID (the AST ID of the source-language scope and an instance discriminator for cloned code), with variables carrying their identity, declaration range, type reference, phase and ethdebug pointer, plus the variable updates optimizer passes will produce. The side table keeps the records together with the type and pointer template resources they reference. Stack values are expressed over generated Yul locals until Yul is compiled to EVM, so the pointer expressions gain one compiler-internal form, YulLocal, written as $$yulLocal in the sidecar only; readers accept it on request and the public resources writer rejects it. The sidecar serialization writes the table as a versioned JSON document and reads it back with the same validation as the resources: unknown formats and members, malformed scope keys, variables whose phase and pointer disagree, unresolvable template references and internal expressions in the resources are reported with their location. The ethdebug document tests cover the sidecar round trip and the validation errors. --- .../ethdebug_internal_debug_info.rst | 5 +- liblangutil/CMakeLists.txt | 4 + liblangutil/EthdebugSchema.cpp | 210 +--------- liblangutil/EthdebugSchema.h | 36 +- liblangutil/SemanticDebugData.h | 105 +++++ .../SemanticDebugDataSerialization.cpp | 370 ++++++++++++++++++ liblangutil/SemanticDebugDataSerialization.h | 54 +++ liblangutil/SemanticDebugDataTable.h | 100 +++++ test/liblangutil/EthdebugDocumentTest.cpp | 6 +- ...r_error_computed_with_pointer.ethdebugjson | 5 + ...idecar_error_empty_identifier.ethdebugjson | 5 + ...decar_error_instance_negative.ethdebugjson | 5 + ...ernal_expression_in_resources.ethdebugjson | 5 + ..._materialized_without_pointer.ethdebugjson | 5 + .../sidecar_error_missing_scopes.ethdebugjson | 5 + ...error_scope_key_negative_zero.ethdebugjson | 5 + ...r_error_scope_key_not_decimal.ethdebugjson | 5 + ..._error_scope_key_out_of_range.ethdebugjson | 5 + .../sidecar_error_unknown_format.ethdebugjson | 5 + .../sidecar_error_unknown_member.ethdebugjson | 5 + .../sidecar_error_unknown_phase.ethdebugjson | 5 + ...car_error_unresolved_template.ethdebugjson | 5 + ...car_error_unsupported_version.ethdebugjson | 5 + ...r_error_update_without_change.ethdebugjson | 5 + .../sidecar_round_trip.ethdebugjson | 214 ++++++++++ 25 files changed, 962 insertions(+), 217 deletions(-) create mode 100644 liblangutil/SemanticDebugData.h create mode 100644 liblangutil/SemanticDebugDataSerialization.cpp create mode 100644 liblangutil/SemanticDebugDataSerialization.h create mode 100644 liblangutil/SemanticDebugDataTable.h create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_computed_with_pointer.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_empty_identifier.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_instance_negative.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_internal_expression_in_resources.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_materialized_without_pointer.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_missing_scopes.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_negative_zero.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_not_decimal.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_out_of_range.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_format.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_member.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_phase.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_unresolved_template.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_unsupported_version.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_error_update_without_change.ethdebugjson create mode 100644 test/liblangutil/ethdebugDocumentTests/sidecar_round_trip.ethdebugjson diff --git a/docs/compiler-guide/ethdebug_internal_debug_info.rst b/docs/compiler-guide/ethdebug_internal_debug_info.rst index 65151c1e2503..53f151019ae4 100644 --- a/docs/compiler-guide/ethdebug_internal_debug_info.rst +++ b/docs/compiler-guide/ethdebug_internal_debug_info.rst @@ -151,8 +151,9 @@ The scope record therefore carries declaration identity, type, and initial recov - Identity of the source-language declaration. Synthetic bindings may omit it. * - ``declarationSourceRange`` - - optional ``SourceLocation`` - - Source range of the declaration. + - optional ethdebug ``materials::SourceRange`` + - Source range of the declaration: the numeric source ID of the ``ethdebug.compilation`` record with the byte offset and length. + ``SourceLocation`` identifies sources by name, which the sidecar never uses as an ID. * - ``typeID`` - optional string - Key of the variable's document in ``ethdebug.resources.types``. diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 2e2c160fc29a..21cd0e32f398 100644 --- a/liblangutil/CMakeLists.txt +++ b/liblangutil/CMakeLists.txt @@ -19,6 +19,10 @@ set(sources Scanner.cpp Scanner.h CharStreamProvider.h + SemanticDebugData.h + SemanticDebugDataSerialization.cpp + SemanticDebugDataSerialization.h + SemanticDebugDataTable.h SemVerHandler.cpp SemVerHandler.h SourceLocation.h diff --git a/liblangutil/EthdebugSchema.cpp b/liblangutil/EthdebugSchema.cpp index 9fe25101cc2f..2e4c8d837f3f 100644 --- a/liblangutil/EthdebugSchema.cpp +++ b/liblangutil/EthdebugSchema.cpp @@ -595,9 +595,7 @@ void schema::program::to_json(Json& _json, Context::Variable const& _contextVari { auto const numProperties = _contextVariable.identifier.has_value() + - _contextVariable.declaration.has_value() + - _contextVariable.type.has_value() + - _contextVariable.pointer.has_value(); + _contextVariable.declaration.has_value(); solRequire(numProperties >= 1, EthdebugException, "Context variable has no properties."); if (_contextVariable.identifier) { @@ -606,13 +604,6 @@ 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) - { - solAssert(!hasInternalExpression(*_contextVariable.pointer), "Context variable pointer contains a compiler-internal expression."); - _json["pointer"] = *_contextVariable.pointer; - } } void schema::program::to_json(Json& _json, Context const& _context) @@ -1357,202 +1348,3 @@ std::map schema::info::pointersFromJson( } return pointers; } - -namespace -{ - -/// Collects the free names of pointers, resolving template references. The -/// free names of a table template are computed once, with the template's -/// own parameters bound, and cached. -class FreeNameCollector -{ -public: - explicit FreeNameCollector(std::map const& _templates): - m_templates(_templates) - {} - - std::set collect(schema::Pointer const& _pointer) - { - std::set free; - std::set bound; - LocalTemplates localTemplates; - visit(_pointer, bound, localTemplates, free); - return free; - } - -private: - using LocalTemplates = std::map; - - /// 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; - }; - - void visit(schema::Pointer::Expression const& _expression, std::set const& _bound, std::set& _free) - { - using Pointer = schema::Pointer; - auto const operands = [&](Pointer::Operands const& _operands) { - for (Pointer::Expression const& operand: _operands) - visit(operand, _bound, _free); - }; - std::visit(util::GenericVisitor{ - // A Yul local is a dependency on the current Yul code by - // definition; nothing binds it. An unbound variable is one too. - [&](Pointer::YulLocal const& _yulLocal) { _free.insert(_yulLocal.name); }, - [&](Pointer::Variable const& _variable) - { - if (!_bound.count(_variable.identifier)) - _free.insert(_variable.identifier); - }, - [&](Pointer::Arithmetic const& _arithmetic) { operands(_arithmetic.operands); }, - [&](Pointer::Keccak256 const& _keccak256) { operands(_keccak256.operands); }, - [&](Pointer::Concat const& _concat) { operands(_concat.operands); }, - [&](Pointer::Resize const& _resize) { visit(*_resize.operand, _bound, _free); }, - // Literals, constants, lookups and reads: the latter two name - // regions, which live in a separate namespace. - [](auto const&) {} - }, _expression.value); - } - - void visit( - std::optional const& _expression, - std::set const& _bound, - std::set& _free - ) - { - if (_expression) - visit(*_expression, _bound, _free); - } - - void visit( - schema::Pointer const& _pointer, - std::set& _bound, - LocalTemplates& _localTemplates, - std::set& _free - ) - { - using Pointer = schema::Pointer; - auto const sub = [&](std::shared_ptr const& _sub) { - if (_sub) - visit(*_sub, _bound, _localTemplates, _free); - }; - std::visit(util::GenericVisitor{ - [&](Pointer::Region const& _region) - { - visit(_region.slot, _bound, _free); - visit(_region.offset, _bound, _free); - visit(_region.length, _bound, _free); - }, - [&](Pointer::Group const& _group) - { - for (Pointer const& member: _group.members) - visit(member, _bound, _localTemplates, _free); - }, - [&](Pointer::List const& _list) - { - visit(_list.count, _bound, _free); - ScopedNames index{_bound, {_list.each}}; - sub(_list.is); - }, - [&](Pointer::Conditional const& _conditional) - { - visit(_conditional.condition, _bound, _free); - sub(_conditional.then); - sub(_conditional.otherwise); - }, - [&](Pointer::Scope const& _scope) - { - // Definitions are ordered: each may reference the earlier ones. - std::vector definedNames; - for (auto const& [name, value]: _scope.definitions) - { - visit(value, _bound, _free); - definedNames.emplace_back(name); - } - ScopedNames defined{_bound, definedNames}; - sub(_scope.in); - }, - [&](Pointer::TemplateReference const& _reference) - { - for (std::string const& name: freeNamesOfTemplate(_reference.name, _localTemplates)) - if (!_bound.count(name)) - _free.insert(name); - }, - [&](Pointer::Templates const& _templates) - { - // Locally defined templates are visible to references inside the target. - std::vector added; - for (auto const& [name, definition]: _templates.templates) - if (_localTemplates.emplace(name, &definition).second) - added.emplace_back(name); - sub(_templates.in); - for (std::string const& name: added) - _localTemplates.erase(name); - } - }, _pointer.value); - } - - /// 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 {}; - - schema::Pointer::Template const* definition = nullptr; - bool fromTable = false; - if (auto const local = _localTemplates.find(_name); local != _localTemplates.end()) - definition = local->second; - else if (auto const it = m_templates.find(_name); it != m_templates.end()) - { - definition = &it->second; - 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; - { - ScopedNames parameters{bound, definition->expect}; - visit(*definition->body, bound, _localTemplates, free); - } - m_resolving.erase(_name); - - if (fromTable) - m_tableTemplateFreeNames.emplace(_name, free); - return free; - } - - std::map const& m_templates; - std::set m_resolving; - std::map> m_tableTemplateFreeNames; -}; - -} - -std::set schema::freeNames(Pointer const& _pointer, std::map const& _templates) -{ - return FreeNameCollector{_templates}.collect(_pointer); -} diff --git a/liblangutil/EthdebugSchema.h b/liblangutil/EthdebugSchema.h index 0889b6503937..a010722231a7 100644 --- a/liblangutil/EthdebugSchema.h +++ b/liblangutil/EthdebugSchema.h @@ -343,6 +343,13 @@ struct Pointer /// @returns whether @a _text names a region: an identifier or `~this`. static bool isRegionReference(std::string_view _text); + /// What a reader accepts beyond ethdebug/format. + struct ReadOptions + { + /// Accept the compiler-internal YulLocal expression. + bool internalExpressions = false; + }; + /// An unsigned number or `0x`-prefixed hex string. struct Literal { @@ -432,6 +439,20 @@ struct Pointer std::shared_ptr operand; }; + /// Compiler-internal, not part of ethdebug/format: a generated Yul local + /// standing in for a stack depth that is only known once Yul has been + /// compiled to EVM. Written as `{ "$$yulLocal": }`, a key no + /// ethdebug/format expression uses, whose own terms start with `~`, in the + /// compiler's sidecar only; public output rejects it (see + /// hasInternalExpression). + struct YulLocal + { + /// @a _name must not be empty. + explicit YulLocal(std::string _name); + + std::string name; + }; + /// ethdebug/format/pointer/expression struct Expression { @@ -444,7 +465,8 @@ struct Pointer Arithmetic, Keccak256, Concat, - Resize + Resize, + YulLocal > value; }; @@ -696,16 +718,20 @@ SourceRange sourceRangeFromJson(Json const& _json, std::string_view _path); Type typeFromJson(Json const& _json, std::string_view _path); Type::Wrapper wrapperFromJson(Json const& _json, std::string_view _path); -Pointer::Expression expressionFromJson(Json const& _json, std::string_view _path); -Pointer pointerFromJson(Json const& _json, std::string_view _path); -Pointer::Template templateFromJson(Json const& _json, std::string_view _path); +Pointer::Expression expressionFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options = {}); +Pointer pointerFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options = {}); +Pointer::Template templateFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options = {}); + +/// Whether @a _pointer, including the templates it defines locally, +/// contains a compiler-internal expression such as YulLocal. +bool hasInternalExpression(Pointer const& _pointer); namespace info { /// The type table of a resources object, keyed by type ID. std::map typesFromJson(Json const& _json, std::string_view _path); /// The pointer table of a resources object, keyed by template name. -std::map pointersFromJson(Json const& _json, std::string_view _path); +std::map pointersFromJson(Json const& _json, std::string_view _path, Pointer::ReadOptions _options = {}); } } diff --git a/liblangutil/SemanticDebugData.h b/liblangutil/SemanticDebugData.h new file mode 100644 index 000000000000..ad9ac8658b07 --- /dev/null +++ b/liblangutil/SemanticDebugData.h @@ -0,0 +1,105 @@ +/* + 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/compiler-guide/ethdebug_internal_debug_info.rst: the scope records that + * describe the variables of a source-language scope in ethdebug terms. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace solidity::langutil +{ + +/// Identifies a scope record: the AST ID of the source-language scope it was +/// generated from, together with an instance discriminator. Code generation +/// produces instance 0 only; passes that clone code give each copy its own +/// instance. In Yul text the instance travels in the `@ast-id-instance` +/// annotation next to `@ast-id`. +struct ScopeID +{ + int64_t astID = 0; + int64_t instance = 0; + + auto operator<=>(ScopeID const&) const = default; +}; + +/// 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 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, +}; + +/// A variable of a scope. @a pointer is present exactly when the phase is +/// Materialized; the sidecar reader and writer enforce this. +struct SemanticDebugVariable +{ + /// The source-level name. Synthetic bindings have none. + std::optional identifier; + /// The AST ID of the source-language declaration. Synthetic bindings have none. + std::optional declarationASTID; + /// The declaration's range, with the source ID of the ethdebug + /// compilation record. + std::optional declarationSourceRange; + /// The ID of the variable's type document in the type resources. + std::optional typeID; + SemanticDebugVariablePhase phase = SemanticDebugVariablePhase::OptimizedOut; + /// Where the value lives, in ethdebug pointer terms. Stack values are + /// expressed over generated Yul locals until Yul is compiled to EVM. + std::optional pointer; +}; + +/// A change of a variable's phase or pointer at the point where the scope +/// record is attached. Produced by optimizer passes, not by code generation. +/// At least one of @a phase and @a pointer is present. +struct SemanticDebugVariableUpdate +{ + int64_t variableASTID = 0; + std::optional phase; + std::optional pointer; +}; + +/// The record of one scope. Immutable once created and shared between the +/// Yul nodes that carry it and the side table. +struct SemanticDebugScope +{ + using ConstPtr = std::shared_ptr; + + std::vector variableDefinitions; + std::vector variableUpdates; +}; + +} diff --git a/liblangutil/SemanticDebugDataSerialization.cpp b/liblangutil/SemanticDebugDataSerialization.cpp new file mode 100644 index 000000000000..a4aa6d346053 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.cpp @@ -0,0 +1,370 @@ +/* + 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::langutil::ethdebug; + +namespace +{ + +using Phase = SemanticDebugVariablePhase; + +[[noreturn]] void invalid(std::string _message) +{ + BOOST_THROW_EXCEPTION(util::JsonValidationError() << util::errinfo_comment(std::move(_message))); +} + +std::string member(std::string_view _path, std::string_view _name) +{ + return std::string(_path) + "." + std::string(_name); +} + +std::string element(std::string_view _path, size_t _index) +{ + return std::string(_path) + "[" + std::to_string(_index) + "]"; +} + +/// Phase values use the compiler's dash-separated convention for +/// enumeration values, like `ast-id` in the debug info selection. +constexpr std::pair phaseNames[]{ + {Phase::Materialized, "materialized"}, + {Phase::Computed, "computed"}, + {Phase::OptimizedOut, "optimized-out"}, +}; + +std::string_view phaseName(Phase _phase) +{ + for (auto const& [phase, name]: phaseNames) + if (phase == _phase) + return name; + solAssert(false); +} + +Phase phaseFromJson(Json const& _json, std::string_view _path) +{ + std::string const name = util::valueOfType(_json, _path); + for (auto const& [phase, phaseName]: phaseNames) + if (name == phaseName) + return phase; + invalid(std::string(_path) + " has the unknown value \"" + name + "\"."); +} + +/// A materialized variable has a pointer; a computed or optimized-out +/// variable does not. +void requirePhaseConsistency(std::optional _phase, bool _hasPointer, std::string_view _path) +{ + if (!_phase) + return; + if (*_phase == Phase::Materialized && !_hasPointer) + invalid(std::string(_path) + " is materialized but has no pointer."); + if (*_phase != Phase::Materialized && _hasPointer) + invalid(std::string(_path) + " is " + std::string(phaseName(*_phase)) + " but has a pointer."); +} + +// Writing + +Json variableToJson(SemanticDebugVariable const& _variable) +{ + solAssert(!_variable.identifier || !_variable.identifier->empty(), "Variable identifier must not be empty."); + solAssert((_variable.phase == Phase::Materialized) == _variable.pointer.has_value(), "Variable phase and pointer disagree."); + Json result = Json::object(); + if (_variable.identifier) + result["identifier"] = *_variable.identifier; + if (_variable.declarationASTID) + result["declarationASTID"] = *_variable.declarationASTID; + if (_variable.declarationSourceRange) + result["declarationSourceRange"] = *_variable.declarationSourceRange; + if (_variable.typeID) + result["typeID"] = *_variable.typeID; + result["phase"] = phaseName(_variable.phase); + if (_variable.pointer) + result["pointer"] = *_variable.pointer; + return result; +} + +Json variableUpdateToJson(SemanticDebugVariableUpdate const& _update) +{ + solAssert(_update.phase || _update.pointer, "A variable update must change the phase or the pointer."); + solAssert(!_update.phase || (*_update.phase == Phase::Materialized) == _update.pointer.has_value(), "Update phase and pointer disagree."); + Json result{{"variableASTID", _update.variableASTID}}; + if (_update.phase) + result["phase"] = phaseName(*_update.phase); + if (_update.pointer) + result["pointer"] = *_update.pointer; + return result; +} + +Json scopeToJson(SemanticDebugScope const& _scope) +{ + Json result{{"variableDefinitions", Json::array()}}; + for (SemanticDebugVariable 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 (SemanticDebugVariableUpdate const& update: _scope.variableUpdates) + result["variableUpdates"].emplace_back(variableUpdateToJson(update)); + } + return result; +} + +// Reading + +/// The keys of the two-level scope map are the canonical decimal forms of +/// the AST ID and the instance: an optional sign, digits without leading +/// zeros, within the 64-bit range. +int64_t decimalKey(std::string const& _text, std::string_view _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; + if (!canonical) + invalid(std::string(_path) + " has the key \"" + _text + "\", which is not a decimal integer."); + + int64_t value = 0; + auto const [end, error] = std::from_chars(_text.data(), _text.data() + _text.size(), value); + if (error != std::errc{} || end != _text.data() + _text.size()) + invalid(std::string(_path) + " has the key \"" + _text + "\", which is out of range."); + return value; +} + +constexpr schema::Pointer::ReadOptions sidecarPointers{.internalExpressions = true}; + +std::optional optionalPointer(Json const& _json, std::string_view _path) +{ + if (Json const* pointer = util::optionalMember(_json, "pointer", _path)) + return schema::pointerFromJson(*pointer, member(_path, "pointer"), sidecarPointers); + return std::nullopt; +} + +SemanticDebugVariable variableFromJson(Json const& _json, std::string_view _path) +{ + util::requireOnlyMembers(_json, {"identifier", "declarationASTID", "declarationSourceRange", "typeID", "phase", "pointer"}, _path); + SemanticDebugVariable variable; + variable.identifier = util::optionalValue(_json, "identifier", _path); + if (variable.identifier && variable.identifier->empty()) + invalid(member(_path, "identifier") + " must not be empty."); + variable.declarationASTID = util::optionalValue(_json, "declarationASTID", _path); + if (Json const* range = util::optionalMember(_json, "declarationSourceRange", _path)) + variable.declarationSourceRange = schema::materials::sourceRangeFromJson(*range, member(_path, "declarationSourceRange")); + variable.typeID = util::optionalValue(_json, "typeID", _path); + variable.phase = phaseFromJson(util::requiredMember(_json, "phase", _path), member(_path, "phase")); + variable.pointer = optionalPointer(_json, _path); + requirePhaseConsistency(variable.phase, variable.pointer.has_value(), _path); + return variable; +} + +SemanticDebugVariableUpdate variableUpdateFromJson(Json const& _json, std::string_view _path) +{ + util::requireOnlyMembers(_json, {"variableASTID", "phase", "pointer"}, _path); + SemanticDebugVariableUpdate update; + update.variableASTID = util::requiredValue(_json, "variableASTID", _path); + if (Json const* phase = util::optionalMember(_json, "phase", _path)) + update.phase = phaseFromJson(*phase, member(_path, "phase")); + update.pointer = optionalPointer(_json, _path); + if (!update.phase && !update.pointer) + invalid(std::string(_path) + " must change the phase or the pointer."); + requirePhaseConsistency(update.phase, update.pointer.has_value(), _path); + return update; +} + +SemanticDebugScope scopeFromJson(Json const& _json, std::string_view _path) +{ + util::requireOnlyMembers(_json, {"variableDefinitions", "variableUpdates"}, _path); + SemanticDebugScope scope; + Json const& variables = util::requireArray(util::requiredMember(_json, "variableDefinitions", _path), member(_path, "variableDefinitions")); + for (size_t index = 0; index < variables.size(); ++index) + scope.variableDefinitions.emplace_back(variableFromJson(variables.at(index), element(member(_path, "variableDefinitions"), index))); + if (Json const* updates = util::optionalMember(_json, "variableUpdates", _path)) + { + util::requireArray(*updates, member(_path, "variableUpdates")); + for (size_t index = 0; index < updates->size(); ++index) + scope.variableUpdates.emplace_back(variableUpdateFromJson(updates->at(index), element(member(_path, "variableUpdates"), index))); + } + return scope; +} + +/// A template reference must resolve against a local templates block in the +/// same pointer document or against the sidecar's pointer table. +void requireResolvableTemplateReferences( + schema::Pointer const& _pointer, + std::set const& _localTemplates, + SemanticDebugDataTable const& _table, + std::string const& _path +) +{ + using Pointer = schema::Pointer; + auto const recurse = [&](std::shared_ptr const& _sub, std::string const& _subPath) { + if (_sub) + requireResolvableTemplateReferences(*_sub, _localTemplates, _table, _subPath); + }; + std::visit(util::GenericVisitor{ + [&](Pointer::TemplateReference const& _reference) + { + if (!_localTemplates.count(_reference.name) && !_table.findPointerTemplate(_reference.name)) + invalid(_path + " references the unknown pointer template \"" + _reference.name + "\"."); + }, + [&](Pointer::Templates const& _templates) + { + std::set localTemplates = _localTemplates; + for (auto const& [templateName, definition]: _templates.templates) + { + recurse(definition.body, member(member(_path, "templates"), templateName)); + localTemplates.insert(templateName); + } + requireResolvableTemplateReferences(*_templates.in, localTemplates, _table, member(_path, "in")); + }, + [&](Pointer::Group const& _group) + { + for (size_t index = 0; index < _group.members.size(); ++index) + requireResolvableTemplateReferences(_group.members[index], _localTemplates, _table, element(member(_path, "group"), index)); + }, + [&](Pointer::List const& _list) { recurse(_list.is, member(member(_path, "list"), "is")); }, + [&](Pointer::Conditional const& _conditional) + { + recurse(_conditional.then, member(_path, "then")); + recurse(_conditional.otherwise, member(_path, "else")); + }, + [&](Pointer::Scope const& _scope) { recurse(_scope.in, member(_path, "in")); }, + [](Pointer::Region const&) {} + }, _pointer.value); +} + +void requireResolvableTemplateReferences(SemanticDebugDataTable const& _table) +{ + for (auto const& [templateName, definition]: _table.pointerTemplates()) + requireResolvableTemplateReferences(*definition.body, {}, _table, member("sidecar.resources.pointers", templateName) + ".for"); + for (auto const& [id, scope]: _table.scopes()) + { + std::string const path = "sidecar.scopes." + std::to_string(id.astID) + "." + std::to_string(id.instance); + for (size_t index = 0; index < scope->variableDefinitions.size(); ++index) + if (auto const& pointer = scope->variableDefinitions[index].pointer) + requireResolvableTemplateReferences(*pointer, {}, _table, member(element(member(path, "variableDefinitions"), index), "pointer")); + for (size_t index = 0; index < scope->variableUpdates.size(); ++index) + if (auto const& pointer = scope->variableUpdates[index].pointer) + requireResolvableTemplateReferences(*pointer, {}, _table, member(element(member(path, "variableUpdates"), index), "pointer")); + } +} + +} + +Json langutil::semanticDebugDataToJson(SemanticDebugDataTable const& _table) +{ + Json result{ + {"format", std::string(SemanticDebugDataFormat)}, + {"version", SemanticDebugDataFormatVersion}, + }; + if (_table.contractName()) + result["contractName"] = *_table.contractName(); + + // The resource tables mirror ethdebug.resources; the templates may carry + // internal expressions here, unlike in the public output. + 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] = 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& [id, scope]: _table.scopes()) + { + solAssert(scope, "Semantic debug data table contains a null scope record."); + scopes[std::to_string(id.astID)][std::to_string(id.instance)] = scopeToJson(*scope); + } + result["scopes"] = std::move(scopes); + return result; +} + +SemanticDebugDataTable langutil::semanticDebugDataFromJson(Json const& _json) +{ + std::string const root = "sidecar"; + util::requireOnlyMembers(_json, {"format", "version", "contractName", "resources", "scopes"}, root); + std::string const format = util::requiredValue(_json, "format", root); + if (format != SemanticDebugDataFormat) + invalid("Unsupported semantic debug data format \"" + format + "\"."); + unsigned const version = util::requiredValue(_json, "version", root); + if (version != SemanticDebugDataFormatVersion) + invalid("Unsupported semantic debug data format version " + std::to_string(version) + "."); + + SemanticDebugDataTable table; + if (std::optional contractName = util::optionalValue(_json, "contractName", root)) + table.setContractName(std::move(*contractName)); + + if (Json const* resources = util::optionalMember(_json, "resources", root)) + { + std::string const resourcesPath = member(root, "resources"); + util::requireOnlyMembers(*resources, {"types", "pointers"}, resourcesPath); + if (Json const* types = util::optionalMember(*resources, "types", resourcesPath)) + for (auto& [id, document]: schema::info::typesFromJson(*types, member(resourcesPath, "types"))) + table.setType(id, std::move(document)); + // Pointer templates are public resources, published verbatim: an + // expression internal to the compiler has no place in them, so the + // reader does not accept one there. + if (Json const* pointers = util::optionalMember(*resources, "pointers", resourcesPath)) + for (auto& [name, definition]: schema::info::pointersFromJson(*pointers, member(resourcesPath, "pointers"))) + table.setPointerTemplate(name, std::move(definition)); + } + + Json const& scopes = util::requireObject(util::requiredMember(_json, "scopes", root), member(root, "scopes")); + for (auto const& [astIDText, instances]: scopes.items()) + { + std::string const astIDPath = member(member(root, "scopes"), astIDText); + int64_t const astID = decimalKey(astIDText, member(root, "scopes")); + util::requireObject(instances, astIDPath); + for (auto const& [instanceText, scope]: instances.items()) + { + int64_t const instance = decimalKey(instanceText, astIDPath); + if (instance < 0) + invalid(astIDPath + " has the key \"" + instanceText + "\", which must not be negative."); + table.set( + ScopeID{astID, instance}, + std::make_shared(scopeFromJson(scope, member(astIDPath, instanceText))) + ); + } + } + + requireResolvableTemplateReferences(table); + return table; +} diff --git a/liblangutil/SemanticDebugDataSerialization.h b/liblangutil/SemanticDebugDataSerialization.h new file mode 100644 index 000000000000..02b3dcb37c96 --- /dev/null +++ b/liblangutil/SemanticDebugDataSerialization.h @@ -0,0 +1,54 @@ +/* + 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 JSON sidecar that carries the semantic debug info side table across a + * textual Yul boundary, as specified in + * docs/compiler-guide/ethdebug_internal_debug_info.rst. + */ + +#pragma once + +#include + +#include + +#include + +namespace solidity::langutil +{ + +constexpr std::string_view SemanticDebugDataFormat = "solidity-ethdebug-semantic-data"; +constexpr unsigned SemanticDebugDataFormatVersion = 1; + +/// Writes @a _table as the sidecar: the format and version, the contract +/// name, the resource tables as ethdebug.resources has them, and the +/// scope records under the decimal AST ID and instance of their scope ID. +/// Throws ethdebug::schema::EthdebugException for a record that cannot be +/// expressed, which the producer never creates. +Json semanticDebugDataToJson(SemanticDebugDataTable const& _table); + +/// Reads a sidecar written by semanticDebugDataToJson, checking it against +/// the format: throws util::JsonValidationError, whose message names the +/// offending location, for an unknown format or version, a malformed +/// member, a scope key that is not a decimal integer in range, a variable +/// whose phase and pointer disagree, a template reference that resolves +/// neither locally nor in the pointer table, or a compiler-internal +/// expression in the pointer table. +SemanticDebugDataTable semanticDebugDataFromJson(Json const& _json); + +} diff --git a/liblangutil/SemanticDebugDataTable.h b/liblangutil/SemanticDebugDataTable.h new file mode 100644 index 000000000000..1aaf332b33b7 --- /dev/null +++ b/liblangutil/SemanticDebugDataTable.h @@ -0,0 +1,100 @@ +/* + 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 their scope ID, + * together with the type and pointer template resources they reference. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace solidity::langutil +{ + +class SemanticDebugDataTable +{ +public: + using Types = std::map; + using PointerTemplates = std::map; + + void setContractName(std::string _contractName) { m_contractName = std::move(_contractName); } + std::optional const& contractName() const { return m_contractName; } + + void set(ScopeID _id, SemanticDebugScope::ConstPtr _scope) { m_scopes[_id] = std::move(_scope); } + SemanticDebugScope::ConstPtr find(ScopeID _id) const + { + auto const it = m_scopes.find(_id); + return it == m_scopes.end() ? nullptr : it->second; + } + std::map const& scopes() const { return m_scopes; } + + /// Type documents keyed by the compiler's type identifier: the same table + /// that ethdebug.resources.types publishes. + void setType(std::string _id, ethdebug::schema::Type _document) { m_types.insert_or_assign(std::move(_id), std::move(_document)); } + ethdebug::schema::Type const* findType(std::string const& _id) const + { + auto const it = m_types.find(_id); + return it == m_types.end() ? nullptr : &it->second; + } + Types const& types() const { return m_types; } + + /// Pointer templates keyed by producer-defined name: the same table that + /// ethdebug.resources.pointers publishes. Variables reference an entry + /// with the pointer schema's `template` reference. + void setPointerTemplate(std::string _name, ethdebug::schema::Pointer::Template _template) + { + m_pointerTemplates.insert_or_assign(std::move(_name), std::move(_template)); + } + ethdebug::schema::Pointer::Template const* findPointerTemplate(std::string const& _name) const + { + auto const it = m_pointerTemplates.find(_name); + return it == m_pointerTemplates.end() ? nullptr : &it->second; + } + PointerTemplates 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& [id, scope]: _other.m_scopes) + m_scopes[id] = scope; + for (auto const& [id, document]: _other.m_types) + m_types.insert_or_assign(id, document); + for (auto const& [name, definition]: _other.m_pointerTemplates) + m_pointerTemplates.insert_or_assign(name, definition); + } + + bool empty() const { return m_scopes.empty() && m_types.empty() && m_pointerTemplates.empty(); } + +private: + std::optional m_contractName; + std::map m_scopes; + Types m_types; + PointerTemplates m_pointerTemplates; +}; + +} diff --git a/test/liblangutil/EthdebugDocumentTest.cpp b/test/liblangutil/EthdebugDocumentTest.cpp index 92f6c23ef82a..34dcc125c2d8 100644 --- a/test/liblangutil/EthdebugDocumentTest.cpp +++ b/test/liblangutil/EthdebugDocumentTest.cpp @@ -19,6 +19,7 @@ #include #include +#include #include @@ -38,7 +39,7 @@ EthdebugDocumentTest::EthdebugDocumentTest(std::string const& _filename): { m_source = m_reader.source(); m_expectation = m_reader.simpleExpectations(); - m_document = m_reader.enumSetting("document", {{"resources", Document::Resources}}, "resources"); + m_document = m_reader.enumSetting("document", {{"resources", Document::Resources}, {"sidecar", Document::Sidecar}}, "resources"); if (!_filename.ends_with(".ethdebugjson")) BOOST_THROW_EXCEPTION(std::runtime_error("Not an ethdebug document test: \"" + _filename + "\". Expected extension: .ethdebugjson.")); } @@ -71,6 +72,9 @@ frontend::test::TestCase::TestResult EthdebugDocumentTest::run(std::ostream& _st output = Json{{"types", serialized["types"]}, {"pointers", serialized["pointers"]}}; break; } + case Document::Sidecar: + output = semanticDebugDataToJson(semanticDebugDataFromJson(input)); + break; } m_obtainedResult = util::jsonPrint(output, util::JsonFormat{util::JsonFormat::Pretty, 4}) + "\n"; } diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_computed_with_pointer.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_computed_with_pointer.ethdebugjson new file mode 100644 index 000000000000..4a173eb16a47 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_computed_with_pointer.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [{"phase": "computed", "pointer": {"location": "stack", "slot": 0}}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableDefinitions[0] is computed but has a pointer. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_empty_identifier.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_empty_identifier.ethdebugjson new file mode 100644 index 000000000000..3ded03456195 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_empty_identifier.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [{"identifier": "", "phase": "computed"}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableDefinitions[0].identifier must not be empty. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_instance_negative.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_instance_negative.ethdebugjson new file mode 100644 index 000000000000..e0cc134aa3cc --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_instance_negative.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"-1": {"variableDefinitions": []}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1 has the key "-1", which must not be negative. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_internal_expression_in_resources.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_internal_expression_in_resources.ethdebugjson new file mode 100644 index 000000000000..2290d4e4c565 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_internal_expression_in_resources.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "resources": {"pointers": {"p": {"expect": [], "for": {"location": "stack", "slot": {"$$yulLocal": "x"}}}}}, "scopes": {}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.resources.pointers.p.for.slot uses the compiler-internal expression key "$$yulLocal", which is not accepted here. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_materialized_without_pointer.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_materialized_without_pointer.ethdebugjson new file mode 100644 index 000000000000..d1ce38632079 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_materialized_without_pointer.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [{"phase": "materialized"}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableDefinitions[0] is materialized but has no pointer. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_missing_scopes.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_missing_scopes.ethdebugjson new file mode 100644 index 000000000000..c867d0c15494 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_missing_scopes.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes is required. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_negative_zero.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_negative_zero.ethdebugjson new file mode 100644 index 000000000000..c89fc917ba7b --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_negative_zero.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"-0": {"0": {"variableDefinitions": []}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes has the key "-0", which is not a decimal integer. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_not_decimal.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_not_decimal.ethdebugjson new file mode 100644 index 000000000000..4a3e7cb4178f --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_not_decimal.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"01": {"0": {"variableDefinitions": []}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes has the key "01", which is not a decimal integer. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_out_of_range.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_out_of_range.ethdebugjson new file mode 100644 index 000000000000..6b1bdbc9df72 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_scope_key_out_of_range.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"99999999999999999999": {"0": {"variableDefinitions": []}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes has the key "99999999999999999999", which is out of range. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_format.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_format.ethdebugjson new file mode 100644 index 000000000000..72a42577b470 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_format.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "something-else", "version": 1, "scopes": {}} +// ==== +// document: sidecar +// ---- +// Error: Unsupported semantic debug data format "something-else". diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_member.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_member.ethdebugjson new file mode 100644 index 000000000000..779f1197ee41 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_member.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {}, "extra": true} +// ==== +// document: sidecar +// ---- +// Error: sidecar has an unknown member "extra". diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_phase.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_phase.ethdebugjson new file mode 100644 index 000000000000..9c2000781405 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unknown_phase.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [{"phase": "gone"}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableDefinitions[0].phase has the unknown value "gone". diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_unresolved_template.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unresolved_template.ethdebugjson new file mode 100644 index 000000000000..49ce2aac4c24 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unresolved_template.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [{"phase": "materialized", "pointer": {"template": "missing"}}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableDefinitions[0].pointer references the unknown pointer template "missing". diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_unsupported_version.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unsupported_version.ethdebugjson new file mode 100644 index 000000000000..d083d75c8488 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_unsupported_version.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 2, "scopes": {}} +// ==== +// document: sidecar +// ---- +// Error: Unsupported semantic debug data format version 2. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_error_update_without_change.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_error_update_without_change.ethdebugjson new file mode 100644 index 000000000000..07814f7cabf1 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_error_update_without_change.ethdebugjson @@ -0,0 +1,5 @@ +{"format": "solidity-ethdebug-semantic-data", "version": 1, "scopes": {"1": {"0": {"variableDefinitions": [], "variableUpdates": [{"variableASTID": 3}]}}}} +// ==== +// document: sidecar +// ---- +// Error: sidecar.scopes.1.0.variableUpdates[0] must change the phase or the pointer. diff --git a/test/liblangutil/ethdebugDocumentTests/sidecar_round_trip.ethdebugjson b/test/liblangutil/ethdebugDocumentTests/sidecar_round_trip.ethdebugjson new file mode 100644 index 000000000000..7204b2bfd407 --- /dev/null +++ b/test/liblangutil/ethdebugDocumentTests/sidecar_round_trip.ethdebugjson @@ -0,0 +1,214 @@ +{ + "format": "solidity-ethdebug-semantic-data", + "version": 1, + "contractName": "C", + "resources": { + "types": { + "t_uint256": {"kind": "uint", "bits": 256}, + "t_mapping": {"kind": "mapping", "contains": {"key": {"type": {"id": "t_uint256"}}, "value": {"type": {"id": "t_uint256"}}}} + }, + "pointers": { + "storage_7_3": {"expect": [], "for": {"name": "counter", "location": "storage", "slot": "0x00"}}, + "storage_7_5": {"expect": ["key"], "for": {"name": "balances", "location": "storage", "slot": {"~keccak256": [{"~wordsized": "key"}, {"~wordsized": "0x01"}]}}} + } + }, + "scopes": { + "7": { + "0": { + "variableDefinitions": [ + {"identifier": "counter", "declarationASTID": 3, "declarationSourceRange": {"source": {"id": 0}, "range": {"offset": 20, "length": 15}}, "typeID": "t_uint256", "phase": "materialized", "pointer": {"template": "storage_7_3"}}, + {"identifier": "balances", "declarationASTID": 5, "typeID": "t_mapping", "phase": "materialized", "pointer": {"template": "storage_7_5"}} + ] + } + }, + "12": { + "0": { + "variableDefinitions": [ + {"identifier": "argument", "declarationASTID": 9, "typeID": "t_uint256", "phase": "materialized", "pointer": {"name": "argument", "location": "stack", "slot": {"$$yulLocal": "var_argument_9"}}}, + {"identifier": "result", "declarationASTID": 11, "typeID": "t_uint256", "phase": "computed"}, + {"declarationASTID": 13, "phase": "optimized-out"} + ] + }, + "2": { + "variableDefinitions": [ + {"identifier": "argument", "declarationASTID": 9, "phase": "materialized", "pointer": {"templates": {"slot": {"expect": ["at"], "for": {"location": "stack", "slot": "at"}}}, "in": {"template": "slot", "yields": {}}}} + ], + "variableUpdates": [ + {"variableASTID": 9, "phase": "optimized-out"}, + {"variableASTID": 11, "phase": "materialized", "pointer": {"location": "stack", "slot": {"$$yulLocal": "var_result_11"}}} + ] + } + }, + "-1": {"0": {"variableDefinitions": []}} + } +} +// ==== +// document: sidecar +// ---- +// { +// "contractName": "C", +// "format": "solidity-ethdebug-semantic-data", +// "resources": { +// "pointers": { +// "storage_7_3": { +// "expect": [], +// "for": { +// "location": "storage", +// "name": "counter", +// "slot": "0x00" +// } +// }, +// "storage_7_5": { +// "expect": [ +// "key" +// ], +// "for": { +// "location": "storage", +// "name": "balances", +// "slot": { +// "~keccak256": [ +// { +// "~wordsized": "key" +// }, +// { +// "~wordsized": "0x01" +// } +// ] +// } +// } +// } +// }, +// "types": { +// "t_mapping": { +// "contains": { +// "key": { +// "type": { +// "id": "t_uint256" +// } +// }, +// "value": { +// "type": { +// "id": "t_uint256" +// } +// } +// }, +// "kind": "mapping" +// }, +// "t_uint256": { +// "bits": 256, +// "kind": "uint" +// } +// } +// }, +// "scopes": { +// "-1": { +// "0": { +// "variableDefinitions": [] +// } +// }, +// "12": { +// "0": { +// "variableDefinitions": [ +// { +// "declarationASTID": 9, +// "identifier": "argument", +// "phase": "materialized", +// "pointer": { +// "location": "stack", +// "name": "argument", +// "slot": { +// "$$yulLocal": "var_argument_9" +// } +// }, +// "typeID": "t_uint256" +// }, +// { +// "declarationASTID": 11, +// "identifier": "result", +// "phase": "computed", +// "typeID": "t_uint256" +// }, +// { +// "declarationASTID": 13, +// "phase": "optimized-out" +// } +// ] +// }, +// "2": { +// "variableDefinitions": [ +// { +// "declarationASTID": 9, +// "identifier": "argument", +// "phase": "materialized", +// "pointer": { +// "in": { +// "template": "slot" +// }, +// "templates": { +// "slot": { +// "expect": [ +// "at" +// ], +// "for": { +// "location": "stack", +// "slot": "at" +// } +// } +// } +// } +// } +// ], +// "variableUpdates": [ +// { +// "phase": "optimized-out", +// "variableASTID": 9 +// }, +// { +// "phase": "materialized", +// "pointer": { +// "location": "stack", +// "slot": { +// "$$yulLocal": "var_result_11" +// } +// }, +// "variableASTID": 11 +// } +// ] +// } +// }, +// "7": { +// "0": { +// "variableDefinitions": [ +// { +// "declarationASTID": 3, +// "declarationSourceRange": { +// "range": { +// "length": 15, +// "offset": 20 +// }, +// "source": { +// "id": 0 +// } +// }, +// "identifier": "counter", +// "phase": "materialized", +// "pointer": { +// "template": "storage_7_3" +// }, +// "typeID": "t_uint256" +// }, +// { +// "declarationASTID": 5, +// "identifier": "balances", +// "phase": "materialized", +// "pointer": { +// "template": "storage_7_5" +// }, +// "typeID": "t_mapping" +// } +// ] +// } +// } +// }, +// "version": 1 +// }