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/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/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/libevmasm/EthdebugSchema.cpp b/libevmasm/EthdebugSchema.cpp deleted file mode 100644 index a38147c45326..000000000000 --- a/libevmasm/EthdebugSchema.cpp +++ /dev/null @@ -1,661 +0,0 @@ -/* - 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::evmasm::ethdebug; - -std::map const schema::materials::Reference::typeNames = { - {Type::Compilation, "compilation"}, - {Type::Source, "source"}, -}; - -std::map const schema::Type::Function::visibilityNames = { - {Visibility::Internal, "internal"}, - {Visibility::External, "external"}, -}; - -std::map const schema::Pointer::constantNames = { - {Constant::WordSize, "~wordsize"}, -}; - -std::map const schema::Pointer::Lookup::propertyNames = { - {Property::Slot, ".slot"}, - {Property::Offset, ".offset"}, - {Property::Length, ".length"}, -}; - -std::map const schema::Pointer::Arithmetic::operatorNames = { - {Operator::Sum, "~sum"}, - {Operator::Difference, "~difference"}, - {Operator::Product, "~product"}, - {Operator::Quotient, "~quotient"}, - {Operator::Remainder, "~remainder"}, -}; - -std::map const schema::Pointer::locationNames = { - {Location::Stack, "stack"}, - {Location::Memory, "memory"}, - {Location::Storage, "storage"}, - {Location::Calldata, "calldata"}, - {Location::Returndata, "returndata"}, - {Location::Transient, "transient"}, - {Location::Code, "code"}, -}; - -std::map const schema::Program::environmentNames = { - {Environment::CALL, "call"}, - {Environment::CREATE, "create"}, -}; - -schema::Type::Specifier::Specifier(std::shared_ptr _type): - value(std::move(_type)) -{ - solAssert(std::get>(value), "Type specifier without a type."); -} - -schema::Type::Definition::Definition(std::optional _name, std::optional _location): - name(std::move(_name)), - location(std::move(_location)) -{ - solAssert(name || location, "Type definition without properties."); -} - -schema::Type::UInt::UInt(unsigned _bits): - bits(_bits) -{ - solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); -} - -schema::Type::Int::Int(unsigned _bits): - bits(_bits) -{ - solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); -} - -schema::Type::UFixed::UFixed(unsigned _bits, unsigned _places): - bits(_bits), - places(_places) -{ - solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); - solAssert(isValidPlaces(places), "Fixed point type must have between 1 and 80 decimal places."); -} - -schema::Type::Fixed::Fixed(unsigned _bits, unsigned _places): - bits(_bits), - places(_places) -{ - solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); - solAssert(isValidPlaces(places), "Fixed point type must have between 1 and 80 decimal places."); -} - -bool schema::Pointer::isIdentifier(std::string_view _text) -{ - auto const isStart = [](char _c) { return std::isalpha(static_cast(_c)) || _c == '_' || _c == '-' || _c == '$'; }; - auto const isRest = [&](char _c) { return isStart(_c) || std::isdigit(static_cast(_c)); }; - if (_text.empty() || !isStart(_text.front())) - return false; - for (char const c: _text) - if (!isRest(c)) - return false; - return true; -} - -bool schema::Pointer::isRegionReference(std::string_view _text) -{ - return _text == "~this" || isIdentifier(_text); -} - -schema::Pointer::Variable::Variable(std::string _identifier): - identifier(std::move(_identifier)) -{ - solAssert(isIdentifier(identifier), "Pointer expression variable \"" + identifier + "\" is not an identifier."); -} - -schema::Pointer::Lookup::Lookup(Property _property, std::string _region): - property(_property), - region(std::move(_region)) -{ - solAssert(isRegionReference(region), "Region reference \"" + region + "\" is not an identifier."); -} - -schema::Pointer::Read::Read(std::string _region): - region(std::move(_region)) -{ - solAssert(isRegionReference(region), "Region reference \"" + region + "\" is not an identifier."); -} - -schema::Pointer::Arithmetic::Arithmetic(Operator _op, Operands _operands): - op(_op), - operands(std::move(_operands)) -{ - if (op == Operator::Difference || op == Operator::Quotient || op == Operator::Remainder) - solAssert(operands.size() == 2, "Difference, quotient and remainder take exactly two operands."); -} - -schema::Pointer::Resize::Resize(std::optional _size, std::shared_ptr _operand): - size(_size), - operand(std::move(_operand)) -{ - solAssert(!size || *size > 0, "Resize expression needs a positive byte width."); - solAssert(operand, "Resize expression without an operand."); -} - -schema::Pointer::Region::Region( - std::optional _name, - Location _location, - std::optional _slot, - std::optional _offset, - std::optional _length -): - name(std::move(_name)), - location(_location), - slot(std::move(_slot)), - offset(std::move(_offset)), - length(std::move(_length)) -{ - if (name) - solAssert(isIdentifier(*name), "Region name \"" + *name + "\" is not an identifier."); - switch (location) - { - case Location::Stack: - case Location::Storage: - case Location::Transient: - // Word-oriented locations address by slot. - solAssert(slot, "A stack, storage or transient region must address its slot."); - break; - case Location::Memory: - case Location::Calldata: - case Location::Returndata: - case Location::Code: - // Byte-oriented locations address by offset and length. - solAssert(!slot && offset && length, "A memory, calldata, returndata or code region must address its offset and length."); - break; - } -} - -schema::Pointer::Group::Group(std::vector _members): - members(std::move(_members)) -{ - solAssert(!members.empty(), "A group pointer must have at least one member."); -} - -schema::Pointer::List::List(Expression _count, std::string _each, std::shared_ptr _is): - count(std::move(_count)), - each(std::move(_each)), - is(std::move(_is)) -{ - solAssert(isIdentifier(each), "List index name \"" + each + "\" is not an identifier."); - solAssert(is, "List element pointer is missing."); -} - -schema::Pointer::Conditional::Conditional( - Expression _condition, - std::shared_ptr _then, - std::shared_ptr _otherwise -): - condition(std::move(_condition)), - then(std::move(_then)), - otherwise(std::move(_otherwise)) -{ - solAssert(then, "Conditional consequent is missing."); -} - -schema::Pointer::Scope::Scope(std::vector> _definitions, std::shared_ptr _in): - definitions(std::move(_definitions)), - in(std::move(_in)) -{ - solAssert(!definitions.empty(), "A scope pointer must define at least one variable."); - for (auto const& [variable, expression]: definitions) - solAssert(isIdentifier(variable), "Scope variable \"" + variable + "\" is not an identifier."); - solAssert(in, "Scope target pointer is missing."); -} - -schema::Pointer::TemplateReference::TemplateReference(std::string _name, std::vector> _yields): - name(std::move(_name)), - yields(std::move(_yields)) -{ - solAssert(isIdentifier(name), "Template name \"" + name + "\" is not an identifier."); - for (auto const& [producedName, newName]: yields) - { - solAssert(isIdentifier(producedName), "Yielded region name \"" + producedName + "\" is not an identifier."); - solAssert(isIdentifier(newName), "Yielded region name \"" + newName + "\" is not an identifier."); - } -} - -schema::Pointer::Template::Template(std::vector _expect, std::shared_ptr _body): - expect(std::move(_expect)), - body(std::move(_body)) -{ - for (std::string const& parameter: expect) - solAssert(isIdentifier(parameter), "Template parameter \"" + parameter + "\" is not an identifier."); - solAssert(body, "Pointer template without a body."); -} - -schema::Pointer::Templates::Templates(std::vector> _templates, std::shared_ptr _in): - templates(std::move(_templates)), - in(std::move(_in)) -{ - for (auto const& [templateName, definition]: templates) - solAssert(isIdentifier(templateName), "Template name \"" + templateName + "\" is not an identifier."); - solAssert(in, "Templates target pointer is missing."); -} - - -void schema::data::to_json(Json& _json, HexValue const& _hexValue) -{ - _json = util::toHex(_hexValue.value, util::HexPrefix::Add); -} - -void schema::data::to_json(Json& _json, Unsigned const& _unsigned) -{ - std::visit(util::GenericVisitor{ - [&](HexValue const& _hexValue) { _json = _hexValue; }, - [&](std::uint64_t const _value) { _json = _value; } - }, _unsigned.value); -} - -void schema::data::to_json(Json& _json, Stamp const& _stamp) -{ - _json["schema"] = _stamp.schema; - _json["version"] = _stamp.version; -} - -void schema::materials::to_json(Json& _json, ID const& _id) -{ - std::visit(util::GenericVisitor{ - [&](std::string const& _hexValue) { _json = _hexValue; }, - [&](std::uint64_t const _value) { _json = _value; } - }, _id.value); -} - -void schema::materials::to_json(Json& _json, Reference const& _source) -{ - _json["id"] = _source.id; - if (_source.type) - _json["type"] = Reference::typeNames.at(*_source.type); -} - -void schema::materials::to_json(Json& _json, SourceRange::Range const& _range) -{ - _json["length"] = _range.length; - _json["offset"] = _range.offset; -} - - -void schema::materials::to_json(Json& _json, SourceRange const& _sourceRange) -{ - _json["source"] = _sourceRange.source; - if (_sourceRange.range) - _json["range"] = *_sourceRange.range; -} - -void schema::materials::to_json(Json& _json, Source const& _source) -{ - _json["id"] = _source.id; - _json["path"] = _source.path; - _json["contents"] = _source.contents; - if (_source.encoding) - _json["encoding"] = *_source.encoding; - _json["language"] = _source.language; -} - -void schema::materials::to_json(Json& _json, Compilation::Compiler const& _compiler) -{ - _json["name"] = _compiler.name; - _json["version"] = _compiler.version; -} - -void schema::materials::to_json(Json& _json, Compilation const& _compilation) -{ - _json["id"] = _compilation.id; - _json["compiler"] = _compilation.compiler; - if (_compilation.settings) - _json["settings"] = *_compilation.settings; - _json["sources"] = _compilation.sources; -} - -void schema::to_json(Json& _json, Type::Reference const& _reference) -{ - _json = Json::object(); - _json["id"] = _reference.id; -} - -void schema::to_json(Json& _json, Type::Specifier const& _specifier) -{ - std::visit(util::GenericVisitor{ - [&](Type::Reference const& _reference) { _json = _reference; }, - [&](std::shared_ptr const& _type) { _json = *_type; } - }, _specifier.value); -} - -void schema::to_json(Json& _json, Type::Wrapper const& _wrapper) -{ - _json = Json::object(); - if (_wrapper.name) - _json["name"] = *_wrapper.name; - _json["type"] = _wrapper.type; -} - -void schema::to_json(Json& _json, Type::Definition const& _definition) -{ - _json = Json::object(); - if (_definition.name) - _json["name"] = *_definition.name; - if (_definition.location) - _json["location"] = *_definition.location; -} - -void schema::to_json(Json& _json, Type const& _type) -{ - _json = Json::object(); - std::visit(util::GenericVisitor{ - [&](Type::UInt const& _uint) - { - _json["kind"] = "uint"; - _json["bits"] = _uint.bits; - }, - [&](Type::Int const& _int) - { - _json["kind"] = "int"; - _json["bits"] = _int.bits; - }, - [&](Type::Bool const&) { _json["kind"] = "bool"; }, - [&](Type::Bytes const& _bytes) - { - _json["kind"] = "bytes"; - if (_bytes.size) - _json["size"] = *_bytes.size; - }, - [&](Type::String const& _string) - { - _json["kind"] = "string"; - if (_string.encoding) - _json["encoding"] = *_string.encoding; - }, - [&](Type::UFixed const& _ufixed) - { - _json["kind"] = "ufixed"; - _json["bits"] = _ufixed.bits; - _json["places"] = _ufixed.places; - }, - [&](Type::Fixed const& _fixed) - { - _json["kind"] = "fixed"; - _json["bits"] = _fixed.bits; - _json["places"] = _fixed.places; - }, - [&](Type::Address const& _address) - { - _json["kind"] = "address"; - if (_address.payable) - _json["payable"] = *_address.payable; - }, - [&](Type::Contract const& _contract) - { - _json["kind"] = "contract"; - if (_contract.payable) - _json["payable"] = *_contract.payable; - if (_contract.kind == Type::Contract::Kind::Library) - _json["library"] = true; - else if (_contract.kind == Type::Contract::Kind::Interface) - _json["interface"] = true; - if (_contract.definition) - _json["definition"] = *_contract.definition; - }, - [&](Type::Enum const& _enum) - { - _json["kind"] = "enum"; - _json["values"] = _enum.values; - if (_enum.definition) - _json["definition"] = *_enum.definition; - }, - [&](Type::Alias const& _alias) - { - _json["kind"] = "alias"; - _json["contains"] = _alias.contains; - if (_alias.definition) - _json["definition"] = *_alias.definition; - }, - [&](Type::Array const& _array) - { - _json["kind"] = "array"; - _json["contains"] = _array.contains; - if (_array.count) - _json["count"] = *_array.count; - }, - [&](Type::Mapping const& _mapping) - { - _json["kind"] = "mapping"; - _json["contains"] = Json{{"key", _mapping.key}, {"value", _mapping.value}}; - }, - [&](Type::Struct const& _struct) - { - _json["kind"] = "struct"; - _json["contains"] = _struct.contains; - if (_struct.definition) - _json["definition"] = *_struct.definition; - }, - [&](Type::Tuple const& _tuple) - { - _json["kind"] = "tuple"; - _json["contains"] = _tuple.contains; - }, - [&](Type::Function const& _function) - { - _json["kind"] = "function"; - _json[Type::Function::visibilityNames.at(_function.visibility)] = true; - Json contains{{"parameters", _function.parameters}}; - if (_function.returns) - contains["returns"] = *_function.returns; - _json["contains"] = std::move(contains); - if (_function.definition) - _json["definition"] = *_function.definition; - } - }, _type.value); -} - -void schema::to_json(Json& _json, Pointer::Expression const& _expression) -{ - std::visit(util::GenericVisitor{ - [&](Pointer::Literal const& _literal) { _json = _literal.value; }, - [&](Pointer::Variable const& _variable) { _json = _variable.identifier; }, - [&](Pointer::Constant const _constant) { _json = Pointer::constantNames.at(_constant); }, - [&](Pointer::Lookup const& _lookup) - { - _json = Json{{Pointer::Lookup::propertyNames.at(_lookup.property), _lookup.region}}; - }, - [&](Pointer::Read const& _read) { _json = Json{{"~read", _read.region}}; }, - [&](Pointer::Arithmetic const& _arithmetic) - { - _json = Json{{Pointer::Arithmetic::operatorNames.at(_arithmetic.op), _arithmetic.operands}}; - }, - [&](Pointer::Keccak256 const& _keccak256) { _json = Json{{"~keccak256", _keccak256.operands}}; }, - [&](Pointer::Concat const& _concat) { _json = Json{{"~concat", _concat.operands}}; }, - [&](Pointer::Resize const& _resize) - { - if (_resize.size) - _json = Json{{"~sized" + std::to_string(*_resize.size), *_resize.operand}}; - else - _json = Json{{"~wordsized", *_resize.operand}}; - } - }, _expression.value); -} - -void schema::to_json(Json& _json, Pointer::Region const& _region) -{ - _json = Json::object(); - if (_region.name) - _json["name"] = *_region.name; - _json["location"] = Pointer::locationNames.at(_region.location); - if (_region.slot) - _json["slot"] = *_region.slot; - if (_region.offset) - _json["offset"] = *_region.offset; - if (_region.length) - _json["length"] = *_region.length; -} - -void schema::to_json(Json& _json, Pointer const& _pointer) -{ - std::visit(util::GenericVisitor{ - [&](Pointer::Region const& _region) { _json = _region; }, - [&](Pointer::Group const& _group) { _json = Json{{"group", _group.members}}; }, - [&](Pointer::List const& _list) - { - _json = Json{{"list", Json{ - {"count", _list.count}, - {"each", _list.each}, - {"is", *_list.is} - }}}; - }, - [&](Pointer::Conditional const& _conditional) - { - _json = Json{{"if", _conditional.condition}, {"then", *_conditional.then}}; - if (_conditional.otherwise) - _json["else"] = *_conditional.otherwise; - }, - [&](Pointer::Scope const& _scope) - { - // Definitions are ordered while JSON object members are not, so each - // definition becomes its own define/in level. - Json inner = *_scope.in; - for (auto definition = _scope.definitions.rbegin(); definition != _scope.definitions.rend(); ++definition) - inner = Json{{"define", Json{{definition->first, definition->second}}}, {"in", std::move(inner)}}; - _json = std::move(inner); - }, - [&](Pointer::TemplateReference const& _reference) - { - _json = Json{{"template", _reference.name}}; - if (!_reference.yields.empty()) - { - Json yields = Json::object(); - for (auto const& [producedName, newName]: _reference.yields) - yields[producedName] = newName; - _json["yields"] = std::move(yields); - } - }, - [&](Pointer::Templates const& _templates) - { - Json templates = Json::object(); - for (auto const& [templateName, definition]: _templates.templates) - templates[templateName] = definition; - _json = Json{{"templates", std::move(templates)}, {"in", *_templates.in}}; - } - }, _pointer.value); -} - -void schema::to_json(Json& _json, Pointer::Template const& _template) -{ - _json = Json{{"expect", _template.expect}, {"for", *_template.body}}; -} - -void schema::to_json(Json& _json, Program::Contract const& _contract) -{ - if (_contract.name) - _json["name"] = *_contract.name; - _json["definition"] = _contract.definition; -} - -void schema::program::to_json(Json& _json, Context::Variable const& _contextVariable) -{ - auto const numProperties = - _contextVariable.identifier.has_value() + - _contextVariable.declaration.has_value(); - solRequire(numProperties >= 1, EthdebugException, "Context variable has no properties."); - if (_contextVariable.identifier) - { - solRequire(!_contextVariable.identifier->empty(), EthdebugException, "Variable identifier must not be empty."); - _json["identifier"] = *_contextVariable.identifier; - } - if (_contextVariable.declaration) - _json["declaration"] = *_contextVariable.declaration; -} - -void schema::program::to_json(Json& _json, Context const& _context) -{ - solRequire(_context.code.has_value() + _context.remark.has_value() + _context.variables.has_value() >= 1, EthdebugException, "Context needs >=1 properties."); - if (_context.code) - _json["code"] = *_context.code; - if (_context.variables) - { - solRequire(!_context.variables->empty(), EthdebugException, "Context variables must not be empty if provided."); - _json["variables"] = *_context.variables; - } - if (_context.remark) - _json["remark"] = *_context.remark; -} - -void schema::program::to_json(Json& _json, Instruction::Operation const& _operation) -{ - _json = { {"mnemonic", _operation.mnemonic} }; - if (!_operation.arguments.empty()) - _json["arguments"] = _operation.arguments; -} - -void schema::program::to_json(Json& _json, Instruction const& _instruction) -{ - _json["offset"] = _instruction.offset; - if (_instruction.operation) - _json["operation"] = *_instruction.operation; - if (_instruction.context) - _json["context"] = *_instruction.context; -} - -void schema::to_json(Json& _json, Program const& _program) -{ - if (_program.ethdebug) - _json["ethdebug"] = *_program.ethdebug; - if (_program.compilation) - _json["compilation"] = *_program.compilation; - _json["contract"] = _program.contract; - _json["environment"] = _program.environment; - if (_program.context) - _json["context"] = *_program.context; - _json["instructions"] = _program.instructions; -} - -void schema::to_json(Json& _json, Program::Environment const& _environment) -{ - _json = Program::environmentNames.at(_environment); -} - -void schema::info::to_json(Json& _json, Resources const& _resources) -{ - if (_resources.ethdebug) - _json["ethdebug"] = *_resources.ethdebug; - _json["compilation"] = _resources.compilation; - _json["types"] = Json::object(); - for (auto const& [id, type]: _resources.types) - _json["types"][id] = type; - _json["pointers"] = Json::object(); - for (auto const& [name, pointerTemplate]: _resources.pointers) - { - solAssert(Pointer::isIdentifier(name), "Pointer template name \"" + name + "\" is not an identifier."); - _json["pointers"][name] = pointerTemplate; - } -} diff --git a/liblangutil/CMakeLists.txt b/liblangutil/CMakeLists.txt index 054d01d91881..21cd0e32f398 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 @@ -17,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 new file mode 100644 index 000000000000..2e4c8d837f3f --- /dev/null +++ b/liblangutil/EthdebugSchema.cpp @@ -0,0 +1,1350 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include + +#include +#include +#include + +#include + +#include +#include + +using namespace solidity; +using namespace solidity::langutil::ethdebug; + +std::map const schema::materials::Reference::typeNames = { + {Type::Compilation, "compilation"}, + {Type::Source, "source"}, +}; + +std::map const schema::Type::Function::visibilityNames = { + {Visibility::Internal, "internal"}, + {Visibility::External, "external"}, +}; + +std::map const schema::Pointer::constantNames = { + {Constant::WordSize, "~wordsize"}, +}; + +std::map const schema::Pointer::Lookup::propertyNames = { + {Property::Slot, ".slot"}, + {Property::Offset, ".offset"}, + {Property::Length, ".length"}, +}; + +std::map const schema::Pointer::Arithmetic::operatorNames = { + {Operator::Sum, "~sum"}, + {Operator::Difference, "~difference"}, + {Operator::Product, "~product"}, + {Operator::Quotient, "~quotient"}, + {Operator::Remainder, "~remainder"}, +}; + +std::map const schema::Pointer::locationNames = { + {Location::Stack, "stack"}, + {Location::Memory, "memory"}, + {Location::Storage, "storage"}, + {Location::Calldata, "calldata"}, + {Location::Returndata, "returndata"}, + {Location::Transient, "transient"}, + {Location::Code, "code"}, +}; + +std::map const schema::Program::environmentNames = { + {Environment::CALL, "call"}, + {Environment::CREATE, "create"}, +}; + +schema::Type::Specifier::Specifier(std::shared_ptr _type): + value(std::move(_type)) +{ + solAssert(std::get>(value), "Type specifier without a type."); +} + +schema::Type::Definition::Definition(std::optional _name, std::optional _location): + name(std::move(_name)), + location(std::move(_location)) +{ + solAssert(name || location, "Type definition without properties."); +} + +schema::Type::UInt::UInt(unsigned _bits): + bits(_bits) +{ + solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); +} + +schema::Type::Int::Int(unsigned _bits): + bits(_bits) +{ + solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); +} + +schema::Type::UFixed::UFixed(unsigned _bits, unsigned _places): + bits(_bits), + places(_places) +{ + solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); + solAssert(isValidPlaces(places), "Fixed point type must have between 1 and 80 decimal places."); +} + +schema::Type::Fixed::Fixed(unsigned _bits, unsigned _places): + bits(_bits), + places(_places) +{ + solAssert(isValidWidth(bits), "Type width must be a multiple of 8 bits up to 256."); + solAssert(isValidPlaces(places), "Fixed point type must have between 1 and 80 decimal places."); +} + +bool schema::Pointer::isIdentifier(std::string_view _text) +{ + auto const isStart = [](char _c) { return std::isalpha(static_cast(_c)) || _c == '_' || _c == '-' || _c == '$'; }; + auto const isRest = [&](char _c) { return isStart(_c) || std::isdigit(static_cast(_c)); }; + if (_text.empty() || !isStart(_text.front())) + return false; + for (char const c: _text) + if (!isRest(c)) + return false; + return true; +} + +bool schema::Pointer::isRegionReference(std::string_view _text) +{ + return _text == "~this" || isIdentifier(_text); +} + +schema::Pointer::Variable::Variable(std::string _identifier): + identifier(std::move(_identifier)) +{ + solAssert(isIdentifier(identifier), "Pointer expression variable \"" + identifier + "\" is not an identifier."); +} + +schema::Pointer::Lookup::Lookup(Property _property, std::string _region): + property(_property), + region(std::move(_region)) +{ + solAssert(isRegionReference(region), "Region reference \"" + region + "\" is not an identifier."); +} + +schema::Pointer::Read::Read(std::string _region): + region(std::move(_region)) +{ + solAssert(isRegionReference(region), "Region reference \"" + region + "\" is not an identifier."); +} + +schema::Pointer::Arithmetic::Arithmetic(Operator _op, Operands _operands): + op(_op), + operands(std::move(_operands)) +{ + if (op == Operator::Difference || op == Operator::Quotient || op == Operator::Remainder) + solAssert(operands.size() == 2, "Difference, quotient and remainder take exactly two operands."); +} + +schema::Pointer::Resize::Resize(std::optional _size, std::shared_ptr _operand): + size(_size), + operand(std::move(_operand)) +{ + solAssert(!size || *size > 0, "Resize expression needs a positive byte width."); + solAssert(operand, "Resize expression without an operand."); +} + +schema::Pointer::YulLocal::YulLocal(std::string _name): + name(std::move(_name)) +{ + solAssert(!name.empty(), "Yul local expression without a name."); +} + +schema::Pointer::Region::Region( + std::optional _name, + Location _location, + std::optional _slot, + std::optional _offset, + std::optional _length +): + name(std::move(_name)), + location(_location), + slot(std::move(_slot)), + offset(std::move(_offset)), + length(std::move(_length)) +{ + if (name) + solAssert(isIdentifier(*name), "Region name \"" + *name + "\" is not an identifier."); + switch (location) + { + case Location::Stack: + case Location::Storage: + case Location::Transient: + // Word-oriented locations address by slot. + solAssert(slot, "A stack, storage or transient region must address its slot."); + break; + case Location::Memory: + case Location::Calldata: + case Location::Returndata: + case Location::Code: + // Byte-oriented locations address by offset and length. + solAssert(!slot && offset && length, "A memory, calldata, returndata or code region must address its offset and length."); + break; + } +} + +schema::Pointer::Group::Group(std::vector _members): + members(std::move(_members)) +{ + solAssert(!members.empty(), "A group pointer must have at least one member."); +} + +schema::Pointer::List::List(Expression _count, std::string _each, std::shared_ptr _is): + count(std::move(_count)), + each(std::move(_each)), + is(std::move(_is)) +{ + solAssert(isIdentifier(each), "List index name \"" + each + "\" is not an identifier."); + solAssert(is, "List element pointer is missing."); +} + +schema::Pointer::Conditional::Conditional( + Expression _condition, + std::shared_ptr _then, + std::shared_ptr _otherwise +): + condition(std::move(_condition)), + then(std::move(_then)), + otherwise(std::move(_otherwise)) +{ + solAssert(then, "Conditional consequent is missing."); +} + +schema::Pointer::Scope::Scope(std::vector> _definitions, std::shared_ptr _in): + definitions(std::move(_definitions)), + in(std::move(_in)) +{ + solAssert(!definitions.empty(), "A scope pointer must define at least one variable."); + for (auto const& [variable, expression]: definitions) + solAssert(isIdentifier(variable), "Scope variable \"" + variable + "\" is not an identifier."); + solAssert(in, "Scope target pointer is missing."); +} + +schema::Pointer::TemplateReference::TemplateReference(std::string _name, std::vector> _yields): + name(std::move(_name)), + yields(std::move(_yields)) +{ + solAssert(isIdentifier(name), "Template name \"" + name + "\" is not an identifier."); + for (auto const& [producedName, newName]: yields) + { + solAssert(isIdentifier(producedName), "Yielded region name \"" + producedName + "\" is not an identifier."); + solAssert(isIdentifier(newName), "Yielded region name \"" + newName + "\" is not an identifier."); + } +} + +schema::Pointer::Template::Template(std::vector _expect, std::shared_ptr _body): + expect(std::move(_expect)), + body(std::move(_body)) +{ + for (std::string const& parameter: expect) + solAssert(isIdentifier(parameter), "Template parameter \"" + parameter + "\" is not an identifier."); + solAssert(body, "Pointer template without a body."); +} + +schema::Pointer::Templates::Templates(std::vector> _templates, std::shared_ptr _in): + templates(std::move(_templates)), + in(std::move(_in)) +{ + for (auto const& [templateName, definition]: templates) + solAssert(isIdentifier(templateName), "Template name \"" + templateName + "\" is not an identifier."); + solAssert(in, "Templates target pointer is missing."); +} + + +void schema::data::to_json(Json& _json, HexValue const& _hexValue) +{ + _json = util::toHex(_hexValue.value, util::HexPrefix::Add); +} + +void schema::data::to_json(Json& _json, Unsigned const& _unsigned) +{ + std::visit(util::GenericVisitor{ + [&](HexValue const& _hexValue) { _json = _hexValue; }, + [&](std::uint64_t const _value) { _json = _value; } + }, _unsigned.value); +} + +void schema::data::to_json(Json& _json, Stamp const& _stamp) +{ + _json["schema"] = _stamp.schema; + _json["version"] = _stamp.version; +} + +void schema::materials::to_json(Json& _json, ID const& _id) +{ + std::visit(util::GenericVisitor{ + [&](std::string const& _hexValue) { _json = _hexValue; }, + [&](std::uint64_t const _value) { _json = _value; } + }, _id.value); +} + +void schema::materials::to_json(Json& _json, Reference const& _source) +{ + _json["id"] = _source.id; + if (_source.type) + _json["type"] = Reference::typeNames.at(*_source.type); +} + +void schema::materials::to_json(Json& _json, SourceRange::Range const& _range) +{ + _json["length"] = _range.length; + _json["offset"] = _range.offset; +} + + +void schema::materials::to_json(Json& _json, SourceRange const& _sourceRange) +{ + _json["source"] = _sourceRange.source; + if (_sourceRange.range) + _json["range"] = *_sourceRange.range; +} + +void schema::materials::to_json(Json& _json, Source const& _source) +{ + _json["id"] = _source.id; + _json["path"] = _source.path; + _json["contents"] = _source.contents; + if (_source.encoding) + _json["encoding"] = *_source.encoding; + _json["language"] = _source.language; +} + +void schema::materials::to_json(Json& _json, Compilation::Compiler const& _compiler) +{ + _json["name"] = _compiler.name; + _json["version"] = _compiler.version; +} + +void schema::materials::to_json(Json& _json, Compilation const& _compilation) +{ + _json["id"] = _compilation.id; + _json["compiler"] = _compilation.compiler; + if (_compilation.settings) + _json["settings"] = *_compilation.settings; + _json["sources"] = _compilation.sources; +} + +void schema::to_json(Json& _json, Type::Reference const& _reference) +{ + _json = Json::object(); + _json["id"] = _reference.id; +} + +void schema::to_json(Json& _json, Type::Specifier const& _specifier) +{ + std::visit(util::GenericVisitor{ + [&](Type::Reference const& _reference) { _json = _reference; }, + [&](std::shared_ptr const& _type) { _json = *_type; } + }, _specifier.value); +} + +void schema::to_json(Json& _json, Type::Wrapper const& _wrapper) +{ + _json = Json::object(); + if (_wrapper.name) + _json["name"] = *_wrapper.name; + _json["type"] = _wrapper.type; +} + +void schema::to_json(Json& _json, Type::Definition const& _definition) +{ + _json = Json::object(); + if (_definition.name) + _json["name"] = *_definition.name; + if (_definition.location) + _json["location"] = *_definition.location; +} + +void schema::to_json(Json& _json, Type const& _type) +{ + _json = Json::object(); + std::visit(util::GenericVisitor{ + [&](Type::UInt const& _uint) + { + _json["kind"] = "uint"; + _json["bits"] = _uint.bits; + }, + [&](Type::Int const& _int) + { + _json["kind"] = "int"; + _json["bits"] = _int.bits; + }, + [&](Type::Bool const&) { _json["kind"] = "bool"; }, + [&](Type::Bytes const& _bytes) + { + _json["kind"] = "bytes"; + if (_bytes.size) + _json["size"] = *_bytes.size; + }, + [&](Type::String const& _string) + { + _json["kind"] = "string"; + if (_string.encoding) + _json["encoding"] = *_string.encoding; + }, + [&](Type::UFixed const& _ufixed) + { + _json["kind"] = "ufixed"; + _json["bits"] = _ufixed.bits; + _json["places"] = _ufixed.places; + }, + [&](Type::Fixed const& _fixed) + { + _json["kind"] = "fixed"; + _json["bits"] = _fixed.bits; + _json["places"] = _fixed.places; + }, + [&](Type::Address const& _address) + { + _json["kind"] = "address"; + if (_address.payable) + _json["payable"] = *_address.payable; + }, + [&](Type::Contract const& _contract) + { + _json["kind"] = "contract"; + if (_contract.payable) + _json["payable"] = *_contract.payable; + if (_contract.kind == Type::Contract::Kind::Library) + _json["library"] = true; + else if (_contract.kind == Type::Contract::Kind::Interface) + _json["interface"] = true; + if (_contract.definition) + _json["definition"] = *_contract.definition; + }, + [&](Type::Enum const& _enum) + { + _json["kind"] = "enum"; + _json["values"] = _enum.values; + if (_enum.definition) + _json["definition"] = *_enum.definition; + }, + [&](Type::Alias const& _alias) + { + _json["kind"] = "alias"; + _json["contains"] = _alias.contains; + if (_alias.definition) + _json["definition"] = *_alias.definition; + }, + [&](Type::Array const& _array) + { + _json["kind"] = "array"; + _json["contains"] = _array.contains; + if (_array.count) + _json["count"] = *_array.count; + }, + [&](Type::Mapping const& _mapping) + { + _json["kind"] = "mapping"; + _json["contains"] = Json{{"key", _mapping.key}, {"value", _mapping.value}}; + }, + [&](Type::Struct const& _struct) + { + _json["kind"] = "struct"; + _json["contains"] = _struct.contains; + if (_struct.definition) + _json["definition"] = *_struct.definition; + }, + [&](Type::Tuple const& _tuple) + { + _json["kind"] = "tuple"; + _json["contains"] = _tuple.contains; + }, + [&](Type::Function const& _function) + { + _json["kind"] = "function"; + _json[Type::Function::visibilityNames.at(_function.visibility)] = true; + Json contains{{"parameters", _function.parameters}}; + if (_function.returns) + contains["returns"] = *_function.returns; + _json["contains"] = std::move(contains); + if (_function.definition) + _json["definition"] = *_function.definition; + } + }, _type.value); +} + +void schema::to_json(Json& _json, Pointer::Expression const& _expression) +{ + std::visit(util::GenericVisitor{ + [&](Pointer::Literal const& _literal) { _json = _literal.value; }, + [&](Pointer::Variable const& _variable) { _json = _variable.identifier; }, + [&](Pointer::Constant const _constant) { _json = Pointer::constantNames.at(_constant); }, + [&](Pointer::Lookup const& _lookup) + { + _json = Json{{Pointer::Lookup::propertyNames.at(_lookup.property), _lookup.region}}; + }, + [&](Pointer::Read const& _read) { _json = Json{{"~read", _read.region}}; }, + [&](Pointer::Arithmetic const& _arithmetic) + { + _json = Json{{Pointer::Arithmetic::operatorNames.at(_arithmetic.op), _arithmetic.operands}}; + }, + [&](Pointer::Keccak256 const& _keccak256) { _json = Json{{"~keccak256", _keccak256.operands}}; }, + [&](Pointer::Concat const& _concat) { _json = Json{{"~concat", _concat.operands}}; }, + [&](Pointer::Resize const& _resize) + { + if (_resize.size) + _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); +} + +void schema::to_json(Json& _json, Pointer::Region const& _region) +{ + _json = Json::object(); + if (_region.name) + _json["name"] = *_region.name; + _json["location"] = Pointer::locationNames.at(_region.location); + if (_region.slot) + _json["slot"] = *_region.slot; + if (_region.offset) + _json["offset"] = *_region.offset; + if (_region.length) + _json["length"] = *_region.length; +} + +void schema::to_json(Json& _json, Pointer const& _pointer) +{ + std::visit(util::GenericVisitor{ + [&](Pointer::Region const& _region) { _json = _region; }, + [&](Pointer::Group const& _group) { _json = Json{{"group", _group.members}}; }, + [&](Pointer::List const& _list) + { + _json = Json{{"list", Json{ + {"count", _list.count}, + {"each", _list.each}, + {"is", *_list.is} + }}}; + }, + [&](Pointer::Conditional const& _conditional) + { + _json = Json{{"if", _conditional.condition}, {"then", *_conditional.then}}; + if (_conditional.otherwise) + _json["else"] = *_conditional.otherwise; + }, + [&](Pointer::Scope const& _scope) + { + // Definitions are ordered while JSON object members are not, so each + // definition becomes its own define/in level. + Json inner = *_scope.in; + for (auto definition = _scope.definitions.rbegin(); definition != _scope.definitions.rend(); ++definition) + inner = Json{{"define", Json{{definition->first, definition->second}}}, {"in", std::move(inner)}}; + _json = std::move(inner); + }, + [&](Pointer::TemplateReference const& _reference) + { + _json = Json{{"template", _reference.name}}; + if (!_reference.yields.empty()) + { + Json yields = Json::object(); + for (auto const& [producedName, newName]: _reference.yields) + yields[producedName] = newName; + _json["yields"] = std::move(yields); + } + }, + [&](Pointer::Templates const& _templates) + { + Json templates = Json::object(); + for (auto const& [templateName, definition]: _templates.templates) + templates[templateName] = definition; + _json = Json{{"templates", std::move(templates)}, {"in", *_templates.in}}; + } + }, _pointer.value); +} + +void schema::to_json(Json& _json, Pointer::Template const& _template) +{ + _json = Json{{"expect", _template.expect}, {"for", *_template.body}}; +} + +void schema::to_json(Json& _json, Program::Contract const& _contract) +{ + if (_contract.name) + _json["name"] = *_contract.name; + _json["definition"] = _contract.definition; +} + +void schema::program::to_json(Json& _json, Context::Variable const& _contextVariable) +{ + auto const numProperties = + _contextVariable.identifier.has_value() + + _contextVariable.declaration.has_value(); + solRequire(numProperties >= 1, EthdebugException, "Context variable has no properties."); + if (_contextVariable.identifier) + { + solRequire(!_contextVariable.identifier->empty(), EthdebugException, "Variable identifier must not be empty."); + _json["identifier"] = *_contextVariable.identifier; + } + if (_contextVariable.declaration) + _json["declaration"] = *_contextVariable.declaration; +} + +void schema::program::to_json(Json& _json, Context const& _context) +{ + solRequire(_context.code.has_value() + _context.remark.has_value() + _context.variables.has_value() >= 1, EthdebugException, "Context needs >=1 properties."); + if (_context.code) + _json["code"] = *_context.code; + if (_context.variables) + { + solRequire(!_context.variables->empty(), EthdebugException, "Context variables must not be empty if provided."); + _json["variables"] = *_context.variables; + } + if (_context.remark) + _json["remark"] = *_context.remark; +} + +void schema::program::to_json(Json& _json, Instruction::Operation const& _operation) +{ + _json = { {"mnemonic", _operation.mnemonic} }; + if (!_operation.arguments.empty()) + _json["arguments"] = _operation.arguments; +} + +void schema::program::to_json(Json& _json, Instruction const& _instruction) +{ + _json["offset"] = _instruction.offset; + if (_instruction.operation) + _json["operation"] = *_instruction.operation; + if (_instruction.context) + _json["context"] = *_instruction.context; +} + +void schema::to_json(Json& _json, Program const& _program) +{ + if (_program.ethdebug) + _json["ethdebug"] = *_program.ethdebug; + if (_program.compilation) + _json["compilation"] = *_program.compilation; + _json["contract"] = _program.contract; + _json["environment"] = _program.environment; + if (_program.context) + _json["context"] = *_program.context; + _json["instructions"] = _program.instructions; +} + +void schema::to_json(Json& _json, Program::Environment const& _environment) +{ + _json = Program::environmentNames.at(_environment); +} + +void schema::info::to_json(Json& _json, Resources const& _resources) +{ + if (_resources.ethdebug) + _json["ethdebug"] = *_resources.ethdebug; + _json["compilation"] = _resources.compilation; + _json["types"] = Json::object(); + for (auto const& [id, type]: _resources.types) + _json["types"][id] = type; + _json["pointers"] = Json::object(); + 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; +} diff --git a/libevmasm/EthdebugSchema.h b/liblangutil/EthdebugSchema.h similarity index 87% rename from libevmasm/EthdebugSchema.h rename to liblangutil/EthdebugSchema.h index 3dcab0b20296..a010722231a7 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 {}; @@ -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; }; @@ -678,4 +700,38 @@ 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::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, 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/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); 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..34dcc125c2d8 --- /dev/null +++ b/test/liblangutil/EthdebugDocumentTest.cpp @@ -0,0 +1,87 @@ +/* + 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::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}, {"sidecar", Document::Sidecar}}, "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; + } + case Document::Sidecar: + output = semanticDebugDataToJson(semanticDebugDataFromJson(input)); + 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/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 +// } 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