From 7582754cd226495512f8360218aa95cc152a3240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 16 Apr 2025 13:37:43 +0200 Subject: [PATCH 1/5] EVMAssemblyStack: Add extra accessors that do not require contract name --- libevmasm/EVMAssemblyStack.cpp | 24 +++++++++++++++++------- libevmasm/EVMAssemblyStack.h | 6 ++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/libevmasm/EVMAssemblyStack.cpp b/libevmasm/EVMAssemblyStack.cpp index 7cb05bf8bf00..bed4ff744ec4 100644 --- a/libevmasm/EVMAssemblyStack.cpp +++ b/libevmasm/EVMAssemblyStack.cpp @@ -74,13 +74,13 @@ void EVMAssemblyStack::assemble() LinkerObject const& EVMAssemblyStack::object(std::string const& _contractName) const { solAssert(_contractName == m_name); - return m_object; + return object(); } LinkerObject const& EVMAssemblyStack::runtimeObject(std::string const& _contractName) const { solAssert(_contractName == m_name); - return m_runtimeObject; + return runtimeObject(); } std::map EVMAssemblyStack::sourceIndices() const @@ -95,13 +95,13 @@ std::map EVMAssemblyStack::sourceIndices() const std::string const* EVMAssemblyStack::sourceMapping(std::string const& _contractName) const { solAssert(_contractName == m_name); - return &m_sourceMapping; + return &sourceMapping(); } std::string const* EVMAssemblyStack::runtimeSourceMapping(std::string const& _contractName) const { solAssert(_contractName == m_name); - return &m_runtimeSourceMapping; + return &runtimeSourceMapping(); } Json EVMAssemblyStack::ethdebug(std::string const& _contractName) const @@ -123,20 +123,30 @@ Json EVMAssemblyStack::ethdebug() const return {}; } -Json EVMAssemblyStack::assemblyJSON(std::string const& _contractName) const +Json EVMAssemblyStack::assemblyJSON() const { - solAssert(_contractName == m_name); solAssert(m_evmAssembly); return m_evmAssembly->assemblyJSON(sourceIndices()); } -std::string EVMAssemblyStack::assemblyString(std::string const& _contractName, StringMap const& _sourceCodes) const +Json EVMAssemblyStack::assemblyJSON(std::string const& _contractName) const { solAssert(_contractName == m_name); + return assemblyJSON(); +} + +std::string EVMAssemblyStack::assemblyString(StringMap const& _sourceCodes) const +{ solAssert(m_evmAssembly); return m_evmAssembly->assemblyString(m_debugInfoSelection, _sourceCodes); } +std::string EVMAssemblyStack::assemblyString(std::string const& _contractName, StringMap const& _sourceCodes) const +{ + solAssert(_contractName == m_name); + return assemblyString(_sourceCodes); +} + std::string const EVMAssemblyStack::filesystemFriendlyName(std::string const& _contractName) const { solAssert(_contractName == m_name); diff --git a/libevmasm/EVMAssemblyStack.h b/libevmasm/EVMAssemblyStack.h index 2fe63c11c2ec..444d444ba9e2 100644 --- a/libevmasm/EVMAssemblyStack.h +++ b/libevmasm/EVMAssemblyStack.h @@ -58,20 +58,26 @@ class EVMAssemblyStack: public AbstractAssemblyStack std::string const& name() const { return m_name; } + LinkerObject const& object() const { return m_object; } LinkerObject const& object(std::string const& _contractName) const override; + LinkerObject const& runtimeObject() const { return m_runtimeObject; } LinkerObject const& runtimeObject(std::string const& _contractName) const override; std::shared_ptr const& evmAssembly() const { return m_evmAssembly; } std::shared_ptr const& evmRuntimeAssembly() const { return m_evmRuntimeAssembly; } + std::string const& sourceMapping() const { return m_sourceMapping; } std::string const* sourceMapping(std::string const& _contractName) const override; + std::string const& runtimeSourceMapping() const { return m_runtimeSourceMapping; } std::string const* runtimeSourceMapping(std::string const& _contractName) const override; Json ethdebug(std::string const& _contractName) const override; Json ethdebugRuntime(std::string const& _contractName) const override; Json ethdebug() const override; + Json assemblyJSON() const; Json assemblyJSON(std::string const& _contractName) const override; + std::string assemblyString(StringMap const& _sourceCodes) const; std::string assemblyString(std::string const& _contractName, StringMap const& _sourceCodes) const override; std::string const filesystemFriendlyName(std::string const& _contractName) const override; From f56831c7c15846c035a232c3981cb7a209e9c804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 14 Mar 2025 04:04:14 +0100 Subject: [PATCH 2/5] TestCaseReader: Expose fileName() --- test/TestCaseReader.h | 1 + 1 file changed, 1 insertion(+) diff --git a/test/TestCaseReader.h b/test/TestCaseReader.h index d1ef95be3c2c..5830e97f3666 100644 --- a/test/TestCaseReader.h +++ b/test/TestCaseReader.h @@ -59,6 +59,7 @@ class TestCaseReader std::size_t lineNumber() const { return m_lineNumber; } std::map const& settings() const { return m_settings; } std::ifstream& stream() { return m_fileStream; } + boost::filesystem::path const& fileName() const { return m_fileName; } std::string simpleExpectations(); From b852d38d626a86522e1660c8087e77825a9fb472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Sat, 19 Apr 2025 04:36:11 +0200 Subject: [PATCH 3/5] Add a transparent comparator to c_instructions to allow string_view lookups --- libevmasm/Instruction.cpp | 2 +- libevmasm/Instruction.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libevmasm/Instruction.cpp b/libevmasm/Instruction.cpp index dad9804d09a8..b5568763573a 100644 --- a/libevmasm/Instruction.cpp +++ b/libevmasm/Instruction.cpp @@ -26,7 +26,7 @@ using namespace solidity; using namespace solidity::util; using namespace solidity::evmasm; -std::map const solidity::evmasm::c_instructions = +std::map> const solidity::evmasm::c_instructions = { { "STOP", Instruction::STOP }, { "ADD", Instruction::ADD }, diff --git a/libevmasm/Instruction.h b/libevmasm/Instruction.h index 137144715296..cfdb85d2fa4c 100644 --- a/libevmasm/Instruction.h +++ b/libevmasm/Instruction.h @@ -322,6 +322,6 @@ InstructionInfo instructionInfo(Instruction _inst, langutil::EVMVersion _evmVers bool isValidInstruction(Instruction _inst); /// Convert from string mnemonic to Instruction type. -extern const std::map c_instructions; +extern const std::map> c_instructions; } From 4c6b7004c10b68ad8d6e641e30c93dd4cd8c4c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 14 Mar 2025 04:51:35 +0100 Subject: [PATCH 4/5] EVM assembly test case with JSON input --- test/CMakeLists.txt | 2 + test/InteractiveTests.h | 3 + test/TestCase.cpp | 7 +- test/libevmasm/EVMAssemblyTest.cpp | 158 ++++++++++++++++++ test/libevmasm/EVMAssemblyTest.h | 70 ++++++++ .../code_generation_error.asmjson | 14 ++ .../isoltestTesting/invalid_json.asmjson | 3 + .../settings_eof_version.asmjson | 13 ++ .../settings_evm_version.asmjson | 13 ++ ...tings_optimizer_constant_optimizer.asmjson | 14 ++ .../settings_optimizer_cse.asmjson | 16 ++ .../settings_optimizer_deduplicate.asmjson | 26 +++ ...expected_executions_per_deployment.asmjson | 26 +++ .../settings_optimizer_inliner.asmjson | 33 ++++ ...ettings_optimizer_jumpdest_remover.asmjson | 14 ++ .../settings_optimizer_peephole.asmjson | 15 ++ .../settings_optimizer_preset.asmjson | 18 ++ .../isoltestTesting/settings_outputs.asmjson | 10 ++ .../isoltestTesting/smoke.asmjson | 11 ++ test/tools/CMakeLists.txt | 1 + test/tools/isoltest.cpp | 2 +- 21 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 test/libevmasm/EVMAssemblyTest.cpp create mode 100644 test/libevmasm/EVMAssemblyTest.h create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/code_generation_error.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/invalid_json.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_eof_version.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_evm_version.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_constant_optimizer.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_cse.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_deduplicate.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_expected_executions_per_deployment.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_inliner.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_jumpdest_remover.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_peephole.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_preset.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/settings_outputs.asmjson create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/smoke.asmjson diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index da115f0d4a5b..6f48b8a8469e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -51,6 +51,8 @@ detect_stray_source_files("${libsolutil_sources}" "libsolutil/") set(libevmasm_sources libevmasm/Assembler.cpp + libevmasm/EVMAssemblyTest.cpp + libevmasm/EVMAssemblyTest.h libevmasm/Optimiser.cpp ) detect_stray_source_files("${libevmasm_sources}" "libevmasm/") diff --git a/test/InteractiveTests.h b/test/InteractiveTests.h index e043fcdbe676..107a6aae0bef 100644 --- a/test/InteractiveTests.h +++ b/test/InteractiveTests.h @@ -42,6 +42,8 @@ #include #include +#include + #include namespace solidity::frontend::test @@ -64,6 +66,7 @@ struct Testsuite Testsuite const g_interactiveTestsuites[] = { /* Title Path Subpath SMT NeedsVM Creator function */ + {"EVM Assembly", "libevmasm", "evmAssemblyTests", false, false, &evmasm::test::EVMAssemblyTest::create}, {"Yul Optimizer", "libyul", "yulOptimizerTests", false, false, &yul::test::YulOptimizerTest::create}, {"Yul Interpreter", "libyul", "yulInterpreterTests", false, false, &yul::test::YulInterpreterTest::create}, {"Yul Object Compiler", "libyul", "objectCompiler", false, false, &yul::test::ObjectCompilerTest::create}, diff --git a/test/TestCase.cpp b/test/TestCase.cpp index da47c26fde90..8a8d2451d1f2 100644 --- a/test/TestCase.cpp +++ b/test/TestCase.cpp @@ -51,9 +51,10 @@ void TestCase::printUpdatedSettings(std::ostream& _stream, std::string const& _l bool TestCase::isTestFilename(boost::filesystem::path const& _filename) { std::string extension = _filename.extension().string(); - return (extension == ".sol" || extension == ".yul" || extension == ".stack") && - !boost::starts_with(_filename.string(), "~") && - !boost::starts_with(_filename.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 == ".asmjson" || extension == ".stack") && + !_filename.string().starts_with('~') && + !_filename.string().starts_with('.'); } bool TestCase::shouldRun() diff --git a/test/libevmasm/EVMAssemblyTest.cpp b/test/libevmasm/EVMAssemblyTest.cpp new file mode 100644 index 000000000000..b497c51476e6 --- /dev/null +++ b/test/libevmasm/EVMAssemblyTest.cpp @@ -0,0 +1,158 @@ +/* + 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 . +*/ + +#include + +#include + +#include +#include + +#include +#include +#include + +#include + +using namespace std::string_literals; +using namespace solidity; +using namespace solidity::test; +using namespace solidity::evmasm; +using namespace solidity::evmasm::test; +using namespace solidity::frontend; +using namespace solidity::frontend::test; +using namespace solidity::langutil; +using namespace solidity::util; + +std::vector const EVMAssemblyTest::c_outputLabels = { + "Assembly", + "Bytecode", + "Opcodes", + "SourceMappings", +}; + +std::unique_ptr EVMAssemblyTest::create(Config const& _config) +{ + return std::make_unique(_config.filename); +} + +EVMAssemblyTest::EVMAssemblyTest(std::string const& _filename): + EVMVersionRestrictedTestCase(_filename) +{ + m_source = m_reader.source(); + m_expectation = m_reader.simpleExpectations(); + + if (!_filename.ends_with(".asmjson")) + BOOST_THROW_EXCEPTION(std::runtime_error("Not an assembly test: \"" + _filename + "\". Allowed extensions: .asmjson.")); + + m_selectedOutputs = m_reader.stringSetting("outputs", "Assembly,Bytecode,Opcodes,SourceMappings"); + OptimisationPreset optimizationPreset = m_reader.enumSetting( + "optimizationPreset", + { + {"none", OptimisationPreset::None}, + {"minimal", OptimisationPreset::Minimal}, + {"standard", OptimisationPreset::Standard}, + {"full", OptimisationPreset::Full}, + }, + "none" + ); + m_optimizerSettings = Assembly::OptimiserSettings::translateSettings(OptimiserSettings::preset(optimizationPreset)); + m_optimizerSettings.expectedExecutionsPerDeployment = m_reader.sizetSetting( + "optimizer.expectedExecutionsPerDeployment", + m_optimizerSettings.expectedExecutionsPerDeployment + ); + + auto const optimizerComponentSetting = [&](std::string const& _component, bool& _setting) { + _setting = m_reader.boolSetting("optimizer." + _component, _setting); + }; + optimizerComponentSetting("inliner", m_optimizerSettings.runInliner); + optimizerComponentSetting("jumpdestRemover", m_optimizerSettings.runJumpdestRemover); + optimizerComponentSetting("peephole", m_optimizerSettings.runPeephole); + optimizerComponentSetting("deduplicate", m_optimizerSettings.runDeduplicate); + optimizerComponentSetting("cse", m_optimizerSettings.runCSE); + optimizerComponentSetting("constantOptimizer", m_optimizerSettings.runConstantOptimiser); + + // TODO: Enable when assembly import for EOF is implemented. + if (CommonOptions::get().eofVersion().has_value()) + m_shouldRun = false; +} + +TestCase::TestResult EVMAssemblyTest::run(std::ostream& _stream, std::string const& _linePrefix, bool const _formatted) +{ + EVMAssemblyStack evmAssemblyStack( + CommonOptions::get().evmVersion(), + CommonOptions::get().eofVersion(), + m_optimizerSettings + ); + + evmAssemblyStack.selectDebugInfo(DebugInfoSelection::AllExceptExperimental()); + + try + { + evmAssemblyStack.parseAndAnalyze(m_reader.fileName().filename().string(), m_source); + } + catch (AssemblyImportException const& _exception) + { + m_obtainedResult = "AssemblyImportException: "s + _exception.what() + "\n"; + return checkResult(_stream, _linePrefix, _formatted); + } + + try + { + evmAssemblyStack.assemble(); + } + catch (Error const& _error) + { + // TODO: EVMAssemblyStack should catch these on its own and provide an error reporter. + soltestAssert(_error.comment(), "Errors must include a message for the user."); + m_obtainedResult = Error::formatErrorType(_error.type()) + ": " + *_error.comment() + "\n"; + return checkResult(_stream, _linePrefix, _formatted); + } + soltestAssert(evmAssemblyStack.compilationSuccessful()); + + auto const produceOutput = [&](std::string const& _output) { + if (_output == "Assembly") + return evmAssemblyStack.assemblyString({{m_reader.fileName().filename().string(), m_source}}); + if (_output == "Bytecode") + return util::toHex(evmAssemblyStack.object().bytecode); + if (_output == "Opcodes") + return disassemble(evmAssemblyStack.object().bytecode, CommonOptions::get().evmVersion()); + if (_output == "SourceMappings") + return evmAssemblyStack.sourceMapping(); + soltestAssert(false); + unreachable(); + }; + + std::set selectedOutputSet; + boost::split(selectedOutputSet, m_selectedOutputs, boost::is_any_of(",")); + for (std::string const& output: c_outputLabels) + if (selectedOutputSet.contains(output)) + { + if (!m_obtainedResult.empty() && m_obtainedResult.back() != '\n') + m_obtainedResult += "\n"; + + // Don't trim on the left to avoid stripping indentation. + std::string content = produceOutput(output); + boost::trim_right(content); + std::string separator = (content.empty() ? "" : (output == "Assembly" ? "\n" : " ")); + m_obtainedResult += output + ":" + separator + content; + } + if (!m_obtainedResult.empty() && m_obtainedResult.back() != '\n') + m_obtainedResult += "\n"; + + return checkResult(_stream, _linePrefix, _formatted); +} diff --git a/test/libevmasm/EVMAssemblyTest.h b/test/libevmasm/EVMAssemblyTest.h new file mode 100644 index 000000000000..f8923c0dc1a1 --- /dev/null +++ b/test/libevmasm/EVMAssemblyTest.h @@ -0,0 +1,70 @@ +/* + 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 + +namespace solidity::evmasm::test +{ + +/// Custom test case that runs the final part of the compiler pipeline (assembling into bytecode). +/// Supports assembly JSON format produced by --asm-json. +/// +/// Available settings: +/// - EVMVersion: The range of EVM versions to run the test for. Inherited from EVMVersionRestrictedTestCase. +/// - bytecodeFormat: The range of bytecode formats (EOF/legacy) to run the test for. Inherited from EVMVersionRestrictedTestCase. +/// - outputs: List of outputs to include in the test. The order of values does NOT determine the order +/// in which the outputs are printed. Supported outputs: Assembly, Bytecode, Opcodes, SourceMappings. +/// The default is to print all outputs. +/// - optimizationPreset: Preset to load as the base optimizer settings. +/// One of: none, minimal, standard, full. The default is none. +/// - optimizer.*: A set of detailed optimizer settings applied on top of the base preset. +/// Each one corresponds to a field in Assembly::OptimiserSettings and uses the value from the +/// preset as its default. Available settings: +/// - optimizer.expectedExecutionsPerDeployment (integer) +/// - optimizer.inliner (bool) +/// - optimizer.jumpdestRemover (bool) +/// - optimizer.peephole (bool) +/// - optimizer.deduplicate (bool) +/// - optimizer.cse (bool) +/// - optimizer.constantOptimizer (bool) +class EVMAssemblyTest: public frontend::test::EVMVersionRestrictedTestCase +{ +public: + static std::unique_ptr create(Config const& _config); + + EVMAssemblyTest(std::string const& _filename); + + TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; + +private: + static std::vector const c_outputLabels; + + std::string m_selectedOutputs; + evmasm::Assembly::OptimiserSettings m_optimizerSettings; +}; + +} diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/code_generation_error.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/code_generation_error.asmjson new file mode 100644 index 000000000000..4297c5f35b80 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/code_generation_error.asmjson @@ -0,0 +1,14 @@ +{ + ".code": [ + {"name": "PUSH [$]", "value": "0"} + ], + ".data": { + "0": { + ".code": [ + {"name": "PUSHIMMUTABLE", "value": "x"} + ] + } + } +} +// ---- +// CodeGenerationError: Some immutables were read from but never assigned, possibly because of optimization. diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/invalid_json.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/invalid_json.asmjson new file mode 100644 index 000000000000..a834108d1ad7 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/invalid_json.asmjson @@ -0,0 +1,3 @@ +{ +// ---- +// AssemblyImportException: Could not parse JSON file. diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_eof_version.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_eof_version.asmjson new file mode 100644 index 000000000000..5785b4b8931e --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_eof_version.asmjson @@ -0,0 +1,13 @@ +{ + ".code": [ + {"name": "CODESIZE"} + ] +} +// ==== +// bytecodeFormat: legacy +// ---- +// Assembly: +// codesize +// Bytecode: 38 +// Opcodes: CODESIZE +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_evm_version.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_evm_version.asmjson new file mode 100644 index 000000000000..a0f042b32e88 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_evm_version.asmjson @@ -0,0 +1,13 @@ +{ + ".code": [ + {"name": "BLOBBASEFEE"} + ] +} +// ==== +// EVMVersion: >=cancun +// ---- +// Assembly: +// blobbasefee +// Bytecode: 4a +// Opcodes: BLOBBASEFEE +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_constant_optimizer.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_constant_optimizer.asmjson new file mode 100644 index 000000000000..a3610217fbaf --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_constant_optimizer.asmjson @@ -0,0 +1,14 @@ +{ + ".code": [ + {"name": "PUSH", "value": "ffffffffffffffff"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.constantOptimizer: true +// ---- +// Assembly: +// sub(shl(0x40, 0x01), 0x01) +// Bytecode: 6001600160401b03 +// Opcodes: PUSH1 0x1 PUSH1 0x1 PUSH1 0x40 SHL SUB +// SourceMappings: :::-:0;;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_cse.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_cse.asmjson new file mode 100644 index 000000000000..c8133b43f799 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_cse.asmjson @@ -0,0 +1,16 @@ +{ + ".code": [ + {"name": "PUSH", "value": "0"}, + {"name": "DUP2"}, + {"name": "SUB"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.cse: true +// ---- +// Assembly: +// dup1 +// Bytecode: 80 +// Opcodes: DUP1 +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_deduplicate.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_deduplicate.asmjson new file mode 100644 index 000000000000..67b87e59e9ee --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_deduplicate.asmjson @@ -0,0 +1,26 @@ +{ + ".code": [ + {"name": "PUSH [tag]", "value": "1"}, + {"name": "PUSH [tag]", "value": "2"}, + {"name": "tag", "value": "1"}, + {"name": "JUMPDEST"}, + {"name": "JUMP"}, + {"name": "tag", "value": "2"}, + {"name": "JUMPDEST"}, + {"name": "JUMP"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.deduplicate: true +// ---- +// Assembly: +// tag_1 +// tag_1 +// tag_1: +// jump +// tag_2: +// jump +// Bytecode: 600460045b565b56 +// Opcodes: PUSH1 0x4 PUSH1 0x4 JUMPDEST JUMP JUMPDEST JUMP +// SourceMappings: :::-:0;;;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_expected_executions_per_deployment.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_expected_executions_per_deployment.asmjson new file mode 100644 index 000000000000..b9fd814d5c8e --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_expected_executions_per_deployment.asmjson @@ -0,0 +1,26 @@ +{ + ".code": [ + {"name": "PUSH [$]", "value": "0"} + ], + ".data": { + "0": { + ".code": [ + {"name": "PUSH", "value": "ffffffffffffffff"} + ] + } + } +} +// ==== +// optimizationPreset: standard +// optimizer.expectedExecutionsPerDeployment: 0 +// ---- +// Assembly: +// dataOffset(sub_0) +// stop +// +// sub_0: assembly { +// sub(shl(0x40, 0x01), 0x01) +// } +// Bytecode: 6003fe6001600160401b03 +// Opcodes: PUSH1 0x3 INVALID PUSH1 0x1 PUSH1 0x1 PUSH1 0x40 SHL SUB +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_inliner.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_inliner.asmjson new file mode 100644 index 000000000000..81be6392c444 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_inliner.asmjson @@ -0,0 +1,33 @@ +{ + ".code": [ + {"name": "PUSH [tag]", "value": "1"}, + {"name": "PUSH [tag]", "value": "2"}, + {"name": "JUMP"}, + {"name": "tag", "value": "1"}, + {"name": "JUMPDEST"}, + {"name": "STOP"}, + {"name": "tag", "value": "2"}, + {"name": "JUMPDEST"}, + {"name": "CALLVALUE"}, + {"name": "SWAP1"}, + {"name": "JUMP"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.inliner: true +// ---- +// Assembly: +// tag_1 +// callvalue +// swap1 +// jump +// tag_1: +// stop +// tag_2: +// callvalue +// swap1 +// jump +// Bytecode: 60053490565b005b349056 +// Opcodes: PUSH1 0x5 CALLVALUE SWAP1 JUMP JUMPDEST STOP JUMPDEST CALLVALUE SWAP1 JUMP +// SourceMappings: :::-:0;;;;;;;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_jumpdest_remover.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_jumpdest_remover.asmjson new file mode 100644 index 000000000000..baca77b992fc --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_jumpdest_remover.asmjson @@ -0,0 +1,14 @@ +{ + ".code": [ + {"name": "tag", "value": "1"}, + {"name": "JUMPDEST"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.jumpdestRemover: true +// ---- +// Assembly: +// Bytecode: +// Opcodes: +// SourceMappings: diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_peephole.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_peephole.asmjson new file mode 100644 index 000000000000..f9e7c3b8d550 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_peephole.asmjson @@ -0,0 +1,15 @@ +{ + ".code": [ + {"name": "STOP"}, + {"name": "STOP"} + ] +} +// ==== +// optimizationPreset: none +// optimizer.peephole: true +// ---- +// Assembly: +// stop +// Bytecode: 00 +// Opcodes: STOP +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_preset.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_preset.asmjson new file mode 100644 index 000000000000..706a3ae8b61a --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_optimizer_preset.asmjson @@ -0,0 +1,18 @@ +{ + ".code": [ + {"name": "PUSH", "value": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"name": "PUSH", "value": "42"}, + {"name": "PUSH", "value": "24"}, + {"name": "SWAP1"}, + {"name": "ADD"} + ] +} +// ==== +// optimizationPreset: full +// ---- +// Assembly: +// not(0x00) +// 0x66 +// Bytecode: 5f196066 +// Opcodes: PUSH0 NOT PUSH1 0x66 +// SourceMappings: :::-:0;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_outputs.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_outputs.asmjson new file mode 100644 index 000000000000..2af7b57c3e75 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/settings_outputs.asmjson @@ -0,0 +1,10 @@ +{ + ".code": [ + {"name": "CALLVALUE"} + ] +} +// ==== +// outputs: SourceMappings,Opcodes +// ---- +// Opcodes: CALLVALUE +// SourceMappings: :::-:0 diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke.asmjson b/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke.asmjson new file mode 100644 index 000000000000..99e527a72587 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke.asmjson @@ -0,0 +1,11 @@ +{ + ".code": [ + {"name": "CALLVALUE"} + ] +} +// ---- +// Assembly: +// callvalue +// Bytecode: 34 +// Opcodes: CALLVALUE +// SourceMappings: :::-:0 diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 5c1d41b7f595..13018bf18512 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(isoltest ../EVMHost.cpp ../TestCase.cpp ../TestCaseReader.cpp + ../libevmasm/EVMAssemblyTest.cpp ../libsolidity/util/BytesUtils.cpp ../libsolidity/util/Common.cpp ../libsolidity/util/ContractABIUtils.cpp diff --git a/test/tools/isoltest.cpp b/test/tools/isoltest.cpp index 57ab4af16034..32309b005507 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|\\.stack))"}; + m_filterExpression = std::regex{"(" + filter + "(\\.sol|\\.yul|\\.asmjson|\\.stack))"}; } bool matches(fs::path const& _path, std::string const& _name) const From f1723b68801e7dedb0daf24566d572b2da2248bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 18 Apr 2025 03:18:05 +0200 Subject: [PATCH 5/5] Support human-readable assembly in EVM assembly test case --- test/CMakeLists.txt | 2 + test/TestCase.cpp | 2 +- test/libevmasm/EVMAssemblyTest.cpp | 29 ++- test/libevmasm/EVMAssemblyTest.h | 16 +- test/libevmasm/PlainAssemblyParser.cpp | 180 ++++++++++++++++++ test/libevmasm/PlainAssemblyParser.h | 79 ++++++++ .../isoltestTesting/comments.asm | 34 ++++ .../isoltestTesting/jumps.asm | 56 ++++++ .../isoltestTesting/operations.asm | 53 ++++++ .../evmAssemblyTests/isoltestTesting/push.asm | 35 ++++ .../isoltestTesting/smoke_plain.asm | 16 ++ test/tools/CMakeLists.txt | 1 + test/tools/isoltest.cpp | 2 +- 13 files changed, 497 insertions(+), 8 deletions(-) create mode 100644 test/libevmasm/PlainAssemblyParser.cpp create mode 100644 test/libevmasm/PlainAssemblyParser.h create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/comments.asm create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/jumps.asm create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/operations.asm create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/push.asm create mode 100644 test/libevmasm/evmAssemblyTests/isoltestTesting/smoke_plain.asm diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6f48b8a8469e..c66963c190d4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,6 +54,8 @@ set(libevmasm_sources libevmasm/EVMAssemblyTest.cpp libevmasm/EVMAssemblyTest.h libevmasm/Optimiser.cpp + libevmasm/PlainAssemblyParser.cpp + libevmasm/PlainAssemblyParser.h ) detect_stray_source_files("${libevmasm_sources}" "libevmasm/") diff --git a/test/TestCase.cpp b/test/TestCase.cpp index 8a8d2451d1f2..efc455ff6ff1 100644 --- a/test/TestCase.cpp +++ b/test/TestCase.cpp @@ -52,7 +52,7 @@ 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 == ".asmjson" || extension == ".stack") && + return (extension == ".sol" || extension == ".yul" || extension == ".asm" || extension == ".asmjson" || extension == ".stack") && !_filename.string().starts_with('~') && !_filename.string().starts_with('.'); } diff --git a/test/libevmasm/EVMAssemblyTest.cpp b/test/libevmasm/EVMAssemblyTest.cpp index b497c51476e6..0c1dbb07ed7c 100644 --- a/test/libevmasm/EVMAssemblyTest.cpp +++ b/test/libevmasm/EVMAssemblyTest.cpp @@ -17,6 +17,8 @@ #include +#include + #include #include @@ -39,6 +41,7 @@ using namespace solidity::langutil; using namespace solidity::util; std::vector const EVMAssemblyTest::c_outputLabels = { + "InputAssemblyJSON", "Assembly", "Bytecode", "Opcodes", @@ -56,8 +59,12 @@ EVMAssemblyTest::EVMAssemblyTest(std::string const& _filename): m_source = m_reader.source(); m_expectation = m_reader.simpleExpectations(); - if (!_filename.ends_with(".asmjson")) - BOOST_THROW_EXCEPTION(std::runtime_error("Not an assembly test: \"" + _filename + "\". Allowed extensions: .asmjson.")); + if (_filename.ends_with(".asmjson")) + m_assemblyFormat = AssemblyFormat::JSON; + else if (_filename.ends_with(".asm")) + m_assemblyFormat = AssemblyFormat::Plain; + else + BOOST_THROW_EXCEPTION(std::runtime_error("Not an assembly test: \"" + _filename + "\". Allowed extensions: .asm, .asmjson.")); m_selectedOutputs = m_reader.stringSetting("outputs", "Assembly,Bytecode,Opcodes,SourceMappings"); OptimisationPreset optimizationPreset = m_reader.enumSetting( @@ -101,9 +108,23 @@ TestCase::TestResult EVMAssemblyTest::run(std::ostream& _stream, std::string con evmAssemblyStack.selectDebugInfo(DebugInfoSelection::AllExceptExperimental()); + std::string assemblyJSON; + switch (m_assemblyFormat) + { + case AssemblyFormat::JSON: + assemblyJSON = m_source; + break; + case AssemblyFormat::Plain: + assemblyJSON = jsonPrint( + PlainAssemblyParser{}.parse(m_reader.fileName().filename().string(), m_source), + {JsonFormat::Pretty, 4} + ); + break; + } + try { - evmAssemblyStack.parseAndAnalyze(m_reader.fileName().filename().string(), m_source); + evmAssemblyStack.parseAndAnalyze(m_reader.fileName().filename().string(), assemblyJSON); } catch (AssemblyImportException const& _exception) { @@ -125,6 +146,8 @@ TestCase::TestResult EVMAssemblyTest::run(std::ostream& _stream, std::string con soltestAssert(evmAssemblyStack.compilationSuccessful()); auto const produceOutput = [&](std::string const& _output) { + if (_output == "InputAssemblyJSON") + return assemblyJSON; if (_output == "Assembly") return evmAssemblyStack.assemblyString({{m_reader.fileName().filename().string(), m_source}}); if (_output == "Bytecode") diff --git a/test/libevmasm/EVMAssemblyTest.h b/test/libevmasm/EVMAssemblyTest.h index f8923c0dc1a1..9059fd716d81 100644 --- a/test/libevmasm/EVMAssemblyTest.h +++ b/test/libevmasm/EVMAssemblyTest.h @@ -31,14 +31,17 @@ namespace solidity::evmasm::test { /// Custom test case that runs the final part of the compiler pipeline (assembling into bytecode). -/// Supports assembly JSON format produced by --asm-json. +/// Supports two kinds of input (depending on file extension): +/// - .asmjson: assembly JSON format produced by --asm-json. +/// - .asm: plain assembly, a more limited but human-readable format that is internally converted +/// to assembly JSON. /// /// Available settings: /// - EVMVersion: The range of EVM versions to run the test for. Inherited from EVMVersionRestrictedTestCase. /// - bytecodeFormat: The range of bytecode formats (EOF/legacy) to run the test for. Inherited from EVMVersionRestrictedTestCase. /// - outputs: List of outputs to include in the test. The order of values does NOT determine the order -/// in which the outputs are printed. Supported outputs: Assembly, Bytecode, Opcodes, SourceMappings. -/// The default is to print all outputs. +/// in which the outputs are printed. Supported outputs: InputAssemblyJSON, Assembly, Bytecode, Opcodes, SourceMappings. +/// The default is to print all outputs except InputAssemblyJSON. /// - optimizationPreset: Preset to load as the base optimizer settings. /// One of: none, minimal, standard, full. The default is none. /// - optimizer.*: A set of detailed optimizer settings applied on top of the base preset. @@ -61,8 +64,15 @@ class EVMAssemblyTest: public frontend::test::EVMVersionRestrictedTestCase TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; private: + enum class AssemblyFormat + { + JSON, + Plain, + }; + static std::vector const c_outputLabels; + AssemblyFormat m_assemblyFormat{}; std::string m_selectedOutputs; evmasm::Assembly::OptimiserSettings m_optimizerSettings; }; diff --git a/test/libevmasm/PlainAssemblyParser.cpp b/test/libevmasm/PlainAssemblyParser.cpp new file mode 100644 index 000000000000..0fb9d62c8c10 --- /dev/null +++ b/test/libevmasm/PlainAssemblyParser.cpp @@ -0,0 +1,180 @@ +/* + 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 . +*/ + +#include + +#include +#include + +#include + +#include + +#include + +#include + +#include + +using namespace std::string_literals; +using namespace solidity; +using namespace solidity::test; +using namespace solidity::evmasm; +using namespace solidity::evmasm::test; +using namespace solidity::langutil; + +Json PlainAssemblyParser::parse(std::string _sourceName, std::string const& _source) +{ + m_sourceName = std::move(_sourceName); + Json codeJSON = Json::array(); + std::istringstream sourceStream(_source); + while (getline(sourceStream, m_line)) + { + advanceLine(m_line); + if (m_lineTokens.empty()) + continue; + + if (c_instructions.contains(currentToken().value)) + { + expectNoMoreArguments(); + codeJSON.push_back({{"name", currentToken().value}}); + } + else if (currentToken().value == "PUSH") + { + if (hasMoreTokens() && nextToken().value == "[tag]") + { + advanceToken(); + std::string_view tagID = expectArgument(); + expectNoMoreArguments(); + codeJSON.push_back({{"name", "PUSH [tag]"}, {"value", tagID}}); + } + else + { + std::string_view immediateArgument = expectArgument(); + expectNoMoreArguments(); + + if (!immediateArgument.starts_with("0x")) + BOOST_THROW_EXCEPTION(std::runtime_error(formatError("The immediate argument to PUSH must be a hex number prefixed with '0x'."))); + + immediateArgument.remove_prefix("0x"s.size()); + codeJSON.push_back({{"name", "PUSH"}, {"value", immediateArgument}}); + } + } + else if (currentToken().value == "tag") + { + std::string_view tagID = expectArgument(); + expectNoMoreArguments(); + + codeJSON.push_back({{"name", "tag"}, {"value", tagID}}); + codeJSON.push_back({{"name", "JUMPDEST"}}); + } + else + BOOST_THROW_EXCEPTION(std::runtime_error(formatError("Unknown instruction."))); + } + return {{".code", codeJSON}}; +} + +PlainAssemblyParser::Token const& PlainAssemblyParser::currentToken() const +{ + soltestAssert(m_tokenIndex < m_lineTokens.size()); + return m_lineTokens[m_tokenIndex]; +} + +PlainAssemblyParser::Token const& PlainAssemblyParser::nextToken() const +{ + soltestAssert(m_tokenIndex + 1 < m_lineTokens.size()); + return m_lineTokens[m_tokenIndex + 1]; +} + +bool PlainAssemblyParser::advanceToken() +{ + if (!hasMoreTokens()) + return false; + + ++m_tokenIndex; + return true; +} + +std::string_view PlainAssemblyParser::expectArgument() +{ + bool hasArgument = advanceToken(); + if (!hasArgument) + BOOST_THROW_EXCEPTION(std::runtime_error(formatError("Missing argument(s)."))); + + return currentToken().value; +} + +void PlainAssemblyParser::expectNoMoreArguments() +{ + bool hasArgument = advanceToken(); + if (hasArgument) + BOOST_THROW_EXCEPTION(std::runtime_error(formatError("Too many arguments."))); +} + +void PlainAssemblyParser::advanceLine(std::string_view _line) +{ + ++m_lineNumber; + m_line = _line; + m_lineTokens = tokenizeLine(m_line); + m_tokenIndex = 0; +} + +std::vector PlainAssemblyParser::tokenizeLine(std::string_view _line) +{ + auto const notWhiteSpace = [](char _c) { return !isWhiteSpace(_c); }; + + std::vector tokens; + auto tokenLocation = boost::find_token(_line, notWhiteSpace, boost::token_compress_on); + while (!tokenLocation.empty()) + { + std::string_view value{tokenLocation.begin(), tokenLocation.end()}; + if (value.starts_with("//")) + break; + + tokens.push_back({ + .value = value, + .position = static_cast(std::distance(_line.begin(), tokenLocation.begin())), + }); + soltestAssert(!value.empty()); + soltestAssert(tokens.back().position < _line.size()); + soltestAssert(tokens.back().position + value.size() <= _line.size()); + + std::string_view tail{tokenLocation.end(), _line.end()}; + tokenLocation = boost::find_token(tail, notWhiteSpace, boost::token_compress_on); + } + + return tokens; +} + +std::string PlainAssemblyParser::formatError(std::string_view _message) const +{ + std::string lineNumberString = std::to_string(m_lineNumber); + std::string padding(lineNumberString.size(), ' '); + std::string underline = std::string(currentToken().position, ' ') + std::string(currentToken().value.size(), '^'); + return fmt::format( + "Error while parsing plain assembly: {}\n" + "{}--> {}\n" + "{} | \n" + "{} | {}\n" + "{} | {}\n", + _message, + padding, m_sourceName, + padding, + m_lineNumber, m_line, + padding, underline + ); +} diff --git a/test/libevmasm/PlainAssemblyParser.h b/test/libevmasm/PlainAssemblyParser.h new file mode 100644 index 000000000000..0154af5f7f19 --- /dev/null +++ b/test/libevmasm/PlainAssemblyParser.h @@ -0,0 +1,79 @@ +/* + 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 + +namespace solidity::evmasm::test +{ + +/// Parser for the plain assembly format. The format is meant to be good enough for humans to read +/// while being straightforward to map to the assembly JSON format that solc can import. +/// +/// Syntax: +/// - Every line consists of zero or more whitespace-separated tokens. +/// - A token that begins with `//` starts a comment, which extends to the end of the line. +/// - A non-empty line represents a single assembly item. +/// - The name of the item is the first thing on the line and may consist of one or more tokens. +/// - One or more arguments follow the name. +/// +/// Supported items: +/// - All instruction names. +/// - PUSH +/// - PUSH [tag] +/// - tag +class PlainAssemblyParser +{ +public: + /// Parses plain assembly format and returns the equivalent assembly JSON. + /// Errors are reported by throwing runtime_error. + Json parse(std::string _sourceName, std::string const& _source); + +protected: + struct Token + { + std::string_view value; ///< Substring of m_line that represents a complete token. + size_t position; ///< Position of the first character of the token within m_line. + }; + + Token const& currentToken() const; + Token const& nextToken() const; + bool hasMoreTokens() const { return m_tokenIndex + 1 < m_lineTokens.size(); } + + bool advanceToken(); + std::string_view expectArgument(); + void expectNoMoreArguments(); + void advanceLine(std::string_view _line); + + static std::vector tokenizeLine(std::string_view _line); + std::string formatError(std::string_view _message) const; + +private: + std::string m_sourceName; ///< Name of the file the source comes from. + size_t m_lineNumber = 0; ///< The number of the current line within the source, 1-based. + std::string m_line; ///< The current line, unparsed. + std::vector m_lineTokens; ///< Decomposition of the current line into tokens (does not include comments). + size_t m_tokenIndex = 0; ///< Points at a token within m_lineTokens. +}; + +} diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/comments.asm b/test/libevmasm/evmAssemblyTests/isoltestTesting/comments.asm new file mode 100644 index 000000000000..13dca3ad1b2f --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/comments.asm @@ -0,0 +1,34 @@ +// +//// comment + // comment +CALLVALUE // 0xff +CALLVALUE //0xff + +PUSH 0xff // comment // //0xff +// + +// +// ==== +// outputs: InputAssemblyJSON,Assembly,Bytecode,Opcodes,SourceMappings +// ---- +// InputAssemblyJSON: { +// ".code": [ +// { +// "name": "CALLVALUE" +// }, +// { +// "name": "CALLVALUE" +// }, +// { +// "name": "PUSH", +// "value": "ff" +// } +// ] +// } +// Assembly: +// callvalue +// callvalue +// 0xff +// Bytecode: 343460ff +// Opcodes: CALLVALUE CALLVALUE PUSH1 0xFF +// SourceMappings: :::-:0;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/jumps.asm b/test/libevmasm/evmAssemblyTests/isoltestTesting/jumps.asm new file mode 100644 index 000000000000..be92671d8141 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/jumps.asm @@ -0,0 +1,56 @@ +PUSH [tag] 1 +JUMP +tag 1 +PUSH 0x01 +JUMPI +PUSH [tag] 0x012AB +tag 0x012AB +// ==== +// outputs: InputAssemblyJSON,Assembly,Bytecode,Opcodes,SourceMappings +// ---- +// InputAssemblyJSON: { +// ".code": [ +// { +// "name": "PUSH [tag]", +// "value": "1" +// }, +// { +// "name": "JUMP" +// }, +// { +// "name": "tag", +// "value": "1" +// }, +// { +// "name": "JUMPDEST" +// }, +// { +// "name": "PUSH", +// "value": "01" +// }, +// { +// "name": "JUMPI" +// }, +// { +// "name": "PUSH [tag]", +// "value": "0x012AB" +// }, +// { +// "name": "tag", +// "value": "0x012AB" +// }, +// { +// "name": "JUMPDEST" +// } +// ] +// } +// Assembly: +// jump(tag_1) +// tag_1: +// 0x01 +// jumpi +// tag_4779 +// tag_4779: +// Bytecode: 6003565b60015760095b +// Opcodes: PUSH1 0x3 JUMP JUMPDEST PUSH1 0x1 JUMPI PUSH1 0x9 JUMPDEST +// SourceMappings: :::-:0;;;;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/operations.asm b/test/libevmasm/evmAssemblyTests/isoltestTesting/operations.asm new file mode 100644 index 000000000000..923b0a6b1f37 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/operations.asm @@ -0,0 +1,53 @@ +NUMBER +SLOAD +ADDRESS +ORIGIN +ADD +DUP1 +SWAP1 +MSTORE8 +STOP +// ==== +// outputs: InputAssemblyJSON,Assembly,Bytecode,Opcodes,SourceMappings +// ---- +// InputAssemblyJSON: { +// ".code": [ +// { +// "name": "NUMBER" +// }, +// { +// "name": "SLOAD" +// }, +// { +// "name": "ADDRESS" +// }, +// { +// "name": "ORIGIN" +// }, +// { +// "name": "ADD" +// }, +// { +// "name": "DUP1" +// }, +// { +// "name": "SWAP1" +// }, +// { +// "name": "MSTORE8" +// }, +// { +// "name": "STOP" +// } +// ] +// } +// Assembly: +// sload(number) +// add(origin, address) +// dup1 +// swap1 +// mstore8 +// stop +// Bytecode: 435430320180905300 +// Opcodes: NUMBER SLOAD ADDRESS ORIGIN ADD DUP1 SWAP1 MSTORE8 STOP +// SourceMappings: :::-:0;;;;;;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/push.asm b/test/libevmasm/evmAssemblyTests/isoltestTesting/push.asm new file mode 100644 index 000000000000..51bc160507c7 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/push.asm @@ -0,0 +1,35 @@ +PUSH 0x0 +PUSH 0x1 +PUSH 0x0123456789ABCDEF +PUSH 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +// ==== +// outputs: InputAssemblyJSON,Assembly,Bytecode,Opcodes,SourceMappings +// ---- +// InputAssemblyJSON: { +// ".code": [ +// { +// "name": "PUSH", +// "value": "0" +// }, +// { +// "name": "PUSH", +// "value": "1" +// }, +// { +// "name": "PUSH", +// "value": "0123456789ABCDEF" +// }, +// { +// "name": "PUSH", +// "value": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +// } +// ] +// } +// Assembly: +// 0x00 +// 0x01 +// 0x0123456789abcdef +// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +// Bytecode: 5f6001670123456789abcdef7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +// Opcodes: PUSH0 PUSH1 0x1 PUSH8 0x123456789ABCDEF PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +// SourceMappings: :::-:0;;; diff --git a/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke_plain.asm b/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke_plain.asm new file mode 100644 index 000000000000..a1714241fb86 --- /dev/null +++ b/test/libevmasm/evmAssemblyTests/isoltestTesting/smoke_plain.asm @@ -0,0 +1,16 @@ +CALLVALUE +// ==== +// outputs: InputAssemblyJSON,Assembly,Bytecode,Opcodes,SourceMappings +// ---- +// InputAssemblyJSON: { +// ".code": [ +// { +// "name": "CALLVALUE" +// } +// ] +// } +// Assembly: +// callvalue +// Bytecode: 34 +// Opcodes: CALLVALUE +// SourceMappings: :::-:0 diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 13018bf18512..289f2f2def35 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(isoltest ../TestCase.cpp ../TestCaseReader.cpp ../libevmasm/EVMAssemblyTest.cpp + ../libevmasm/PlainAssemblyParser.cpp ../libsolidity/util/BytesUtils.cpp ../libsolidity/util/Common.cpp ../libsolidity/util/ContractABIUtils.cpp diff --git a/test/tools/isoltest.cpp b/test/tools/isoltest.cpp index 32309b005507..26efc089020b 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|\\.asmjson|\\.stack))"}; + m_filterExpression = std::regex{"(" + filter + "(\\.sol|\\.yul|\\.asm|\\.asmjson|\\.stack))"}; } bool matches(fs::path const& _path, std::string const& _name) const