diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 02a45088f935..d23649f49a91 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -130,7 +130,9 @@ set(libsolidity_util_sources libsolidity/util/ContractABIUtils.h libsolidity/util/SoltestErrors.h libsolidity/util/SoltestTypes.h - libsolidity/util/TestFileParser.cpp + libsolidity/util/StandardJSONCompiler.cpp + libsolidity/util/StandardJSONCompiler.h + libsolidity/util/TestFileParser.cpp libsolidity/util/TestFileParser.h libsolidity/util/TestFileParserTests.cpp libsolidity/util/TestFunctionCall.cpp diff --git a/test/ExecutionFramework.h b/test/ExecutionFramework.h index 44812e230ab0..02700a2935e6 100644 --- a/test/ExecutionFramework.h +++ b/test/ExecutionFramework.h @@ -65,7 +65,7 @@ class ExecutionFramework std::string const& _contractName = "", bytes const& _arguments = {}, std::map const& _libraryAddresses = {}, - std::optional const& _sourceName = std::nullopt + std::optional const& _mainSourceName = std::nullopt ) = 0; bytes const& compileAndRun( diff --git a/test/libsolidity/GasCosts.cpp b/test/libsolidity/GasCosts.cpp index d8dad9e02705..f7c8d8d1bd5c 100644 --- a/test/libsolidity/GasCosts.cpp +++ b/test/libsolidity/GasCosts.cpp @@ -19,7 +19,9 @@ * Tests that check that the cost of certain operations stay within range. */ +#include #include + #include #include #include @@ -38,7 +40,10 @@ namespace solidity::frontend::test #define CHECK_DEPLOY_GAS(_gasNoOpt, _gasOpt, _evmVersion) \ do \ { \ - u256 metaCost = GasMeter::dataGas(m_compiler.cborMetadata(m_compiler.lastContractName()), true, _evmVersion); \ + auto const& output = m_compiler.output(); \ + auto const* contract = output.contract(); \ + soltestAssert(contract); \ + u256 metaCost = GasMeter::dataGas(bytes{}, true, _evmVersion); \ u256 gasOpt{_gasOpt}; \ u256 gasNoOpt{_gasNoOpt}; \ u256 gas = m_optimiserSettings == OptimiserSettings::minimal() ? gasNoOpt : gasOpt; \ @@ -90,7 +95,7 @@ BOOST_AUTO_TEST_CASE(string_storage) } } )"; - m_compiler.setMetadataFormat(CompilerStack::MetadataFormat::NoMetadata); + m_appendCBORMetadata = false; compileAndRun(sourceCode); @@ -202,8 +207,15 @@ BOOST_AUTO_TEST_CASE(single_callvaluecheck) } )"; compileAndRun(sourceCode); - size_t bytecodeSizeNonpayable = m_compiler.object("Nonpayable").bytecode.size(); - size_t bytecodeSizePayable = m_compiler.object("Payable").bytecode.size(); + auto const& output = m_compiler.output(); + auto const* nonpayable = output.contract("Nonpayable"); + auto const* payable = output.contract("Payable"); + + soltestAssert(nonpayable); + soltestAssert(payable); + + size_t bytecodeSizeNonpayable = nonpayable->evm().bytecode.object.size(); + size_t bytecodeSizePayable = payable->evm().bytecode.object.size(); auto evmVersion = solidity::test::CommonOptions::get().evmVersion(); if (evmVersion < EVMVersion::shanghai()) diff --git a/test/libsolidity/GasMeter.cpp b/test/libsolidity/GasMeter.cpp index 9012ad083c02..b7fb32bd1e9d 100644 --- a/test/libsolidity/GasMeter.cpp +++ b/test/libsolidity/GasMeter.cpp @@ -21,10 +21,16 @@ * Unit tests for the gas estimator. */ +#include "libsolidity/interface/StandardJSONInput.h" #include + +#include +#include + #include #include #include + #include #include @@ -42,25 +48,42 @@ class GasMeterTestFramework: public SolidityExecutionFramework public: void compile(std::string const& _sourceCode) { - m_compiler.reset(); - m_compiler.setSources({{"", "pragma solidity >=0.0;\n" - "// SPDX-License-Identifier: GPL-3.0\n" + _sourceCode}}); - m_compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize); - m_compiler.setEVMVersion(m_evmVersion); - BOOST_REQUIRE_MESSAGE(m_compiler.compile(), "Compiling contract failed"); + m_compilerInput = json::StandardJSONInput{ + .sources = {{"", withPreamble(_sourceCode)}}, + .settings = json::input::Settings{ + .optimizer = json::input::Optimizer{ + .enable = CommonOptions::get().optimize, + .runs = 200, + .details = json::input::OptimizerDetails{} + }, + .evmVersion = m_evmVersion, + } + }; + m_compiler.compile(m_compilerInput); + + BOOST_REQUIRE_MESSAGE(m_compiler.output().success(), "Compiling contract failed"); } void testCreationTimeGas(std::string const& _sourceCode, u256 const& _tolerance = u256(0)) { compileAndRun(_sourceCode); + auto state = std::make_shared(); - PathGasMeter meter(*m_compiler.assemblyItems(m_compiler.lastContractName()), solidity::test::CommonOptions::get().evmVersion()); + auto const* contract = m_compiler.output().contract(); + + soltestAssert(contract); + + PathGasMeter meter( + {}, // TODO(erikli): Fix! + CommonOptions::get().evmVersion() + ); GasMeter::GasConsumption gas = meter.estimateMax(0, state); - u256 bytecodeSize(m_compiler.runtimeObject(m_compiler.lastContractName()).bytecode.size()); + bytes const& object = contract->evm().bytecode.object; + u256 bytecodeSize(object.size()); // costs for deployment gas += bytecodeSize * GasCosts::createDataGas; // costs for transaction - gas += gasForTransaction(m_compiler.object(m_compiler.lastContractName()).bytecode, true); + gas += gasForTransaction(object, true); BOOST_REQUIRE(!gas.isInfinite); BOOST_CHECK_LE(m_gasUsed, gas.value); @@ -71,6 +94,10 @@ class GasMeterTestFramework: public SolidityExecutionFramework /// against the actual gas usage computed by the VM on the given set of argument variants. void testRunTimeGas(std::string const& _sig, std::vector _argumentVariants, u256 const& _tolerance = u256(0)) { + auto const* contract = m_compiler.output().contract(); + + soltestAssert(contract); + u256 gasUsed = 0; GasMeter::GasConsumption gas; util::FixedHash<4> hash = util::selectorFromSignatureH32(_sig); @@ -82,8 +109,8 @@ class GasMeterTestFramework: public SolidityExecutionFramework gas = std::max(gas, gasForTransaction(hash.asBytes() + arguments, false)); } - gas += GasEstimator(solidity::test::CommonOptions::get().evmVersion()).functionalEstimation( - *m_compiler.runtimeAssemblyItems(m_compiler.lastContractName()), + gas += GasEstimator(CommonOptions::get().evmVersion()).functionalEstimation( + {}, // TODO(erikli): Fix! _sig ); BOOST_REQUIRE(!gas.isInfinite); @@ -93,7 +120,7 @@ class GasMeterTestFramework: public SolidityExecutionFramework static GasMeter::GasConsumption gasForTransaction(bytes const& _data, bool _isCreation) { - auto evmVersion = solidity::test::CommonOptions::get().evmVersion(); + auto evmVersion = CommonOptions::get().evmVersion(); GasMeter::GasConsumption gas = _isCreation ? GasCosts::txCreateGas : GasCosts::txGas; for (auto i: _data) gas += i != 0 ? GasCosts::txDataNonZeroGas(evmVersion) : GasCosts::txDataZeroGas; diff --git a/test/libsolidity/SemanticTest.cpp b/test/libsolidity/SemanticTest.cpp index b335dfc017e8..368b2ad5e638 100644 --- a/test/libsolidity/SemanticTest.cpp +++ b/test/libsolidity/SemanticTest.cpp @@ -11,19 +11,31 @@ You should have received a copy of the GNU General Public License along with solidity. If not, see . */ - +#include "libsolidity/interface/Common.h" +#include "libsolidity/interface/StandardJSONInput.h" +#include "libsolidity/util/SoltestErrors.h" +#include #include -#include -#include #include #include +#include + +#include +#include + +#include #include #include #include #include +#include +#include +#include +#include + #include #include #include @@ -33,6 +45,7 @@ #include #include #include +#include using namespace solidity; using namespace solidity::yul; @@ -45,6 +58,74 @@ using namespace boost::unit_test; using namespace std::string_literals; namespace fs = boost::filesystem; +namespace +{ + std::string formatInputType(frontend::json::output::ABIParameter const& _input) + { + if (_input.type == "tuple") + { + soltestAssert(_input.components, "key \"components\" is not allowed to be empty for tuples"); + auto types = _input.components.value() | ranges::views::transform([&](auto const& input) { + return formatInputType(input); + }) | ranges::to; + return "(" + boost::algorithm::join(types, ",") + ")"; + } + return _input.type; + } + + strings formatInputTypes(frontend::json::output::ABIEvent const& _event, bool _indexed) + { + return _event.inputs | ranges::views::filter([&](auto const& input) { + return input.indexed == _indexed; + }) | ranges::views::transform([&](auto const& input) { + return formatInputType(input); + }) | ranges::to(); + } + + std::string formatSignature(frontend::json::output::ABIEvent const& _event) + { + auto signatureTypes = _event.inputs | ranges::views::transform([&](auto const& input) { + return formatInputType(input); + }) | ranges::to; + return _event.name + "(" + boost::algorithm::join(signatureTypes, ",") + ")"; + } + + std::string formatParameter(frontend::json::output::ABIEvent const* _event, bool _indexed, size_t _index, bytes const& _data) + { + auto isPrintableASCII = [](bytes const& s) + { + bool zeroes = true; + for (auto c: s) + { + if (static_cast(c) != 0x00) + { + zeroes = false; + if (static_cast(c) <= 0x1f || static_cast(c) >= 0x7f) + return false; + } else + break; + } + return !zeroes; + }; + + ABIType abiType(ABIType::Type::Hex); + if (isPrintableASCII(_data)) + abiType = ABIType(ABIType::Type::String); + if (_event) + { + auto indexedTypes = formatInputTypes(*_event, true); + auto nonIndexedTypes = formatInputTypes(*_event, false); + auto const& types = _indexed ? indexedTypes : nonIndexedTypes; + if (_index < types.size()) + { + if (types.at(_index) == "bool") + abiType = ABIType(ABIType::Type::Boolean); + } + } + return BytesUtils::formatBytes(_data, abiType); + } +} + std::ostream& solidity::frontend::test::operator<<(std::ostream& _output, RequiresYulOptimizer _requiresYulOptimizer) { switch (_requiresYulOptimizer) @@ -113,15 +194,21 @@ SemanticTest::SemanticTest( m_allowNonExistingFunctions = m_reader.boolSetting("allowNonExistingFunctions", false); m_testCaseWantsSSACFGRun = m_reader.boolSetting("compileViaSSACFG", false); - m_compiler.setExperimental(m_reader.boolSetting("experimental", false)); + + if (!m_compilerInput.settings) + m_compilerInput.settings = json::input::Settings{}; + + m_compilerInput.settings->experimental = m_reader.boolSetting("experimental", false); parseExpectations(m_reader.stream()); soltestAssert(!m_tests.empty(), "No tests specified in " + _filename); if (m_enforceGasCost) { - m_compiler.setMetadataFormat(CompilerStack::MetadataFormat::NoMetadata); - m_compiler.setMetadataHash(MetadataHash::None); + if (!m_compilerInput.settings->metadata) + m_compilerInput.settings->metadata = json::input::Metadata{}; + m_compilerInput.settings->metadata->appendCBOR = false; + m_compilerInput.settings->metadata->bytecodeHash = MetadataHash::None; } } @@ -198,52 +285,38 @@ std::vector SemanticTest::makeSideEffectHooks() const }; } -std::string SemanticTest::formatEventParameter(std::optional _signature, bool _indexed, size_t _index, bytes const& _data) +std::vector SemanticTest::eventSideEffectHook(FunctionCall const&) const { - auto isPrintableASCII = [](bytes const& s) - { - bool zeroes = true; - for (auto c: s) - { - if (static_cast(c) != 0x00) - { - zeroes = false; - if (static_cast(c) <= 0x1f || static_cast(c) >= 0x7f) - return false; - } else - break; - } - return !zeroes; - }; + using namespace json::output; - ABIType abiType(ABIType::Type::Hex); - if (isPrintableASCII(_data)) - abiType = ABIType(ABIType::Type::String); - if (_signature.has_value()) + std::vector sideEffects; + for (LogRecord const& log: ExecutionFramework::recordedLogs()) { - std::vector const& types = _indexed ? _signature->indexedTypes : _signature->nonIndexedTypes; - if (_index < types.size()) + auto contracts = m_compiler.output().contracts(); + auto entries = contracts | ranges::views::transform([](auto const* contract) { + return ranges::views::all(contract->abi()); + }) | ranges::views::join; + + auto events = entries | ranges::views::filter([](auto const& entry) { + return std::holds_alternative(entry); + }) | ranges::views::transform([](auto const& entry) { + return std::get(entry); + }) | ranges::to>(); + + + json::output::ABIEvent const* event = nullptr; + if (!log.topics.empty()) { - if (types.at(_index) == "bool") - abiType = ABIType(ABIType::Type::Boolean); + auto e = ranges::find_if(events, [&](auto const& _e) { + return keccak256(formatSignature(_e)) == log.topics[0] && !_e.isAnonymous; + }); + event = (e != events.end()) ? &*e : nullptr; } - } - return BytesUtils::formatBytes(_data, abiType); -} -std::vector SemanticTest::eventSideEffectHook(FunctionCall const&) const -{ - std::vector sideEffects; - std::vector recordedLogs = ExecutionFramework::recordedLogs(); - for (LogRecord const& log: recordedLogs) - { - std::optional eventSignature; - if (!log.topics.empty()) - eventSignature = matchEvent(log.topics[0]); std::stringstream sideEffect; sideEffect << "emit "; - if (eventSignature.has_value()) - sideEffect << eventSignature.value().signature; + if (event) + sideEffect << formatSignature(*event); else sideEffect << ""; @@ -254,8 +327,8 @@ std::vector SemanticTest::eventSideEffectHook(FunctionCall const&) size_t index{0}; for (h256 const& topic: log.topics) { - if (!eventSignature.has_value() || index != 0) - eventStrings.push_back("#" + formatEventParameter(eventSignature, true, index, topic.asBytes())); + if (!event || index != 0) + eventStrings.push_back("#" + formatParameter(event, true, index, topic.asBytes())); ++index; } @@ -264,7 +337,7 @@ std::vector SemanticTest::eventSideEffectHook(FunctionCall const&) { auto begin = log.data.begin() + static_cast(index * 32); bytes const& data = bytes{begin, begin + 32}; - eventStrings.emplace_back(formatEventParameter(eventSignature, false, index, data)); + eventStrings.emplace_back(formatParameter(event, false, index, data)); } if (!eventStrings.empty()) @@ -275,31 +348,6 @@ std::vector SemanticTest::eventSideEffectHook(FunctionCall const&) return sideEffects; } -std::optional SemanticTest::matchEvent(util::h256 const& hash) const -{ - std::optional result; - for (std::string& contractName: m_compiler.contractNames()) - { - ContractDefinition const& contract = m_compiler.contractDefinition(contractName); - for (EventDefinition const* event: contract.events() + contract.usedInterfaceEvents()) - { - FunctionTypePointer eventFunctionType = event->functionType(true); - if (!event->isAnonymous() && keccak256(eventFunctionType->externalSignature()) == hash) - { - AnnotatedEventSignature eventInfo; - eventInfo.signature = eventFunctionType->externalSignature(); - for (auto const& param: event->parameters()) - if (param->isIndexed()) - eventInfo.indexedTypes.emplace_back(param->type()->toString(true)); - else - eventInfo.nonIndexedTypes.emplace_back(param->type()->toString(true)); - result = eventInfo; - } - } - } - return result; -} - frontend::OptimiserSettings SemanticTest::optimizerSettingsFor(RequiresYulOptimizer _requiresYulOptimizer) { switch (_requiresYulOptimizer) @@ -379,7 +427,7 @@ TestCase::TestResult SemanticTest::runTest( for (TestFunctionCall& test: m_tests) test.reset(); - std::map libraries; + std::map libraries; bool constructed = false; @@ -398,13 +446,14 @@ TestCase::TestResult SemanticTest::runTest( } else if (test.call().kind == FunctionCall::Kind::Library) { + auto name = fmt::format("{}:{}", test.call().libraryFile, test.call().signature); soltestAssert( - deploy(test.call().signature, 0, {}, libraries) && m_transactionSuccessful, + deploy(name, 0, {}, libraries) && m_transactionSuccessful, "Failed to deploy library " + test.call().signature); // For convenience, in semantic tests we assume that an unqualified name like `L` is equivalent to one // with an empty source unit name (`:L`). This is fine because the compiler never uses unqualified // names in the Yul code it produces and does not allow `linkersymbol()` at all in inline assembly. - libraries[test.call().libraryFile + ":" + test.call().signature] = m_contractAddress; + libraries[name] = m_contractAddress; continue; } else @@ -431,7 +480,13 @@ TestCase::TestResult SemanticTest::runTest( } else { + ContractName contractName{m_sources.mainSourceFile, ""}; + + auto const* contract = m_compiler.output().contract(contractName); + soltestAssert(contract); + bytes output; + if (test.call().kind == FunctionCall::Kind::LowLevel) output = callLowLevel(test.call().arguments.rawBytes(), test.call().value.value); else if (test.call().kind == FunctionCall::Kind::Builtin) @@ -449,7 +504,7 @@ TestCase::TestResult SemanticTest::runTest( { soltestAssert( m_allowNonExistingFunctions || - m_compiler.interfaceSymbols(m_compiler.lastContractName(m_sources.mainSourceFile))["methods"].contains(test.call().signature), + contract->evm().methodIdentifiers.contains(test.call().signature), "The function " + test.call().signature + " is not known to the compiler" ); @@ -476,8 +531,9 @@ TestCase::TestResult SemanticTest::runTest( test.setFailure(!m_transactionSuccessful); test.setRawBytes(std::move(output)); + if (test.call().kind != FunctionCall::Kind::LowLevel) - test.setContractABI(m_compiler.contractABI(m_compiler.lastContractName(m_sources.mainSourceFile))); + test.setContractABI(Json{contract->raw().at("abi")}); } std::vector effects; diff --git a/test/libsolidity/SemanticTest.h b/test/libsolidity/SemanticTest.h index 2421fdf29a7d..d000674e1b6e 100644 --- a/test/libsolidity/SemanticTest.h +++ b/test/libsolidity/SemanticTest.h @@ -30,13 +30,6 @@ namespace solidity::frontend::test { -struct AnnotatedEventSignature -{ - std::string signature; - std::vector indexedTypes; - std::vector nonIndexedTypes; -}; - enum class RequiresYulOptimizer { False, @@ -109,8 +102,6 @@ class SemanticTest: public SolidityExecutionFramework, public EVMVersionRestrict std::map makeBuiltins(); std::vector makeSideEffectHooks() const; std::vector eventSideEffectHook(FunctionCall const&) const; - std::optional matchEvent(util::h256 const& hash) const; - static std::string formatEventParameter(std::optional _signature, bool _indexed, size_t _index, bytes const& _data); OptimiserSettings optimizerSettingsFor(RequiresYulOptimizer _requiresYulOptimizer); diff --git a/test/libsolidity/SolidityEndToEndTest.cpp b/test/libsolidity/SolidityEndToEndTest.cpp index f6e8ccf452b7..f9b7548ee0b0 100644 --- a/test/libsolidity/SolidityEndToEndTest.cpp +++ b/test/libsolidity/SolidityEndToEndTest.cpp @@ -2815,9 +2815,17 @@ BOOST_AUTO_TEST_CASE(include_creation_bytecode_only_once) } )"; compileAndRun(sourceCode); + + auto const& output = m_compiler.output(); + auto const* contractDouble = output.contract(std::string_view{"Double"}); + auto const* contractSingle = output.contract("Single"); + + solAssert(contractDouble); + solAssert(contractSingle); + BOOST_CHECK_LE( - double(m_compiler.object("Double").bytecode.size()), - 1.2 * double(m_compiler.object("Single").bytecode.size()) + static_cast(contractDouble->evm().bytecode.object.size()), + 1.2 * static_cast(contractSingle->evm().bytecode.object.size()) ); } diff --git a/test/libsolidity/SolidityExecutionFramework.cpp b/test/libsolidity/SolidityExecutionFramework.cpp index fc9087e926fc..ab4e9ec37ae0 100644 --- a/test/libsolidity/SolidityExecutionFramework.cpp +++ b/test/libsolidity/SolidityExecutionFramework.cpp @@ -21,17 +21,26 @@ * Framework for executing Solidity contracts and testing them against C++ implementation. */ +#include "liblangutil/SourceLocation.h" +#include "libsolidity/interface/StandardJSONInput.h" +#include +#include #include #include +#include +#include #include -#include #include #include +#include #include -#include +#include +#include +#include + #include using namespace solidity; @@ -40,52 +49,103 @@ using namespace solidity::frontend::test; using namespace solidity::langutil; using namespace solidity::test; +namespace +{ + std::shared_ptr convertError(json::output::Error const& _error) + { + return std::make_shared(_error.toInternalError()); + } +} + bytes SolidityExecutionFramework::multiSourceCompileContract( std::map const& _sourceCode, - std::optional const& _mainSourceName, std::string const& _contractName, - std::map const& _libraryAddresses + std::map const& _libraryAddresses, + std::optional const& _mainSourceName ) { if (_mainSourceName.has_value()) solAssert(_sourceCode.find(_mainSourceName.value()) != _sourceCode.end(), ""); - m_compiler.reset(); - m_compiler.setSources(withPreamble( - _sourceCode, - solidity::test::CommonOptions::get().useABIEncoderV1 // _addAbicoderV1Pragma - )); - m_compiler.setLibraries(_libraryAddresses); - m_compiler.setRevertStringBehaviour(m_revertStrings); - m_compiler.setEVMVersion(m_evmVersion); - m_compiler.setEOFVersion(m_eofVersion); - m_compiler.setOptimiserSettings(m_optimiserSettings); - m_compiler.setViaIR(m_compileViaYul); - m_compiler.setViaSSACFG(m_compileViaSSACFG); - m_compiler.setRevertStringBehaviour(m_revertStrings); - if (!m_appendCBORMetadata) { - m_compiler.setMetadataFormat(CompilerStack::MetadataFormat::NoMetadata); - } - m_compiler.setMetadataHash(m_metadataHash); + m_compilerInput = json::StandardJSONInput{ + .sources = withPreamble( + _sourceCode, + solidity::test::CommonOptions::get().useABIEncoderV1 // _addAbicoderV1Pragma + ), + .libraries = _libraryAddresses, + .settings = json::input::Settings{ + .optimizer = json::input::Optimizer{ + .enable = false, + .runs = m_optimiserSettings.expectedExecutionsPerDeployment, + .details = json::input::OptimizerDetails{ + .peephole = m_optimiserSettings.runPeephole, + .inliner = m_optimiserSettings.runInliner, + .jumpdestRemover = m_optimiserSettings.runJumpdestRemover, + .orderLiterals = m_optimiserSettings.runOrderLiterals, + .deduplicate = m_optimiserSettings.runDeduplicate, + .cse = m_optimiserSettings.runCSE, + .constantOptimizer = m_optimiserSettings.runConstantOptimiser, + .simpleCounterForLoopUncheckedIncrement = m_optimiserSettings.simpleCounterForLoopUncheckedIncrement, + .yul = m_optimiserSettings.runYulOptimiser, + .yulDetails = json::input::YulOptimizerDetails{ + .stackAllocation = m_optimiserSettings.optimizeStackAllocation, + .optimizerSteps = m_optimiserSettings.yulOptimiserSteps, + } + } + }, + .evmVersion = m_evmVersion, + .eofVersion = m_eofVersion, + .viaIR = m_compileViaYul, + .debug = json::input::Debug{ + .revertStrings = m_revertStrings + }, + .metadata = json::input::Metadata{ + .appendCBOR = m_appendCBORMetadata, + .bytecodeHash = m_metadataHash, + } + } + }; + StandardJSONOutputExt const& output = m_compiler.compile(m_compilerInput); - if (!m_compiler.compile()) + if (!output.success()) { // The testing framework expects an exception for // "unimplemented" yul IR generation. - if (m_compileViaYul) - for (auto const& error: m_compiler.errors()) - if (error->type() == langutil::Error::Type::CodeGenerationError) - BOOST_THROW_EXCEPTION(*error); - langutil::SourceReferenceFormatter{std::cerr, m_compiler, true, false} - .printErrorInformation(m_compiler.errors()); + auto codeGenError = ranges::find_if(output.errors(), [](auto const& e) { + return e.type == Error::Type::CodeGenerationError; + }); + + if (m_compileViaYul && codeGenError != output.errors().end()) + BOOST_THROW_EXCEPTION(*convertError(*codeGenError)); + + auto errors = output.errors() | ranges::views::transform(convertError) | ranges::to(); + for (auto const& [name, code]: m_compilerInput.sources) + fmt::print("\n{}\n", SourceReferenceFormatter::formatErrorInformation( + errors, + SingletonCharStreamProvider{CharStream{code, name}}, + true, + false + )); + BOOST_ERROR("Compiling contract failed"); } - std::string contractName(_contractName.empty() ? m_compiler.lastContractName(_mainSourceName) : _contractName); - evmasm::LinkerObject obj = m_compiler.object(contractName); - BOOST_REQUIRE(obj.linkReferences.empty()); + + // Construct `ContractName` with the contract name given, and use `_mainSourceName` + // if the contract's name source prefix is empty. + auto const [sourceName, contractName, _] = decomposeContractName(_contractName); + ContractName lookupName{ + sourceName.empty() ? _mainSourceName.value_or("") : sourceName, + contractName + }; + + auto const* contract = output.contract(lookupName); + soltestAssert(contract); + soltestAssert(contract->evm().bytecode.linkReferences.empty()); + if (m_showMetadata) - std::cout << "metadata: " << m_compiler.metadata(contractName) << std::endl; - return obj.bytecode; + std::cout << "metadata: " << contract->metadata() << std::endl; + + return contract->evm().bytecode.object; } bytes SolidityExecutionFramework::compileContract( @@ -96,8 +156,8 @@ bytes SolidityExecutionFramework::compileContract( { return multiSourceCompileContract( {{"", _sourceCode}}, - std::nullopt, _contractName, - _libraryAddresses + _libraryAddresses, + std::nullopt ); } diff --git a/test/libsolidity/SolidityExecutionFramework.h b/test/libsolidity/SolidityExecutionFramework.h index 397b78bfc397..5ed4dbc51a79 100644 --- a/test/libsolidity/SolidityExecutionFramework.h +++ b/test/libsolidity/SolidityExecutionFramework.h @@ -25,21 +25,25 @@ #include -#include - -#include #include - +#include +#include #include +#include +#include +#include + namespace solidity::frontend::test { -class SolidityExecutionFramework: public solidity::test::ExecutionFramework +using namespace solidity::test; + +class SolidityExecutionFramework: public ExecutionFramework { public: - SolidityExecutionFramework(): m_showMetadata(solidity::test::CommonOptions::get().showMetadata) {} + SolidityExecutionFramework(): m_showMetadata(CommonOptions::get().showMetadata) {} explicit SolidityExecutionFramework( langutil::EVMVersion _evmVersion, std::optional _eofVersion, @@ -48,7 +52,7 @@ class SolidityExecutionFramework: public solidity::test::ExecutionFramework ): ExecutionFramework(_evmVersion, _vmPaths), m_eofVersion(_eofVersion), - m_showMetadata(solidity::test::CommonOptions::get().showMetadata), + m_showMetadata(CommonOptions::get().showMetadata), m_appendCBORMetadata(_appendCBORMetadata) {} @@ -57,11 +61,11 @@ class SolidityExecutionFramework: public solidity::test::ExecutionFramework u256 const& _value = 0, std::string const& _contractName = "", bytes const& _arguments = {}, - std::map const& _libraryAddresses = {}, - std::optional const& _sourceName = std::nullopt + std::map const& _libraryAddresses = {}, + std::optional const& _mainSourceName = std::nullopt ) override { - bytes bytecode = multiSourceCompileContract(_sourceCode, _sourceName, _contractName, _libraryAddresses); + bytes bytecode = multiSourceCompileContract(_sourceCode, _contractName, _libraryAddresses, _mainSourceName); sendMessage(bytecode, _arguments, true, _value); return m_output; } @@ -69,20 +73,20 @@ class SolidityExecutionFramework: public solidity::test::ExecutionFramework bytes compileContract( std::string const& _sourceCode, std::string const& _contractName = "", - std::map const& _libraryAddresses = {} + std::map const& _libraryAddresses = {} ); bytes multiSourceCompileContract( std::map const& _sources, - std::optional const& _mainSourceName = std::nullopt, std::string const& _contractName = "", - std::map const& _libraryAddresses = {} + std::map const& _libraryAddresses = {}, + std::optional const& _mainSourceName = std::nullopt ); protected: - using CompilerStack = solidity::frontend::CompilerStack; std::optional m_eofVersion; - CompilerStack m_compiler; + json::StandardJSONInput m_compilerInput; + StandardJSONCompiler m_compiler; bool m_compileViaYul = false; bool m_compileViaSSACFG = false; bool m_showMetadata = false; diff --git a/test/libsolidity/util/Common.cpp b/test/libsolidity/util/Common.cpp index 6f96b7f6e5f7..0708c0e2667e 100644 --- a/test/libsolidity/util/Common.cpp +++ b/test/libsolidity/util/Common.cpp @@ -64,3 +64,26 @@ std::string test::stripPreReleaseWarning(std::string const& _stderrContent) std::string output = std::regex_replace(_stderrContent, preReleaseWarningRegex, ""); return std::regex_replace(std::move(output), noOutputRegex, ""); } + +std::tuple test::decomposeContractName(std::string_view const _name) +{ + std::string source; + std::string contract; + bool isUnqualified; + + auto const pos = _name.find(':'); + if (pos != std::string::npos) + { + isUnqualified = false; + source = _name.substr(0, pos); + contract = _name.substr(pos + 1); + } + else + { + isUnqualified = true; + contract = _name; + } + + return {source, contract, isUnqualified}; +} + diff --git a/test/libsolidity/util/Common.h b/test/libsolidity/util/Common.h index 3f1750a66b56..d93a7b3fcb96 100644 --- a/test/libsolidity/util/Common.h +++ b/test/libsolidity/util/Common.h @@ -17,6 +17,7 @@ // SPDX-License-Identifier: GPL-3.0 /// Utilities shared by multiple libsolidity tests. +#pragma once #include @@ -34,4 +35,77 @@ StringMap withPreamble(StringMap _sources, bool _addAbicoderV1Pragma = false); std::string stripPreReleaseWarning(std::string const& _stderrContent); +/// @returns the decomposed parts of a contract name (source and contract, as well as flag +/// that indicates if @param _name was unqualified) +std::tuple decomposeContractName(std::string_view const _name); + +/** + * Helper for contract lookups. A `ContractName` can be initialized from + * either an unqualified contract name like "C" or a qualified name + * like ":C" or "lib.sol:C". + */ +class ContractName +{ +public: + /// Construct an empty, unqualified contract name + ContractName(): m_isUnqualified(true) {} + + /// Construct a fully-qualified contract name. + /// @param _source the source name + /// @param _contract the contract name + ContractName(std::string _source, std::string _contract): + m_source(std::move(_source)), + m_contract(std::move(_contract)), + m_isUnqualified(false) + {} + + /// Support implicit conversion from e.g. string literals. + template + requires std::is_convertible_v + ContractName(T const& _name) + { + auto const [source, contract, isUnqualified] = decomposeContractName(_name); + m_source = source; + m_contract = contract; + m_isUnqualified = isUnqualified; + } + + /// @returns true, if either the unqualified or the fully-qualified name matches. + bool operator==(ContractName const& _other) const + { + auto otherContract = std::string{_other.contract()}; + auto otherSource = std::string{_other.source()}; + + return m_contract == otherContract || + m_source + ":" + m_contract == otherSource + ":" + otherContract; + } + + /// @returns the source name. + std::string_view const source() const + { + return m_source; + } + + /// @returns the contractname. + std::string_view const contract() const + { + return m_contract; + } + + /// @returns false if this was initialized from an unqualified contract + /// name like "C". + bool isUnqualified() const + { + return m_isUnqualified; + } + +private: + /// The source name. Can be empty. + std::string m_source; + /// The contract name. Can be empty. + std::string m_contract; + /// True, if this is an unqualified contract name. + bool m_isUnqualified; +}; + } // namespace solidity::frontend::test diff --git a/test/libsolidity/util/StandardJSONCompiler.cpp b/test/libsolidity/util/StandardJSONCompiler.cpp new file mode 100644 index 000000000000..f2d76626e4cb --- /dev/null +++ b/test/libsolidity/util/StandardJSONCompiler.cpp @@ -0,0 +1,60 @@ +#include + +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +using namespace solidity; +using namespace solidity::evmasm; +using namespace solidity::frontend::json; +using namespace solidity::frontend::test; +using namespace solidity::langutil; +using namespace solidity::util; + +std::vector const StandardJSONOutputExt::contracts() const +{ + return m_base.contracts() | ranges::views::values | ranges::views::join | ranges::views::transform([](auto const& contract) { + return &contract; + }) | ranges::to(); +} + +output::Contract const* StandardJSONOutputExt::contract(ContractName const& _name) const +{ + auto const& sourceUnits = m_base.contracts(); + auto const [sourceName, contractName] = std::pair{std::string{_name.source()}, _name.contract()}; + + auto source = sourceUnits.find(sourceName); + if (source == sourceUnits.end()) + return nullptr; + + auto const& contracts = source->second; + if (contractName.empty() && contracts.empty()) + return nullptr; + + auto order = m_compilationOrder.find(sourceName); + if (order == m_compilationOrder.end() || order->second.empty()) + return nullptr; + + auto lookupName = contractName.empty() ? order->second.back() : _name; + auto contract = ranges::find_if(contracts, [&](auto const& contract) { return contract.name() == lookupName; }); + + return (contract != contracts.end()) ? &*contract : nullptr; +} + diff --git a/test/libsolidity/util/StandardJSONCompiler.h b/test/libsolidity/util/StandardJSONCompiler.h new file mode 100644 index 000000000000..4c8359547249 --- /dev/null +++ b/test/libsolidity/util/StandardJSONCompiler.h @@ -0,0 +1,139 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace solidity; +using namespace solidity::util; + +namespace solidity::frontend::test +{ + +/** + * + */ +class StandardJSONOutputExt +{ +public: + /// + explicit StandardJSONOutputExt(json::StandardJSONOutput _base): + m_base(std::move(_base)) + { + m_compilationOrder = m_base.sources() | ranges::views::transform([](auto const& entry) { + auto name = entry.first; + auto source = entry.second; + + if (!source.ast) + return std::pair{name, std::vector{}}; + + auto order = source.ast.value().at("nodes") | ranges::views::filter([](auto const& node) { + return node.at("nodeType") == "ContractDefinition"; + }) | ranges::views::transform([&](auto const& node) { + return ContractName{name, node.at("name")}; + }) | ranges::to; + + return std::pair{name, order}; + }) | ranges::to(); + } + + /// + std::vector const& errors() const + { + return m_base.errors(); + } + + /// + bool success() const + { + return !ranges::any_of(errors(), [](auto const& e) { + return e.type != langutil::Error::Type::Info && e.type != langutil::Error::Type::Warning; + }); + } + + /// + std::vector const contracts() const; + + /// + json::output::Contract const* contract(ContractName const& _contractName = {}) const; + +private: + /// + json::StandardJSONOutput m_base; + /// + std::map> m_compilationOrder; +}; + +template +concept StandardJSONOutputType = std::constructible_from; + +/** + * Provides an interface to the compiler under test. + */ +template +class StandardJSONCompiler +{ +public: + /// Takes the current compiler input, requests the compiler under test to compile + /// and stores its output. + /// @returns the stored output + /// @param _input to pass to the compiler + Output const& compile(json::StandardJSONInput const& _input) + { + auto output = StandardCompiler{}.compile(_input); + m_output.emplace(StandardJSONOutputExt{std::move(output)}); + return this->output(); + } + + /// @returns the stored output generated during previous compilation. + Output const& output() const + { + solAssert(m_output.has_value(), "No output found. Please compile first."); + return m_output.value(); + } + +private: + /// Last generated output. Will be none before initial compilation. + std::optional m_output; +}; + +} diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 0a1c33873595..5a3fd325a1e2 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(isoltest ../libsolidity/util/BytesUtils.cpp ../libsolidity/util/Common.cpp ../libsolidity/util/ContractABIUtils.cpp + ../libsolidity/util/StandardJSONCompiler.cpp ../libsolidity/util/TestFileParser.cpp ../libsolidity/util/TestFunctionCall.cpp ../libsolidity/GasTest.cpp