diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f891ff7e1d..b4be1043c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: - name: Generate Binary run: >- - pip install . && + pip install --no-binary pycryptodome . && pip install pyinstaller && make freeze diff --git a/.github/workflows/era-tester.yml b/.github/workflows/era-tester.yml index 8a2a3e50ce..187b5c03a2 100644 --- a/.github/workflows/era-tester.yml +++ b/.github/workflows/era-tester.yml @@ -101,11 +101,11 @@ jobs: if: ${{ github.ref != 'refs/heads/master' }} run: | cd era-compiler-tester - cargo run --release --bin compiler-tester -- -v --path=tests/vyper/ --mode="M0B0 ${{ env.VYPER_VERSION }}" + cargo run --release --bin compiler-tester -- --path=tests/vyper/ --mode="M0B0 ${{ env.VYPER_VERSION }}" - name: Run tester (slow) # Run era tester across the LLVM optimization matrix if: ${{ github.ref == 'refs/heads/master' }} run: | cd era-compiler-tester - cargo run --release --bin compiler-tester -- -v --path=tests/vyper/ --mode="M*B* ${{ env.VYPER_VERSION }}" + cargo run --release --bin compiler-tester -- --path=tests/vyper/ --mode="M*B* ${{ env.VYPER_VERSION }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42e0524b13..b6399b3ae9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,8 +79,8 @@ jobs: strategy: matrix: python-version: [["3.10", "310"], ["3.11", "311"]] - # run in default (optimized) and --no-optimize mode - flag: ["core", "no-opt"] + # run in modes: --optimize [gas, none, codesize] + flag: ["core", "no-opt", "codesize"] name: py${{ matrix.python-version[1] }}-${{ matrix.flag }} diff --git a/docs/compiling-a-contract.rst b/docs/compiling-a-contract.rst index 6295226bca..208771a5a9 100644 --- a/docs/compiling-a-contract.rst +++ b/docs/compiling-a-contract.rst @@ -99,6 +99,11 @@ See :ref:`searching_for_imports` for more information on Vyper's import system. Online Compilers ================ +Try VyperLang! +----------------- + +`Try VyperLang! `_ is a JupterHub instance hosted by the Vyper team as a sandbox for developing and testing contracts in Vyper. It requires github for login, and supports deployment via the browser. + Remix IDE --------- @@ -109,22 +114,33 @@ Remix IDE While the Vyper version of the Remix IDE compiler is updated on a regular basis, it might be a bit behind the latest version found in the master branch of the repository. Make sure the byte code matches the output from your local compiler. +.. _evm-version: + Setting the Target EVM Version ============================== -When you compile your contract code, you can specify the Ethereum Virtual Machine version to compile for, to avoid particular features or behaviours. +When you compile your contract code, you can specify the target Ethereum Virtual Machine version to compile for, to access or avoid particular features. You can specify the version either with a source code pragma or as a compiler option. It is recommended to use the compiler option when you want flexibility (for instance, ease of deploying across different chains), and the source code pragma when you want bytecode reproducibility (for instance, when verifying code on a block explorer). + +.. note:: + If the evm version specified by the compiler options conflicts with the source code pragma, an exception will be raised and compilation will not continue. + +For instance, the adding the following pragma to a contract indicates that it should be compiled for the "shanghai" fork of the EVM. + +.. code-block:: python + + #pragma evm-version shanghai .. warning:: - Compiling for the wrong EVM version can result in wrong, strange and failing behaviour. Please ensure, especially if running a private chain, that you use matching EVM versions. + Compiling for the wrong EVM version can result in wrong, strange, or failing behavior. Please ensure, especially if running a private chain, that you use matching EVM versions. -When compiling via ``vyper``, include the ``--evm-version`` flag: +When compiling via the ``vyper`` CLI, you can specify the EVM version option using the ``--evm-version`` flag: :: $ vyper --evm-version [VERSION] -When using the JSON interface, include the ``"evmVersion"`` key within the ``"settings"`` field: +When using the JSON interface, you can include the ``"evmVersion"`` key within the ``"settings"`` field: .. code-block:: javascript @@ -213,9 +229,10 @@ The following example describes the expected input format of ``vyper-json``. Com // Optional "settings": { "evmVersion": "shanghai", // EVM version to compile for. Can be istanbul, berlin, paris, shanghai (default) or cancun (experimental!). - // optional, whether or not optimizations are turned on - // defaults to true - "optimize": true, + // optional, optimization mode + // defaults to "gas". can be one of "gas", "codesize", "none", + // false and true (the last two are for backwards compatibility). + "optimize": "gas", // optional, whether or not the bytecode should include Vyper's signature // defaults to true "bytecodeMetadata": true, diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index 8eb2c1da78..c7abb3e645 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -9,16 +9,47 @@ This section provides a quick overview of the types of data present within a con .. _structure-versions: -Version Pragma +Pragmas ============== -Vyper supports a version pragma to ensure that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM `_ style syntax. +Vyper supports several source code directives to control compiler modes and help with build reproducibility. + +Version Pragma +-------------- + +The version pragma ensures that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM `_ style syntax. + +As of 0.3.10, the recommended way to specify the version pragma is as follows: .. code-block:: python - # @version ^0.2.0 + #pragma version ^0.3.0 + +The following declaration is equivalent, and, prior to 0.3.10, was the only supported method to specify the compiler version: + +.. code-block:: python + + # @version ^0.3.0 + + +In the above examples, the contract will only compile with Vyper versions ``0.3.x``. + +Optimization Mode +----------------- + +The optimization mode can be one of ``"none"``, ``"codesize"``, or ``"gas"`` (default). For instance, the following contract will be compiled in a way which tries to minimize codesize: + +.. code-block:: python + + #pragma optimize codesize + +The optimization mode can also be set as a compiler option. If the compiler option conflicts with the source code pragma, an exception will be raised and compilation will not continue. + +EVM Version +----------------- + +The EVM version can be set with the ``evm-version`` pragma, which is documented in :ref:`evm-version`. -In the above example, the contract only compiles with Vyper versions ``0.2.x``. .. _structure-state-variables: diff --git a/setup.cfg b/setup.cfg index d18ffe2ac7..dd4a32a3ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,6 @@ addopts = -n auto --cov-report html --cov-report xml --cov=vyper - --hypothesis-show-statistics python_files = test_*.py testpaths = tests markers = diff --git a/setup.py b/setup.py index 05cb52259d..36a138aacd 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ "flake8-bugbear==20.1.4", "flake8-use-fstring==1.1", "isort==5.9.3", - "mypy==0.910", + "mypy==0.982", ], "docs": ["recommonmark", "sphinx>=6.0,<7.0", "sphinx_rtd_theme>=1.2,<1.3"], "dev": ["ipython", "pre-commit", "pyinstaller", "twine"], diff --git a/tests/ast/test_pre_parser.py b/tests/ast/test_pre_parser.py index 8501bb8749..150ee55edf 100644 --- a/tests/ast/test_pre_parser.py +++ b/tests/ast/test_pre_parser.py @@ -1,6 +1,7 @@ import pytest -from vyper.ast.pre_parser import validate_version_pragma +from vyper.ast.pre_parser import pre_parse, validate_version_pragma +from vyper.compiler.settings import OptimizationLevel, Settings from vyper.exceptions import VersionException SRC_LINE = (1, 0) # Dummy source line @@ -51,14 +52,14 @@ def set_version(version): @pytest.mark.parametrize("file_version", valid_versions) def test_valid_version_pragma(file_version, mock_version): mock_version(COMPILER_VERSION) - validate_version_pragma(f" @version {file_version}", (SRC_LINE)) + validate_version_pragma(f"{file_version}", (SRC_LINE)) @pytest.mark.parametrize("file_version", invalid_versions) def test_invalid_version_pragma(file_version, mock_version): mock_version(COMPILER_VERSION) with pytest.raises(VersionException): - validate_version_pragma(f" @version {file_version}", (SRC_LINE)) + validate_version_pragma(f"{file_version}", (SRC_LINE)) prerelease_valid_versions = [ @@ -98,11 +99,85 @@ def test_invalid_version_pragma(file_version, mock_version): @pytest.mark.parametrize("file_version", prerelease_valid_versions) def test_prerelease_valid_version_pragma(file_version, mock_version): mock_version(PRERELEASE_COMPILER_VERSION) - validate_version_pragma(f" @version {file_version}", (SRC_LINE)) + validate_version_pragma(file_version, (SRC_LINE)) @pytest.mark.parametrize("file_version", prerelease_invalid_versions) def test_prerelease_invalid_version_pragma(file_version, mock_version): mock_version(PRERELEASE_COMPILER_VERSION) with pytest.raises(VersionException): - validate_version_pragma(f" @version {file_version}", (SRC_LINE)) + validate_version_pragma(file_version, (SRC_LINE)) + + +pragma_examples = [ + ( + """ + """, + Settings(), + ), + ( + """ + #pragma optimize codesize + """, + Settings(optimize=OptimizationLevel.CODESIZE), + ), + ( + """ + #pragma optimize none + """, + Settings(optimize=OptimizationLevel.NONE), + ), + ( + """ + #pragma optimize gas + """, + Settings(optimize=OptimizationLevel.GAS), + ), + ( + """ + #pragma version 0.3.10 + """, + Settings(compiler_version="0.3.10"), + ), + ( + """ + #pragma evm-version shanghai + """, + Settings(evm_version="shanghai"), + ), + ( + """ + #pragma optimize codesize + #pragma evm-version shanghai + """, + Settings(evm_version="shanghai", optimize=OptimizationLevel.GAS), + ), + ( + """ + #pragma version 0.3.10 + #pragma evm-version shanghai + """, + Settings(evm_version="shanghai", compiler_version="0.3.10"), + ), + ( + """ + #pragma version 0.3.10 + #pragma optimize gas + """, + Settings(compiler_version="0.3.10", optimize=OptimizationLevel.GAS), + ), + ( + """ + #pragma version 0.3.10 + #pragma evm-version shanghai + #pragma optimize gas + """, + Settings(compiler_version="0.3.10", optimize=OptimizationLevel.GAS, evm_version="shanghai"), + ), +] + + +@pytest.mark.parametrize("code, expected_pragmas", pragma_examples) +def parse_pragmas(code, expected_pragmas): + pragmas, _, _ = pre_parse(code) + assert pragmas == expected_pragmas diff --git a/tests/base_conftest.py b/tests/base_conftest.py index 29809a074d..a78562e982 100644 --- a/tests/base_conftest.py +++ b/tests/base_conftest.py @@ -12,6 +12,7 @@ from vyper import compiler from vyper.ast.grammar import parse_vyper_source +from vyper.compiler.settings import Settings class VyperMethod: @@ -111,14 +112,16 @@ def w3(tester): return w3 -def _get_contract(w3, source_code, no_optimize, *args, **kwargs): +def _get_contract(w3, source_code, optimize, *args, **kwargs): + settings = Settings() + settings.evm_version = kwargs.pop("evm_version", None) + settings.optimize = optimize out = compiler.compile_code( source_code, # test that metadata gets generated ["abi", "bytecode", "metadata"], + settings=settings, interface_codes=kwargs.pop("interface_codes", None), - no_optimize=no_optimize, - evm_version=kwargs.pop("evm_version", None), show_gas_estimates=True, # Enable gas estimates for testing ) parse_vyper_source(source_code) # Test grammar. @@ -135,13 +138,15 @@ def _get_contract(w3, source_code, no_optimize, *args, **kwargs): return w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract) -def _deploy_blueprint_for(w3, source_code, no_optimize, initcode_prefix=b"", **kwargs): +def _deploy_blueprint_for(w3, source_code, optimize, initcode_prefix=b"", **kwargs): + settings = Settings() + settings.evm_version = kwargs.pop("evm_version", None) + settings.optimize = optimize out = compiler.compile_code( source_code, ["abi", "bytecode"], interface_codes=kwargs.pop("interface_codes", None), - no_optimize=no_optimize, - evm_version=kwargs.pop("evm_version", None), + settings=settings, show_gas_estimates=True, # Enable gas estimates for testing ) parse_vyper_source(source_code) # Test grammar. @@ -173,17 +178,17 @@ def factory(address): @pytest.fixture(scope="module") -def deploy_blueprint_for(w3, no_optimize): +def deploy_blueprint_for(w3, optimize): def deploy_blueprint_for(source_code, *args, **kwargs): - return _deploy_blueprint_for(w3, source_code, no_optimize, *args, **kwargs) + return _deploy_blueprint_for(w3, source_code, optimize, *args, **kwargs) return deploy_blueprint_for @pytest.fixture(scope="module") -def get_contract(w3, no_optimize): +def get_contract(w3, optimize): def get_contract(source_code, *args, **kwargs): - return _get_contract(w3, source_code, no_optimize, *args, **kwargs) + return _get_contract(w3, source_code, optimize, *args, **kwargs) return get_contract diff --git a/tests/cli/vyper_json/test_get_settings.py b/tests/cli/vyper_json/test_get_settings.py index 7530e85ef8..bbe5dab113 100644 --- a/tests/cli/vyper_json/test_get_settings.py +++ b/tests/cli/vyper_json/test_get_settings.py @@ -3,7 +3,6 @@ import pytest from vyper.cli.vyper_json import get_evm_version -from vyper.evm.opcodes import DEFAULT_EVM_VERSION from vyper.exceptions import JSONError @@ -31,7 +30,3 @@ def test_early_evm(evm_version): @pytest.mark.parametrize("evm_version", ["istanbul", "berlin", "paris", "shanghai", "cancun"]) def test_valid_evm(evm_version): assert evm_version == get_evm_version({"settings": {"evmVersion": evm_version}}) - - -def test_default_evm(): - assert get_evm_version({}) == DEFAULT_EVM_VERSION diff --git a/tests/compiler/asm/test_asm_optimizer.py b/tests/compiler/asm/test_asm_optimizer.py index f4a245e168..47b70a8c70 100644 --- a/tests/compiler/asm/test_asm_optimizer.py +++ b/tests/compiler/asm/test_asm_optimizer.py @@ -1,6 +1,7 @@ import pytest from vyper.compiler.phases import CompilerData +from vyper.compiler.settings import OptimizationLevel, Settings codes = [ """ @@ -72,7 +73,7 @@ def __init__(): @pytest.mark.parametrize("code", codes) def test_dead_code_eliminator(code): - c = CompilerData(code, no_optimize=True) + c = CompilerData(code, settings=Settings(optimize=OptimizationLevel.NONE)) initcode_asm = [i for i in c.assembly if not isinstance(i, list)] runtime_asm = c.assembly_runtime @@ -87,7 +88,7 @@ def test_dead_code_eliminator(code): for s in (ctor_only_label, runtime_only_label): assert s + "_runtime" in runtime_asm - c = CompilerData(code, no_optimize=False) + c = CompilerData(code, settings=Settings(optimize=OptimizationLevel.GAS)) initcode_asm = [i for i in c.assembly if not isinstance(i, list)] runtime_asm = c.assembly_runtime diff --git a/tests/compiler/test_opcodes.py b/tests/compiler/test_opcodes.py index b9841b92f0..20f45ced6b 100644 --- a/tests/compiler/test_opcodes.py +++ b/tests/compiler/test_opcodes.py @@ -59,5 +59,8 @@ def test_get_opcodes(evm_version): assert "PUSH0" in ops if evm_version in ("cancun",): - assert "TLOAD" in ops - assert "TSTORE" in ops + for op in ("TLOAD", "TSTORE", "MCOPY"): + assert op in ops + else: + for op in ("TLOAD", "TSTORE", "MCOPY"): + assert op not in ops diff --git a/tests/compiler/test_pre_parser.py b/tests/compiler/test_pre_parser.py index 4b747bb7d1..1761e74bad 100644 --- a/tests/compiler/test_pre_parser.py +++ b/tests/compiler/test_pre_parser.py @@ -1,6 +1,8 @@ -from pytest import raises +import pytest -from vyper.exceptions import SyntaxException +from vyper.compiler import compile_code +from vyper.compiler.settings import OptimizationLevel, Settings +from vyper.exceptions import StructureException, SyntaxException def test_semicolon_prohibited(get_contract): @@ -10,7 +12,7 @@ def test() -> int128: return a + b """ - with raises(SyntaxException): + with pytest.raises(SyntaxException): get_contract(code) @@ -70,6 +72,57 @@ def test(): assert get_contract(code) +def test_version_pragma2(get_contract): + # new, `#pragma` way of doing things + from vyper import __version__ + + installed_version = ".".join(__version__.split(".")[:3]) + + code = f""" +#pragma version {installed_version} + +@external +def test(): + pass + """ + assert get_contract(code) + + +def test_evm_version_check(assert_compile_failed): + code = """ +#pragma evm-version berlin + """ + assert compile_code(code, settings=Settings(evm_version=None)) is not None + assert compile_code(code, settings=Settings(evm_version="berlin")) is not None + # should fail if compile options indicate different evm version + # from source pragma + with pytest.raises(StructureException): + compile_code(code, settings=Settings(evm_version="shanghai")) + + +def test_optimization_mode_check(): + code = """ +#pragma optimize codesize + """ + assert compile_code(code, settings=Settings(optimize=None)) + # should fail if compile options indicate different optimization mode + # from source pragma + with pytest.raises(StructureException): + compile_code(code, settings=Settings(optimize=OptimizationLevel.GAS)) + with pytest.raises(StructureException): + compile_code(code, settings=Settings(optimize=OptimizationLevel.NONE)) + + +def test_optimization_mode_check_none(): + code = """ +#pragma optimize none + """ + assert compile_code(code, settings=Settings(optimize=None)) + # "none" conflicts with "gas" + with pytest.raises(StructureException): + compile_code(code, settings=Settings(optimize=OptimizationLevel.GAS)) + + def test_version_empty_version(assert_compile_failed, get_contract): code = """ #@version @@ -110,5 +163,5 @@ def foo(): convert( """ - with raises(SyntaxException): + with pytest.raises(SyntaxException): get_contract(code) diff --git a/tests/conftest.py b/tests/conftest.py index 1cc9e4e72e..9c9c4191b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from vyper import compiler from vyper.codegen.ir_node import IRnode +from vyper.compiler.settings import OptimizationLevel from vyper.ir import compile_ir, optimizer from .base_conftest import VyperContract, _get_contract, zero_gas_price_strategy @@ -36,12 +37,18 @@ def set_evm_verbose_logging(): def pytest_addoption(parser): - parser.addoption("--no-optimize", action="store_true", help="disable asm and IR optimizations") + parser.addoption( + "--optimize", + choices=["codesize", "gas", "none"], + default="gas", + help="change optimization mode", + ) @pytest.fixture(scope="module") -def no_optimize(pytestconfig): - return pytestconfig.getoption("no_optimize") +def optimize(pytestconfig): + flag = pytestconfig.getoption("optimize") + return OptimizationLevel.from_string(flag) @pytest.fixture @@ -58,13 +65,13 @@ def bytes_helper(str, length): @pytest.fixture -def get_contract_from_ir(w3, no_optimize): +def get_contract_from_ir(w3, optimize): def ir_compiler(ir, *args, **kwargs): ir = IRnode.from_list(ir) - if not no_optimize: + if optimize != OptimizationLevel.NONE: ir = optimizer.optimize(ir) bytecode, _ = compile_ir.assembly_to_evm( - compile_ir.compile_to_assembly(ir, no_optimize=no_optimize) + compile_ir.compile_to_assembly(ir, optimize=optimize) ) abi = kwargs.get("abi") or [] c = w3.eth.contract(abi=abi, bytecode=bytecode) @@ -80,7 +87,7 @@ def ir_compiler(ir, *args, **kwargs): @pytest.fixture(scope="module") -def get_contract_module(no_optimize): +def get_contract_module(optimize): """ This fixture is used for Hypothesis tests to ensure that the same contract is called over multiple runs of the test. @@ -93,7 +100,7 @@ def get_contract_module(no_optimize): w3.eth.set_gas_price_strategy(zero_gas_price_strategy) def get_contract_module(source_code, *args, **kwargs): - return _get_contract(w3, source_code, no_optimize, *args, **kwargs) + return _get_contract(w3, source_code, optimize, *args, **kwargs) return get_contract_module @@ -138,9 +145,9 @@ def set_decorator_to_contract_function(w3, tester, contract, source_code, func): @pytest.fixture -def get_contract_with_gas_estimation(tester, w3, no_optimize): +def get_contract_with_gas_estimation(tester, w3, optimize): def get_contract_with_gas_estimation(source_code, *args, **kwargs): - contract = _get_contract(w3, source_code, no_optimize, *args, **kwargs) + contract = _get_contract(w3, source_code, optimize, *args, **kwargs) for abi_ in contract._classic_contract.functions.abi: if abi_["type"] == "function": set_decorator_to_contract_function(w3, tester, contract, source_code, abi_["name"]) @@ -150,9 +157,9 @@ def get_contract_with_gas_estimation(source_code, *args, **kwargs): @pytest.fixture -def get_contract_with_gas_estimation_for_constants(w3, no_optimize): +def get_contract_with_gas_estimation_for_constants(w3, optimize): def get_contract_with_gas_estimation_for_constants(source_code, *args, **kwargs): - return _get_contract(w3, source_code, no_optimize, *args, **kwargs) + return _get_contract(w3, source_code, optimize, *args, **kwargs) return get_contract_with_gas_estimation_for_constants diff --git a/tests/examples/factory/test_factory.py b/tests/examples/factory/test_factory.py index 15becc05f1..0c5cf61b04 100644 --- a/tests/examples/factory/test_factory.py +++ b/tests/examples/factory/test_factory.py @@ -2,6 +2,7 @@ from eth_utils import keccak import vyper +from vyper.compiler.settings import Settings @pytest.fixture @@ -30,12 +31,12 @@ def create_exchange(token, factory): @pytest.fixture -def factory(get_contract, no_optimize): +def factory(get_contract, optimize): with open("examples/factory/Exchange.vy") as f: code = f.read() exchange_interface = vyper.compile_code( - code, output_formats=["bytecode_runtime"], no_optimize=no_optimize + code, output_formats=["bytecode_runtime"], settings=Settings(optimize=optimize) ) exchange_deployed_bytecode = exchange_interface["bytecode_runtime"] diff --git a/tests/grammar/test_grammar.py b/tests/grammar/test_grammar.py index 7e220b58ae..d665ca2544 100644 --- a/tests/grammar/test_grammar.py +++ b/tests/grammar/test_grammar.py @@ -106,5 +106,6 @@ def has_no_docstrings(c): @hypothesis.settings(deadline=400, max_examples=500, suppress_health_check=(HealthCheck.too_slow,)) def test_grammar_bruteforce(code): if utf8_encodable(code): - tree = parse_to_ast(pre_parse(code + "\n")[1]) + _, _, reformatted_code = pre_parse(code + "\n") + tree = parse_to_ast(reformatted_code) assert isinstance(tree, Module) diff --git a/tests/parser/features/test_assignment.py b/tests/parser/features/test_assignment.py index 29ec820484..e550f60541 100644 --- a/tests/parser/features/test_assignment.py +++ b/tests/parser/features/test_assignment.py @@ -367,17 +367,33 @@ def foo(): """ assert_compile_failed(lambda: get_contract_with_gas_estimation(code), InvalidType) - -def test_assign_rhs_lhs_overlap(get_contract): # GH issue 2418 - code = """ + + +overlap_codes = [ + """ @external def bug(xs: uint256[2]) -> uint256[2]: # Initial value ys: uint256[2] = xs ys = [ys[1], ys[0]] return ys + """, """ +foo: uint256[2] +@external +def bug(xs: uint256[2]) -> uint256[2]: + # Initial value + self.foo = xs + self.foo = [self.foo[1], self.foo[0]] + return self.foo + """, + # TODO add transient tests when it's available +] + + +@pytest.mark.parametrize("code", overlap_codes) +def test_assign_rhs_lhs_overlap(get_contract, code): c = get_contract(code) assert c.bug([1, 2]) == [2, 1] diff --git a/tests/parser/features/test_immutable.py b/tests/parser/features/test_immutable.py index 7300d0f2d9..47f7fc748e 100644 --- a/tests/parser/features/test_immutable.py +++ b/tests/parser/features/test_immutable.py @@ -1,5 +1,7 @@ import pytest +from vyper.compiler.settings import OptimizationLevel + @pytest.mark.parametrize( "typ,value", @@ -269,7 +271,7 @@ def __init__(to_copy: address): # GH issue 3101, take 2 def test_immutables_initialized2(get_contract, get_contract_from_ir): dummy_contract = get_contract_from_ir( - ["deploy", 0, ["seq"] + ["invalid"] * 600, 0], no_optimize=True + ["deploy", 0, ["seq"] + ["invalid"] * 600, 0], optimize=OptimizationLevel.NONE ) # rekt because immutables section extends past allocated memory diff --git a/tests/parser/features/test_transient.py b/tests/parser/features/test_transient.py index 53354beca8..718f5ae314 100644 --- a/tests/parser/features/test_transient.py +++ b/tests/parser/features/test_transient.py @@ -1,6 +1,7 @@ import pytest from vyper.compiler import compile_code +from vyper.compiler.settings import Settings from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import StructureException @@ -13,20 +14,22 @@ def test_transient_blocked(evm_version): code = """ my_map: transient(HashMap[address, uint256]) """ + settings = Settings(evm_version=evm_version) if EVM_VERSIONS[evm_version] >= EVM_VERSIONS["cancun"]: - assert compile_code(code, evm_version=evm_version) is not None + assert compile_code(code, settings=settings) is not None else: with pytest.raises(StructureException): - compile_code(code, evm_version=evm_version) + compile_code(code, settings=settings) @pytest.mark.parametrize("evm_version", list(post_cancun.keys())) def test_transient_compiles(evm_version): # test transient keyword at least generates TLOAD/TSTORE opcodes + settings = Settings(evm_version=evm_version) getter_code = """ my_map: public(transient(HashMap[address, uint256])) """ - t = compile_code(getter_code, evm_version=evm_version, output_formats=["opcodes_runtime"]) + t = compile_code(getter_code, settings=settings, output_formats=["opcodes_runtime"]) t = t["opcodes_runtime"].split(" ") assert "TLOAD" in t @@ -39,7 +42,7 @@ def test_transient_compiles(evm_version): def setter(k: address, v: uint256): self.my_map[k] = v """ - t = compile_code(setter_code, evm_version=evm_version, output_formats=["opcodes_runtime"]) + t = compile_code(setter_code, settings=settings, output_formats=["opcodes_runtime"]) t = t["opcodes_runtime"].split(" ") assert "TLOAD" not in t @@ -52,9 +55,7 @@ def setter(k: address, v: uint256): def setter(k: address, v: uint256): self.my_map[k] = v """ - t = compile_code( - getter_setter_code, evm_version=evm_version, output_formats=["opcodes_runtime"] - ) + t = compile_code(getter_setter_code, settings=settings, output_formats=["opcodes_runtime"]) t = t["opcodes_runtime"].split(" ") assert "TLOAD" in t diff --git a/tests/parser/functions/test_bitwise.py b/tests/parser/functions/test_bitwise.py index 3e18bd292c..3ba74034ac 100644 --- a/tests/parser/functions/test_bitwise.py +++ b/tests/parser/functions/test_bitwise.py @@ -1,7 +1,6 @@ import pytest from vyper.compiler import compile_code -from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import InvalidLiteral, InvalidOperation, TypeMismatch from vyper.utils import unsigned_to_signed @@ -32,16 +31,14 @@ def _shr(x: uint256, y: uint256) -> uint256: """ -@pytest.mark.parametrize("evm_version", list(EVM_VERSIONS)) -def test_bitwise_opcodes(evm_version): - opcodes = compile_code(code, ["opcodes"], evm_version=evm_version)["opcodes"] +def test_bitwise_opcodes(): + opcodes = compile_code(code, ["opcodes"])["opcodes"] assert "SHL" in opcodes assert "SHR" in opcodes -@pytest.mark.parametrize("evm_version", list(EVM_VERSIONS)) -def test_test_bitwise(get_contract_with_gas_estimation, evm_version): - c = get_contract_with_gas_estimation(code, evm_version=evm_version) +def test_test_bitwise(get_contract_with_gas_estimation): + c = get_contract_with_gas_estimation(code) x = 126416208461208640982146408124 y = 7128468721412412459 assert c._bitwise_and(x, y) == (x & y) @@ -55,8 +52,7 @@ def test_test_bitwise(get_contract_with_gas_estimation, evm_version): assert c._shl(t, s) == (t << s) % (2**256) -@pytest.mark.parametrize("evm_version", list(EVM_VERSIONS.keys())) -def test_signed_shift(get_contract_with_gas_estimation, evm_version): +def test_signed_shift(get_contract_with_gas_estimation): code = """ @external def _sar(x: int256, y: uint256) -> int256: @@ -66,7 +62,7 @@ def _sar(x: int256, y: uint256) -> int256: def _shl(x: int256, y: uint256) -> int256: return x << y """ - c = get_contract_with_gas_estimation(code, evm_version=evm_version) + c = get_contract_with_gas_estimation(code) x = 126416208461208640982146408124 y = 7128468721412412459 cases = [x, y, -x, -y] @@ -97,8 +93,7 @@ def baz(a: uint256, b: uint256, c: uint256) -> (uint256, uint256): assert tuple(c.baz(1, 6, 14)) == (1 + 8 | ~6 & 14 * 2, (1 + 8 | ~6) & 14 * 2) == (25, 24) -@pytest.mark.parametrize("evm_version", list(EVM_VERSIONS)) -def test_literals(get_contract, evm_version): +def test_literals(get_contract): code = """ @external def _shr(x: uint256) -> uint256: @@ -109,7 +104,7 @@ def _shl(x: uint256) -> uint256: return x << 3 """ - c = get_contract(code, evm_version=evm_version) + c = get_contract(code) assert c._shr(80) == 10 assert c._shl(80) == 640 diff --git a/tests/parser/functions/test_create_functions.py b/tests/parser/functions/test_create_functions.py index 64e0823146..876d50b27d 100644 --- a/tests/parser/functions/test_create_functions.py +++ b/tests/parser/functions/test_create_functions.py @@ -5,6 +5,7 @@ import vyper.ir.compile_ir as compile_ir from vyper.codegen.ir_node import IRnode +from vyper.compiler.settings import OptimizationLevel from vyper.utils import EIP_170_LIMIT, checksum_encode, keccak256 @@ -232,7 +233,9 @@ def test(code_ofst: uint256) -> address: # zeroes (so no matter which offset, create_from_blueprint will # return empty code) ir = IRnode.from_list(["deploy", 0, ["seq"] + ["stop"] * initcode_len, 0]) - bytecode, _ = compile_ir.assembly_to_evm(compile_ir.compile_to_assembly(ir, no_optimize=True)) + bytecode, _ = compile_ir.assembly_to_evm( + compile_ir.compile_to_assembly(ir, optimize=OptimizationLevel.NONE) + ) # manually deploy the bytecode c = w3.eth.contract(abi=[], bytecode=bytecode) deploy_transaction = c.constructor() diff --git a/tests/parser/functions/test_slice.py b/tests/parser/functions/test_slice.py index 11d834bf42..3064ee308e 100644 --- a/tests/parser/functions/test_slice.py +++ b/tests/parser/functions/test_slice.py @@ -1,6 +1,8 @@ +import hypothesis.strategies as st import pytest +from hypothesis import given, settings -from vyper.exceptions import ArgumentException +from vyper.exceptions import ArgumentException, TypeMismatch _fun_bytes32_bounds = [(0, 32), (3, 29), (27, 5), (0, 5), (5, 3), (30, 2)] @@ -9,14 +11,6 @@ def _generate_bytes(length): return bytes(list(range(length))) -# good numbers to try -_fun_numbers = [0, 1, 5, 31, 32, 33, 64, 99, 100, 101] - - -# [b"", b"\x01", b"\x02"...] -_bytes_examples = [_generate_bytes(i) for i in _fun_numbers if i <= 100] - - def test_basic_slice(get_contract_with_gas_estimation): code = """ @external @@ -31,12 +25,16 @@ def slice_tower_test(inp1: Bytes[50]) -> Bytes[50]: assert x == b"klmnopqrst", x -@pytest.mark.parametrize("bytesdata", _bytes_examples) -@pytest.mark.parametrize("start", _fun_numbers) +# note: optimization boundaries at 32, 64 and 320 depending on mode +_draw_1024 = st.integers(min_value=0, max_value=1024) +_draw_1024_1 = st.integers(min_value=1, max_value=1024) +_bytes_1024 = st.binary(min_size=0, max_size=1024) + + @pytest.mark.parametrize("literal_start", (True, False)) -@pytest.mark.parametrize("length", _fun_numbers) @pytest.mark.parametrize("literal_length", (True, False)) -@pytest.mark.fuzzing +@given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024) +@settings(max_examples=25, deadline=None) def test_slice_immutable( get_contract, assert_compile_failed, @@ -46,47 +44,48 @@ def test_slice_immutable( literal_start, length, literal_length, + length_bound, ): _start = start if literal_start else "start" _length = length if literal_length else "length" code = f""" -IMMUTABLE_BYTES: immutable(Bytes[100]) -IMMUTABLE_SLICE: immutable(Bytes[100]) +IMMUTABLE_BYTES: immutable(Bytes[{length_bound}]) +IMMUTABLE_SLICE: immutable(Bytes[{length_bound}]) @external -def __init__(inp: Bytes[100], start: uint256, length: uint256): +def __init__(inp: Bytes[{length_bound}], start: uint256, length: uint256): IMMUTABLE_BYTES = inp IMMUTABLE_SLICE = slice(IMMUTABLE_BYTES, {_start}, {_length}) @external -def do_splice() -> Bytes[100]: +def do_splice() -> Bytes[{length_bound}]: return IMMUTABLE_SLICE """ + def _get_contract(): + return get_contract(code, bytesdata, start, length) + if ( - (start + length > 100 and literal_start and literal_length) - or (literal_length and length > 100) - or (literal_start and start > 100) + (start + length > length_bound and literal_start and literal_length) + or (literal_length and length > length_bound) + or (literal_start and start > length_bound) or (literal_length and length < 1) ): - assert_compile_failed( - lambda: get_contract(code, bytesdata, start, length), ArgumentException - ) - elif start + length > len(bytesdata): - assert_tx_failed(lambda: get_contract(code, bytesdata, start, length)) + assert_compile_failed(lambda: _get_contract(), ArgumentException) + elif start + length > len(bytesdata) or (len(bytesdata) > length_bound): + # deploy fail + assert_tx_failed(lambda: _get_contract()) else: - c = get_contract(code, bytesdata, start, length) + c = _get_contract() assert c.do_splice() == bytesdata[start : start + length] @pytest.mark.parametrize("location", ("storage", "calldata", "memory", "literal", "code")) -@pytest.mark.parametrize("bytesdata", _bytes_examples) -@pytest.mark.parametrize("start", _fun_numbers) @pytest.mark.parametrize("literal_start", (True, False)) -@pytest.mark.parametrize("length", _fun_numbers) @pytest.mark.parametrize("literal_length", (True, False)) -@pytest.mark.fuzzing +@given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024) +@settings(max_examples=25, deadline=None) def test_slice_bytes( get_contract, assert_compile_failed, @@ -97,9 +96,10 @@ def test_slice_bytes( literal_start, length, literal_length, + length_bound, ): if location == "memory": - spliced_code = "foo: Bytes[100] = inp" + spliced_code = f"foo: Bytes[{length_bound}] = inp" foo = "foo" elif location == "storage": spliced_code = "self.foo = inp" @@ -120,31 +120,38 @@ def test_slice_bytes( _length = length if literal_length else "length" code = f""" -foo: Bytes[100] -IMMUTABLE_BYTES: immutable(Bytes[100]) +foo: Bytes[{length_bound}] +IMMUTABLE_BYTES: immutable(Bytes[{length_bound}]) @external -def __init__(foo: Bytes[100]): +def __init__(foo: Bytes[{length_bound}]): IMMUTABLE_BYTES = foo @external -def do_slice(inp: Bytes[100], start: uint256, length: uint256) -> Bytes[100]: +def do_slice(inp: Bytes[{length_bound}], start: uint256, length: uint256) -> Bytes[{length_bound}]: {spliced_code} return slice({foo}, {_start}, {_length}) """ - length_bound = len(bytesdata) if location == "literal" else 100 + def _get_contract(): + return get_contract(code, bytesdata) + + data_length = len(bytesdata) if location == "literal" else length_bound if ( - (start + length > length_bound and literal_start and literal_length) - or (literal_length and length > length_bound) - or (literal_start and start > length_bound) + (start + length > data_length and literal_start and literal_length) + or (literal_length and length > data_length) + or (location == "literal" and len(bytesdata) > length_bound) + or (literal_start and start > data_length) or (literal_length and length < 1) ): - assert_compile_failed(lambda: get_contract(code, bytesdata), ArgumentException) + assert_compile_failed(lambda: _get_contract(), (ArgumentException, TypeMismatch)) + elif len(bytesdata) > data_length: + # deploy fail + assert_tx_failed(lambda: _get_contract()) elif start + length > len(bytesdata): - c = get_contract(code, bytesdata) + c = _get_contract() assert_tx_failed(lambda: c.do_slice(bytesdata, start, length)) else: - c = get_contract(code, bytesdata) + c = _get_contract() assert c.do_slice(bytesdata, start, length) == bytesdata[start : start + length], code diff --git a/tests/parser/parser_utils/test_annotate_and_optimize_ast.py b/tests/parser/parser_utils/test_annotate_and_optimize_ast.py index 6f2246c6c0..68a07178bb 100644 --- a/tests/parser/parser_utils/test_annotate_and_optimize_ast.py +++ b/tests/parser/parser_utils/test_annotate_and_optimize_ast.py @@ -29,7 +29,7 @@ def foo() -> int128: def get_contract_info(source_code): - class_types, reformatted_code = pre_parse(source_code) + _, class_types, reformatted_code = pre_parse(source_code) py_ast = python_ast.parse(reformatted_code) annotate_python_ast(py_ast, reformatted_code, class_types) diff --git a/tests/parser/syntax/test_addmulmod.py b/tests/parser/syntax/test_addmulmod.py new file mode 100644 index 0000000000..ddff4d3e01 --- /dev/null +++ b/tests/parser/syntax/test_addmulmod.py @@ -0,0 +1,27 @@ +import pytest + +from vyper.exceptions import InvalidType + +fail_list = [ + ( # bad AST nodes given as arguments + """ +@external +def foo() -> uint256: + return uint256_addmod(1.1, 1.2, 3.0) + """, + InvalidType, + ), + ( # bad AST nodes given as arguments + """ +@external +def foo() -> uint256: + return uint256_mulmod(1.1, 1.2, 3.0) + """, + InvalidType, + ), +] + + +@pytest.mark.parametrize("code,exc", fail_list) +def test_add_mod_fail(assert_compile_failed, get_contract, code, exc): + assert_compile_failed(lambda: get_contract(code), exc) diff --git a/tests/parser/syntax/test_address_code.py b/tests/parser/syntax/test_address_code.py index 25fe1be0b4..70ba5cbbf7 100644 --- a/tests/parser/syntax/test_address_code.py +++ b/tests/parser/syntax/test_address_code.py @@ -5,6 +5,7 @@ from web3 import Web3 from vyper import compiler +from vyper.compiler.settings import Settings from vyper.exceptions import NamespaceCollision, StructureException, VyperException # For reproducibility, use precompiled data of `hello: public(uint256)` using vyper 0.3.1 @@ -161,7 +162,7 @@ def test_address_code_compile_success(code: str): compiler.compile_code(code) -def test_address_code_self_success(get_contract, no_optimize: bool): +def test_address_code_self_success(get_contract, optimize): code = """ code_deployment: public(Bytes[32]) @@ -174,8 +175,9 @@ def code_runtime() -> Bytes[32]: return slice(self.code, 0, 32) """ contract = get_contract(code) + settings = Settings(optimize=optimize) code_compiled = compiler.compile_code( - code, output_formats=["bytecode", "bytecode_runtime"], no_optimize=no_optimize + code, output_formats=["bytecode", "bytecode_runtime"], settings=settings ) assert contract.code_deployment() == bytes.fromhex(code_compiled["bytecode"][2:])[:32] assert contract.code_runtime() == bytes.fromhex(code_compiled["bytecode_runtime"][2:])[:32] diff --git a/tests/parser/syntax/test_chainid.py b/tests/parser/syntax/test_chainid.py index be960f2fc5..2b6e08cbc4 100644 --- a/tests/parser/syntax/test_chainid.py +++ b/tests/parser/syntax/test_chainid.py @@ -1,6 +1,7 @@ import pytest from vyper import compiler +from vyper.compiler.settings import Settings from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import InvalidType, TypeMismatch @@ -12,8 +13,9 @@ def test_evm_version(evm_version): def foo(): a: uint256 = chain.id """ + settings = Settings(evm_version=evm_version) - assert compiler.compile_code(code, evm_version=evm_version) is not None + assert compiler.compile_code(code, settings=settings) is not None fail_list = [ diff --git a/tests/parser/syntax/test_codehash.py b/tests/parser/syntax/test_codehash.py index e4b6d90d8d..5074d14636 100644 --- a/tests/parser/syntax/test_codehash.py +++ b/tests/parser/syntax/test_codehash.py @@ -1,12 +1,13 @@ import pytest from vyper.compiler import compile_code +from vyper.compiler.settings import Settings from vyper.evm.opcodes import EVM_VERSIONS from vyper.utils import keccak256 @pytest.mark.parametrize("evm_version", list(EVM_VERSIONS)) -def test_get_extcodehash(get_contract, evm_version, no_optimize): +def test_get_extcodehash(get_contract, evm_version, optimize): code = """ a: address @@ -31,9 +32,8 @@ def foo3() -> bytes32: def foo4() -> bytes32: return self.a.codehash """ - compiled = compile_code( - code, ["bytecode_runtime"], evm_version=evm_version, no_optimize=no_optimize - ) + settings = Settings(evm_version=evm_version, optimize=optimize) + compiled = compile_code(code, ["bytecode_runtime"], settings=settings) bytecode = bytes.fromhex(compiled["bytecode_runtime"][2:]) hash_ = keccak256(bytecode) diff --git a/tests/parser/syntax/test_self_balance.py b/tests/parser/syntax/test_self_balance.py index 949cdde324..63db58e347 100644 --- a/tests/parser/syntax/test_self_balance.py +++ b/tests/parser/syntax/test_self_balance.py @@ -1,6 +1,7 @@ import pytest from vyper import compiler +from vyper.compiler.settings import Settings from vyper.evm.opcodes import EVM_VERSIONS @@ -18,7 +19,8 @@ def get_balance() -> uint256: def __default__(): pass """ - opcodes = compiler.compile_code(code, ["opcodes"], evm_version=evm_version)["opcodes"] + settings = Settings(evm_version=evm_version) + opcodes = compiler.compile_code(code, ["opcodes"], settings=settings)["opcodes"] if EVM_VERSIONS[evm_version] >= EVM_VERSIONS["istanbul"]: assert "SELFBALANCE" in opcodes else: diff --git a/tests/parser/types/test_dynamic_array.py b/tests/parser/types/test_dynamic_array.py index cb55c42870..9231d1979f 100644 --- a/tests/parser/types/test_dynamic_array.py +++ b/tests/parser/types/test_dynamic_array.py @@ -1543,7 +1543,7 @@ def bar(x: int128) -> DynArray[int128, 3]: assert c.bar(7) == [7, 14] -def test_nested_struct_of_lists(get_contract, assert_compile_failed, no_optimize): +def test_nested_struct_of_lists(get_contract, assert_compile_failed, optimize): code = """ struct nestedFoo: a1: DynArray[DynArray[DynArray[uint256, 2], 2], 2] @@ -1584,14 +1584,9 @@ def bar2() -> uint256: newFoo.b1[1][0][0].a1[0][1][1] + \\ newFoo.b1[0][1][0].a1[0][0][0] """ - - if no_optimize: - # fails at assembly stage with too many stack variables - assert_compile_failed(lambda: get_contract(code), Exception) - else: - c = get_contract(code) - assert c.bar() == [[[3, 7], [7, 3]], [[7, 3], [0, 0]]] - assert c.bar2() == 0 + c = get_contract(code) + assert c.bar() == [[[3, 7], [7, 3]], [[7, 3], [0, 0]]] + assert c.bar2() == 0 def test_tuple_of_lists(get_contract): diff --git a/tests/signatures/test_method_id_conflicts.py b/tests/signatures/test_method_id_conflicts.py index 262348c12a..35c10300b4 100644 --- a/tests/signatures/test_method_id_conflicts.py +++ b/tests/signatures/test_method_id_conflicts.py @@ -67,6 +67,13 @@ def gfah(): pass @view def eexo(): pass """, + """ +# check collision with ID = 0x00000000 +wycpnbqcyf:public(uint256) + +@external +def randallsRevenge_ilxaotc(): pass + """, ] diff --git a/tox.ini b/tox.ini index 5ddd01d7d4..9b63630f58 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,8 @@ envlist = usedevelop = True commands = core: pytest -m "not fuzzing" --showlocals {posargs:tests/} - no-opt: pytest -m "not fuzzing" --showlocals --no-optimize {posargs:tests/} + no-opt: pytest -m "not fuzzing" --showlocals --optimize none {posargs:tests/} + codesize: pytest -m "not fuzzing" --showlocals --optimize codesize {posargs:tests/} basepython = py310: python3.10 py311: python3.11 diff --git a/vyper/ast/__init__.py b/vyper/ast/__init__.py index 5695ceab7c..e5b81f1e7f 100644 --- a/vyper/ast/__init__.py +++ b/vyper/ast/__init__.py @@ -6,7 +6,7 @@ from . import nodes, validation from .natspec import parse_natspec from .nodes import compare_nodes -from .utils import ast_to_dict, parse_to_ast +from .utils import ast_to_dict, parse_to_ast, parse_to_ast_with_settings # adds vyper.ast.nodes classes into the local namespace for name, obj in ( diff --git a/vyper/ast/nodes.pyi b/vyper/ast/nodes.pyi index 6465453cb8..47c9af8526 100644 --- a/vyper/ast/nodes.pyi +++ b/vyper/ast/nodes.pyi @@ -4,6 +4,7 @@ from typing import Any, Optional, Sequence, Type, Union from .natspec import parse_natspec as parse_natspec from .utils import ast_to_dict as ast_to_dict from .utils import parse_to_ast as parse_to_ast +from .utils import parse_to_ast_with_settings as parse_to_ast_with_settings NODE_BASE_ATTRIBUTES: Any NODE_SRC_ATTRIBUTES: Any diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index f29150a5d3..35153af9d5 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -1,11 +1,15 @@ import io import re from tokenize import COMMENT, NAME, OP, TokenError, TokenInfo, tokenize, untokenize -from typing import Tuple from semantic_version import NpmSpec, Version -from vyper.exceptions import SyntaxException, VersionException +from vyper.compiler.settings import OptimizationLevel, Settings + +# seems a bit early to be importing this but we want it to validate the +# evm-version pragma +from vyper.evm.opcodes import EVM_VERSIONS +from vyper.exceptions import StructureException, SyntaxException, VersionException from vyper.typing import ModificationOffsets, ParserPosition VERSION_ALPHA_RE = re.compile(r"(?<=\d)a(?=\d)") # 0.1.0a17 @@ -33,10 +37,7 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None: # NOTE: should be `x.y.z.*` installed_version = ".".join(__version__.split(".")[:3]) - version_arr = version_str.split("@version") - - raw_file_version = version_arr[1].strip() - strict_file_version = _convert_version_str(raw_file_version) + strict_file_version = _convert_version_str(version_str) strict_compiler_version = Version(_convert_version_str(installed_version)) if len(strict_file_version) == 0: @@ -46,14 +47,14 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None: npm_spec = NpmSpec(strict_file_version) except ValueError: raise VersionException( - f'Version specification "{raw_file_version}" is not a valid NPM semantic ' + f'Version specification "{version_str}" is not a valid NPM semantic ' f"version specification", start, ) if not npm_spec.match(strict_compiler_version): raise VersionException( - f'Version specification "{raw_file_version}" is not compatible ' + f'Version specification "{version_str}" is not compatible ' f'with compiler version "{installed_version}"', start, ) @@ -66,7 +67,7 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None: VYPER_EXPRESSION_TYPES = {"log"} -def pre_parse(code: str) -> Tuple[ModificationOffsets, str]: +def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, str]: """ Re-formats a vyper source string into a python source string and performs some validation. More specifically, @@ -93,6 +94,7 @@ def pre_parse(code: str) -> Tuple[ModificationOffsets, str]: """ result = [] modification_offsets: ModificationOffsets = {} + settings = Settings() try: code_bytes = code.encode("utf-8") @@ -108,8 +110,39 @@ def pre_parse(code: str) -> Tuple[ModificationOffsets, str]: end = token.end line = token.line - if typ == COMMENT and "@version" in string: - validate_version_pragma(string[1:], start) + if typ == COMMENT: + contents = string[1:].strip() + if contents.startswith("@version"): + if settings.compiler_version is not None: + raise StructureException("compiler version specified twice!", start) + compiler_version = contents.removeprefix("@version ").strip() + validate_version_pragma(compiler_version, start) + settings.compiler_version = compiler_version + + if string.startswith("#pragma "): + pragma = string.removeprefix("#pragma").strip() + if pragma.startswith("version "): + if settings.compiler_version is not None: + raise StructureException("pragma version specified twice!", start) + compiler_version = pragma.removeprefix("version ".strip()) + validate_version_pragma(compiler_version, start) + settings.compiler_version = compiler_version + + if pragma.startswith("optimize "): + if settings.optimize is not None: + raise StructureException("pragma optimize specified twice!", start) + try: + mode = pragma.removeprefix("optimize").strip() + settings.optimize = OptimizationLevel.from_string(mode) + except ValueError: + raise StructureException(f"Invalid optimization mode `{mode}`", start) + if pragma.startswith("evm-version "): + if settings.evm_version is not None: + raise StructureException("pragma evm-version specified twice!", start) + evm_version = pragma.removeprefix("evm-version").strip() + if evm_version not in EVM_VERSIONS: + raise StructureException("Invalid evm version: `{evm_version}`", start) + settings.evm_version = evm_version if typ == NAME and string in ("class", "yield"): raise SyntaxException( @@ -130,4 +163,4 @@ def pre_parse(code: str) -> Tuple[ModificationOffsets, str]: except TokenError as e: raise SyntaxException(e.args[0], code, e.args[1][0], e.args[1][1]) from e - return modification_offsets, untokenize(result).decode("utf-8") + return settings, modification_offsets, untokenize(result).decode("utf-8") diff --git a/vyper/ast/utils.py b/vyper/ast/utils.py index fc8aad227c..4e669385ab 100644 --- a/vyper/ast/utils.py +++ b/vyper/ast/utils.py @@ -1,18 +1,23 @@ import ast as python_ast -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from vyper.ast import nodes as vy_ast from vyper.ast.annotation import annotate_python_ast from vyper.ast.pre_parser import pre_parse +from vyper.compiler.settings import Settings from vyper.exceptions import CompilerPanic, ParserException, SyntaxException -def parse_to_ast( +def parse_to_ast(*args: Any, **kwargs: Any) -> vy_ast.Module: + return parse_to_ast_with_settings(*args, **kwargs)[1] + + +def parse_to_ast_with_settings( source_code: str, source_id: int = 0, contract_name: Optional[str] = None, add_fn_node: Optional[str] = None, -) -> vy_ast.Module: +) -> tuple[Settings, vy_ast.Module]: """ Parses a Vyper source string and generates basic Vyper AST nodes. @@ -34,7 +39,7 @@ def parse_to_ast( """ if "\x00" in source_code: raise ParserException("No null bytes (\\x00) allowed in the source code.") - class_types, reformatted_code = pre_parse(source_code) + settings, class_types, reformatted_code = pre_parse(source_code) try: py_ast = python_ast.parse(reformatted_code) except SyntaxError as e: @@ -51,7 +56,9 @@ def parse_to_ast( annotate_python_ast(py_ast, source_code, class_types, source_id, contract_name) # Convert to Vyper AST. - return vy_ast.get_node(py_ast) # type: ignore + module = vy_ast.get_node(py_ast) + assert isinstance(module, vy_ast.Module) # mypy hint + return settings, module def ast_to_dict(ast_struct: Union[vy_ast.VyperNode, List]) -> Union[Dict, List]: diff --git a/vyper/ast/validation.py b/vyper/ast/validation.py index 7742d60c01..36a6a0484c 100644 --- a/vyper/ast/validation.py +++ b/vyper/ast/validation.py @@ -48,7 +48,7 @@ def validate_call_args( arg_count = (arg_count[0], 2**64) if arg_count[0] == arg_count[1]: - arg_count == arg_count[0] + arg_count = arg_count[0] if isinstance(node.func, vy_ast.Attribute): msg = f" for call to '{node.func.attr}'" diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index cfd6e5a8cd..105baa47d6 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -1376,7 +1376,7 @@ def evaluate(self, node): validate_call_args(node, 2) for arg in node.args: - if not isinstance(arg, vy_ast.Num): + if not isinstance(arg, vy_ast.Int): raise UnfoldableNode if arg.value < 0 or arg.value >= 2**256: raise InvalidLiteral("Value out of range for uint256", arg) @@ -1402,7 +1402,7 @@ def evaluate(self, node): validate_call_args(node, 2) for arg in node.args: - if not isinstance(arg, vy_ast.Num): + if not isinstance(arg, vy_ast.Int): raise UnfoldableNode if arg.value < 0 or arg.value >= 2**256: raise InvalidLiteral("Value out of range for uint256", arg) @@ -1428,7 +1428,7 @@ def evaluate(self, node): validate_call_args(node, 2) for arg in node.args: - if not isinstance(arg, vy_ast.Num): + if not isinstance(arg, vy_ast.Int): raise UnfoldableNode if arg.value < 0 or arg.value >= 2**256: raise InvalidLiteral("Value out of range for uint256", arg) @@ -1453,7 +1453,7 @@ def evaluate(self, node): self.__class__._warned = True validate_call_args(node, 1) - if not isinstance(node.args[0], vy_ast.Num): + if not isinstance(node.args[0], vy_ast.Int): raise UnfoldableNode value = node.args[0].value @@ -1480,7 +1480,7 @@ def evaluate(self, node): self.__class__._warned = True validate_call_args(node, 2) - if [i for i in node.args if not isinstance(i, vy_ast.Num)]: + if [i for i in node.args if not isinstance(i, vy_ast.Int)]: raise UnfoldableNode value, shift = [i.value for i in node.args] if value < 0 or value >= 2**256: @@ -1528,10 +1528,10 @@ class _AddMulMod(BuiltinFunction): def evaluate(self, node): validate_call_args(node, 3) - if isinstance(node.args[2], vy_ast.Num) and node.args[2].value == 0: + if isinstance(node.args[2], vy_ast.Int) and node.args[2].value == 0: raise ZeroDivisionException("Modulo by 0", node.args[2]) for arg in node.args: - if not isinstance(arg, vy_ast.Num): + if not isinstance(arg, vy_ast.Int): raise UnfoldableNode if arg.value < 0 or arg.value >= 2**256: raise InvalidLiteral("Value out of range for uint256", arg) diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index f5e113116d..55e0fc82b2 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -5,13 +5,13 @@ import warnings from collections import OrderedDict from pathlib import Path -from typing import Dict, Iterable, Iterator, Set, TypeVar +from typing import Dict, Iterable, Iterator, Optional, Set, TypeVar import vyper import vyper.codegen.ir_node as ir_node from vyper.cli import vyper_json from vyper.cli.utils import extract_file_interface_imports, get_interface_file_path -from vyper.compiler.settings import VYPER_TRACEBACK_LIMIT +from vyper.compiler.settings import VYPER_TRACEBACK_LIMIT, OptimizationLevel, Settings from vyper.evm.opcodes import DEFAULT_EVM_VERSION, EVM_VERSIONS from vyper.typing import ContractCodes, ContractPath, OutputFormats @@ -37,8 +37,6 @@ ir - Intermediate representation in list format ir_json - Intermediate representation in JSON format hex-ir - Output IR and assembly constants in hex instead of decimal -no-optimize - Do not optimize (don't use this for production code) -no-bytecode-metadata - Do not add metadata to bytecode """ combined_json_outputs = [ @@ -104,10 +102,10 @@ def _parse_args(argv): help=f"Select desired EVM version (default {DEFAULT_EVM_VERSION}). " "note: cancun support is EXPERIMENTAL", choices=list(EVM_VERSIONS), - default=DEFAULT_EVM_VERSION, dest="evm_version", ) parser.add_argument("--no-optimize", help="Do not optimize", action="store_true") + parser.add_argument("--optimize", help="Optimization flag", choices=["gas", "codesize", "none"]) parser.add_argument( "--no-bytecode-metadata", help="Do not add metadata to bytecode", action="store_true" ) @@ -153,13 +151,28 @@ def _parse_args(argv): output_formats = tuple(uniq(args.format.split(","))) + if args.no_optimize and args.optimize: + raise ValueError("Cannot use `--no-optimize` and `--optimize` at the same time!") + + settings = Settings() + + if args.no_optimize: + settings.optimize = OptimizationLevel.NONE + elif args.optimize is not None: + settings.optimize = OptimizationLevel.from_string(args.optimize) + + if args.evm_version: + settings.evm_version = args.evm_version + + if args.verbose: + print(f"using `{settings}`", file=sys.stderr) + compiled = compile_files( args.input_files, output_formats, args.root_folder, args.show_gas_estimates, - args.evm_version, - args.no_optimize, + settings, args.storage_layout, args.no_bytecode_metadata, ) @@ -253,9 +266,8 @@ def compile_files( output_formats: OutputFormats, root_folder: str = ".", show_gas_estimates: bool = False, - evm_version: str = DEFAULT_EVM_VERSION, - no_optimize: bool = False, - storage_layout: Iterable[str] = None, + settings: Optional[Settings] = None, + storage_layout: Optional[Iterable[str]] = None, no_bytecode_metadata: bool = False, ) -> OrderedDict: root_path = Path(root_folder).resolve() @@ -296,8 +308,7 @@ def compile_files( final_formats, exc_handler=exc_handler, interface_codes=get_interface_codes(root_path, contract_sources), - evm_version=evm_version, - no_optimize=no_optimize, + settings=settings, storage_layouts=storage_layouts, show_gas_estimates=show_gas_estimates, no_bytecode_metadata=no_bytecode_metadata, diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 9778848aa2..4a1c91550e 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -5,11 +5,12 @@ import sys import warnings from pathlib import Path -from typing import Any, Callable, Dict, Hashable, List, Tuple, Union +from typing import Any, Callable, Dict, Hashable, List, Optional, Tuple, Union import vyper from vyper.cli.utils import extract_file_interface_imports, get_interface_file_path -from vyper.evm.opcodes import DEFAULT_EVM_VERSION, EVM_VERSIONS +from vyper.compiler.settings import OptimizationLevel, Settings +from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import JSONError from vyper.typing import ContractCodes, ContractPath from vyper.utils import keccak256 @@ -144,11 +145,15 @@ def _standardize_path(path_str: str) -> str: return path.as_posix() -def get_evm_version(input_dict: Dict) -> str: +def get_evm_version(input_dict: Dict) -> Optional[str]: if "settings" not in input_dict: - return DEFAULT_EVM_VERSION + return None + + # TODO: move this validation somewhere it can be reused more easily + evm_version = input_dict["settings"].get("evmVersion") + if evm_version is None: + return None - evm_version = input_dict["settings"].get("evmVersion", DEFAULT_EVM_VERSION) if evm_version in ( "homestead", "tangerineWhistle", @@ -360,7 +365,21 @@ def compile_from_input_dict( raise JSONError(f"Invalid language '{input_dict['language']}' - Only Vyper is supported.") evm_version = get_evm_version(input_dict) - no_optimize = not input_dict["settings"].get("optimize", True) + + optimize = input_dict["settings"].get("optimize") + if isinstance(optimize, bool): + # bool optimization level for backwards compatibility + warnings.warn( + "optimize: is deprecated! please use one of 'gas', 'codesize', 'none'." + ) + optimize = OptimizationLevel.default() if optimize else OptimizationLevel.NONE + elif isinstance(optimize, str): + optimize = OptimizationLevel.from_string(optimize) + else: + assert optimize is None + + settings = Settings(evm_version=evm_version, optimize=optimize) + no_bytecode_metadata = not input_dict["settings"].get("bytecodeMetadata", True) contract_sources: ContractCodes = get_input_dict_contracts(input_dict) @@ -383,8 +402,7 @@ def compile_from_input_dict( output_formats[contract_path], interface_codes=interface_codes, initial_id=id_, - no_optimize=no_optimize, - evm_version=evm_version, + settings=settings, no_bytecode_metadata=no_bytecode_metadata, ) except Exception as exc: diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index 58d9db9889..f47f88ac85 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -1,6 +1,11 @@ +import contextlib +from typing import Generator + from vyper import ast as vy_ast from vyper.codegen.ir_node import Encoding, IRnode +from vyper.compiler.settings import OptimizationLevel from vyper.evm.address_space import CALLDATA, DATA, IMMUTABLES, MEMORY, STORAGE, TRANSIENT +from vyper.evm.opcodes import version_check from vyper.exceptions import CompilerPanic, StructureException, TypeCheckFailure, TypeMismatch from vyper.semantics.types import ( AddressT, @@ -19,13 +24,7 @@ from vyper.semantics.types.shortcuts import BYTES32_T, INT256_T, UINT256_T from vyper.semantics.types.subscriptable import SArrayT from vyper.semantics.types.user import EnumT -from vyper.utils import ( - GAS_CALLDATACOPY_WORD, - GAS_CODECOPY_WORD, - GAS_IDENTITY, - GAS_IDENTITYWORD, - ceil32, -) +from vyper.utils import GAS_COPY_WORD, GAS_IDENTITY, GAS_IDENTITYWORD, ceil32 DYNAMIC_ARRAY_OVERHEAD = 1 @@ -90,12 +89,16 @@ def _identity_gas_bound(num_bytes): return GAS_IDENTITY + GAS_IDENTITYWORD * (ceil32(num_bytes) // 32) +def _mcopy_gas_bound(num_bytes): + return GAS_COPY_WORD * ceil32(num_bytes) // 32 + + def _calldatacopy_gas_bound(num_bytes): - return GAS_CALLDATACOPY_WORD * ceil32(num_bytes) // 32 + return GAS_COPY_WORD * ceil32(num_bytes) // 32 def _codecopy_gas_bound(num_bytes): - return GAS_CODECOPY_WORD * ceil32(num_bytes) // 32 + return GAS_COPY_WORD * ceil32(num_bytes) // 32 # Copy byte array word-for-word (including layout) @@ -107,25 +110,33 @@ def make_byte_array_copier(dst, src): _check_assign_bytes(dst, src) # TODO: remove this branch, copy_bytes and get_bytearray_length should handle - if src.value == "~empty": + if src.value == "~empty" or src.typ.maxlen == 0: # set length word to 0. return STORE(dst, 0) with src.cache_when_complex("src") as (b1, src): - with get_bytearray_length(src).cache_when_complex("len") as (b2, len_): - max_bytes = src.typ.maxlen + has_storage = STORAGE in (src.location, dst.location) + is_memory_copy = dst.location == src.location == MEMORY + batch_uses_identity = is_memory_copy and not version_check(begin="cancun") + if src.typ.maxlen <= 32 and (has_storage or batch_uses_identity): + # it's cheaper to run two load/stores instead of copy_bytes ret = ["seq"] - - dst_ = bytes_data_ptr(dst) - src_ = bytes_data_ptr(src) - - ret.append(copy_bytes(dst_, src_, len_, max_bytes)) - - # store length + # store length word + len_ = get_bytearray_length(src) ret.append(STORE(dst, len_)) - return b1.resolve(b2.resolve(ret)) + # store the single data word. + dst_data_ptr = bytes_data_ptr(dst) + src_data_ptr = bytes_data_ptr(src) + ret.append(STORE(dst_data_ptr, LOAD(src_data_ptr))) + return b1.resolve(ret) + + # batch copy the bytearray (including length word) using copy_bytes + len_ = add_ofst(get_bytearray_length(src), 32) + max_bytes = src.typ.maxlen + 32 + ret = copy_bytes(dst, src, len_, max_bytes) + return b1.resolve(ret) def bytes_data_ptr(ptr): @@ -210,19 +221,17 @@ def _dynarray_make_setter(dst, src): loop_body.annotation = f"{dst}[i] = {src}[i]" ret.append(["repeat", i, 0, count, src.typ.count, loop_body]) + # write the length word after data is copied + ret.append(STORE(dst, count)) else: element_size = src.typ.value_type.memory_bytes_required - # number of elements * size of element in bytes - n_bytes = _mul(count, element_size) - max_bytes = src.typ.count * element_size + # number of elements * size of element in bytes + length word + n_bytes = add_ofst(_mul(count, element_size), 32) + max_bytes = 32 + src.typ.count * element_size - src_ = dynarray_data_ptr(src) - dst_ = dynarray_data_ptr(dst) - ret.append(copy_bytes(dst_, src_, n_bytes, max_bytes)) - - # write the length word after data is copied - ret.append(STORE(dst, count)) + # batch copy the entire dynarray, including length word + ret.append(copy_bytes(dst, src, n_bytes, max_bytes)) return b1.resolve(b2.resolve(ret)) @@ -258,7 +267,6 @@ def copy_bytes(dst, src, length, length_bound): assert src.is_pointer and dst.is_pointer # fast code for common case where num bytes is small - # TODO expand this for more cases where num words is less than ~8 if length_bound <= 32: copy_op = STORE(dst, LOAD(src)) ret = IRnode.from_list(copy_op, annotation=annotation) @@ -268,8 +276,12 @@ def copy_bytes(dst, src, length, length_bound): # special cases: batch copy to memory # TODO: iloadbytes if src.location == MEMORY: - copy_op = ["staticcall", "gas", 4, src, length, dst, length] - gas_bound = _identity_gas_bound(length_bound) + if version_check(begin="cancun"): + copy_op = ["mcopy", dst, src, length] + gas_bound = _mcopy_gas_bound(length_bound) + else: + copy_op = ["staticcall", "gas", 4, src, length, dst, length] + gas_bound = _identity_gas_bound(length_bound) elif src.location == CALLDATA: copy_op = ["calldatacopy", dst, src, length] gas_bound = _calldatacopy_gas_bound(length_bound) @@ -876,6 +888,38 @@ def make_setter(left, right): return _complex_make_setter(left, right) +_opt_level = OptimizationLevel.GAS + + +@contextlib.contextmanager +def anchor_opt_level(new_level: OptimizationLevel) -> Generator: + """ + Set the global optimization level variable for the duration of this + context manager. + """ + assert isinstance(new_level, OptimizationLevel) + + global _opt_level + try: + tmp = _opt_level + _opt_level = new_level + yield + finally: + _opt_level = tmp + + +def _opt_codesize(): + return _opt_level == OptimizationLevel.CODESIZE + + +def _opt_gas(): + return _opt_level == OptimizationLevel.GAS + + +def _opt_none(): + return _opt_level == OptimizationLevel.NONE + + def _complex_make_setter(left, right): if right.value == "~empty" and left.location == MEMORY: # optimized memzero @@ -891,11 +935,69 @@ def _complex_make_setter(left, right): assert is_tuple_like(left.typ) keys = left.typ.tuple_keys() - # if len(keyz) == 0: - # return IRnode.from_list(["pass"]) + if left.is_pointer and right.is_pointer and right.encoding == Encoding.VYPER: + # both left and right are pointers, see if we want to batch copy + # instead of unrolling the loop. + assert left.encoding == Encoding.VYPER + len_ = left.typ.memory_bytes_required + + has_storage = STORAGE in (left.location, right.location) + if has_storage: + if _opt_codesize(): + # assuming PUSH2, a single sstore(dst (sload src)) is 8 bytes, + # sstore(add (dst ofst), (sload (add (src ofst)))) is 16 bytes, + # whereas loop overhead is 16-17 bytes. + base_cost = 3 + if left._optimized.is_literal: + # code size is smaller since add is performed at compile-time + base_cost += 1 + if right._optimized.is_literal: + base_cost += 1 + # the formula is a heuristic, but it works. + # (CMC 2023-07-14 could get more detailed for PUSH1 vs + # PUSH2 etc but not worried about that too much now, + # it's probably better to add a proper unroll rule in the + # optimizer.) + should_batch_copy = len_ >= 32 * base_cost + elif _opt_gas(): + # kind of arbitrary, but cut off when code used > ~160 bytes + should_batch_copy = len_ >= 32 * 10 + else: + assert _opt_none() + # don't care, just generate the most readable version + should_batch_copy = True + else: + # find a cutoff for memory copy where identity is cheaper + # than unrolled mloads/mstores + # if MCOPY is available, mcopy is *always* better (except in + # the 1 word case, but that is already handled by copy_bytes). + if right.location == MEMORY and _opt_gas() and not version_check(begin="cancun"): + # cost for 0th word - (mstore dst (mload src)) + base_unroll_cost = 12 + nth_word_cost = base_unroll_cost + if not left._optimized.is_literal: + # (mstore (add N dst) (mload src)) + nth_word_cost += 6 + if not right._optimized.is_literal: + # (mstore dst (mload (add N src))) + nth_word_cost += 6 + + identity_base_cost = 115 # staticcall 4 gas dst len src len + + n_words = ceil32(len_) // 32 + should_batch_copy = ( + base_unroll_cost + (nth_word_cost * (n_words - 1)) >= identity_base_cost + ) + + # calldata to memory, code to memory, cancun, or codesize - + # batch copy is always better. + else: + should_batch_copy = True + + if should_batch_copy: + return copy_bytes(left, right, len_, len_) - # general case - # TODO use copy_bytes when the generated code is above a certain size + # general case, unroll with left.cache_when_complex("_L") as (b1, left), right.cache_when_complex("_R") as (b2, right): for k in keys: l_i = get_element_ptr(left, k, array_bounds_check=False) diff --git a/vyper/codegen/ir_node.py b/vyper/codegen/ir_node.py index d36a18ec66..0895e5f02d 100644 --- a/vyper/codegen/ir_node.py +++ b/vyper/codegen/ir_node.py @@ -38,11 +38,6 @@ def __repr__(self) -> str: __mul__ = __add__ -def push_label_to_stack(labelname: str) -> str: - # items prefixed with `_sym_` are ignored until asm phase - return "_sym_" + labelname - - class Encoding(Enum): # vyper encoding, default for memory variables VYPER = auto() @@ -54,10 +49,7 @@ class Encoding(Enum): # this creates a magical block which maps to IR `with` class _WithBuilder: def __init__(self, ir_node, name, should_inline=False): - # TODO figure out how to fix this circular import - from vyper.ir.optimizer import optimize - - if should_inline and optimize(ir_node).is_complex_ir: + if should_inline and ir_node._optimized.is_complex_ir: # this can only mean trouble raise CompilerPanic("trying to inline a complex IR node") @@ -371,6 +363,13 @@ def is_pointer(self): # eventually return self.location is not None + @property # probably could be cached_property but be paranoid + def _optimized(self): + # TODO figure out how to fix this circular import + from vyper.ir.optimizer import optimize + + return optimize(self) + # This function is slightly confusing but abstracts a common pattern: # when an IR value needs to be computed once and then cached as an # IR value (if it is expensive, or more importantly if its computation @@ -387,13 +386,11 @@ def is_pointer(self): # return builder.resolve(ret) # ``` def cache_when_complex(self, name): - from vyper.ir.optimizer import optimize - # for caching purposes, see if the ir_node will be optimized # because a non-literal expr could turn into a literal, # (e.g. `(add 1 2)`) # TODO this could really be moved into optimizer.py - should_inline = not optimize(self).is_complex_ir + should_inline = not self._optimized.is_complex_ir return _WithBuilder(self, name, should_inline) diff --git a/vyper/codegen/return_.py b/vyper/codegen/return_.py index b8468f3eb1..56bea2b8da 100644 --- a/vyper/codegen/return_.py +++ b/vyper/codegen/return_.py @@ -21,7 +21,7 @@ def make_return_stmt(ir_val: IRnode, stmt: Any, context: Context) -> Optional[IRnode]: func_t = context.func_t - jump_to_exit = ["exit_to", f"_sym_{func_t._ir_info.exit_sequence_label}"] + jump_to_exit = ["exit_to", func_t._ir_info.exit_sequence_label] if context.return_type is None: if stmt.value is not None: @@ -43,7 +43,8 @@ def finalize(fill_return_buffer): return IRnode.from_list(["seq", fill_return_buffer, cleanup_loops, jump_to_exit]) if context.return_type is None: - jump_to_exit += ["return_pc"] + if context.is_internal: + jump_to_exit += ["return_pc"] return finalize(["seq"]) if context.is_internal: diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index 311576194b..c320e6889c 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -1,5 +1,5 @@ from vyper.codegen.core import _freshname, eval_once_check, make_setter -from vyper.codegen.ir_node import IRnode, push_label_to_stack +from vyper.codegen.ir_node import IRnode from vyper.evm.address_space import MEMORY from vyper.exceptions import StateAccessViolation from vyper.semantics.types.subscriptable import TupleT @@ -104,7 +104,7 @@ def ir_for_self_call(stmt_expr, context): if return_buffer is not None: goto_op += [return_buffer] # pass return label to subroutine - goto_op += [push_label_to_stack(return_label)] + goto_op.append(["symbol", return_label]) call_sequence = ["seq"] call_sequence.append(eval_once_check(_freshname(stmt_expr.node_source_code))) diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index 7be45ce832..0b3c0d8191 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -5,7 +5,8 @@ import vyper.codegen.core as codegen import vyper.compiler.output as output from vyper.compiler.phases import CompilerData -from vyper.evm.opcodes import DEFAULT_EVM_VERSION, evm_wrapper +from vyper.compiler.settings import Settings +from vyper.evm.opcodes import DEFAULT_EVM_VERSION, anchor_evm_version from vyper.typing import ( ContractCodes, ContractPath, @@ -46,15 +47,14 @@ } -@evm_wrapper def compile_codes( contract_sources: ContractCodes, output_formats: Union[OutputDict, OutputFormats, None] = None, exc_handler: Union[Callable, None] = None, interface_codes: Union[InterfaceDict, InterfaceImports, None] = None, initial_id: int = 0, - no_optimize: bool = False, - storage_layouts: Dict[ContractPath, StorageLayout] = None, + settings: Settings = None, + storage_layouts: Optional[dict[ContractPath, Optional[StorageLayout]]] = None, show_gas_estimates: bool = False, no_bytecode_metadata: bool = False, ) -> OrderedDict: @@ -73,11 +73,8 @@ def compile_codes( two arguments - the name of the contract, and the exception that was raised initial_id: int, optional The lowest source ID value to be used when generating the source map. - evm_version: str, optional - The target EVM ruleset to compile for. If not given, defaults to the latest - implemented ruleset. - no_optimize: bool, optional - Turn off optimizations. Defaults to False + settings: Settings, optional + Compiler settings show_gas_estimates: bool, optional Show gas estimates for abi and ir output modes interface_codes: Dict, optional @@ -98,6 +95,7 @@ def compile_codes( Dict Compiler output as `{'contract name': {'output key': "output data"}}` """ + settings = settings or Settings() if output_formats is None: output_formats = ("bytecode",) @@ -121,27 +119,30 @@ def compile_codes( # make IR output the same between runs codegen.reset_names() - compiler_data = CompilerData( - source_code, - contract_name, - interfaces, - source_id, - no_optimize, - storage_layout_override, - show_gas_estimates, - no_bytecode_metadata, - ) - for output_format in output_formats[contract_name]: - if output_format not in OUTPUT_FORMATS: - raise ValueError(f"Unsupported format type {repr(output_format)}") - try: - out.setdefault(contract_name, {}) - out[contract_name][output_format] = OUTPUT_FORMATS[output_format](compiler_data) - except Exception as exc: - if exc_handler is not None: - exc_handler(contract_name, exc) - else: - raise exc + + with anchor_evm_version(settings.evm_version): + compiler_data = CompilerData( + source_code, + contract_name, + interfaces, + source_id, + settings, + storage_layout_override, + show_gas_estimates, + no_bytecode_metadata, + ) + for output_format in output_formats[contract_name]: + if output_format not in OUTPUT_FORMATS: + raise ValueError(f"Unsupported format type {repr(output_format)}") + try: + out.setdefault(contract_name, {}) + formatter = OUTPUT_FORMATS[output_format] + out[contract_name][output_format] = formatter(compiler_data) + except Exception as exc: + if exc_handler is not None: + exc_handler(contract_name, exc) + else: + raise exc return out @@ -153,9 +154,8 @@ def compile_code( contract_source: str, output_formats: Optional[OutputFormats] = None, interface_codes: Optional[InterfaceImports] = None, - evm_version: str = DEFAULT_EVM_VERSION, - no_optimize: bool = False, - storage_layout_override: StorageLayout = None, + settings: Settings = None, + storage_layout_override: Optional[StorageLayout] = None, show_gas_estimates: bool = False, ) -> dict: """ @@ -171,8 +171,8 @@ def compile_code( evm_version: str, optional The target EVM ruleset to compile for. If not given, defaults to the latest implemented ruleset. - no_optimize: bool, optional - Turn off optimizations. Defaults to False + settings: Settings, optional + Compiler settings. show_gas_estimates: bool, optional Show gas estimates for abi and ir output modes interface_codes: Dict, optional @@ -194,8 +194,7 @@ def compile_code( contract_sources, output_formats, interface_codes=interface_codes, - evm_version=evm_version, - no_optimize=no_optimize, + settings=settings, storage_layouts=storage_layouts, show_gas_estimates=show_gas_estimates, )[UNKNOWN_CONTRACT_NAME] diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index f061bd8e18..63d92d9a47 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -218,9 +218,7 @@ def _build_asm(asm_list): def build_source_map_output(compiler_data: CompilerData) -> OrderedDict: _, line_number_map = compile_ir.assembly_to_evm( - compiler_data.assembly_runtime, - insert_vyper_signature=True, - disable_bytecode_metadata=compiler_data.no_bytecode_metadata, + compiler_data.assembly_runtime, insert_vyper_signature=False ) # Sort line_number_map out = OrderedDict() diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 5156aa1bbd..4e1bd9e6c3 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -5,8 +5,11 @@ from vyper import ast as vy_ast from vyper.codegen import module +from vyper.codegen.core import anchor_opt_level from vyper.codegen.global_context import GlobalContext from vyper.codegen.ir_node import IRnode +from vyper.compiler.settings import OptimizationLevel, Settings +from vyper.exceptions import StructureException from vyper.ir import compile_ir, optimizer from vyper.semantics import set_data_positions, validate_semantics from vyper.semantics.types.function import ContractFunctionT @@ -49,7 +52,7 @@ def __init__( contract_name: str = "VyperContract", interface_codes: Optional[InterfaceImports] = None, source_id: int = 0, - no_optimize: bool = False, + settings: Settings = None, storage_layout: StorageLayout = None, show_gas_estimates: bool = False, no_bytecode_metadata: bool = False, @@ -69,8 +72,8 @@ def __init__( * JSON interfaces are given as lists, vyper interfaces as strings source_id : int, optional ID number used to identify this contract in the source map. - no_optimize: bool, optional - Turn off optimizations. Defaults to False + settings: Settings + Set optimization mode. show_gas_estimates: bool, optional Show gas estimates for abi and ir output modes no_bytecode_metadata: bool, optional @@ -80,14 +83,45 @@ def __init__( self.source_code = source_code self.interface_codes = interface_codes self.source_id = source_id - self.no_optimize = no_optimize self.storage_layout_override = storage_layout self.show_gas_estimates = show_gas_estimates self.no_bytecode_metadata = no_bytecode_metadata + self.settings = settings or Settings() @cached_property - def vyper_module(self) -> vy_ast.Module: - return generate_ast(self.source_code, self.source_id, self.contract_name) + def _generate_ast(self): + settings, ast = generate_ast(self.source_code, self.source_id, self.contract_name) + # validate the compiler settings + # XXX: this is a bit ugly, clean up later + if settings.evm_version is not None: + if ( + self.settings.evm_version is not None + and self.settings.evm_version != settings.evm_version + ): + raise StructureException( + f"compiler settings indicate evm version {self.settings.evm_version}, " + f"but source pragma indicates {settings.evm_version}." + ) + + self.settings.evm_version = settings.evm_version + + if settings.optimize is not None: + if self.settings.optimize is not None and self.settings.optimize != settings.optimize: + raise StructureException( + f"compiler options indicate optimization mode {self.settings.optimize}, " + f"but source pragma indicates {settings.optimize}." + ) + self.settings.optimize = settings.optimize + + # ensure defaults + if self.settings.optimize is None: + self.settings.optimize = OptimizationLevel.default() + + return ast + + @cached_property + def vyper_module(self): + return self._generate_ast @cached_property def vyper_module_unfolded(self) -> vy_ast.Module: @@ -119,7 +153,7 @@ def global_ctx(self) -> GlobalContext: @cached_property def _ir_output(self): # fetch both deployment and runtime IR - return generate_ir_nodes(self.global_ctx, self.no_optimize) + return generate_ir_nodes(self.global_ctx, self.settings.optimize) @property def ir_nodes(self) -> IRnode: @@ -142,23 +176,20 @@ def function_signatures(self) -> dict[str, ContractFunctionT]: @cached_property def assembly(self) -> list: - return generate_assembly(self.ir_nodes, self.no_optimize) + return generate_assembly(self.ir_nodes, self.settings.optimize) @cached_property def assembly_runtime(self) -> list: - return generate_assembly(self.ir_runtime, self.no_optimize) + return generate_assembly(self.ir_runtime, self.settings.optimize) @cached_property def bytecode(self) -> bytes: - return generate_bytecode( - self.assembly, is_runtime=False, no_bytecode_metadata=self.no_bytecode_metadata - ) + insert_vyper_signature = not self.no_bytecode_metadata + return generate_bytecode(self.assembly, insert_vyper_signature=insert_vyper_signature) @cached_property def bytecode_runtime(self) -> bytes: - return generate_bytecode( - self.assembly_runtime, is_runtime=True, no_bytecode_metadata=self.no_bytecode_metadata - ) + return generate_bytecode(self.assembly_runtime, insert_vyper_signature=False) @cached_property def blueprint_bytecode(self) -> bytes: @@ -172,7 +203,9 @@ def blueprint_bytecode(self) -> bytes: return deploy_bytecode + blueprint_bytecode -def generate_ast(source_code: str, source_id: int, contract_name: str) -> vy_ast.Module: +def generate_ast( + source_code: str, source_id: int, contract_name: str +) -> tuple[Settings, vy_ast.Module]: """ Generate a Vyper AST from source code. @@ -190,7 +223,7 @@ def generate_ast(source_code: str, source_id: int, contract_name: str) -> vy_ast vy_ast.Module Top-level Vyper AST node """ - return vy_ast.parse_to_ast(source_code, source_id, contract_name) + return vy_ast.parse_to_ast_with_settings(source_code, source_id, contract_name) def generate_unfolded_ast( @@ -236,7 +269,9 @@ def generate_folded_ast( return vyper_module_folded, symbol_tables -def generate_ir_nodes(global_ctx: GlobalContext, no_optimize: bool) -> tuple[IRnode, IRnode]: +def generate_ir_nodes( + global_ctx: GlobalContext, optimize: OptimizationLevel +) -> tuple[IRnode, IRnode]: """ Generate the intermediate representation (IR) from the contextualized AST. @@ -256,14 +291,15 @@ def generate_ir_nodes(global_ctx: GlobalContext, no_optimize: bool) -> tuple[IRn IR to generate deployment bytecode IR to generate runtime bytecode """ - ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) - if not no_optimize: + with anchor_opt_level(optimize): + ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) + if optimize != OptimizationLevel.NONE: ir_nodes = optimizer.optimize(ir_nodes) ir_runtime = optimizer.optimize(ir_runtime) return ir_nodes, ir_runtime -def generate_assembly(ir_nodes: IRnode, no_optimize: bool = False) -> list: +def generate_assembly(ir_nodes: IRnode, optimize: Optional[OptimizationLevel] = None) -> list: """ Generate assembly instructions from IR. @@ -277,7 +313,8 @@ def generate_assembly(ir_nodes: IRnode, no_optimize: bool = False) -> list: list List of assembly instructions. """ - assembly = compile_ir.compile_to_assembly(ir_nodes, no_optimize=no_optimize) + optimize = optimize or OptimizationLevel.default() + assembly = compile_ir.compile_to_assembly(ir_nodes, optimize=optimize) if _find_nested_opcode(assembly, "DEBUG"): warnings.warn( @@ -295,9 +332,7 @@ def _find_nested_opcode(assembly, key): return any(_find_nested_opcode(x, key) for x in sublists) -def generate_bytecode( - assembly: list, is_runtime: bool = False, no_bytecode_metadata: bool = False -) -> bytes: +def generate_bytecode(assembly: list, insert_vyper_signature: bool) -> bytes: """ Generate bytecode from assembly instructions. @@ -311,6 +346,4 @@ def generate_bytecode( bytes Final compiled bytecode. """ - return compile_ir.assembly_to_evm( - assembly, insert_vyper_signature=is_runtime, disable_bytecode_metadata=no_bytecode_metadata - )[0] + return compile_ir.assembly_to_evm(assembly, insert_vyper_signature=insert_vyper_signature)[0] diff --git a/vyper/compiler/settings.py b/vyper/compiler/settings.py index 09ced0dcb8..bb5e9cdc25 100644 --- a/vyper/compiler/settings.py +++ b/vyper/compiler/settings.py @@ -1,4 +1,6 @@ import os +from dataclasses import dataclass +from enum import Enum from typing import Optional VYPER_COLOR_OUTPUT = os.environ.get("VYPER_COLOR_OUTPUT", "0") == "1" @@ -12,3 +14,31 @@ VYPER_TRACEBACK_LIMIT = int(_tb_limit_str) else: VYPER_TRACEBACK_LIMIT = None + + +class OptimizationLevel(Enum): + NONE = 1 + GAS = 2 + CODESIZE = 3 + + @classmethod + def from_string(cls, val): + match val: + case "none": + return cls.NONE + case "gas": + return cls.GAS + case "codesize": + return cls.CODESIZE + raise ValueError(f"unrecognized optimization level: {val}") + + @classmethod + def default(cls): + return cls.GAS + + +@dataclass +class Settings: + compiler_version: Optional[str] = None + optimize: Optional[OptimizationLevel] = None + evm_version: Optional[str] = None diff --git a/vyper/evm/opcodes.py b/vyper/evm/opcodes.py index 7550d047b5..767d634c89 100644 --- a/vyper/evm/opcodes.py +++ b/vyper/evm/opcodes.py @@ -1,4 +1,5 @@ -from typing import Dict, Optional +import contextlib +from typing import Dict, Generator, Optional from vyper.exceptions import CompilerPanic from vyper.typing import OpcodeGasCost, OpcodeMap, OpcodeRulesetMap, OpcodeRulesetValue, OpcodeValue @@ -88,6 +89,7 @@ "MSIZE": (0x59, 0, 1, 2), "GAS": (0x5A, 0, 1, 2), "JUMPDEST": (0x5B, 0, 0, 1), + "MCOPY": (0x5E, 3, 0, (None, None, None, None, None, 3)), "PUSH0": (0x5F, 0, 1, 2), "PUSH1": (0x60, 0, 1, 3), "PUSH2": (0x61, 0, 1, 3), @@ -170,8 +172,8 @@ "INVALID": (0xFE, 0, 0, 0), "DEBUG": (0xA5, 1, 0, 0), "BREAKPOINT": (0xA6, 0, 0, 0), - "TLOAD": (0x5C, 1, 1, 100), - "TSTORE": (0x5D, 2, 0, 100), + "TLOAD": (0x5C, 1, 1, (None, None, None, None, None, 100)), + "TSTORE": (0x5D, 2, 0, (None, None, None, None, None, 100)), } PSEUDO_OPCODES: OpcodeMap = { @@ -206,17 +208,16 @@ IR_OPCODES: OpcodeMap = {**OPCODES, **PSEUDO_OPCODES} -def evm_wrapper(fn, *args, **kwargs): - def _wrapper(*args, **kwargs): - global active_evm_version - evm_version = kwargs.pop("evm_version", None) or DEFAULT_EVM_VERSION - active_evm_version = EVM_VERSIONS[evm_version] - try: - return fn(*args, **kwargs) - finally: - active_evm_version = EVM_VERSIONS[DEFAULT_EVM_VERSION] - - return _wrapper +@contextlib.contextmanager +def anchor_evm_version(evm_version: Optional[str] = None) -> Generator: + global active_evm_version + anchor = active_evm_version + evm_version = evm_version or DEFAULT_EVM_VERSION + active_evm_version = EVM_VERSIONS[evm_version] + try: + yield + finally: + active_evm_version = anchor def _gas(value: OpcodeValue, idx: int) -> Optional[OpcodeRulesetValue]: diff --git a/vyper/ir/compile_ir.py b/vyper/ir/compile_ir.py index b2a58fa8c9..a9064a44fa 100644 --- a/vyper/ir/compile_ir.py +++ b/vyper/ir/compile_ir.py @@ -3,6 +3,7 @@ import math from vyper.codegen.ir_node import IRnode +from vyper.compiler.settings import OptimizationLevel from vyper.evm.opcodes import get_opcodes, version_check from vyper.exceptions import CodegenPanic, CompilerPanic from vyper.utils import MemoryPositions @@ -118,14 +119,14 @@ def _rewrite_return_sequences(ir_node, label_params=None): args[0].value = "pass" else: # handle jump to cleanup - assert is_symbol(args[0].value) ir_node.value = "seq" _t = ["seq"] if "return_buffer" in label_params: _t.append(["pop", "pass"]) - dest = args[0].value[5:] # `_sym_foo` -> `foo` + dest = args[0].value + # works for both internal and external exit_to more_args = ["pass" if t.value == "return_pc" else t for t in args[1:]] _t.append(["goto", dest] + more_args) ir_node.args = IRnode.from_list(_t, source_pos=ir_node.source_pos).args @@ -201,7 +202,7 @@ def apply_line_no_wrapper(*args, **kwargs): @apply_line_numbers -def compile_to_assembly(code, no_optimize=False): +def compile_to_assembly(code, optimize=OptimizationLevel.GAS): global _revert_label _revert_label = mksymbol("revert") @@ -212,7 +213,7 @@ def compile_to_assembly(code, no_optimize=False): res = _compile_to_assembly(code) _add_postambles(res) - if not no_optimize: + if optimize != OptimizationLevel.NONE: _optimize_assembly(res) return res @@ -296,6 +297,7 @@ def _height_of(witharg): return o # batch copy from data section of the currently executing code to memory + # (probably should have named this dcopy but oh well) elif code.value == "dloadbytes": dst = code.args[0] src = code.args[1] @@ -667,8 +669,8 @@ def _height_of(witharg): o.extend(["_sym_" + str(code.args[0]), "JUMP"]) return o # push a literal symbol - elif isinstance(code.value, str) and is_symbol(code.value): - return [code.value] + elif code.value == "symbol": + return ["_sym_" + str(code.args[0])] # set a symbol as a location. elif code.value == "label": label_name = code.args[0].value @@ -968,9 +970,7 @@ def adjust_pc_maps(pc_maps, ofst): return ret -def assembly_to_evm( - assembly, pc_ofst=0, insert_vyper_signature=False, disable_bytecode_metadata=False -): +def assembly_to_evm(assembly, pc_ofst=0, insert_vyper_signature=False): """ Assembles assembly into EVM @@ -994,7 +994,7 @@ def assembly_to_evm( runtime_code, runtime_code_start, runtime_code_end = None, None, None bytecode_suffix = b"" - if (not disable_bytecode_metadata) and insert_vyper_signature: + if insert_vyper_signature: # CBOR encoded: {"vyper": [major,minor,patch]} bytecode_suffix += b"\xa1\x65vyper\x83" + bytes(list(version_tuple)) bytecode_suffix += len(bytecode_suffix).to_bytes(2, "big") @@ -1011,11 +1011,7 @@ def assembly_to_evm( for i, item in enumerate(assembly): if isinstance(item, list): assert runtime_code is None, "Multiple subcodes" - runtime_code, runtime_map = assembly_to_evm( - item, - insert_vyper_signature=True, - disable_bytecode_metadata=disable_bytecode_metadata, - ) + runtime_code, runtime_map = assembly_to_evm(item) assert item[0].startswith("_DEPLOY_MEM_OFST_") assert ctor_mem_size is None diff --git a/vyper/ir/optimizer.py b/vyper/ir/optimizer.py index b13c6f79f8..40e02e79c7 100644 --- a/vyper/ir/optimizer.py +++ b/vyper/ir/optimizer.py @@ -2,6 +2,7 @@ from typing import List, Optional, Tuple, Union from vyper.codegen.ir_node import IRnode +from vyper.evm.opcodes import version_check from vyper.exceptions import CompilerPanic, StaticAssertionException from vyper.utils import ( ceil32, @@ -472,6 +473,7 @@ def finalize(val, args): if value == "seq": changed |= _merge_memzero(argz) changed |= _merge_calldataload(argz) + changed |= _merge_mload(argz) changed |= _remove_empty_seqs(argz) # (seq x) => (x) for cleanliness and @@ -636,12 +638,26 @@ def _remove_empty_seqs(argz): def _merge_calldataload(argz): - # look for sequential operations copying from calldata to memory - # and merge them into a single calldatacopy operation + return _merge_load(argz, "calldataload", "calldatacopy") + + +def _merge_dload(argz): + return _merge_load(argz, "dload", "dloadbytes") + + +def _merge_mload(argz): + if not version_check(begin="cancun"): + return False + return _merge_load(argz, "mload", "mcopy") + + +def _merge_load(argz, _LOAD, _COPY): + # look for sequential operations copying from X to Y + # and merge them into a single copy operation changed = False mstore_nodes: List = [] - initial_mem_offset = 0 - initial_calldata_offset = 0 + initial_dst_offset = 0 + initial_src_offset = 0 total_length = 0 idx = None for i, ir_node in enumerate(argz): @@ -649,19 +665,19 @@ def _merge_calldataload(argz): if ( ir_node.value == "mstore" and isinstance(ir_node.args[0].value, int) - and ir_node.args[1].value == "calldataload" + and ir_node.args[1].value == _LOAD and isinstance(ir_node.args[1].args[0].value, int) ): # mstore of a zero value - mem_offset = ir_node.args[0].value - calldata_offset = ir_node.args[1].args[0].value + dst_offset = ir_node.args[0].value + src_offset = ir_node.args[1].args[0].value if not mstore_nodes: - initial_mem_offset = mem_offset - initial_calldata_offset = calldata_offset + initial_dst_offset = dst_offset + initial_src_offset = src_offset idx = i if ( - initial_mem_offset + total_length == mem_offset - and initial_calldata_offset + total_length == calldata_offset + initial_dst_offset + total_length == dst_offset + and initial_src_offset + total_length == src_offset ): mstore_nodes.append(ir_node) total_length += 32 @@ -676,7 +692,7 @@ def _merge_calldataload(argz): if len(mstore_nodes) > 1: changed = True new_ir = IRnode.from_list( - ["calldatacopy", initial_mem_offset, initial_calldata_offset, total_length], + [_COPY, initial_dst_offset, initial_src_offset, total_length], source_pos=mstore_nodes[0].source_pos, ) # replace first copy operation with optimized node and remove the rest @@ -684,8 +700,8 @@ def _merge_calldataload(argz): # note: del xs[k:l] deletes l - k items del argz[idx + 1 : idx + len(mstore_nodes)] - initial_mem_offset = 0 - initial_calldata_offset = 0 + initial_dst_offset = 0 + initial_src_offset = 0 total_length = 0 mstore_nodes.clear() diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index cb8e93ff28..d916dcf119 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -98,7 +98,7 @@ def __init__( _ns = Namespace() # note that we don't just copy the namespace because # there are constructor issues. - _ns.update({k: namespace[k] for k in namespace._scopes[-1]}) + _ns.update({k: namespace[k] for k in namespace._scopes[-1]}) # type: ignore module_node._metadata["namespace"] = _ns # check for collisions between 4byte function selectors diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index a554a8b71a..f252c84373 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -24,7 +24,7 @@ from vyper.semantics.types.bytestrings import BytesT, StringT from vyper.semantics.types.primitives import AddressT, BoolT, BytesM_T, IntegerT from vyper.semantics.types.subscriptable import DArrayT, SArrayT, TupleT -from vyper.utils import checksum_encode +from vyper.utils import checksum_encode, int_to_fourbytes def _validate_op(node, types_list, validation_fn_name): @@ -600,8 +600,13 @@ def validate_unique_method_ids(functions: List) -> None: seen = set() for method_id in method_ids: if method_id in seen: - collision_str = ", ".join(i.name for i in functions if method_id in i.method_ids) - raise StructureException(f"Methods have conflicting IDs: {collision_str}") + collision_str = ", ".join( + x for i in functions for x in i.method_ids.keys() if i.method_ids[x] == method_id + ) + collision_hex = int_to_fourbytes(method_id).hex() + raise StructureException( + f"Methods produce colliding method ID `0x{collision_hex}`: {collision_str}" + ) seen.add(method_id) diff --git a/vyper/semantics/namespace.py b/vyper/semantics/namespace.py index 82a5d5cf3e..b88bc3d817 100644 --- a/vyper/semantics/namespace.py +++ b/vyper/semantics/namespace.py @@ -20,9 +20,13 @@ class Namespace(dict): List of sets containing the key names for each scope """ + def __new__(cls, *args, **kwargs): + self = super().__new__(cls, *args, **kwargs) + self._scopes = [] + return self + def __init__(self): super().__init__() - self._scopes = [] # NOTE cyclic imports! # TODO: break this cycle by providing an `init_vyper_namespace` in 3rd module from vyper.builtins.functions import get_builtin_functions diff --git a/vyper/utils.py b/vyper/utils.py index 2440117d0c..3d9d9cb416 100644 --- a/vyper/utils.py +++ b/vyper/utils.py @@ -196,8 +196,7 @@ def calc_mem_gas(memsize): # Specific gas usage GAS_IDENTITY = 15 GAS_IDENTITYWORD = 3 -GAS_CODECOPY_WORD = 3 -GAS_CALLDATACOPY_WORD = 3 +GAS_COPY_WORD = 3 # i.e., W_copy from YP # A decimal value can store multiples of 1/DECIMAL_DIVISOR MAX_DECIMAL_PLACES = 10