diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 94eb5ec04c..baa8decacc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,7 @@ ### Commit message -Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see [our commit message style guide](../../blob/master/docs/style-guide.rst#best-practices-1) for what we would ideally like to see in a commit message.) +Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see [our commit message style guide](../../master/docs/style-guide.rst#best-practices-1) for what we would ideally like to see in a commit message.) ### Description for the changelog diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd78e2fff8..8d23368eb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -104,7 +104,7 @@ jobs: run: pip install tox - name: Run Tox - run: TOXENV=py${{ matrix.python-version[1] }} tox -r -- --optimize ${{ matrix.opt-mode }} ${{ matrix.debug && '--enable-compiler-debug-mode' || '' }} --reruns 10 --reruns-delay 1 -r aR tests/ + run: TOXENV=py${{ matrix.python-version[1] }} tox -r -- --optimize ${{ matrix.opt-mode }} ${{ matrix.debug && '--enable-compiler-debug-mode' || '' }} -r aR tests/ - name: Upload Coverage uses: codecov/codecov-action@v1 @@ -148,12 +148,12 @@ jobs: # fetch test durations # NOTE: if the tests get poorly distributed, run this and commit the resulting `.test_durations` file to the `vyper-test-durations` repo. - # `TOXENV=fuzzing tox -r -- --store-durations --reruns 10 --reruns-delay 1 -r aR tests/` + # `TOXENV=fuzzing tox -r -- --store-durations -r aR tests/` - name: Fetch test-durations run: curl --location "https://raw.githubusercontent.com/vyperlang/vyper-test-durations/5982755ee8459f771f2e8622427c36494646e1dd/test_durations" -o .test_durations - name: Run Tox - run: TOXENV=fuzzing tox -r -- --splits 60 --group ${{ matrix.group }} --splitting-algorithm least_duration --reruns 10 --reruns-delay 1 -r aR tests/ + run: TOXENV=fuzzing tox -r -- --splits 60 --group ${{ matrix.group }} --splitting-algorithm least_duration -r aR tests/ - name: Upload Coverage uses: codecov/codecov-action@v1 diff --git a/FUNDING.yml b/FUNDING.yml index 81e82160d0..efb9eb01b7 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1 +1 @@ -custom: https://gitcoin.co/grants/200/vyper-smart-contract-language-2 +custom: https://etherscan.io/address/0x70CCBE10F980d80b7eBaab7D2E3A73e87D67B775 diff --git a/SECURITY.md b/SECURITY.md index c7bdad4ee7..0a054b2c93 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,7 +48,7 @@ https://github.com/vyperlang/vyper/security/advisories If you think you have found a security vulnerability with a project that has used Vyper, please report the vulnerability to the relevant project's security disclosure program prior -to reporting to us. If one is not available, please email your vulnerability to security@vyperlang.org. +to reporting to us. If one is not available, submit it at https://github.com/vyperlang/vyper/security/advisories. **Please Do Not Log An Issue** mentioning the vulnerability. diff --git a/docs/built-in-functions.rst b/docs/built-in-functions.rst index bfaa8fdd5e..45cf9ec8c2 100644 --- a/docs/built-in-functions.rst +++ b/docs/built-in-functions.rst @@ -184,13 +184,14 @@ Vyper has three built-ins for contract creation; all three contract creation bui The implementation of ``create_copy_of`` assumes that the code at ``target`` is smaller than 16MB. While this is much larger than the EIP-170 constraint of 24KB, it is a conservative size limit intended to future-proof deployer contracts in case the EIP-170 constraint is lifted. If the code at ``target`` is larger than 16MB, the behavior of ``create_copy_of`` is undefined. -.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, code_offset=0, [, salt: bytes32]) -> address +.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, raw_args: bool = False, code_offset: int = 0, [, salt: bytes32]) -> address Copy the code of ``target`` into memory and execute it as initcode. In other words, this operation interprets the code at ``target`` not as regular runtime code, but directly as initcode. The ``*args`` are interpreted as constructor arguments, and are ABI-encoded and included when executing the initcode. * ``target``: Address of the blueprint to invoke * ``*args``: Constructor arguments to forward to the initcode. * ``value``: The wei value to send to the new contract address (Optional, default 0) + * ``raw_args``: If ``True``, ``*args`` must be a single ``Bytes[...]`` argument, which will be interpreted as a raw bytes buffer to forward to the create operation (which is useful for instance, if pre- ABI-encoded data is passed in from elsewhere). (Optional, default ``False``) * ``code_offset``: The offset to start the ``EXTCODECOPY`` from (Optional, default 0) * ``salt``: A ``bytes32`` value utilized by the deterministic ``CREATE2`` opcode (Optional, if not supplied, ``CREATE`` is used) @@ -201,7 +202,7 @@ Vyper has three built-ins for contract creation; all three contract creation bui @external def foo(blueprint: address) -> address: arg1: uint256 = 18 - arg2: String = "some string" + arg2: String[32] = "some string" return create_from_blueprint(blueprint, arg1, arg2, code_offset=1) .. note:: @@ -226,7 +227,7 @@ Vyper has three built-ins for contract creation; all three contract creation bui * ``to``: Destination address to call to * ``data``: Data to send to the destination address * ``max_outsize``: Maximum length of the bytes array returned from the call. If the returned call data exceeds this length, only this number of bytes is returned. (Optional, default ``0``) - * ``gas``: The amount of gas to attach to the call. If not set, all remaining gas is forwarded. + * ``gas``: The amount of gas to attach to the call. (Optional, defaults to ``msg.gas``). * ``value``: The wei value to send to the address (Optional, default ``0``) * ``is_delegate_call``: If ``True``, the call will be sent as ``DELEGATECALL`` (Optional, default ``False``) * ``is_static_call``: If ``True``, the call will be sent as ``STATICCALL`` (Optional, default ``False``) @@ -264,6 +265,10 @@ Vyper has three built-ins for contract creation; all three contract creation bui assert success return response + .. note:: + + Regarding "forwarding all gas", note that, while Vyper will provide ``msg.gas`` to the call, in practice, there are some subtleties around forwarding all remaining gas on the EVM which are out of scope of this documentation and could be subject to change. For instance, see the language in EIP-150 around "all but one 64th". + .. py:function:: raw_log(topics: bytes32[4], data: Union[Bytes, bytes32]) -> None Provides low level access to the ``LOG`` opcodes, emitting a log without having to specify an ABI type. @@ -500,7 +505,7 @@ Data Manipulation * ``b``: ``Bytes`` list to extract from * ``start``: Start point to extract from - * ``output_type``: Type of output (``bytes32``, ``integer``, or ``address``). Defaults to ``bytes32``. + * ``output_type``: Type of output (``bytesM``, ``integer``, or ``address``). Defaults to ``bytes32``. Returns a value of the type specified by ``output_type``. diff --git a/docs/compiling-a-contract.rst b/docs/compiling-a-contract.rst index 6d1cdf98d7..b529d1efb1 100644 --- a/docs/compiling-a-contract.rst +++ b/docs/compiling-a-contract.rst @@ -197,6 +197,7 @@ The following is a list of supported EVM versions, and changes in the compiler i - The ``transient`` keyword allows declaration of variables which live in transient storage - Functions marked with ``@nonreentrant`` are protected with TLOAD/TSTORE instead of SLOAD/SSTORE + - The ``MCOPY`` opcode will be generated automatically by the compiler for most memory operations. diff --git a/docs/control-structures.rst b/docs/control-structures.rst index fc8a472ff6..873135709a 100644 --- a/docs/control-structures.rst +++ b/docs/control-structures.rst @@ -271,16 +271,25 @@ Ranges are created using the ``range`` function. The following examples are vali ``STOP`` is a literal integer greater than zero. ``i`` begins as zero and increments by one until it is equal to ``STOP``. +.. code-block:: python + + for i in range(stop, bound=N): + ... + +Here, ``stop`` can be a variable with integer type, greater than zero. ``N`` must be a compile-time constant. ``i`` begins as zero and increments by one until it is equal to ``stop``. If ``stop`` is larger than ``N``, execution will revert at runtime. In certain cases, you may not have a guarantee that ``stop`` is less than ``N``, but still want to avoid the possibility of runtime reversion. To accomplish this, use the ``bound=`` keyword in combination with ``min(stop, N)`` as the argument to ``range``, like ``range(min(stop, N), bound=N)``. This is helpful for use cases like chunking up operations on larger arrays across multiple transactions. + +Another use of range can be with ``START`` and ``STOP`` bounds. + .. code-block:: python for i in range(START, STOP): ... -``START`` and ``STOP`` are literal integers, with ``STOP`` being a greater value than ``START``. ``i`` begins as ``START`` and increments by one until it is equal to ``STOP``. +Here, ``START`` and ``STOP`` are literal integers, with ``STOP`` being a greater value than ``START``. ``i`` begins as ``START`` and increments by one until it is equal to ``STOP``. .. code-block:: python for i in range(a, a + N): ... -``a`` is a variable with an integer type and ``N`` is a literal integer greater than zero. ``i`` begins as ``a`` and increments by one until it is equal to ``a + N``. +``a`` is a variable with an integer type and ``N`` is a literal integer greater than zero. ``i`` begins as ``a`` and increments by one until it is equal to ``a + N``. If ``a + N`` would overflow, execution will revert. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index da86c5c0ce..3db11dc451 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -14,17 +14,13 @@ Release Notes for advisory links: :'<,'>s/\v(https:\/\/github.com\/vyperlang\/vyper\/security\/advisories\/)([-A-Za-z0-9]+)/(`\2 <\1\2>`_)/g -.. - v0.3.10 ("Black Adder") - *********************** - -v0.3.10rc1 -********** +v0.3.10 ("Black Adder") +*********************** -Date released: 2023-09-06 +Date released: 2023-10-04 ========================= -v0.3.10 is a performance focused release. It adds a ``codesize`` optimization mode (`#3493 `_), adds new vyper-specific ``#pragma`` directives (`#3493 `_), uses Cancun's ``MCOPY`` opcode for some compiler generated code (`#3483 `_), and generates selector tables which now feature O(1) performance (`#3496 `_). +v0.3.10 is a performance focused release that additionally ships numerous bugfixes. It adds a ``codesize`` optimization mode (`#3493 `_), adds new vyper-specific ``#pragma`` directives (`#3493 `_), uses Cancun's ``MCOPY`` opcode for some compiler generated code (`#3483 `_), and generates selector tables which now feature O(1) performance (`#3496 `_). Breaking changes: ----------------- @@ -32,6 +28,7 @@ Breaking changes: - add runtime code layout to initcode (`#3584 `_) - drop evm versions through istanbul (`#3470 `_) - remove vyper signature from runtime (`#3471 `_) +- only allow valid identifiers to be nonreentrant keys (`#3605 `_) Non-breaking changes and improvements: -------------------------------------- @@ -46,12 +43,15 @@ Notable fixes: - fix ``ecrecover()`` behavior when signature is invalid (`GHSA-f5x6-7qgp-jhf3 `_, `#3586 `_) - fix: order of evaluation for some builtins (`#3583 `_, `#3587 `_) +- fix: memory allocation in certain builtins using ``msize`` (`#3610 `_) +- fix: ``_abi_decode()`` input validation in certain complex expressions (`#3626 `_) - fix: pycryptodome for arm builds (`#3485 `_) - let params of internal functions be mutable (`#3473 `_) - typechecking of folded builtins in (`#3490 `_) - update tload/tstore opcodes per latest 1153 EIP spec (`#3484 `_) - fix: raw_call type when max_outsize=0 is set (`#3572 `_) - fix: implements check for indexed event arguments (`#3570 `_) +- fix: type-checking for ``_abi_decode()`` arguments (`#3626 `_) Other docs updates, chores and fixes: ------------------------------------- diff --git a/docs/resources.rst b/docs/resources.rst index 7f0d0600a9..a3dfa480ed 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -3,45 +3,47 @@ Other resources and learning material ##################################### -Vyper has an active community. You can find third party tutorials, -examples, courses and other learning material. +Vyper has an active community. You can find third-party tutorials, examples, courses, and other learning material. General ------- -- `Ape Academy - Learn how to build vyper projects `__ by ApeWorX -- `More Vyper by Example `__ by Smart Contract Engineer -- `Vyper cheat Sheet `__ -- `Vyper Hub for development `__ -- `Vyper greatest hits smart contract examples `__ +- `Ape Academy – Learn how to build Vyper projects `_ by ApeWorX +- `More Vyper by Example `_ by Smart Contract Engineer +- `Vyper cheat Sheet `_ +- `Vyper Hub for development `_ +- `Vyper greatest hits smart contract examples `_ +- `A curated list of Vyper resources, libraries, tools, and more `_ Frameworks and tooling ---------------------- -- `ApeWorX - The Ethereum development framework for Python Developers, Data Scientists, and Security Professionals `__ -- `Foundry x Vyper - Foundry template to compile Vyper contracts `__ -- `Snekmate - Vyper smart contract building blocks `__ -- `Serpentor - A set of smart contracts tools for governance `__ -- `Smart contract development frameworks and tools for Vyper on Ethreum.org `__ +- `Titanoboa – An experimental Vyper interpreter with pretty tracebacks, forking, debugging features and more `_ +- `ApeWorX – The Ethereum development framework for Python Developers, Data Scientists, and Security Professionals `_ +- `VyperDeployer – A helper smart contract to compile and test Vyper contracts in Foundry `_ +- `🐍 snekmate – Vyper smart contract building blocks `_ +- `Serpentor – A set of smart contracts tools for governance `_ +- `Smart contract development frameworks and tools for Vyper on Ethreum.org `_ Security -------- -- `VyperPunk - learn to secure and hack Vyper smart contracts `__ -- `VyperExamples - Vyper vulnerability examples `__ +- `VyperPunk – learn to secure and hack Vyper smart contracts `_ +- `VyperExamples – Vyper vulnerability examples `_ Conference presentations ------------------------ -- `Vyper Smart Contract Programming Language by Patrick Collins (2022, 30 mins) `__ -- `Python and DeFi by Curve Finance (2022, 15 mins) `__ -- `My experience with Vyper over the years by Benjamin Scherrey (2022, 15 mins) `__ -- `Short introduction to Vyper by Edison Que (3 mins) `__ +- `Vyper Smart Contract Programming Language by Patrick Collins (2022, 30 mins) `_ +- `Python and DeFi by Curve Finance (2022, 15 mins) `_ +- `My experience with Vyper over the years by Benjamin Scherrey (2022, 15 mins) `_ +- `Short introduction to Vyper by Edison Que (3 mins) `_ Unmaintained ------------ These resources have not been updated for a while, but may still offer interesting content. -- `Awesome Vyper curated resources `__ -- `Brownie - Python framework for developing smart contracts (deprecated) `__ +- `Awesome Vyper curated resources `_ +- `Brownie – Python framework for developing smart contracts (deprecated) `_ +- `Foundry x Vyper – Foundry template to compile Vyper contracts `_ diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index d2c5d48d96..3861bf4380 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -17,7 +17,7 @@ Vyper supports several source code directives to control compiler modes and help 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. Starting from v0.4.0 and up, version strings will use `PEP440 version specifiers _`. +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. Starting from v0.4.0 and up, version strings will use `PEP440 version specifiers `_. As of 0.3.10, the recommended way to specify the version pragma is as follows: @@ -25,6 +25,10 @@ As of 0.3.10, the recommended way to specify the version pragma is as follows: #pragma version ^0.3.0 +.. note:: + + Both pragma directive versions ``#pragma`` and ``# pragma`` are supported. + The following declaration is equivalent, and, prior to 0.3.10, was the only supported method to specify the compiler version: .. code-block:: python diff --git a/examples/crowdfund.vy b/examples/crowdfund.vy index 3891ad0b74..56b34308f1 100644 --- a/examples/crowdfund.vy +++ b/examples/crowdfund.vy @@ -18,15 +18,15 @@ def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256): @external @payable def participate(): - assert block.timestamp < self.deadline, "deadline not met (yet)" + assert block.timestamp < self.deadline, "deadline has expired" self.funders[msg.sender] += msg.value # Enough money was raised! Send funds to the beneficiary @external def finalize(): - assert block.timestamp >= self.deadline, "deadline has passed" - assert self.balance >= self.goal, "the goal has not been reached" + assert block.timestamp >= self.deadline, "deadline has not expired yet" + assert self.balance >= self.goal, "goal has not been reached" selfdestruct(self.beneficiary) diff --git a/examples/tokens/ERC1155ownable.vy b/examples/tokens/ERC1155ownable.vy index 8094225f18..f1070b8f89 100644 --- a/examples/tokens/ERC1155ownable.vy +++ b/examples/tokens/ERC1155ownable.vy @@ -214,7 +214,6 @@ def mint(receiver: address, id: uint256, amount:uint256): @param receiver the account that will receive the minted token @param id the ID of the token @param amount of tokens for this ID - @param data the data associated with this mint. Usually stays empty """ assert not self.paused, "The contract has been paused" assert self.owner == msg.sender, "Only the contract owner can mint" @@ -232,7 +231,6 @@ def mintBatch(receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: Dy @param receiver the account that will receive the minted token @param ids array of ids for the tokens @param amounts amounts of tokens for each ID in the ids array - @param data the data associated with this mint. Usually stays empty """ assert not self.paused, "The contract has been paused" assert self.owner == msg.sender, "Only the contract owner can mint" diff --git a/setup.py b/setup.py index c81b9bed4a..40efb436c5 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def _global_version(version): "importlib-metadata", "wheel", ], - setup_requires=["pytest-runner", "setuptools_scm"], + setup_requires=["pytest-runner", "setuptools_scm>=7.1.0,<8.0.0"], tests_require=extras_require["test"], extras_require=extras_require, entry_points={ diff --git a/tests/base_conftest.py b/tests/base_conftest.py deleted file mode 100644 index 81e8dedc36..0000000000 --- a/tests/base_conftest.py +++ /dev/null @@ -1,216 +0,0 @@ -import json - -import pytest -import web3.exceptions -from eth_tester import EthereumTester, PyEVMBackend -from eth_tester.exceptions import TransactionFailed -from eth_utils.toolz import compose -from hexbytes import HexBytes -from web3 import Web3 -from web3.contract import Contract -from web3.providers.eth_tester import EthereumTesterProvider - -from vyper import compiler -from vyper.ast.grammar import parse_vyper_source -from vyper.compiler.settings import Settings - - -class VyperMethod: - ALLOWED_MODIFIERS = {"call", "estimateGas", "transact", "buildTransaction"} - - def __init__(self, function, normalizers=None): - self._function = function - self._function._return_data_normalizers = normalizers - - def __call__(self, *args, **kwargs): - return self.__prepared_function(*args, **kwargs) - - def __prepared_function(self, *args, **kwargs): - if not kwargs: - modifier, modifier_dict = "call", {} - fn_abi = [ - x - for x in self._function.contract_abi - if x.get("name") == self._function.function_identifier - ].pop() - # To make tests faster just supply some high gas value. - modifier_dict.update({"gas": fn_abi.get("gas", 0) + 500000}) - elif len(kwargs) == 1: - modifier, modifier_dict = kwargs.popitem() - if modifier not in self.ALLOWED_MODIFIERS: - raise TypeError(f"The only allowed keyword arguments are: {self.ALLOWED_MODIFIERS}") - else: - raise TypeError(f"Use up to one keyword argument, one of: {self.ALLOWED_MODIFIERS}") - return getattr(self._function(*args), modifier)(modifier_dict) - - -class VyperContract: - """ - An alternative Contract Factory which invokes all methods as `call()`, - unless you add a keyword argument. The keyword argument assigns the prep method. - This call - > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) - is equivalent to this call in the classic contract: - > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) - """ - - def __init__(self, classic_contract, method_class=VyperMethod): - classic_contract._return_data_normalizers += CONCISE_NORMALIZERS - self._classic_contract = classic_contract - self.address = self._classic_contract.address - protected_fn_names = [fn for fn in dir(self) if not fn.endswith("__")] - - try: - fn_names = [fn["name"] for fn in self._classic_contract.functions._functions] - except web3.exceptions.NoABIFunctionsFound: - fn_names = [] - - for fn_name in fn_names: - # Override namespace collisions - if fn_name in protected_fn_names: - raise AttributeError(f"{fn_name} is protected!") - else: - _classic_method = getattr(self._classic_contract.functions, fn_name) - _concise_method = method_class( - _classic_method, self._classic_contract._return_data_normalizers - ) - setattr(self, fn_name, _concise_method) - - @classmethod - def factory(cls, *args, **kwargs): - return compose(cls, Contract.factory(*args, **kwargs)) - - -def _none_addr(datatype, data): - if datatype == "address" and int(data, base=16) == 0: - return (datatype, None) - else: - return (datatype, data) - - -CONCISE_NORMALIZERS = (_none_addr,) - - -@pytest.fixture(scope="module") -def tester(): - # set absurdly high gas limit so that london basefee never adjusts - # (note: 2**63 - 1 is max that evm allows) - custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 10**10}) - custom_genesis["base_fee_per_gas"] = 0 - backend = PyEVMBackend(genesis_parameters=custom_genesis) - return EthereumTester(backend=backend) - - -def zero_gas_price_strategy(web3, transaction_params=None): - return 0 # zero gas price makes testing simpler. - - -@pytest.fixture(scope="module") -def w3(tester): - w3 = Web3(EthereumTesterProvider(tester)) - w3.eth.set_gas_price_strategy(zero_gas_price_strategy) - return w3 - - -def _get_contract(w3, source_code, optimize, *args, override_opt_level=None, **kwargs): - settings = Settings() - settings.evm_version = kwargs.pop("evm_version", None) - settings.optimize = override_opt_level or optimize - out = compiler.compile_code( - source_code, - # test that metadata gets generated - ["abi", "bytecode", "metadata"], - settings=settings, - interface_codes=kwargs.pop("interface_codes", None), - show_gas_estimates=True, # Enable gas estimates for testing - ) - parse_vyper_source(source_code) # Test grammar. - json.dumps(out["metadata"]) # test metadata is json serializable - abi = out["abi"] - bytecode = out["bytecode"] - value = kwargs.pop("value_in_eth", 0) * 10**18 # Handle deploying with an eth value. - c = w3.eth.contract(abi=abi, bytecode=bytecode) - deploy_transaction = c.constructor(*args) - tx_info = {"from": w3.eth.accounts[0], "value": value, "gasPrice": 0} - tx_info.update(kwargs) - tx_hash = deploy_transaction.transact(tx_info) - address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] - return w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract) - - -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), - settings=settings, - show_gas_estimates=True, # Enable gas estimates for testing - ) - parse_vyper_source(source_code) # Test grammar. - abi = out["abi"] - bytecode = HexBytes(initcode_prefix) + HexBytes(out["bytecode"]) - bytecode_len = len(bytecode) - bytecode_len_hex = hex(bytecode_len)[2:].rjust(4, "0") - # prepend a quick deploy preamble - deploy_preamble = HexBytes("61" + bytecode_len_hex + "3d81600a3d39f3") - deploy_bytecode = HexBytes(deploy_preamble) + bytecode - - deployer_abi = [] # just a constructor - c = w3.eth.contract(abi=deployer_abi, bytecode=deploy_bytecode) - deploy_transaction = c.constructor() - tx_info = {"from": w3.eth.accounts[0], "value": 0, "gasPrice": 0} - - tx_hash = deploy_transaction.transact(tx_info) - address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] - - # sanity check - assert w3.eth.get_code(address) == bytecode, (w3.eth.get_code(address), bytecode) - - def factory(address): - return w3.eth.contract( - address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract - ) - - return w3.eth.contract(address, bytecode=deploy_bytecode), factory - - -@pytest.fixture(scope="module") -def deploy_blueprint_for(w3, optimize): - def deploy_blueprint_for(source_code, *args, **kwargs): - return _deploy_blueprint_for(w3, source_code, optimize, *args, **kwargs) - - return deploy_blueprint_for - - -@pytest.fixture(scope="module") -def get_contract(w3, optimize): - def get_contract(source_code, *args, **kwargs): - return _get_contract(w3, source_code, optimize, *args, **kwargs) - - return get_contract - - -@pytest.fixture -def get_logs(w3): - def get_logs(tx_hash, c, event_name): - tx_receipt = w3.eth.get_transaction_receipt(tx_hash) - return c._classic_contract.events[event_name]().process_receipt(tx_receipt) - - return get_logs - - -@pytest.fixture(scope="module") -def assert_tx_failed(tester): - def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): - snapshot_id = tester.take_snapshot() - with pytest.raises(exception) as excinfo: - function_to_test() - tester.revert_to_snapshot(snapshot_id) - if exc_text: - # TODO test equality - assert exc_text in str(excinfo.value), (exc_text, excinfo.value) - - return assert_tx_failed diff --git a/tests/cli/vyper_compile/test_compile_files.py b/tests/cli/vyper_compile/test_compile_files.py deleted file mode 100644 index 31cf622658..0000000000 --- a/tests/cli/vyper_compile/test_compile_files.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from vyper.cli.vyper_compile import compile_files - - -def test_combined_json_keys(tmp_path): - bar_path = tmp_path.joinpath("bar.vy") - with bar_path.open("w") as fp: - fp.write("") - - combined_keys = { - "bytecode", - "bytecode_runtime", - "blueprint_bytecode", - "abi", - "source_map", - "layout", - "method_identifiers", - "userdoc", - "devdoc", - } - compile_data = compile_files([bar_path], ["combined_json"], root_folder=tmp_path) - - assert set(compile_data.keys()) == {"bar.vy", "version"} - assert set(compile_data["bar.vy"].keys()) == combined_keys - - -def test_invalid_root_path(): - with pytest.raises(FileNotFoundError): - compile_files([], [], root_folder="path/that/does/not/exist") diff --git a/tests/cli/vyper_compile/test_import_paths.py b/tests/cli/vyper_compile/test_import_paths.py deleted file mode 100644 index 81f209113f..0000000000 --- a/tests/cli/vyper_compile/test_import_paths.py +++ /dev/null @@ -1,260 +0,0 @@ -import pytest - -from vyper.cli.vyper_compile import compile_files, get_interface_file_path - -FOO_CODE = """ -{} - -struct FooStruct: - foo_: uint256 - -@external -def foo() -> FooStruct: - return FooStruct({{foo_: 13}}) - -@external -def bar(a: address) -> FooStruct: - return {}(a).bar() -""" - -BAR_CODE = """ -struct FooStruct: - foo_: uint256 -@external -def bar() -> FooStruct: - return FooStruct({foo_: 13}) -""" - - -SAME_FOLDER_IMPORT_STMT = [ - ("import Bar as Bar", "Bar"), - ("import contracts.Bar as Bar", "Bar"), - ("from . import Bar", "Bar"), - ("from contracts import Bar", "Bar"), - ("from ..contracts import Bar", "Bar"), - ("from . import Bar as FooBar", "FooBar"), - ("from contracts import Bar as FooBar", "FooBar"), - ("from ..contracts import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt,alias", SAME_FOLDER_IMPORT_STMT) -def test_import_same_folder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - with tmp_path.joinpath("contracts/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -SUBFOLDER_IMPORT_STMT = [ - ("import other.Bar as Bar", "Bar"), - ("import contracts.other.Bar as Bar", "Bar"), - ("from other import Bar", "Bar"), - ("from contracts.other import Bar", "Bar"), - ("from .other import Bar", "Bar"), - ("from ..contracts.other import Bar", "Bar"), - ("from other import Bar as FooBar", "FooBar"), - ("from contracts.other import Bar as FooBar", "FooBar"), - ("from .other import Bar as FooBar", "FooBar"), - ("from ..contracts.other import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt, alias", SUBFOLDER_IMPORT_STMT) -def test_import_subfolder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - tmp_path.joinpath("contracts/other").mkdir() - with tmp_path.joinpath("contracts/other/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -OTHER_FOLDER_IMPORT_STMT = [ - ("import interfaces.Bar as Bar", "Bar"), - ("from interfaces import Bar", "Bar"), - ("from ..interfaces import Bar", "Bar"), - ("from interfaces import Bar as FooBar", "FooBar"), - ("from ..interfaces import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt, alias", OTHER_FOLDER_IMPORT_STMT) -def test_import_other_folder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - tmp_path.joinpath("interfaces").mkdir() - with tmp_path.joinpath("interfaces/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -def test_import_parent_folder(tmp_path, assert_compile_failed): - tmp_path.joinpath("contracts").mkdir() - tmp_path.joinpath("contracts/baz").mkdir() - - foo_path = tmp_path.joinpath("contracts/baz/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format("from ... import Bar", "Bar")) - - with tmp_path.joinpath("Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - # Cannot perform relative import outside of base folder - with pytest.raises(FileNotFoundError): - compile_files([foo_path], ["combined_json"], root_folder=tmp_path.joinpath("contracts")) - - -META_IMPORT_STMT = [ - "import Meta as Meta", - "import contracts.Meta as Meta", - "from . import Meta", - "from contracts import Meta", -] - - -@pytest.mark.parametrize("import_stmt", META_IMPORT_STMT) -def test_import_self_interface(import_stmt, tmp_path): - # a contract can access its derived interface by importing itself - code = f""" -{import_stmt} - -struct FooStruct: - foo_: uint256 - -@external -def know_thyself(a: address) -> FooStruct: - return Meta(a).be_known() - -@external -def be_known() -> FooStruct: - return FooStruct({{foo_: 42}}) - """ - - tmp_path.joinpath("contracts").mkdir() - - meta_path = tmp_path.joinpath("contracts/Meta.vy") - with meta_path.open("w") as fp: - fp.write(code) - - assert compile_files([meta_path], ["combined_json"], root_folder=tmp_path) - - -DERIVED_IMPORT_STMT_BAZ = ["import Foo as Foo", "from . import Foo"] - -DERIVED_IMPORT_STMT_FOO = ["import Bar as Bar", "from . import Bar"] - - -@pytest.mark.parametrize("import_stmt_baz", DERIVED_IMPORT_STMT_BAZ) -@pytest.mark.parametrize("import_stmt_foo", DERIVED_IMPORT_STMT_FOO) -def test_derived_interface_imports(import_stmt_baz, import_stmt_foo, tmp_path): - # contracts-as-interfaces should be able to contain import statements - baz_code = f""" -{import_stmt_baz} - -struct FooStruct: - foo_: uint256 - -@external -def foo(a: address) -> FooStruct: - return Foo(a).foo() - -@external -def bar(_foo: address, _bar: address) -> FooStruct: - return Foo(_foo).bar(_bar) - """ - - with tmp_path.joinpath("Foo.vy").open("w") as fp: - fp.write(FOO_CODE.format(import_stmt_foo, "Bar")) - - with tmp_path.joinpath("Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - baz_path = tmp_path.joinpath("Baz.vy") - with baz_path.open("w") as fp: - fp.write(baz_code) - - assert compile_files([baz_path], ["combined_json"], root_folder=tmp_path) - - -def test_local_namespace(tmp_path): - # interface code namespaces should be isolated - # all of these contract should be able to compile together - codes = [ - "import foo as FooBar", - "import bar as FooBar", - "import foo as BarFoo", - "import bar as BarFoo", - ] - struct_def = """ -struct FooStruct: - foo_: uint256 - - """ - - compile_paths = [] - for i, code in enumerate(codes): - code += struct_def - path = tmp_path.joinpath(f"code{i}.vy") - with path.open("w") as fp: - fp.write(code) - compile_paths.append(path) - - for file_name in ("foo.vy", "bar.vy"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files(compile_paths, ["combined_json"], root_folder=tmp_path) - - -def test_get_interface_file_path(tmp_path): - for file_name in ("foo.vy", "foo.json", "bar.vy", "baz.json", "potato"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write("") - - tmp_path.joinpath("interfaces").mkdir() - for file_name in ("interfaces/foo.json", "interfaces/bar"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write("") - - base_paths = [tmp_path, tmp_path.joinpath("interfaces")] - assert get_interface_file_path(base_paths, "foo") == tmp_path.joinpath("foo.vy") - assert get_interface_file_path(base_paths, "bar") == tmp_path.joinpath("bar.vy") - assert get_interface_file_path(base_paths, "baz") == tmp_path.joinpath("baz.json") - - base_paths = [tmp_path.joinpath("interfaces"), tmp_path] - assert get_interface_file_path(base_paths, "foo") == tmp_path.joinpath("interfaces/foo.json") - assert get_interface_file_path(base_paths, "bar") == tmp_path.joinpath("bar.vy") - assert get_interface_file_path(base_paths, "baz") == tmp_path.joinpath("baz.json") - - with pytest.raises(Exception): - get_interface_file_path(base_paths, "potato") - - -def test_compile_outside_root_path(tmp_path): - foo_path = tmp_path.joinpath("foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format("import bar as Bar", "Bar")) - - bar_path = tmp_path.joinpath("bar.vy") - with bar_path.open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path, bar_path], ["combined_json"], root_folder=".") diff --git a/tests/cli/vyper_json/test_compile_from_input_dict.py b/tests/cli/vyper_json/test_compile_from_input_dict.py deleted file mode 100644 index a6d0a23100..0000000000 --- a/tests/cli/vyper_json/test_compile_from_input_dict.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 - -from copy import deepcopy - -import pytest - -import vyper -from vyper.cli.vyper_json import ( - TRANSLATE_MAP, - compile_from_input_dict, - exc_handler_raises, - exc_handler_to_dict, -) -from vyper.exceptions import InvalidType, JSONError, SyntaxException - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) - -@external -def baz() -> uint256: - return self.balance -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - -BAD_SYNTAX_CODE = """ -def bar()>: -""" - -BAD_COMPILER_CODE = """ -@external -def oopsie(a: uint256) -> bool: - return 42 -""" - -BAR_ABI = [ - { - "name": "bar", - "outputs": [{"type": "bool", "name": "out"}], - "inputs": [{"type": "uint256", "name": "a"}], - "stateMutability": "nonpayable", - "type": "function", - "gas": 313, - } -] - -INPUT_JSON = { - "language": "Vyper", - "sources": { - "contracts/foo.vy": {"content": FOO_CODE}, - "contracts/bar.vy": {"content": BAR_CODE}, - }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, - "settings": {"outputSelection": {"*": ["*"]}}, -} - - -def test_root_folder_not_exists(): - with pytest.raises(FileNotFoundError): - compile_from_input_dict({}, root_folder="/path/that/does/not/exist") - - -def test_wrong_language(): - with pytest.raises(JSONError): - compile_from_input_dict({"language": "Solidity"}) - - -def test_exc_handler_raises_syntax(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} - with pytest.raises(SyntaxException): - compile_from_input_dict(input_json, exc_handler_raises) - - -def test_exc_handler_to_dict_syntax(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} - result, _ = compile_from_input_dict(input_json, exc_handler_to_dict) - assert "errors" in result - assert len(result["errors"]) == 1 - error = result["errors"][0] - assert error["component"] == "parser" - assert error["type"] == "SyntaxException" - - -def test_exc_handler_raises_compiler(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} - with pytest.raises(InvalidType): - compile_from_input_dict(input_json, exc_handler_raises) - - -def test_exc_handler_to_dict_compiler(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} - result, _ = compile_from_input_dict(input_json, exc_handler_to_dict) - assert sorted(result.keys()) == ["compiler", "errors"] - assert result["compiler"] == f"vyper-{vyper.__version__}" - assert len(result["errors"]) == 1 - error = result["errors"][0] - assert error["component"] == "compiler" - assert error["type"] == "InvalidType" - - -def test_source_ids_increment(): - input_json = deepcopy(INPUT_JSON) - input_json["settings"]["outputSelection"] = {"*": ["evm.deployedBytecode.sourceMap"]} - result, _ = compile_from_input_dict(input_json) - assert result["contracts/bar.vy"]["source_map"]["pc_pos_map_compressed"].startswith("-1:-1:0") - assert result["contracts/foo.vy"]["source_map"]["pc_pos_map_compressed"].startswith("-1:-1:1") - - -def test_outputs(): - result, _ = compile_from_input_dict(INPUT_JSON) - assert sorted(result.keys()) == ["contracts/bar.vy", "contracts/foo.vy"] - assert sorted(result["contracts/bar.vy"].keys()) == sorted(set(TRANSLATE_MAP.values())) - - -def test_relative_import_paths(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["contracts/potato/baz/baz.vy"] = {"content": """from ... import foo"""} - input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": """from . import baz"""} - input_json["sources"]["contracts/potato/footato.vy"] = {"content": """from baz import baz"""} - compile_from_input_dict(input_json) diff --git a/tests/cli/vyper_json/test_compile_json.py b/tests/cli/vyper_json/test_compile_json.py deleted file mode 100644 index f03006c4ad..0000000000 --- a/tests/cli/vyper_json/test_compile_json.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 - -import json -from copy import deepcopy - -import pytest - -from vyper.cli.vyper_json import compile_from_input_dict, compile_json -from vyper.exceptions import JSONError - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - -BAR_ABI = [ - { - "name": "bar", - "outputs": [{"type": "bool", "name": "out"}], - "inputs": [{"type": "uint256", "name": "a"}], - "stateMutability": "nonpayable", - "type": "function", - "gas": 313, - } -] - -INPUT_JSON = { - "language": "Vyper", - "sources": { - "contracts/foo.vy": {"content": FOO_CODE}, - "contracts/bar.vy": {"content": BAR_CODE}, - }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, - "settings": {"outputSelection": {"*": ["*"]}}, -} - - -def test_input_formats(): - assert compile_json(INPUT_JSON) == compile_json(json.dumps(INPUT_JSON)) - - -def test_bad_json(): - with pytest.raises(JSONError): - compile_json("this probably isn't valid JSON, is it") - - -def test_keyerror_becomes_jsonerror(): - input_json = deepcopy(INPUT_JSON) - del input_json["sources"] - with pytest.raises(KeyError): - compile_from_input_dict(input_json) - with pytest.raises(JSONError): - compile_json(input_json) diff --git a/tests/cli/vyper_json/test_get_contracts.py b/tests/cli/vyper_json/test_get_contracts.py deleted file mode 100644 index 86a5052f72..0000000000 --- a/tests/cli/vyper_json/test_get_contracts.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from vyper.cli.vyper_json import get_input_dict_contracts -from vyper.exceptions import JSONError -from vyper.utils import keccak256 - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - - -def test_no_sources(): - with pytest.raises(KeyError): - get_input_dict_contracts({}) - - -def test_contracts_urls(): - with pytest.raises(JSONError): - get_input_dict_contracts({"sources": {"foo.vy": {"urls": ["https://foo.code.com/"]}}}) - - -def test_contracts_no_content_key(): - with pytest.raises(JSONError): - get_input_dict_contracts({"sources": {"foo.vy": FOO_CODE}}) - - -def test_contracts_keccak(): - hash_ = keccak256(FOO_CODE.encode()).hex() - - input_json = {"sources": {"foo.vy": {"content": FOO_CODE, "keccak256": hash_}}} - get_input_dict_contracts(input_json) - - input_json["sources"]["foo.vy"]["keccak256"] = "0x" + hash_ - get_input_dict_contracts(input_json) - - input_json["sources"]["foo.vy"]["keccak256"] = "0x1234567890" - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contracts_bad_path(): - input_json = {"sources": {"../foo.vy": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contract_collision(): - # ./foo.vy and foo.vy will resolve to the same path - input_json = {"sources": {"./foo.vy": {"content": FOO_CODE}, "foo.vy": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contracts_return_value(): - input_json = { - "sources": {"foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}} - } - result = get_input_dict_contracts(input_json) - assert result == {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE} diff --git a/tests/cli/vyper_json/test_interfaces.py b/tests/cli/vyper_json/test_interfaces.py deleted file mode 100644 index 7804ae1c3d..0000000000 --- a/tests/cli/vyper_json/test_interfaces.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from vyper.cli.vyper_json import get_input_dict_interfaces, get_interface_codes -from vyper.exceptions import JSONError - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - -BAR_ABI = [ - { - "name": "bar", - "outputs": [{"type": "bool", "name": "out"}], - "inputs": [{"type": "uint256", "name": "a"}], - "stateMutability": "nonpayable", - "type": "function", - "gas": 313, - } -] - - -# get_input_dict_interfaces tests - - -def test_no_interfaces(): - result = get_input_dict_interfaces({}) - assert isinstance(result, dict) - assert not result - - -def test_interface_collision(): - input_json = {"interfaces": {"bar.json": {"abi": BAR_ABI}, "bar.vy": {"content": BAR_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_interfaces_wrong_suffix(): - input_json = {"interfaces": {"foo.abi": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - input_json = {"interfaces": {"interface.folder/foo": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_json_no_abi(): - input_json = {"interfaces": {"bar.json": {"content": BAR_ABI}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_vy_no_content(): - input_json = {"interfaces": {"bar.vy": {"abi": BAR_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_interfaces_output(): - input_json = { - "interfaces": { - "bar.json": {"abi": BAR_ABI}, - "interface.folder/bar2.vy": {"content": BAR_CODE}, - } - } - result = get_input_dict_interfaces(input_json) - assert isinstance(result, dict) - assert result == { - "bar": {"type": "json", "code": BAR_ABI}, - "interface.folder/bar2": {"type": "vyper", "code": BAR_CODE}, - } - - -def test_manifest_output(): - input_json = {"interfaces": {"bar.json": {"contractTypes": {"Bar": {"abi": BAR_ABI}}}}} - result = get_input_dict_interfaces(input_json) - assert isinstance(result, dict) - assert result == {"Bar": {"type": "json", "code": BAR_ABI}} - - -# get_interface_codes tests - - -def test_interface_codes_from_contracts(): - # interface should be generated from contract - assert get_interface_codes( - None, "foo.vy", {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE}, {} - ) - assert get_interface_codes( - None, "foo/foo.vy", {"foo/foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE}, {} - ) - - -def test_interface_codes_from_interfaces(): - # existing interface should be given preference over contract-as-interface - contracts = {"foo.vy": FOO_CODE, "contacts/bar.vy": BAR_CODE} - result = get_interface_codes(None, "foo.vy", contracts, {"contracts/bar": "bar"}) - assert result["Bar"] == "bar" - - -def test_root_path(tmp_path): - tmp_path.joinpath("contracts").mkdir() - with tmp_path.joinpath("contracts/bar.vy").open("w") as fp: - fp.write("bar") - - with pytest.raises(FileNotFoundError): - get_interface_codes(None, "foo.vy", {"foo.vy": FOO_CODE}, {}) - - # interface from file system should take lowest priority - result = get_interface_codes(tmp_path, "foo.vy", {"foo.vy": FOO_CODE}, {}) - assert result["Bar"] == {"code": "bar", "type": "vyper"} - contracts = {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE} - result = get_interface_codes(None, "foo.vy", contracts, {}) - assert result["Bar"] == {"code": BAR_CODE, "type": "vyper"} diff --git a/tests/cli/vyper_json/test_output_dict.py b/tests/cli/vyper_json/test_output_dict.py deleted file mode 100644 index e2a3466ccf..0000000000 --- a/tests/cli/vyper_json/test_output_dict.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 - -import vyper -from vyper.cli.vyper_json import format_to_output_dict -from vyper.compiler import OUTPUT_FORMATS, compile_codes - -FOO_CODE = """ -@external -def foo() -> bool: - return True -""" - - -def test_keys(): - compiler_data = compile_codes({"foo.vy": FOO_CODE}, output_formats=list(OUTPUT_FORMATS.keys())) - output_json = format_to_output_dict(compiler_data) - assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] - assert output_json["compiler"] == f"vyper-{vyper.__version__}" - data = compiler_data["foo.vy"] - assert output_json["sources"]["foo.vy"] == {"id": 0, "ast": data["ast_dict"]["ast"]} - assert output_json["contracts"]["foo.vy"]["foo"] == { - "abi": data["abi"], - "devdoc": data["devdoc"], - "interface": data["interface"], - "ir": data["ir_dict"], - "userdoc": data["userdoc"], - "metadata": data["metadata"], - "evm": { - "bytecode": {"object": data["bytecode"], "opcodes": data["opcodes"]}, - "deployedBytecode": { - "object": data["bytecode_runtime"], - "opcodes": data["opcodes_runtime"], - "sourceMap": data["source_map"]["pc_pos_map_compressed"], - "sourceMapFull": data["source_map_full"], - }, - "methodIdentifiers": data["method_identifiers"], - }, - } diff --git a/tests/cli/vyper_json/test_output_selection.py b/tests/cli/vyper_json/test_output_selection.py deleted file mode 100644 index c72f06f5a7..0000000000 --- a/tests/cli/vyper_json/test_output_selection.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from vyper.cli.vyper_json import TRANSLATE_MAP, get_input_dict_output_formats -from vyper.exceptions import JSONError - - -def test_no_outputs(): - with pytest.raises(KeyError): - get_input_dict_output_formats({}, {}) - - -def test_invalid_output(): - input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "foobar"]}}} - sources = {"foo.vy": ""} - with pytest.raises(JSONError): - get_input_dict_output_formats(input_json, sources) - - -def test_unknown_contract(): - input_json = {"settings": {"outputSelection": {"bar.vy": ["abi"]}}} - sources = {"foo.vy": ""} - with pytest.raises(JSONError): - get_input_dict_output_formats(input_json, sources) - - -@pytest.mark.parametrize("output", TRANSLATE_MAP.items()) -def test_translate_map(output): - input_json = {"settings": {"outputSelection": {"foo.vy": [output[0]]}}} - sources = {"foo.vy": ""} - assert get_input_dict_output_formats(input_json, sources) == {"foo.vy": [output[1]]} - - -def test_star(): - input_json = {"settings": {"outputSelection": {"*": ["*"]}}} - sources = {"foo.vy": "", "bar.vy": ""} - expected = sorted(set(TRANSLATE_MAP.values())) - result = get_input_dict_output_formats(input_json, sources) - assert result == {"foo.vy": expected, "bar.vy": expected} - - -def test_evm(): - input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "evm"]}}} - sources = {"foo.vy": ""} - expected = ["abi"] + sorted(v for k, v in TRANSLATE_MAP.items() if k.startswith("evm")) - result = get_input_dict_output_formats(input_json, sources) - assert result == {"foo.vy": expected} - - -def test_solc_style(): - input_json = {"settings": {"outputSelection": {"foo.vy": {"": ["abi"], "foo.vy": ["ir"]}}}} - sources = {"foo.vy": ""} - assert get_input_dict_output_formats(input_json, sources) == {"foo.vy": ["abi", "ir_dict"]} diff --git a/tests/conftest.py b/tests/conftest.py index d519ca3100..216fb32b0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,28 +1,39 @@ +import json import logging from functools import wraps +import hypothesis import pytest +import web3.exceptions from eth_tester import EthereumTester, PyEVMBackend +from eth_tester.exceptions import TransactionFailed from eth_utils import setup_DEBUG2_logging +from eth_utils.toolz import compose from hexbytes import HexBytes from web3 import Web3 +from web3.contract import Contract from web3.providers.eth_tester import EthereumTesterProvider from vyper import compiler +from vyper.ast.grammar import parse_vyper_source from vyper.codegen.ir_node import IRnode -from vyper.compiler.settings import OptimizationLevel, _set_debug_mode +from vyper.compiler.input_bundle import FilesystemInputBundle +from vyper.compiler.settings import OptimizationLevel, Settings, _set_debug_mode from vyper.ir import compile_ir, optimizer -from .base_conftest import VyperContract, _get_contract, zero_gas_price_strategy - -# Import the base_conftest fixtures -pytest_plugins = ["tests.base_conftest", "tests.fixtures.memorymock"] +# Import the base fixtures +pytest_plugins = ["tests.fixtures.memorymock"] ############ # PATCHING # ############ +# disable hypothesis deadline globally +hypothesis.settings.register_profile("ci", deadline=None) +hypothesis.settings.load_profile("ci") + + def set_evm_verbose_logging(): logger = logging.getLogger("eth.vm.computation.Computation") setup_DEBUG2_logging() @@ -64,6 +75,36 @@ def keccak(): return Web3.keccak +@pytest.fixture +def make_file(tmp_path): + # writes file_contents to file_name, creating it in the + # tmp_path directory. returns final path. + def fn(file_name, file_contents): + path = tmp_path / file_name + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as f: + f.write(file_contents) + + return path + + return fn + + +# this can either be used for its side effects (to prepare a call +# to get_contract), or the result can be provided directly to +# compile_code / CompilerData. +@pytest.fixture +def make_input_bundle(tmp_path, make_file): + def fn(sources_dict): + for file_name, file_contents in sources_dict.items(): + make_file(file_name, file_contents) + return FilesystemInputBundle([tmp_path]) + + return fn + + +# TODO: remove me, this is just string.encode("utf-8").ljust() +# only used in test_logging.py. @pytest.fixture def bytes_helper(): def bytes_helper(str, length): @@ -72,45 +113,35 @@ def bytes_helper(str, length): return bytes_helper -@pytest.fixture -def get_contract_from_ir(w3, optimize): - def ir_compiler(ir, *args, **kwargs): - ir = IRnode.from_list(ir) - if optimize != OptimizationLevel.NONE: - ir = optimizer.optimize(ir) - bytecode, _ = compile_ir.assembly_to_evm( - compile_ir.compile_to_assembly(ir, optimize=optimize) - ) - abi = kwargs.get("abi") or [] - c = w3.eth.contract(abi=abi, bytecode=bytecode) - deploy_transaction = c.constructor() - tx_hash = deploy_transaction.transact() - address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] - contract = w3.eth.contract( - address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract - ) - return contract +def _none_addr(datatype, data): + if datatype == "address" and int(data, base=16) == 0: + return (datatype, None) + else: + return (datatype, data) - return ir_compiler + +CONCISE_NORMALIZERS = (_none_addr,) @pytest.fixture(scope="module") -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. - """ - custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 4500000}) +def tester(): + # set absurdly high gas limit so that london basefee never adjusts + # (note: 2**63 - 1 is max that evm allows) + custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 10**10}) custom_genesis["base_fee_per_gas"] = 0 backend = PyEVMBackend(genesis_parameters=custom_genesis) - tester = EthereumTester(backend=backend) - w3 = Web3(EthereumTesterProvider(tester)) - w3.eth.set_gas_price_strategy(zero_gas_price_strategy) + return EthereumTester(backend=backend) - def get_contract_module(source_code, *args, **kwargs): - return _get_contract(w3, source_code, optimize, *args, **kwargs) - return get_contract_module +def zero_gas_price_strategy(web3, transaction_params=None): + return 0 # zero gas price makes testing simpler. + + +@pytest.fixture(scope="module") +def w3(tester): + w3 = Web3(EthereumTesterProvider(tester)) + w3.eth.set_gas_price_strategy(zero_gas_price_strategy) + return w3 def get_compiler_gas_estimate(code, func): @@ -152,6 +183,130 @@ def set_decorator_to_contract_function(w3, tester, contract, source_code, func): setattr(contract, func, func_with_decorator) +class VyperMethod: + ALLOWED_MODIFIERS = {"call", "estimateGas", "transact", "buildTransaction"} + + def __init__(self, function, normalizers=None): + self._function = function + self._function._return_data_normalizers = normalizers + + def __call__(self, *args, **kwargs): + return self.__prepared_function(*args, **kwargs) + + def __prepared_function(self, *args, **kwargs): + if not kwargs: + modifier, modifier_dict = "call", {} + fn_abi = [ + x + for x in self._function.contract_abi + if x.get("name") == self._function.function_identifier + ].pop() + # To make tests faster just supply some high gas value. + modifier_dict.update({"gas": fn_abi.get("gas", 0) + 500000}) + elif len(kwargs) == 1: + modifier, modifier_dict = kwargs.popitem() + if modifier not in self.ALLOWED_MODIFIERS: + raise TypeError(f"The only allowed keyword arguments are: {self.ALLOWED_MODIFIERS}") + else: + raise TypeError(f"Use up to one keyword argument, one of: {self.ALLOWED_MODIFIERS}") + return getattr(self._function(*args), modifier)(modifier_dict) + + +class VyperContract: + """ + An alternative Contract Factory which invokes all methods as `call()`, + unless you add a keyword argument. The keyword argument assigns the prep method. + This call + > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) + is equivalent to this call in the classic contract: + > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) + """ + + def __init__(self, classic_contract, method_class=VyperMethod): + classic_contract._return_data_normalizers += CONCISE_NORMALIZERS + self._classic_contract = classic_contract + self.address = self._classic_contract.address + protected_fn_names = [fn for fn in dir(self) if not fn.endswith("__")] + + try: + fn_names = [fn["name"] for fn in self._classic_contract.functions._functions] + except web3.exceptions.NoABIFunctionsFound: + fn_names = [] + + for fn_name in fn_names: + # Override namespace collisions + if fn_name in protected_fn_names: + raise AttributeError(f"{fn_name} is protected!") + else: + _classic_method = getattr(self._classic_contract.functions, fn_name) + _concise_method = method_class( + _classic_method, self._classic_contract._return_data_normalizers + ) + setattr(self, fn_name, _concise_method) + + @classmethod + def factory(cls, *args, **kwargs): + return compose(cls, Contract.factory(*args, **kwargs)) + + +@pytest.fixture +def get_contract_from_ir(w3, optimize): + def ir_compiler(ir, *args, **kwargs): + ir = IRnode.from_list(ir) + if optimize != OptimizationLevel.NONE: + ir = optimizer.optimize(ir) + bytecode, _ = compile_ir.assembly_to_evm( + compile_ir.compile_to_assembly(ir, optimize=optimize) + ) + abi = kwargs.get("abi") or [] + c = w3.eth.contract(abi=abi, bytecode=bytecode) + deploy_transaction = c.constructor() + tx_hash = deploy_transaction.transact() + address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] + contract = w3.eth.contract( + address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract + ) + return contract + + return ir_compiler + + +def _get_contract( + w3, source_code, optimize, *args, override_opt_level=None, input_bundle=None, **kwargs +): + settings = Settings() + settings.evm_version = kwargs.pop("evm_version", None) + settings.optimize = override_opt_level or optimize + out = compiler.compile_code( + source_code, + # test that metadata and natspecs get generated + output_formats=["abi", "bytecode", "metadata", "userdoc", "devdoc"], + settings=settings, + input_bundle=input_bundle, + show_gas_estimates=True, # Enable gas estimates for testing + ) + parse_vyper_source(source_code) # Test grammar. + json.dumps(out["metadata"]) # test metadata is json serializable + abi = out["abi"] + bytecode = out["bytecode"] + value = kwargs.pop("value_in_eth", 0) * 10**18 # Handle deploying with an eth value. + c = w3.eth.contract(abi=abi, bytecode=bytecode) + deploy_transaction = c.constructor(*args) + tx_info = {"from": w3.eth.accounts[0], "value": value, "gasPrice": 0} + tx_info.update(kwargs) + tx_hash = deploy_transaction.transact(tx_info) + address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] + return w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract) + + +@pytest.fixture(scope="module") +def get_contract(w3, optimize): + def fn(source_code, *args, **kwargs): + return _get_contract(w3, source_code, optimize, *args, **kwargs) + + return fn + + @pytest.fixture def get_contract_with_gas_estimation(tester, w3, optimize): def get_contract_with_gas_estimation(source_code, *args, **kwargs): @@ -172,6 +327,73 @@ def get_contract_with_gas_estimation_for_constants(source_code, *args, **kwargs) return get_contract_with_gas_estimation_for_constants +@pytest.fixture(scope="module") +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. + """ + custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 4500000}) + custom_genesis["base_fee_per_gas"] = 0 + backend = PyEVMBackend(genesis_parameters=custom_genesis) + tester = EthereumTester(backend=backend) + w3 = Web3(EthereumTesterProvider(tester)) + w3.eth.set_gas_price_strategy(zero_gas_price_strategy) + + def get_contract_module(source_code, *args, **kwargs): + return _get_contract(w3, source_code, optimize, *args, **kwargs) + + return get_contract_module + + +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, + output_formats=["abi", "bytecode", "metadata", "userdoc", "devdoc"], + settings=settings, + show_gas_estimates=True, # Enable gas estimates for testing + ) + parse_vyper_source(source_code) # Test grammar. + abi = out["abi"] + bytecode = HexBytes(initcode_prefix) + HexBytes(out["bytecode"]) + bytecode_len = len(bytecode) + bytecode_len_hex = hex(bytecode_len)[2:].rjust(4, "0") + # prepend a quick deploy preamble + deploy_preamble = HexBytes("61" + bytecode_len_hex + "3d81600a3d39f3") + deploy_bytecode = HexBytes(deploy_preamble) + bytecode + + deployer_abi = [] # just a constructor + c = w3.eth.contract(abi=deployer_abi, bytecode=deploy_bytecode) + deploy_transaction = c.constructor() + tx_info = {"from": w3.eth.accounts[0], "value": 0, "gasPrice": 0} + + tx_hash = deploy_transaction.transact(tx_info) + address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"] + + # sanity check + assert w3.eth.get_code(address) == bytecode, (w3.eth.get_code(address), bytecode) + + def factory(address): + return w3.eth.contract( + address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract + ) + + return w3.eth.contract(address, bytecode=deploy_bytecode), factory + + +@pytest.fixture(scope="module") +def deploy_blueprint_for(w3, optimize): + def deploy_blueprint_for(source_code, *args, **kwargs): + return _deploy_blueprint_for(w3, source_code, optimize, *args, **kwargs) + + return deploy_blueprint_for + + +# TODO: this should not be a fixture. +# remove me and replace all uses with `with pytest.raises`. @pytest.fixture def assert_compile_failed(): def assert_compile_failed(function_to_test, exception=Exception): @@ -181,6 +403,7 @@ def assert_compile_failed(function_to_test, exception=Exception): return assert_compile_failed +# TODO this should not be a fixture @pytest.fixture def search_for_sublist(): def search_for_sublist(ir, sublist): @@ -242,3 +465,27 @@ def assert_side_effects_invoked(side_effects_contract, side_effects_trigger, n=1 assert end_value == start_value + n return assert_side_effects_invoked + + +@pytest.fixture +def get_logs(w3): + def get_logs(tx_hash, c, event_name): + tx_receipt = w3.eth.get_transaction_receipt(tx_hash) + return c._classic_contract.events[event_name]().process_receipt(tx_receipt) + + return get_logs + + +# TODO replace me with function like `with anchor_state()` +@pytest.fixture(scope="module") +def assert_tx_failed(tester): + def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): + snapshot_id = tester.take_snapshot() + with pytest.raises(exception) as excinfo: + function_to_test() + tester.revert_to_snapshot(snapshot_id) + if exc_text: + # TODO test equality + assert exc_text in str(excinfo.value), (exc_text, excinfo.value) + + return assert_tx_failed diff --git a/tests/compiler/ir/__init__.py b/tests/functional/__init__.py similarity index 100% rename from tests/compiler/ir/__init__.py rename to tests/functional/__init__.py diff --git a/tests/parser/exceptions/__init__.py b/tests/functional/builtins/codegen/__init__.py similarity index 100% rename from tests/parser/exceptions/__init__.py rename to tests/functional/builtins/codegen/__init__.py diff --git a/tests/parser/functions/test_abi.py b/tests/functional/builtins/codegen/test_abi.py similarity index 100% rename from tests/parser/functions/test_abi.py rename to tests/functional/builtins/codegen/test_abi.py diff --git a/tests/parser/functions/test_abi_decode.py b/tests/functional/builtins/codegen/test_abi_decode.py similarity index 91% rename from tests/parser/functions/test_abi_decode.py rename to tests/functional/builtins/codegen/test_abi_decode.py index 2f9b93057d..242841e1cf 100644 --- a/tests/parser/functions/test_abi_decode.py +++ b/tests/functional/builtins/codegen/test_abi_decode.py @@ -25,7 +25,7 @@ def test_abi_decode_complex(get_contract): @external def abi_decode(x: Bytes[160]) -> (address, int128, bool, decimal, bytes32): - a: address = ZERO_ADDRESS + a: address = empty(address) b: int128 = 0 c: bool = False d: decimal = 0.0 @@ -39,7 +39,7 @@ def abi_decode_struct(x: Bytes[544]) -> Human: name: "", pet: Animal({ name: "", - address_: ZERO_ADDRESS, + address_: empty(address), id_: 0, is_furry: False, price: 0.0, @@ -344,6 +344,34 @@ def abi_decode(x: Bytes[96]) -> (uint256, uint256): assert_tx_failed(lambda: c.abi_decode(input_)) +def test_clamper_nested_uint8(get_contract, assert_tx_failed): + # check that _abi_decode clamps on word-types even when it is in a nested expression + # decode -> validate uint8 -> revert if input >= 256 -> cast back to uint256 + contract = """ +@external +def abi_decode(x: uint256) -> uint256: + a: uint256 = convert(_abi_decode(slice(msg.data, 4, 32), (uint8)), uint256) + return a + """ + c = get_contract(contract) + assert c.abi_decode(255) == 255 + assert_tx_failed(lambda: c.abi_decode(256)) + + +def test_clamper_nested_bytes(get_contract, assert_tx_failed): + # check that _abi_decode clamps dynamic even when it is in a nested expression + # decode -> validate Bytes[20] -> revert if len(input) > 20 -> convert back to -> add 1 + contract = """ +@external +def abi_decode(x: Bytes[96]) -> Bytes[21]: + a: Bytes[21] = concat(b"a", _abi_decode(x, Bytes[20])) + return a + """ + c = get_contract(contract) + assert c.abi_decode(abi.encode("(bytes)", (b"bc",))) == b"abc" + assert_tx_failed(lambda: c.abi_decode(abi.encode("(bytes)", (b"a" * 22,)))) + + @pytest.mark.parametrize( "output_typ,input_", [ diff --git a/tests/parser/functions/test_abi_encode.py b/tests/functional/builtins/codegen/test_abi_encode.py similarity index 100% rename from tests/parser/functions/test_abi_encode.py rename to tests/functional/builtins/codegen/test_abi_encode.py diff --git a/tests/parser/functions/test_addmod.py b/tests/functional/builtins/codegen/test_addmod.py similarity index 100% rename from tests/parser/functions/test_addmod.py rename to tests/functional/builtins/codegen/test_addmod.py diff --git a/tests/parser/types/value/test_as_wei_value.py b/tests/functional/builtins/codegen/test_as_wei_value.py similarity index 75% rename from tests/parser/types/value/test_as_wei_value.py rename to tests/functional/builtins/codegen/test_as_wei_value.py index 249ac4b2ff..cc27507e7c 100644 --- a/tests/parser/types/value/test_as_wei_value.py +++ b/tests/functional/builtins/codegen/test_as_wei_value.py @@ -91,3 +91,36 @@ def foo(a: {data_type}) -> uint256: c = get_contract(code) assert c.foo(0) == 0 + + +def test_ext_call(w3, side_effects_contract, assert_side_effects_invoked, get_contract): + code = """ +@external +def foo(a: Foo) -> uint256: + return as_wei_value(a.foo(7), "ether") + +interface Foo: + def foo(x: uint8) -> uint8: nonpayable + """ + + c1 = side_effects_contract("uint8") + c2 = get_contract(code) + + assert c2.foo(c1.address) == w3.to_wei(7, "ether") + assert_side_effects_invoked(c1, lambda: c2.foo(c1.address, transact={})) + + +def test_internal_call(w3, get_contract_with_gas_estimation): + code = """ +@external +def foo() -> uint256: + return as_wei_value(self.bar(), "ether") + +@internal +def bar() -> uint8: + return 7 + """ + + c = get_contract_with_gas_estimation(code) + + assert c.foo() == w3.to_wei(7, "ether") diff --git a/tests/parser/functions/test_bitwise.py b/tests/functional/builtins/codegen/test_bitwise.py similarity index 98% rename from tests/parser/functions/test_bitwise.py rename to tests/functional/builtins/codegen/test_bitwise.py index 3ba74034ac..1d62a5be79 100644 --- a/tests/parser/functions/test_bitwise.py +++ b/tests/functional/builtins/codegen/test_bitwise.py @@ -32,7 +32,7 @@ def _shr(x: uint256, y: uint256) -> uint256: def test_bitwise_opcodes(): - opcodes = compile_code(code, ["opcodes"])["opcodes"] + opcodes = compile_code(code, output_formats=["opcodes"])["opcodes"] assert "SHL" in opcodes assert "SHR" in opcodes diff --git a/tests/parser/functions/test_ceil.py b/tests/functional/builtins/codegen/test_ceil.py similarity index 100% rename from tests/parser/functions/test_ceil.py rename to tests/functional/builtins/codegen/test_ceil.py diff --git a/tests/parser/functions/test_concat.py b/tests/functional/builtins/codegen/test_concat.py similarity index 100% rename from tests/parser/functions/test_concat.py rename to tests/functional/builtins/codegen/test_concat.py diff --git a/tests/parser/functions/test_convert.py b/tests/functional/builtins/codegen/test_convert.py similarity index 97% rename from tests/parser/functions/test_convert.py rename to tests/functional/builtins/codegen/test_convert.py index eb8449447c..b5ce613235 100644 --- a/tests/parser/functions/test_convert.py +++ b/tests/functional/builtins/codegen/test_convert.py @@ -534,24 +534,6 @@ def foo(a: {typ}) -> Status: assert_compile_failed(lambda: get_contract_with_gas_estimation(contract), TypeMismatch) -# TODO CMC 2022-04-06 I think this test is somewhat unnecessary. -@pytest.mark.parametrize( - "builtin_constant,out_type,out_value", - [("ZERO_ADDRESS", "bool", False), ("msg.sender", "bool", True)], -) -def test_convert_builtin_constant( - get_contract_with_gas_estimation, builtin_constant, out_type, out_value -): - contract = f""" -@external -def convert_builtin_constant() -> {out_type}: - return convert({builtin_constant}, {out_type}) - """ - - c = get_contract_with_gas_estimation(contract) - assert c.convert_builtin_constant() == out_value - - # uint256 conversion is currently valid due to type inference on literals # not quite working yet same_type_conversion_blocked = sorted(TEST_TYPES - {UINT256_T}) diff --git a/tests/parser/functions/test_create_functions.py b/tests/functional/builtins/codegen/test_create_functions.py similarity index 70% rename from tests/parser/functions/test_create_functions.py rename to tests/functional/builtins/codegen/test_create_functions.py index 876d50b27d..fa7729d98e 100644 --- a/tests/parser/functions/test_create_functions.py +++ b/tests/functional/builtins/codegen/test_create_functions.py @@ -431,3 +431,212 @@ def test2(target: address, salt: bytes32) -> address: # test2 = c.test2(b"\x01", salt) # assert HexBytes(test2) == create2_address_of(c.address, salt, vyper_initcode(b"\x01")) # assert_tx_failed(lambda: c.test2(bytecode, salt)) + + +# XXX: these various tests to check the msize allocator for +# create_copy_of and create_from_blueprint depend on calling convention +# and variables writing to memory. think of ways to make more robust to +# changes in calling convention and memory layout +@pytest.mark.parametrize("blueprint_prefix", [b"", b"\xfe", b"\xfe\71\x00"]) +def test_create_from_blueprint_complex_value( + get_contract, deploy_blueprint_for, w3, blueprint_prefix +): + # check msize allocator does not get trampled by value= kwarg + code = """ +var: uint256 + +@external +@payable +def __init__(x: uint256): + self.var = x + +@external +def foo()-> uint256: + return self.var + """ + + prefix_len = len(blueprint_prefix) + + some_constant = b"\00" * 31 + b"\x0c" + + deployer_code = f""" +created_address: public(address) +x: constant(Bytes[32]) = {some_constant} + +@internal +def foo() -> uint256: + g:uint256 = 42 + return 3 + +@external +@payable +def test(target: address): + self.created_address = create_from_blueprint( + target, + x, + code_offset={prefix_len}, + value=self.foo(), + raw_args=True + ) + """ + + foo_contract = get_contract(code, 12) + expected_runtime_code = w3.eth.get_code(foo_contract.address) + + f, FooContract = deploy_blueprint_for(code, initcode_prefix=blueprint_prefix) + + d = get_contract(deployer_code) + + d.test(f.address, transact={"value": 3}) + + test = FooContract(d.created_address()) + assert w3.eth.get_code(test.address) == expected_runtime_code + assert test.foo() == 12 + + +@pytest.mark.parametrize("blueprint_prefix", [b"", b"\xfe", b"\xfe\71\x00"]) +def test_create_from_blueprint_complex_salt_raw_args( + get_contract, deploy_blueprint_for, w3, blueprint_prefix +): + # test msize allocator does not get trampled by salt= kwarg + code = """ +var: uint256 + +@external +@payable +def __init__(x: uint256): + self.var = x + +@external +def foo()-> uint256: + return self.var + """ + + some_constant = b"\00" * 31 + b"\x0c" + prefix_len = len(blueprint_prefix) + + deployer_code = f""" +created_address: public(address) + +x: constant(Bytes[32]) = {some_constant} +salt: constant(bytes32) = keccak256("kebab") + +@internal +def foo() -> bytes32: + g:uint256 = 42 + return salt + +@external +@payable +def test(target: address): + self.created_address = create_from_blueprint( + target, + x, + code_offset={prefix_len}, + salt=self.foo(), + raw_args= True + ) + """ + + foo_contract = get_contract(code, 12) + expected_runtime_code = w3.eth.get_code(foo_contract.address) + + f, FooContract = deploy_blueprint_for(code, initcode_prefix=blueprint_prefix) + + d = get_contract(deployer_code) + + d.test(f.address, transact={}) + + test = FooContract(d.created_address()) + assert w3.eth.get_code(test.address) == expected_runtime_code + assert test.foo() == 12 + + +@pytest.mark.parametrize("blueprint_prefix", [b"", b"\xfe", b"\xfe\71\x00"]) +def test_create_from_blueprint_complex_salt_no_constructor_args( + get_contract, deploy_blueprint_for, w3, blueprint_prefix +): + # test msize allocator does not get trampled by salt= kwarg + code = """ +var: uint256 + +@external +@payable +def __init__(): + self.var = 12 + +@external +def foo()-> uint256: + return self.var + """ + + prefix_len = len(blueprint_prefix) + deployer_code = f""" +created_address: public(address) + +salt: constant(bytes32) = keccak256("kebab") + +@external +@payable +def test(target: address): + self.created_address = create_from_blueprint( + target, + code_offset={prefix_len}, + salt=keccak256(_abi_encode(target)) + ) + """ + + foo_contract = get_contract(code) + expected_runtime_code = w3.eth.get_code(foo_contract.address) + + f, FooContract = deploy_blueprint_for(code, initcode_prefix=blueprint_prefix) + + d = get_contract(deployer_code) + + d.test(f.address, transact={}) + + test = FooContract(d.created_address()) + assert w3.eth.get_code(test.address) == expected_runtime_code + assert test.foo() == 12 + + +def test_create_copy_of_complex_kwargs(get_contract, w3): + # test msize allocator does not get trampled by salt= kwarg + complex_salt = """ +created_address: public(address) + +@external +def test(target: address) -> address: + self.created_address = create_copy_of( + target, + salt=keccak256(_abi_encode(target)) + ) + return self.created_address + + """ + + c = get_contract(complex_salt) + bytecode = w3.eth.get_code(c.address) + c.test(c.address, transact={}) + test1 = c.created_address() + assert w3.eth.get_code(test1) == bytecode + + # test msize allocator does not get trampled by value= kwarg + complex_value = """ +created_address: public(address) + +@external +@payable +def test(target: address) -> address: + value: uint256 = 2 + self.created_address = create_copy_of(target, value = [2,2,2][value]) + return self.created_address + + """ + + c = get_contract(complex_value) + bytecode = w3.eth.get_code(c.address) + + c.test(c.address, transact={"value": 2}) + test1 = c.created_address() + assert w3.eth.get_code(test1) == bytecode diff --git a/tests/parser/functions/test_ec.py b/tests/functional/builtins/codegen/test_ec.py similarity index 100% rename from tests/parser/functions/test_ec.py rename to tests/functional/builtins/codegen/test_ec.py diff --git a/tests/parser/functions/test_ecrecover.py b/tests/functional/builtins/codegen/test_ecrecover.py similarity index 100% rename from tests/parser/functions/test_ecrecover.py rename to tests/functional/builtins/codegen/test_ecrecover.py diff --git a/tests/parser/functions/test_empty.py b/tests/functional/builtins/codegen/test_empty.py similarity index 97% rename from tests/parser/functions/test_empty.py rename to tests/functional/builtins/codegen/test_empty.py index c10d03550a..c3627785dc 100644 --- a/tests/parser/functions/test_empty.py +++ b/tests/functional/builtins/codegen/test_empty.py @@ -87,8 +87,8 @@ def foo(): self.foobar = empty(address) bar = empty(address) - assert self.foobar == ZERO_ADDRESS - assert bar == ZERO_ADDRESS + assert self.foobar == empty(address) + assert bar == empty(address) """, """ @external @@ -214,12 +214,12 @@ def foo(): self.foobar = empty(address[3]) bar = empty(address[3]) - assert self.foobar[0] == ZERO_ADDRESS - assert self.foobar[1] == ZERO_ADDRESS - assert self.foobar[2] == ZERO_ADDRESS - assert bar[0] == ZERO_ADDRESS - assert bar[1] == ZERO_ADDRESS - assert bar[2] == ZERO_ADDRESS + assert self.foobar[0] == empty(address) + assert self.foobar[1] == empty(address) + assert self.foobar[2] == empty(address) + assert bar[0] == empty(address) + assert bar[1] == empty(address) + assert bar[2] == empty(address) """, ], ) @@ -376,14 +376,14 @@ def foo(): assert self.foobar.c == False assert self.foobar.d == 0.0 assert self.foobar.e == 0x0000000000000000000000000000000000000000000000000000000000000000 - assert self.foobar.f == ZERO_ADDRESS + assert self.foobar.f == empty(address) assert bar.a == 0 assert bar.b == 0 assert bar.c == False assert bar.d == 0.0 assert bar.e == 0x0000000000000000000000000000000000000000000000000000000000000000 - assert bar.f == ZERO_ADDRESS + assert bar.f == empty(address) """ c = get_contract_with_gas_estimation(code) diff --git a/tests/parser/functions/test_extract32.py b/tests/functional/builtins/codegen/test_extract32.py similarity index 100% rename from tests/parser/functions/test_extract32.py rename to tests/functional/builtins/codegen/test_extract32.py diff --git a/tests/parser/functions/test_floor.py b/tests/functional/builtins/codegen/test_floor.py similarity index 100% rename from tests/parser/functions/test_floor.py rename to tests/functional/builtins/codegen/test_floor.py diff --git a/tests/parser/functions/test_interfaces.py b/tests/functional/builtins/codegen/test_interfaces.py similarity index 70% rename from tests/parser/functions/test_interfaces.py rename to tests/functional/builtins/codegen/test_interfaces.py index c16e188cfd..8cb0124f29 100644 --- a/tests/parser/functions/test_interfaces.py +++ b/tests/functional/builtins/codegen/test_interfaces.py @@ -1,10 +1,15 @@ +import json from decimal import Decimal import pytest -from vyper.cli.utils import extract_file_interface_imports -from vyper.compiler import compile_code, compile_codes -from vyper.exceptions import ArgumentException, InterfaceViolation, StructureException +from vyper.compiler import compile_code +from vyper.exceptions import ( + ArgumentException, + InterfaceViolation, + NamespaceCollision, + StructureException, +) def test_basic_extract_interface(): @@ -24,7 +29,7 @@ def allowance(_owner: address, _spender: address) -> (uint256, uint256): return 1, 2 """ - out = compile_code(code, ["interface"]) + out = compile_code(code, output_formats=["interface"]) out = out["interface"] code_pass = "\n".join(code.split("\n")[:-2] + [" pass"]) # replace with a pass statement. @@ -55,8 +60,9 @@ def allowance(_owner: address, _spender: address) -> (uint256, uint256): view def test(_owner: address): nonpayable """ - out = compile_codes({"one.vy": code}, ["external_interface"])["one.vy"] - out = out["external_interface"] + out = compile_code(code, contract_name="One.vy", output_formats=["external_interface"])[ + "external_interface" + ] assert interface.strip() == out.strip() @@ -75,7 +81,7 @@ def test() -> bool: assert_compile_failed(lambda: compile_code(code), InterfaceViolation) -def test_external_interface_parsing(assert_compile_failed): +def test_external_interface_parsing(make_input_bundle, assert_compile_failed): interface_code = """ @external def foo() -> uint256: @@ -86,7 +92,7 @@ def bar() -> uint256: pass """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) code = """ import a as FooBarInterface @@ -102,7 +108,7 @@ def bar() -> uint256: return 2 """ - assert compile_code(code, interface_codes=interface_codes) + assert compile_code(code, input_bundle=input_bundle) not_implemented_code = """ import a as FooBarInterface @@ -116,18 +122,17 @@ def foo() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) -def test_missing_event(assert_compile_failed): +def test_missing_event(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -140,19 +145,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event types match -def test_malformed_event(assert_compile_failed): +def test_malformed_event(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -168,19 +172,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event non-indexed arg needs to match interface -def test_malformed_events_indexed(assert_compile_failed): +def test_malformed_events_indexed(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -197,19 +200,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event indexed arg needs to match interface -def test_malformed_events_indexed2(assert_compile_failed): +def test_malformed_events_indexed2(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: indexed(uint256) """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -226,43 +228,47 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) VALID_IMPORT_CODE = [ # import statement, import path without suffix - ("import a as Foo", "a"), - ("import b.a as Foo", "b/a"), - ("import Foo as Foo", "Foo"), - ("from a import Foo", "a/Foo"), - ("from b.a import Foo", "b/a/Foo"), - ("from .a import Foo", "./a/Foo"), - ("from ..a import Foo", "../a/Foo"), + ("import a as Foo", "a.vy"), + ("import b.a as Foo", "b/a.vy"), + ("import Foo as Foo", "Foo.vy"), + ("from a import Foo", "a/Foo.vy"), + ("from b.a import Foo", "b/a/Foo.vy"), + ("from .a import Foo", "./a/Foo.vy"), + ("from ..a import Foo", "../a/Foo.vy"), ] -@pytest.mark.parametrize("code", VALID_IMPORT_CODE) -def test_extract_file_interface_imports(code): - assert extract_file_interface_imports(code[0]) == {"Foo": code[1]} +@pytest.mark.parametrize("code,filename", VALID_IMPORT_CODE) +def test_extract_file_interface_imports(code, filename, make_input_bundle): + input_bundle = make_input_bundle({filename: ""}) + + assert compile_code(code, input_bundle=input_bundle) is not None BAD_IMPORT_CODE = [ - "import a", # must alias absolute imports - "import a as A\nimport a as A", # namespace collisions - "from b import a\nfrom a import a", - "from . import a\nimport a as a", - "import a as a\nfrom . import a", + ("import a", StructureException), # must alias absolute imports + ("import a as A\nimport a as A", NamespaceCollision), + ("from b import a\nfrom . import a", NamespaceCollision), + ("from . import a\nimport a as a", NamespaceCollision), + ("import a as a\nfrom . import a", NamespaceCollision), ] -@pytest.mark.parametrize("code", BAD_IMPORT_CODE) -def test_extract_file_interface_imports_raises(code, assert_compile_failed): - assert_compile_failed(lambda: extract_file_interface_imports(code), StructureException) +@pytest.mark.parametrize("code,exception_type", BAD_IMPORT_CODE) +def test_extract_file_interface_imports_raises( + code, exception_type, assert_compile_failed, make_input_bundle +): + input_bundle = make_input_bundle({"a.vy": "", "b/a.vy": ""}) # dummy + assert_compile_failed(lambda: compile_code(code, input_bundle=input_bundle), exception_type) -def test_external_call_to_interface(w3, get_contract): +def test_external_call_to_interface(w3, get_contract, make_input_bundle): token_code = """ balanceOf: public(HashMap[address, uint256]) @@ -271,6 +277,8 @@ def transfer(to: address, _value: uint256): self.balanceOf[to] += _value """ + input_bundle = make_input_bundle({"one.vy": token_code}) + code = """ import one as TokenCode @@ -292,9 +300,7 @@ def test(): """ erc20 = get_contract(token_code) - test_c = get_contract( - code, *[erc20.address], interface_codes={"TokenCode": {"type": "vyper", "code": token_code}} - ) + test_c = get_contract(code, *[erc20.address], input_bundle=input_bundle) sender = w3.eth.accounts[0] assert erc20.balanceOf(sender) == 0 @@ -313,7 +319,7 @@ def test(): ("epsilon(decimal)", "decimal", Decimal("1E-10")), ], ) -def test_external_call_to_interface_kwarg(get_contract, kwarg, typ, expected): +def test_external_call_to_interface_kwarg(get_contract, kwarg, typ, expected, make_input_bundle): code_a = f""" @external @view @@ -321,6 +327,8 @@ def foo(_max: {typ} = {kwarg}) -> {typ}: return _max """ + input_bundle = make_input_bundle({"one.vy": code_a}) + code_b = f""" import one as ContractA @@ -331,11 +339,7 @@ def bar(a_address: address) -> {typ}: """ contract_a = get_contract(code_a) - contract_b = get_contract( - code_b, - *[contract_a.address], - interface_codes={"ContractA": {"type": "vyper", "code": code_a}}, - ) + contract_b = get_contract(code_b, *[contract_a.address], input_bundle=input_bundle) assert contract_b.bar(contract_a.address) == expected @@ -368,9 +372,7 @@ def test(): """ erc20 = get_contract(token_code) - test_c = get_contract( - code, *[erc20.address], interface_codes={"TokenCode": {"type": "vyper", "code": token_code}} - ) + test_c = get_contract(code, *[erc20.address]) sender = w3.eth.accounts[0] assert erc20.balanceOf(sender) == 0 @@ -440,11 +442,7 @@ def test_fail3() -> int256: """ bad_c = get_contract(external_contract) - c = get_contract( - code, - bad_c.address, - interface_codes={"BadCode": {"type": "vyper", "code": external_contract}}, - ) + c = get_contract(code, bad_c.address) assert bad_c.ok() == 1 assert bad_c.should_fail() == -(2**255) @@ -502,7 +500,9 @@ def test_fail2() -> Bytes[3]: # test data returned from external interface gets clamped -def test_json_abi_bytes_clampers(get_contract, assert_tx_failed, assert_compile_failed): +def test_json_abi_bytes_clampers( + get_contract, assert_tx_failed, assert_compile_failed, make_input_bundle +): external_contract = """ @external def returns_Bytes3() -> Bytes[3]: @@ -546,18 +546,15 @@ def test_fail3() -> Bytes[3]: """ bad_c = get_contract(external_contract) - bad_c_interface = { - "BadJSONInterface": { - "type": "json", - "code": compile_code(external_contract, ["abi"])["abi"], - } - } + + bad_json_interface = json.dumps(compile_code(external_contract, output_formats=["abi"])["abi"]) + input_bundle = make_input_bundle({"BadJSONInterface.json": bad_json_interface}) assert_compile_failed( - lambda: get_contract(should_not_compile, interface_codes=bad_c_interface), ArgumentException + lambda: get_contract(should_not_compile, input_bundle=input_bundle), ArgumentException ) - c = get_contract(code, bad_c.address, interface_codes=bad_c_interface) + c = get_contract(code, bad_c.address, input_bundle=input_bundle) assert bad_c.returns_Bytes3() == b"123" assert_tx_failed(lambda: c.test_fail1()) @@ -565,7 +562,7 @@ def test_fail3() -> Bytes[3]: assert_tx_failed(lambda: c.test_fail3()) -def test_units_interface(w3, get_contract): +def test_units_interface(w3, get_contract, make_input_bundle): code = """ import balanceof as BalanceOf @@ -576,49 +573,41 @@ def test_units_interface(w3, get_contract): def balanceOf(owner: address) -> uint256: return as_wei_value(1, "ether") """ + interface_code = """ @external @view def balanceOf(owner: address) -> uint256: pass """ - interface_codes = {"BalanceOf": {"type": "vyper", "code": interface_code}} - c = get_contract(code, interface_codes=interface_codes) + + input_bundle = make_input_bundle({"balanceof.vy": interface_code}) + + c = get_contract(code, input_bundle=input_bundle) assert c.balanceOf(w3.eth.accounts[0]) == w3.to_wei(1, "ether") -def test_local_and_global_interface_namespaces(): +def test_simple_implements(make_input_bundle): interface_code = """ @external def foo() -> uint256: pass """ - global_interface_codes = { - "FooInterface": {"type": "vyper", "code": interface_code}, - "BarInterface": {"type": "vyper", "code": interface_code}, - } - local_interface_codes = { - "FooContract": {"FooInterface": {"type": "vyper", "code": interface_code}}, - "BarContract": {"BarInterface": {"type": "vyper", "code": interface_code}}, - } - code = """ -import a as {0} +import a as FooInterface -implements: {0} +implements: FooInterface @external def foo() -> uint256: return 1 """ - codes = {"FooContract": code.format("FooInterface"), "BarContract": code.format("BarInterface")} + input_bundle = make_input_bundle({"a.vy": interface_code}) - global_compiled = compile_codes(codes, interface_codes=global_interface_codes) - local_compiled = compile_codes(codes, interface_codes=local_interface_codes) - assert global_compiled == local_compiled + assert compile_code(code, input_bundle=input_bundle) is not None def test_self_interface_is_allowed(get_contract): @@ -724,20 +713,28 @@ def convert_v1_abi(abi): @pytest.mark.parametrize("type_str", [i[0] for i in type_str_params]) -def test_json_interface_implements(type_str): +def test_json_interface_implements(type_str, make_input_bundle, make_file): code = interface_test_code.format(type_str) - abi = compile_code(code, ["abi"])["abi"] + abi = compile_code(code, output_formats=["abi"])["abi"] + code = f"import jsonabi as jsonabi\nimplements: jsonabi\n{code}" - compile_code(code, interface_codes={"jsonabi": {"type": "json", "code": abi}}) - compile_code(code, interface_codes={"jsonabi": {"type": "json", "code": convert_v1_abi(abi)}}) + + input_bundle = make_input_bundle({"jsonabi.json": json.dumps(abi)}) + + compile_code(code, input_bundle=input_bundle) + + # !!! overwrite the file + make_file("jsonabi.json", json.dumps(convert_v1_abi(abi))) + + compile_code(code, input_bundle=input_bundle) @pytest.mark.parametrize("type_str,value", type_str_params) -def test_json_interface_calls(get_contract, type_str, value): +def test_json_interface_calls(get_contract, type_str, value, make_input_bundle, make_file): code = interface_test_code.format(type_str) - abi = compile_code(code, ["abi"])["abi"] + abi = compile_code(code, output_formats=["abi"])["abi"] c1 = get_contract(code) code = f""" @@ -748,9 +745,11 @@ def test_json_interface_calls(get_contract, type_str, value): def test_call(a: address, b: {type_str}) -> {type_str}: return jsonabi(a).test_json(b) """ - c2 = get_contract(code, interface_codes={"jsonabi": {"type": "json", "code": abi}}) + input_bundle = make_input_bundle({"jsonabi.json": json.dumps(abi)}) + + c2 = get_contract(code, input_bundle=input_bundle) assert c2.test_call(c1.address, value) == value - c3 = get_contract( - code, interface_codes={"jsonabi": {"type": "json", "code": convert_v1_abi(abi)}} - ) + + make_file("jsonabi.json", json.dumps(convert_v1_abi(abi))) + c3 = get_contract(code, input_bundle=input_bundle) assert c3.test_call(c1.address, value) == value diff --git a/tests/parser/functions/test_is_contract.py b/tests/functional/builtins/codegen/test_is_contract.py similarity index 100% rename from tests/parser/functions/test_is_contract.py rename to tests/functional/builtins/codegen/test_is_contract.py diff --git a/tests/parser/functions/test_keccak256.py b/tests/functional/builtins/codegen/test_keccak256.py similarity index 100% rename from tests/parser/functions/test_keccak256.py rename to tests/functional/builtins/codegen/test_keccak256.py diff --git a/tests/parser/functions/test_length.py b/tests/functional/builtins/codegen/test_length.py similarity index 100% rename from tests/parser/functions/test_length.py rename to tests/functional/builtins/codegen/test_length.py diff --git a/tests/parser/functions/test_method_id.py b/tests/functional/builtins/codegen/test_method_id.py similarity index 100% rename from tests/parser/functions/test_method_id.py rename to tests/functional/builtins/codegen/test_method_id.py diff --git a/tests/parser/functions/test_minmax.py b/tests/functional/builtins/codegen/test_minmax.py similarity index 100% rename from tests/parser/functions/test_minmax.py rename to tests/functional/builtins/codegen/test_minmax.py diff --git a/tests/parser/functions/test_minmax_value.py b/tests/functional/builtins/codegen/test_minmax_value.py similarity index 100% rename from tests/parser/functions/test_minmax_value.py rename to tests/functional/builtins/codegen/test_minmax_value.py diff --git a/tests/parser/functions/test_mulmod.py b/tests/functional/builtins/codegen/test_mulmod.py similarity index 100% rename from tests/parser/functions/test_mulmod.py rename to tests/functional/builtins/codegen/test_mulmod.py diff --git a/tests/parser/functions/test_raw_call.py b/tests/functional/builtins/codegen/test_raw_call.py similarity index 73% rename from tests/parser/functions/test_raw_call.py rename to tests/functional/builtins/codegen/test_raw_call.py index 9c6fba79e7..5bb23447e4 100644 --- a/tests/parser/functions/test_raw_call.py +++ b/tests/functional/builtins/codegen/test_raw_call.py @@ -274,8 +274,8 @@ def test_raw_call(_target: address): def test_raw_call(_target: address): raw_call(_target, method_id("foo()"), max_outsize=0) """ - output1 = compile_code(code1, ["bytecode", "bytecode_runtime"]) - output2 = compile_code(code2, ["bytecode", "bytecode_runtime"]) + output1 = compile_code(code1, output_formats=["bytecode", "bytecode_runtime"]) + output2 = compile_code(code2, output_formats=["bytecode", "bytecode_runtime"]) assert output1 == output2 @@ -296,8 +296,8 @@ def test_raw_call(_target: address) -> bool: a: bool = raw_call(_target, method_id("foo()"), max_outsize=0, revert_on_failure=False) return a """ - output1 = compile_code(code1, ["bytecode", "bytecode_runtime"]) - output2 = compile_code(code2, ["bytecode", "bytecode_runtime"]) + output1 = compile_code(code1, output_formats=["bytecode", "bytecode_runtime"]) + output2 = compile_code(code2, output_formats=["bytecode", "bytecode_runtime"]) assert output1 == output2 @@ -426,6 +426,164 @@ def baz(_addr: address, should_raise: bool) -> uint256: assert caller.baz(target.address, False) == 3 +# XXX: these test_raw_call_clean_mem* tests depend on variables and +# calling convention writing to memory. think of ways to make more +# robust to changes to calling convention and memory layout. + + +def test_raw_call_msg_data_clean_mem(get_contract): + # test msize uses clean memory and does not get overwritten by + # any raw_call() arguments + code = """ +identity: constant(address) = 0x0000000000000000000000000000000000000004 + +@external +def foo(): + pass + +@internal +@view +def get_address()->address: + a:uint256 = 121 # 0x79 + return identity +@external +def bar(f: uint256, u: uint256) -> Bytes[100]: + # embed an internal call in the calculation of address + a: Bytes[100] = raw_call(self.get_address(), msg.data, max_outsize=100) + return a + """ + + c = get_contract(code) + assert ( + c.bar(1, 2).hex() == "ae42e951" + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + ) + + +def test_raw_call_clean_mem2(get_contract): + # test msize uses clean memory and does not get overwritten by + # any raw_call() arguments, another way + code = """ +buf: Bytes[100] + +@external +def bar(f: uint256, g: uint256, h: uint256) -> Bytes[100]: + # embed a memory modifying expression in the calculation of address + self.buf = raw_call( + [0x0000000000000000000000000000000000000004,][f-1], + msg.data, + max_outsize=100 + ) + return self.buf + """ + c = get_contract(code) + + assert ( + c.bar(1, 2, 3).hex() == "9309b76e" + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + "0000000000000000000000000000000000000000000000000000000000000003" + ) + + +def test_raw_call_clean_mem3(get_contract): + # test msize uses clean memory and does not get overwritten by + # any raw_call() arguments, and also test order of evaluation for + # scope_multi + code = """ +buf: Bytes[100] +canary: String[32] + +@internal +def bar() -> address: + self.canary = "bar" + return 0x0000000000000000000000000000000000000004 + +@internal +def goo() -> uint256: + self.canary = "goo" + return 0 + +@external +def foo() -> String[32]: + self.buf = raw_call(self.bar(), msg.data, value = self.goo(), max_outsize=100) + return self.canary + """ + c = get_contract(code) + assert c.foo() == "goo" + + +def test_raw_call_clean_mem_kwargs_value(get_contract): + # test msize uses clean memory and does not get overwritten by + # any raw_call() kwargs + code = """ +buf: Bytes[100] + +# add a dummy function to trigger memory expansion in the selector table routine +@external +def foo(): + pass + +@internal +def _value() -> uint256: + x: uint256 = 1 + return x + +@external +def bar(f: uint256) -> Bytes[100]: + # embed a memory modifying expression in the calculation of address + self.buf = raw_call( + 0x0000000000000000000000000000000000000004, + msg.data, + max_outsize=100, + value=self._value() + ) + return self.buf + """ + c = get_contract(code, value=1) + + assert ( + c.bar(13).hex() == "0423a132" + "000000000000000000000000000000000000000000000000000000000000000d" + ) + + +def test_raw_call_clean_mem_kwargs_gas(get_contract): + # test msize uses clean memory and does not get overwritten by + # any raw_call() kwargs + code = """ +buf: Bytes[100] + +# add a dummy function to trigger memory expansion in the selector table routine +@external +def foo(): + pass + +@internal +def _gas() -> uint256: + x: uint256 = msg.gas + return x + +@external +def bar(f: uint256) -> Bytes[100]: + # embed a memory modifying expression in the calculation of address + self.buf = raw_call( + 0x0000000000000000000000000000000000000004, + msg.data, + max_outsize=100, + gas=self._gas() + ) + return self.buf + """ + c = get_contract(code, value=1) + + assert ( + c.bar(15).hex() == "0423a132" + "000000000000000000000000000000000000000000000000000000000000000f" + ) + + uncompilable_code = [ ( """ diff --git a/tests/parser/functions/test_send.py b/tests/functional/builtins/codegen/test_send.py similarity index 100% rename from tests/parser/functions/test_send.py rename to tests/functional/builtins/codegen/test_send.py diff --git a/tests/parser/functions/test_sha256.py b/tests/functional/builtins/codegen/test_sha256.py similarity index 100% rename from tests/parser/functions/test_sha256.py rename to tests/functional/builtins/codegen/test_sha256.py diff --git a/tests/parser/functions/test_slice.py b/tests/functional/builtins/codegen/test_slice.py similarity index 82% rename from tests/parser/functions/test_slice.py rename to tests/functional/builtins/codegen/test_slice.py index 6229b47921..53e092019f 100644 --- a/tests/parser/functions/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -32,11 +32,11 @@ def slice_tower_test(inp1: Bytes[50]) -> Bytes[50]: _bytes_1024 = st.binary(min_size=0, max_size=1024) -@pytest.mark.parametrize("literal_start", (True, False)) -@pytest.mark.parametrize("literal_length", (True, False)) +@pytest.mark.parametrize("use_literal_start", (True, False)) +@pytest.mark.parametrize("use_literal_length", (True, False)) @pytest.mark.parametrize("opt_level", list(OptimizationLevel)) @given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024) -@settings(max_examples=100, deadline=None) +@settings(max_examples=100) @pytest.mark.fuzzing def test_slice_immutable( get_contract, @@ -45,13 +45,13 @@ def test_slice_immutable( opt_level, bytesdata, start, - literal_start, + use_literal_start, length, - literal_length, + use_literal_length, length_bound, ): - _start = start if literal_start else "start" - _length = length if literal_length else "length" + _start = start if use_literal_start else "start" + _length = length if use_literal_length else "length" code = f""" IMMUTABLE_BYTES: immutable(Bytes[{length_bound}]) @@ -71,10 +71,10 @@ def _get_contract(): return get_contract(code, bytesdata, start, length, override_opt_level=opt_level) 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) - or (literal_length and length < 1) + (start + length > length_bound and use_literal_start and use_literal_length) + or (use_literal_length and length > length_bound) + or (use_literal_start and start > length_bound) + or (use_literal_length and length == 0) ): assert_compile_failed(lambda: _get_contract(), ArgumentException) elif start + length > len(bytesdata) or (len(bytesdata) > length_bound): @@ -86,13 +86,13 @@ def _get_contract(): @pytest.mark.parametrize("location", ("storage", "calldata", "memory", "literal", "code")) -@pytest.mark.parametrize("literal_start", (True, False)) -@pytest.mark.parametrize("literal_length", (True, False)) +@pytest.mark.parametrize("use_literal_start", (True, False)) +@pytest.mark.parametrize("use_literal_length", (True, False)) @pytest.mark.parametrize("opt_level", list(OptimizationLevel)) @given(start=_draw_1024, length=_draw_1024, length_bound=_draw_1024_1, bytesdata=_bytes_1024) -@settings(max_examples=100, deadline=None) +@settings(max_examples=100) @pytest.mark.fuzzing -def test_slice_bytes( +def test_slice_bytes_fuzz( get_contract, assert_compile_failed, assert_tx_failed, @@ -100,18 +100,28 @@ def test_slice_bytes( location, bytesdata, start, - literal_start, + use_literal_start, length, - literal_length, + use_literal_length, length_bound, ): + preamble = "" if location == "memory": spliced_code = f"foo: Bytes[{length_bound}] = inp" foo = "foo" elif location == "storage": + preamble = f""" +foo: Bytes[{length_bound}] + """ spliced_code = "self.foo = inp" foo = "self.foo" elif location == "code": + preamble = f""" +IMMUTABLE_BYTES: immutable(Bytes[{length_bound}]) +@external +def __init__(foo: Bytes[{length_bound}]): + IMMUTABLE_BYTES = foo + """ spliced_code = "" foo = "IMMUTABLE_BYTES" elif location == "literal": @@ -123,15 +133,11 @@ def test_slice_bytes( else: raise Exception("unreachable") - _start = start if literal_start else "start" - _length = length if literal_length else "length" + _start = start if use_literal_start else "start" + _length = length if use_literal_length else "length" code = f""" -foo: Bytes[{length_bound}] -IMMUTABLE_BYTES: immutable(Bytes[{length_bound}]) -@external -def __init__(foo: Bytes[{length_bound}]): - IMMUTABLE_BYTES = foo +{preamble} @external def do_slice(inp: Bytes[{length_bound}], start: uint256, length: uint256) -> Bytes[{length_bound}]: @@ -142,24 +148,40 @@ def do_slice(inp: Bytes[{length_bound}], start: uint256, length: uint256) -> Byt def _get_contract(): return get_contract(code, bytesdata, override_opt_level=opt_level) - data_length = len(bytesdata) if location == "literal" else length_bound - if ( - (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) - ): + # length bound is the container size; input_bound is the bound on the input + # (which can be different, if the input is a literal) + input_bound = length_bound + slice_output_too_large = False + + if location == "literal": + input_bound = len(bytesdata) + + # ex.: + # @external + # def do_slice(inp: Bytes[1], start: uint256, length: uint256) -> Bytes[1]: + # return slice(b'\x00\x00', 0, length) + output_length = length if use_literal_length else input_bound + slice_output_too_large = output_length > length_bound + + end = start + length + + compile_time_oob = ( + (use_literal_length and (length > input_bound or length == 0)) + or (use_literal_start and start > input_bound) + or (use_literal_start and use_literal_length and start + length > input_bound) + ) + + if compile_time_oob or slice_output_too_large: assert_compile_failed(lambda: _get_contract(), (ArgumentException, TypeMismatch)) - elif len(bytesdata) > data_length: + elif location == "code" and len(bytesdata) > length_bound: # deploy fail assert_tx_failed(lambda: _get_contract()) - elif start + length > len(bytesdata): + elif end > len(bytesdata) or len(bytesdata) > length_bound: c = _get_contract() assert_tx_failed(lambda: c.do_slice(bytesdata, start, length)) else: c = _get_contract() - assert c.do_slice(bytesdata, start, length) == bytesdata[start : start + length], code + assert c.do_slice(bytesdata, start, length) == bytesdata[start:end], code def test_slice_private(get_contract): diff --git a/tests/parser/functions/test_mkstr.py b/tests/functional/builtins/codegen/test_uint2str.py similarity index 100% rename from tests/parser/functions/test_mkstr.py rename to tests/functional/builtins/codegen/test_uint2str.py diff --git a/tests/parser/functions/test_unary.py b/tests/functional/builtins/codegen/test_unary.py similarity index 100% rename from tests/parser/functions/test_unary.py rename to tests/functional/builtins/codegen/test_unary.py diff --git a/tests/parser/functions/test_unsafe_math.py b/tests/functional/builtins/codegen/test_unsafe_math.py similarity index 100% rename from tests/parser/functions/test_unsafe_math.py rename to tests/functional/builtins/codegen/test_unsafe_math.py diff --git a/tests/builtins/folding/test_abs.py b/tests/functional/builtins/folding/test_abs.py similarity index 94% rename from tests/builtins/folding/test_abs.py rename to tests/functional/builtins/folding/test_abs.py index 58f861ed0c..1c919d7826 100644 --- a/tests/builtins/folding/test_abs.py +++ b/tests/functional/builtins/folding/test_abs.py @@ -8,7 +8,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(a=st.integers(min_value=-(2**255) + 1, max_value=2**255 - 1)) @example(a=0) def test_abs(get_contract, a): @@ -27,7 +27,7 @@ def foo(a: int256) -> int256: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(a=st.integers(min_value=2**255, max_value=2**256 - 1)) def test_abs_upper_bound_folding(get_contract, a): source = f""" diff --git a/tests/builtins/folding/test_addmod_mulmod.py b/tests/functional/builtins/folding/test_addmod_mulmod.py similarity index 95% rename from tests/builtins/folding/test_addmod_mulmod.py rename to tests/functional/builtins/folding/test_addmod_mulmod.py index 0514dea18a..33dcc62984 100644 --- a/tests/builtins/folding/test_addmod_mulmod.py +++ b/tests/functional/builtins/folding/test_addmod_mulmod.py @@ -9,7 +9,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(a=st_uint256, b=st_uint256, c=st_uint256) @pytest.mark.parametrize("fn_name", ["uint256_addmod", "uint256_mulmod"]) def test_modmath(get_contract, a, b, c, fn_name): diff --git a/tests/builtins/folding/test_bitwise.py b/tests/functional/builtins/folding/test_bitwise.py similarity index 95% rename from tests/builtins/folding/test_bitwise.py rename to tests/functional/builtins/folding/test_bitwise.py index d28e482589..63e733644f 100644 --- a/tests/builtins/folding/test_bitwise.py +++ b/tests/functional/builtins/folding/test_bitwise.py @@ -14,7 +14,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("op", ["&", "|", "^"]) @given(a=st_uint256, b=st_uint256) def test_bitwise_ops(get_contract, a, b, op): @@ -34,7 +34,7 @@ def foo(a: uint256, b: uint256) -> uint256: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("op", ["<<", ">>"]) @given(a=st_uint256, b=st.integers(min_value=0, max_value=256)) def test_bitwise_shift_unsigned(get_contract, a, b, op): @@ -64,7 +64,7 @@ def foo(a: uint256, b: uint256) -> uint256: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("op", ["<<", ">>"]) @given(a=st_sint256, b=st.integers(min_value=0, max_value=256)) def test_bitwise_shift_signed(get_contract, a, b, op): @@ -92,7 +92,7 @@ def foo(a: int256, b: uint256) -> int256: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(value=st_uint256) def test_bitwise_not(get_contract, value): source = """ diff --git a/tests/builtins/folding/test_epsilon.py b/tests/functional/builtins/folding/test_epsilon.py similarity index 100% rename from tests/builtins/folding/test_epsilon.py rename to tests/functional/builtins/folding/test_epsilon.py diff --git a/tests/builtins/folding/test_floor_ceil.py b/tests/functional/builtins/folding/test_floor_ceil.py similarity index 95% rename from tests/builtins/folding/test_floor_ceil.py rename to tests/functional/builtins/folding/test_floor_ceil.py index 763f8fec63..87db23889a 100644 --- a/tests/builtins/folding/test_floor_ceil.py +++ b/tests/functional/builtins/folding/test_floor_ceil.py @@ -13,7 +13,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(value=st_decimals) @example(value=Decimal("0.9999999999")) @example(value=Decimal("0.0000000001")) diff --git a/tests/builtins/folding/test_fold_as_wei_value.py b/tests/functional/builtins/folding/test_fold_as_wei_value.py similarity index 94% rename from tests/builtins/folding/test_fold_as_wei_value.py rename to tests/functional/builtins/folding/test_fold_as_wei_value.py index 11d23bd3bf..210ab51f0d 100644 --- a/tests/builtins/folding/test_fold_as_wei_value.py +++ b/tests/functional/builtins/folding/test_fold_as_wei_value.py @@ -19,7 +19,7 @@ @pytest.mark.fuzzing -@settings(max_examples=10, deadline=1000) +@settings(max_examples=10) @given(value=st_decimals) @pytest.mark.parametrize("denom", denoms) def test_decimal(get_contract, value, denom): @@ -38,7 +38,7 @@ def foo(a: decimal) -> uint256: @pytest.mark.fuzzing -@settings(max_examples=10, deadline=1000) +@settings(max_examples=10) @given(value=st.integers(min_value=0, max_value=2**128)) @pytest.mark.parametrize("denom", denoms) def test_integer(get_contract, value, denom): diff --git a/tests/builtins/folding/test_keccak_sha.py b/tests/functional/builtins/folding/test_keccak_sha.py similarity index 93% rename from tests/builtins/folding/test_keccak_sha.py rename to tests/functional/builtins/folding/test_keccak_sha.py index 8e283566de..a2fe460dd1 100644 --- a/tests/builtins/folding/test_keccak_sha.py +++ b/tests/functional/builtins/folding/test_keccak_sha.py @@ -10,7 +10,7 @@ @pytest.mark.fuzzing @given(value=st.text(alphabet=alphabet, min_size=0, max_size=100)) -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("fn_name", ["keccak256", "sha256"]) def test_string(get_contract, value, fn_name): source = f""" @@ -29,7 +29,7 @@ def foo(a: String[100]) -> bytes32: @pytest.mark.fuzzing @given(value=st.binary(min_size=0, max_size=100)) -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("fn_name", ["keccak256", "sha256"]) def test_bytes(get_contract, value, fn_name): source = f""" @@ -48,7 +48,7 @@ def foo(a: Bytes[100]) -> bytes32: @pytest.mark.fuzzing @given(value=st.binary(min_size=1, max_size=100)) -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @pytest.mark.parametrize("fn_name", ["keccak256", "sha256"]) def test_hex(get_contract, value, fn_name): source = f""" diff --git a/tests/builtins/folding/test_len.py b/tests/functional/builtins/folding/test_len.py similarity index 100% rename from tests/builtins/folding/test_len.py rename to tests/functional/builtins/folding/test_len.py diff --git a/tests/builtins/folding/test_min_max.py b/tests/functional/builtins/folding/test_min_max.py similarity index 94% rename from tests/builtins/folding/test_min_max.py rename to tests/functional/builtins/folding/test_min_max.py index e2d33237ca..309f7519c0 100644 --- a/tests/builtins/folding/test_min_max.py +++ b/tests/functional/builtins/folding/test_min_max.py @@ -18,7 +18,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_decimals, right=st_decimals) @pytest.mark.parametrize("fn_name", ["min", "max"]) def test_decimal(get_contract, left, right, fn_name): @@ -37,7 +37,7 @@ def foo(a: decimal, b: decimal) -> decimal: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_int128, right=st_int128) @pytest.mark.parametrize("fn_name", ["min", "max"]) def test_int128(get_contract, left, right, fn_name): @@ -56,7 +56,7 @@ def foo(a: int128, b: int128) -> int128: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_uint256, right=st_uint256) @pytest.mark.parametrize("fn_name", ["min", "max"]) def test_min_uint256(get_contract, left, right, fn_name): diff --git a/tests/builtins/folding/test_powmod.py b/tests/functional/builtins/folding/test_powmod.py similarity index 94% rename from tests/builtins/folding/test_powmod.py rename to tests/functional/builtins/folding/test_powmod.py index fdc0e300ab..8667ec93fd 100644 --- a/tests/builtins/folding/test_powmod.py +++ b/tests/functional/builtins/folding/test_powmod.py @@ -9,7 +9,7 @@ @pytest.mark.fuzzing -@settings(max_examples=100, deadline=1000) +@settings(max_examples=100) @given(a=st_uint256, b=st_uint256) def test_powmod_uint256(get_contract, a, b): source = """ diff --git a/tests/parser/functions/__init__.py b/tests/functional/codegen/__init__.py similarity index 100% rename from tests/parser/functions/__init__.py rename to tests/functional/codegen/__init__.py diff --git a/tests/parser/functions/test_default_function.py b/tests/functional/codegen/calling_convention/test_default_function.py similarity index 99% rename from tests/parser/functions/test_default_function.py rename to tests/functional/codegen/calling_convention/test_default_function.py index 4aa0b04a77..4ad68697ac 100644 --- a/tests/parser/functions/test_default_function.py +++ b/tests/functional/codegen/calling_convention/test_default_function.py @@ -41,7 +41,7 @@ def test_basic_default_default_param_function(w3, get_logs, get_contract_with_ga @external @payable def fooBar(a: int128 = 12345) -> int128: - log Sent(ZERO_ADDRESS) + log Sent(empty(address)) return a @external diff --git a/tests/parser/functions/test_default_parameters.py b/tests/functional/codegen/calling_convention/test_default_parameters.py similarity index 100% rename from tests/parser/functions/test_default_parameters.py rename to tests/functional/codegen/calling_convention/test_default_parameters.py diff --git a/tests/parser/features/external_contracts/test_erc20_abi.py b/tests/functional/codegen/calling_convention/test_erc20_abi.py similarity index 100% rename from tests/parser/features/external_contracts/test_erc20_abi.py rename to tests/functional/codegen/calling_convention/test_erc20_abi.py diff --git a/tests/parser/features/external_contracts/test_external_contract_calls.py b/tests/functional/codegen/calling_convention/test_external_contract_calls.py similarity index 99% rename from tests/parser/features/external_contracts/test_external_contract_calls.py rename to tests/functional/codegen/calling_convention/test_external_contract_calls.py index b3cc6f5576..12fcde2f4f 100644 --- a/tests/parser/features/external_contracts/test_external_contract_calls.py +++ b/tests/functional/codegen/calling_convention/test_external_contract_calls.py @@ -775,9 +775,9 @@ def foo() -> (address, Bytes[3], address): view @external def bar(arg1: address) -> (address, Bytes[3], address): - a: address = ZERO_ADDRESS + a: address = empty(address) b: Bytes[3] = b"" - c: address = ZERO_ADDRESS + c: address = empty(address) a, b, c = Foo(arg1).foo() return a, b, c """ @@ -808,9 +808,9 @@ def foo() -> (address, Bytes[3], address): view @external def bar(arg1: address) -> (address, Bytes[3], address): - a: address = ZERO_ADDRESS + a: address = empty(address) b: Bytes[3] = b"" - c: address = ZERO_ADDRESS + c: address = empty(address) a, b, c = Foo(arg1).foo() return a, b, c """ @@ -841,9 +841,9 @@ def foo() -> (address, Bytes[3], address): view @external def bar(arg1: address) -> (address, Bytes[3], address): - a: address = ZERO_ADDRESS + a: address = empty(address) b: Bytes[3] = b"" - c: address = ZERO_ADDRESS + c: address = empty(address) a, b, c = Foo(arg1).foo() return a, b, c """ @@ -1538,7 +1538,7 @@ def out_literals() -> (int128, address, Bytes[10]) : view @external def test(addr: address) -> (int128, address, Bytes[10]): a: int128 = 0 - b: address = ZERO_ADDRESS + b: address = empty(address) c: Bytes[10] = b"" (a, b, c) = Test(addr).out_literals() return a, b,c diff --git a/tests/parser/features/external_contracts/test_modifiable_external_contract_calls.py b/tests/functional/codegen/calling_convention/test_modifiable_external_contract_calls.py similarity index 100% rename from tests/parser/features/external_contracts/test_modifiable_external_contract_calls.py rename to tests/functional/codegen/calling_convention/test_modifiable_external_contract_calls.py diff --git a/tests/parser/functions/test_return.py b/tests/functional/codegen/calling_convention/test_return.py similarity index 100% rename from tests/parser/functions/test_return.py rename to tests/functional/codegen/calling_convention/test_return.py diff --git a/tests/parser/functions/test_return_struct.py b/tests/functional/codegen/calling_convention/test_return_struct.py similarity index 98% rename from tests/parser/functions/test_return_struct.py rename to tests/functional/codegen/calling_convention/test_return_struct.py index 425caedb75..cdd8342d8a 100644 --- a/tests/parser/functions/test_return_struct.py +++ b/tests/functional/codegen/calling_convention/test_return_struct.py @@ -17,7 +17,7 @@ def test() -> Voter: return a """ - out = compile_code(code, ["abi"]) + out = compile_code(code, output_formats=["abi"]) abi = out["abi"][0] assert abi["name"] == "test" @@ -38,7 +38,7 @@ def test() -> Voter: return a """ - out = compile_code(code, ["abi"]) + out = compile_code(code, output_formats=["abi"]) abi = out["abi"][0] assert abi["name"] == "test" diff --git a/tests/parser/functions/test_return_tuple.py b/tests/functional/codegen/calling_convention/test_return_tuple.py similarity index 98% rename from tests/parser/functions/test_return_tuple.py rename to tests/functional/codegen/calling_convention/test_return_tuple.py index 87b7cdcde3..b375839147 100644 --- a/tests/parser/functions/test_return_tuple.py +++ b/tests/functional/codegen/calling_convention/test_return_tuple.py @@ -99,7 +99,7 @@ def out_literals() -> (int128, address, Bytes[10]): @external def test() -> (int128, address, Bytes[10]): a: int128 = 0 - b: address = ZERO_ADDRESS + b: address = empty(address) c: Bytes[10] = b"" (a, b, c) = self._out_literals() return a, b, c @@ -138,7 +138,7 @@ def test2() -> (int128, address): @external def test3() -> (address, int128): - x: address = ZERO_ADDRESS + x: address = empty(address) self.a, self.c, x, self.d = self._out_literals() return x, self.a """ diff --git a/tests/parser/features/external_contracts/test_self_call_struct.py b/tests/functional/codegen/calling_convention/test_self_call_struct.py similarity index 100% rename from tests/parser/features/external_contracts/test_self_call_struct.py rename to tests/functional/codegen/calling_convention/test_self_call_struct.py diff --git a/tests/functional/codegen/test_struct_return.py b/tests/functional/codegen/calling_convention/test_struct_return.py similarity index 100% rename from tests/functional/codegen/test_struct_return.py rename to tests/functional/codegen/calling_convention/test_struct_return.py diff --git a/tests/functional/codegen/test_tuple_return.py b/tests/functional/codegen/calling_convention/test_tuple_return.py similarity index 100% rename from tests/functional/codegen/test_tuple_return.py rename to tests/functional/codegen/calling_convention/test_tuple_return.py diff --git a/tests/parser/functions/test_block_number.py b/tests/functional/codegen/environment_variables/test_block_number.py similarity index 100% rename from tests/parser/functions/test_block_number.py rename to tests/functional/codegen/environment_variables/test_block_number.py diff --git a/tests/parser/functions/test_block.py b/tests/functional/codegen/environment_variables/test_blockhash.py similarity index 100% rename from tests/parser/functions/test_block.py rename to tests/functional/codegen/environment_variables/test_blockhash.py diff --git a/tests/parser/functions/test_tx.py b/tests/functional/codegen/environment_variables/test_tx.py similarity index 100% rename from tests/parser/functions/test_tx.py rename to tests/functional/codegen/environment_variables/test_tx.py diff --git a/tests/parser/features/decorators/test_nonreentrant.py b/tests/functional/codegen/features/decorators/test_nonreentrant.py similarity index 100% rename from tests/parser/features/decorators/test_nonreentrant.py rename to tests/functional/codegen/features/decorators/test_nonreentrant.py diff --git a/tests/parser/features/decorators/test_payable.py b/tests/functional/codegen/features/decorators/test_payable.py similarity index 100% rename from tests/parser/features/decorators/test_payable.py rename to tests/functional/codegen/features/decorators/test_payable.py diff --git a/tests/parser/features/decorators/test_private.py b/tests/functional/codegen/features/decorators/test_private.py similarity index 99% rename from tests/parser/features/decorators/test_private.py rename to tests/functional/codegen/features/decorators/test_private.py index 7c92f72af9..51e6d90ee1 100644 --- a/tests/parser/features/decorators/test_private.py +++ b/tests/functional/codegen/features/decorators/test_private.py @@ -304,7 +304,7 @@ def test(a: bytes32) -> (bytes32, uint256, int128): b: uint256 = 1 c: int128 = 1 d: int128 = 123 - f: bytes32 = EMPTY_BYTES32 + f: bytes32 = empty(bytes32) f, b, c = self._test(a) assert d == 123 return f, b, c diff --git a/tests/parser/features/decorators/test_public.py b/tests/functional/codegen/features/decorators/test_public.py similarity index 100% rename from tests/parser/features/decorators/test_public.py rename to tests/functional/codegen/features/decorators/test_public.py diff --git a/tests/parser/features/decorators/test_pure.py b/tests/functional/codegen/features/decorators/test_pure.py similarity index 100% rename from tests/parser/features/decorators/test_pure.py rename to tests/functional/codegen/features/decorators/test_pure.py diff --git a/tests/parser/features/decorators/test_view.py b/tests/functional/codegen/features/decorators/test_view.py similarity index 100% rename from tests/parser/features/decorators/test_view.py rename to tests/functional/codegen/features/decorators/test_view.py diff --git a/tests/parser/features/iteration/test_break.py b/tests/functional/codegen/features/iteration/test_break.py similarity index 100% rename from tests/parser/features/iteration/test_break.py rename to tests/functional/codegen/features/iteration/test_break.py diff --git a/tests/parser/features/iteration/test_continue.py b/tests/functional/codegen/features/iteration/test_continue.py similarity index 100% rename from tests/parser/features/iteration/test_continue.py rename to tests/functional/codegen/features/iteration/test_continue.py diff --git a/tests/parser/features/iteration/test_for_in_list.py b/tests/functional/codegen/features/iteration/test_for_in_list.py similarity index 99% rename from tests/parser/features/iteration/test_for_in_list.py rename to tests/functional/codegen/features/iteration/test_for_in_list.py index bfd960a787..fb01cc98eb 100644 --- a/tests/parser/features/iteration/test_for_in_list.py +++ b/tests/functional/codegen/features/iteration/test_for_in_list.py @@ -230,7 +230,7 @@ def iterate_return_second() -> address: count += 1 if count == 2: return i - return ZERO_ADDRESS + return empty(address) """ c = get_contract_with_gas_estimation(code) diff --git a/tests/parser/features/iteration/test_for_range.py b/tests/functional/codegen/features/iteration/test_for_range.py similarity index 98% rename from tests/parser/features/iteration/test_for_range.py rename to tests/functional/codegen/features/iteration/test_for_range.py index 395dd28231..ed6235d992 100644 --- a/tests/parser/features/iteration/test_for_range.py +++ b/tests/functional/codegen/features/iteration/test_for_range.py @@ -20,12 +20,12 @@ def test_range_bound(get_contract, assert_tx_failed): def repeat(n: uint256) -> uint256: x: uint256 = 0 for i in range(n, bound=6): - x += i + x += i + 1 return x """ c = get_contract(code) for n in range(7): - assert c.repeat(n) == sum(range(n)) + assert c.repeat(n) == sum(i + 1 for i in range(n)) # check codegen inserts assertion for n greater than bound assert_tx_failed(lambda: c.repeat(7)) diff --git a/tests/parser/features/iteration/test_range_in.py b/tests/functional/codegen/features/iteration/test_range_in.py similarity index 100% rename from tests/parser/features/iteration/test_range_in.py rename to tests/functional/codegen/features/iteration/test_range_in.py diff --git a/tests/parser/features/test_address_balance.py b/tests/functional/codegen/features/test_address_balance.py similarity index 100% rename from tests/parser/features/test_address_balance.py rename to tests/functional/codegen/features/test_address_balance.py diff --git a/tests/parser/features/test_assert.py b/tests/functional/codegen/features/test_assert.py similarity index 100% rename from tests/parser/features/test_assert.py rename to tests/functional/codegen/features/test_assert.py diff --git a/tests/parser/features/test_assert_unreachable.py b/tests/functional/codegen/features/test_assert_unreachable.py similarity index 100% rename from tests/parser/features/test_assert_unreachable.py rename to tests/functional/codegen/features/test_assert_unreachable.py diff --git a/tests/parser/features/test_assignment.py b/tests/functional/codegen/features/test_assignment.py similarity index 88% rename from tests/parser/features/test_assignment.py rename to tests/functional/codegen/features/test_assignment.py index e550f60541..cd26659a5c 100644 --- a/tests/parser/features/test_assignment.py +++ b/tests/functional/codegen/features/test_assignment.py @@ -331,7 +331,7 @@ def foo(): @external def foo(): y: int128 = 1 - z: bytes32 = EMPTY_BYTES32 + z: bytes32 = empty(bytes32) z = y """, """ @@ -344,7 +344,7 @@ def foo(): @external def foo(): y: uint256 = 1 - z: bytes32 = EMPTY_BYTES32 + z: bytes32 = empty(bytes32) z = y """, ], @@ -442,3 +442,63 @@ def bug(p: Point) -> Point: """ c = get_contract(code) assert c.bug((1, 2)) == (2, 1) + + +mload_merge_codes = [ + ( + """ +@external +def foo() -> uint256[4]: + # copy "backwards" + xs: uint256[4] = [1, 2, 3, 4] + +# dst < src + xs[0] = xs[1] + xs[1] = xs[2] + xs[2] = xs[3] + + return xs + """, + [2, 3, 4, 4], + ), + ( + """ +@external +def foo() -> uint256[4]: + # copy "forwards" + xs: uint256[4] = [1, 2, 3, 4] + +# src < dst + xs[1] = xs[0] + xs[2] = xs[1] + xs[3] = xs[2] + + return xs + """, + [1, 1, 1, 1], + ), + ( + """ +@external +def foo() -> uint256[5]: + # partial "forward" copy + xs: uint256[5] = [1, 2, 3, 4, 5] + +# src < dst + xs[2] = xs[0] + xs[3] = xs[1] + xs[4] = xs[2] + + return xs + """, + [1, 2, 1, 2, 1], + ), +] + + +# functional test that mload merging does not occur when source and dest +# buffers overlap. (note: mload merging only applies after cancun) +@pytest.mark.parametrize("code,expected_result", mload_merge_codes) +def test_mcopy_overlap(get_contract, code, expected_result): + c = get_contract(code) + assert c.foo() == expected_result diff --git a/tests/parser/features/test_bytes_map_keys.py b/tests/functional/codegen/features/test_bytes_map_keys.py similarity index 100% rename from tests/parser/features/test_bytes_map_keys.py rename to tests/functional/codegen/features/test_bytes_map_keys.py diff --git a/tests/parser/features/test_clampers.py b/tests/functional/codegen/features/test_clampers.py similarity index 100% rename from tests/parser/features/test_clampers.py rename to tests/functional/codegen/features/test_clampers.py diff --git a/tests/parser/features/test_comments.py b/tests/functional/codegen/features/test_comments.py similarity index 100% rename from tests/parser/features/test_comments.py rename to tests/functional/codegen/features/test_comments.py diff --git a/tests/parser/features/test_comparison.py b/tests/functional/codegen/features/test_comparison.py similarity index 100% rename from tests/parser/features/test_comparison.py rename to tests/functional/codegen/features/test_comparison.py diff --git a/tests/parser/features/test_conditionals.py b/tests/functional/codegen/features/test_conditionals.py similarity index 100% rename from tests/parser/features/test_conditionals.py rename to tests/functional/codegen/features/test_conditionals.py diff --git a/tests/parser/features/test_constructor.py b/tests/functional/codegen/features/test_constructor.py similarity index 100% rename from tests/parser/features/test_constructor.py rename to tests/functional/codegen/features/test_constructor.py diff --git a/tests/parser/features/test_gas.py b/tests/functional/codegen/features/test_gas.py similarity index 100% rename from tests/parser/features/test_gas.py rename to tests/functional/codegen/features/test_gas.py diff --git a/tests/parser/features/test_immutable.py b/tests/functional/codegen/features/test_immutable.py similarity index 100% rename from tests/parser/features/test_immutable.py rename to tests/functional/codegen/features/test_immutable.py diff --git a/tests/parser/features/test_init.py b/tests/functional/codegen/features/test_init.py similarity index 95% rename from tests/parser/features/test_init.py rename to tests/functional/codegen/features/test_init.py index 83bcbc95ea..29a466e869 100644 --- a/tests/parser/features/test_init.py +++ b/tests/functional/codegen/features/test_init.py @@ -15,7 +15,7 @@ def __init__(a: uint256): assert c.val() == 123 # Make sure the init code does not access calldata - assembly = vyper.compile_code(code, ["asm"])["asm"].split(" ") + assembly = vyper.compile_code(code, output_formats=["asm"])["asm"].split(" ") ir_return_idx_start = assembly.index("{") ir_return_idx_end = assembly.index("}") diff --git a/tests/parser/features/test_internal_call.py b/tests/functional/codegen/features/test_internal_call.py similarity index 99% rename from tests/parser/features/test_internal_call.py rename to tests/functional/codegen/features/test_internal_call.py index d7a41acbc0..f10d22ec99 100644 --- a/tests/parser/features/test_internal_call.py +++ b/tests/functional/codegen/features/test_internal_call.py @@ -669,7 +669,7 @@ def test_internal_call_kwargs(get_contract, typ1, strategy1, typ2, strategy2): # GHSA-ph9x-4vc9-m39g @given(kwarg1=strategy1, default1=strategy1, kwarg2=strategy2, default2=strategy2) - @settings(deadline=None, max_examples=5) # len(cases) * len(cases) * 5 * 5 + @settings(max_examples=5) # len(cases) * len(cases) * 5 * 5 def fuzz(kwarg1, kwarg2, default1, default2): code = f""" @internal diff --git a/tests/parser/features/test_logging.py b/tests/functional/codegen/features/test_logging.py similarity index 100% rename from tests/parser/features/test_logging.py rename to tests/functional/codegen/features/test_logging.py diff --git a/tests/parser/features/test_logging_bytes_extended.py b/tests/functional/codegen/features/test_logging_bytes_extended.py similarity index 100% rename from tests/parser/features/test_logging_bytes_extended.py rename to tests/functional/codegen/features/test_logging_bytes_extended.py diff --git a/tests/parser/features/test_logging_from_call.py b/tests/functional/codegen/features/test_logging_from_call.py similarity index 100% rename from tests/parser/features/test_logging_from_call.py rename to tests/functional/codegen/features/test_logging_from_call.py diff --git a/tests/functional/codegen/features/test_memory_alloc.py b/tests/functional/codegen/features/test_memory_alloc.py new file mode 100644 index 0000000000..ee6d15c67c --- /dev/null +++ b/tests/functional/codegen/features/test_memory_alloc.py @@ -0,0 +1,16 @@ +import pytest + +from vyper.compiler import compile_code +from vyper.exceptions import MemoryAllocationException + + +def test_memory_overflow(): + code = """ +@external +def zzz(x: DynArray[uint256, 2**59]): # 2**64 / 32 bytes per word == 2**59 + y: uint256[7] = [0,0,0,0,0,0,0] + + y[6] = y[5] + """ + with pytest.raises(MemoryAllocationException): + compile_code(code) diff --git a/tests/parser/features/test_memory_dealloc.py b/tests/functional/codegen/features/test_memory_dealloc.py similarity index 93% rename from tests/parser/features/test_memory_dealloc.py rename to tests/functional/codegen/features/test_memory_dealloc.py index de82f03296..814bf0d3bb 100644 --- a/tests/parser/features/test_memory_dealloc.py +++ b/tests/functional/codegen/features/test_memory_dealloc.py @@ -9,7 +9,7 @@ def sendit(): nonpayable @external def foo(target: address) -> uint256[2]: - log Shimmy(ZERO_ADDRESS, 3) + log Shimmy(empty(address), 3) amount: uint256 = 1 flargen: uint256 = 42 Other(target).sendit() diff --git a/tests/parser/features/test_packing.py b/tests/functional/codegen/features/test_packing.py similarity index 100% rename from tests/parser/features/test_packing.py rename to tests/functional/codegen/features/test_packing.py diff --git a/tests/parser/features/test_reverting.py b/tests/functional/codegen/features/test_reverting.py similarity index 100% rename from tests/parser/features/test_reverting.py rename to tests/functional/codegen/features/test_reverting.py diff --git a/tests/parser/features/test_short_circuiting.py b/tests/functional/codegen/features/test_short_circuiting.py similarity index 100% rename from tests/parser/features/test_short_circuiting.py rename to tests/functional/codegen/features/test_short_circuiting.py diff --git a/tests/parser/features/test_string_map_keys.py b/tests/functional/codegen/features/test_string_map_keys.py similarity index 100% rename from tests/parser/features/test_string_map_keys.py rename to tests/functional/codegen/features/test_string_map_keys.py diff --git a/tests/parser/features/test_ternary.py b/tests/functional/codegen/features/test_ternary.py similarity index 100% rename from tests/parser/features/test_ternary.py rename to tests/functional/codegen/features/test_ternary.py diff --git a/tests/parser/features/test_transient.py b/tests/functional/codegen/features/test_transient.py similarity index 100% rename from tests/parser/features/test_transient.py rename to tests/functional/codegen/features/test_transient.py diff --git a/tests/parser/integration/test_basics.py b/tests/functional/codegen/integration/test_basics.py similarity index 100% rename from tests/parser/integration/test_basics.py rename to tests/functional/codegen/integration/test_basics.py diff --git a/tests/parser/integration/test_crowdfund.py b/tests/functional/codegen/integration/test_crowdfund.py similarity index 98% rename from tests/parser/integration/test_crowdfund.py rename to tests/functional/codegen/integration/test_crowdfund.py index c45a60d9c7..47c63dc015 100644 --- a/tests/parser/integration/test_crowdfund.py +++ b/tests/functional/codegen/integration/test_crowdfund.py @@ -1,3 +1,4 @@ +# TODO: check, this is probably redundant with examples/test_crowdfund.py def test_crowdfund(w3, tester, get_contract_with_gas_estimation_for_constants): crowdfund = """ diff --git a/tests/parser/integration/test_escrow.py b/tests/functional/codegen/integration/test_escrow.py similarity index 96% rename from tests/parser/integration/test_escrow.py rename to tests/functional/codegen/integration/test_escrow.py index 2982ff9eae..1578f5a418 100644 --- a/tests/parser/integration/test_escrow.py +++ b/tests/functional/codegen/integration/test_escrow.py @@ -9,7 +9,7 @@ def test_arbitration_code(w3, get_contract_with_gas_estimation, assert_tx_failed @external def setup(_seller: address, _arbitrator: address): - if self.buyer == ZERO_ADDRESS: + if self.buyer == empty(address): self.buyer = msg.sender self.seller = _seller self.arbitrator = _arbitrator @@ -43,7 +43,7 @@ def test_arbitration_code_with_init(w3, assert_tx_failed, get_contract_with_gas_ @external @payable def __init__(_seller: address, _arbitrator: address): - if self.buyer == ZERO_ADDRESS: + if self.buyer == empty(address): self.buyer = msg.sender self.seller = _seller self.arbitrator = _arbitrator diff --git a/tests/parser/globals/test_getters.py b/tests/functional/codegen/storage_variables/test_getters.py similarity index 100% rename from tests/parser/globals/test_getters.py rename to tests/functional/codegen/storage_variables/test_getters.py diff --git a/tests/parser/globals/test_setters.py b/tests/functional/codegen/storage_variables/test_setters.py similarity index 100% rename from tests/parser/globals/test_setters.py rename to tests/functional/codegen/storage_variables/test_setters.py diff --git a/tests/parser/globals/test_globals.py b/tests/functional/codegen/storage_variables/test_storage_variable.py similarity index 100% rename from tests/parser/globals/test_globals.py rename to tests/functional/codegen/storage_variables/test_storage_variable.py diff --git a/tests/parser/test_call_graph_stability.py b/tests/functional/codegen/test_call_graph_stability.py similarity index 98% rename from tests/parser/test_call_graph_stability.py rename to tests/functional/codegen/test_call_graph_stability.py index a6193610e2..4c85c330f3 100644 --- a/tests/parser/test_call_graph_stability.py +++ b/tests/functional/codegen/test_call_graph_stability.py @@ -15,7 +15,7 @@ def _valid_identifier(attr): # random names for functions -@settings(max_examples=20, deadline=None) +@settings(max_examples=20) @given( st.lists( st.tuples( diff --git a/tests/parser/test_selector_table.py b/tests/functional/codegen/test_selector_table.py similarity index 84% rename from tests/parser/test_selector_table.py rename to tests/functional/codegen/test_selector_table.py index 3ac50707c2..161cd480fd 100644 --- a/tests/parser/test_selector_table.py +++ b/tests/functional/codegen/test_selector_table.py @@ -446,7 +446,7 @@ def aILR4U1Z()->uint256: seed=st.integers(min_value=0, max_value=2**64 - 1), ) @pytest.mark.fuzzing -@settings(max_examples=10, deadline=None) +@settings(max_examples=10) def test_sparse_jumptable_probe_depth(n_methods, seed): sigs = [f"foo{i + seed}()" for i in range(n_methods)] _, buckets = generate_sparse_jumptable_buckets(sigs) @@ -466,7 +466,7 @@ def test_sparse_jumptable_probe_depth(n_methods, seed): seed=st.integers(min_value=0, max_value=2**64 - 1), ) @pytest.mark.fuzzing -@settings(max_examples=10, deadline=None) +@settings(max_examples=10) def test_dense_jumptable_bucket_size(n_methods, seed): sigs = [f"foo{i + seed}()" for i in range(n_methods)] n = len(sigs) @@ -478,66 +478,72 @@ def test_dense_jumptable_bucket_size(n_methods, seed): assert n_buckets / n < 0.4 or n < 10 +@st.composite +def generate_methods(draw, max_calldata_bytes): + max_default_args = draw(st.integers(min_value=0, max_value=4)) + default_fn_mutability = draw(st.sampled_from(["", "@pure", "@view", "@nonpayable", "@payable"])) + + return ( + max_default_args, + default_fn_mutability, + draw( + st.lists( + st.tuples( + # function id: + st.integers(min_value=0), + # mutability: + st.sampled_from(["@pure", "@view", "@nonpayable", "@payable"]), + # n calldata words: + st.integers(min_value=0, max_value=max_calldata_bytes // 32), + # n bytes to strip from calldata + st.integers(min_value=1, max_value=4), + # n default args + st.integers(min_value=0, max_value=max_default_args), + ), + unique_by=lambda x: x[0], + min_size=1, + max_size=100, + ) + ), + ) + + @pytest.mark.parametrize("opt_level", list(OptimizationLevel)) # dense selector table packing boundaries at 256 and 65336 @pytest.mark.parametrize("max_calldata_bytes", [255, 256, 65336]) -@settings(max_examples=5, deadline=None) -@given( - seed=st.integers(min_value=0, max_value=2**64 - 1), - max_default_args=st.integers(min_value=0, max_value=4), - default_fn_mutability=st.sampled_from(["", "@pure", "@view", "@nonpayable", "@payable"]), -) @pytest.mark.fuzzing def test_selector_table_fuzz( - max_calldata_bytes, - seed, - max_default_args, - opt_level, - default_fn_mutability, - w3, - get_contract, - assert_tx_failed, - get_logs, + max_calldata_bytes, opt_level, w3, get_contract, assert_tx_failed, get_logs ): - def abi_sig(calldata_words, i, n_default_args): - args = [] if not calldata_words else [f"uint256[{calldata_words}]"] - args.extend(["uint256"] * n_default_args) - argstr = ",".join(args) - return f"foo{seed + i}({argstr})" + def abi_sig(func_id, calldata_words, n_default_args): + params = [] if not calldata_words else [f"uint256[{calldata_words}]"] + params.extend(["uint256"] * n_default_args) + paramstr = ",".join(params) + return f"foo{func_id}({paramstr})" - def generate_func_def(mutability, calldata_words, i, n_default_args): + def generate_func_def(func_id, mutability, calldata_words, n_default_args): arglist = [] if not calldata_words else [f"x: uint256[{calldata_words}]"] for j in range(n_default_args): arglist.append(f"x{j}: uint256 = 0") args = ", ".join(arglist) - _log_return = f"log _Return({i})" if mutability == "@payable" else "" + _log_return = f"log _Return({func_id})" if mutability == "@payable" else "" return f""" @external {mutability} -def foo{seed + i}({args}) -> uint256: +def foo{func_id}({args}) -> uint256: {_log_return} - return {i} + return {func_id} """ - @given( - methods=st.lists( - st.tuples( - st.sampled_from(["@pure", "@view", "@nonpayable", "@payable"]), - st.integers(min_value=0, max_value=max_calldata_bytes // 32), - # n bytes to strip from calldata - st.integers(min_value=1, max_value=4), - # n default args - st.integers(min_value=0, max_value=max_default_args), - ), - min_size=1, - max_size=100, - ) - ) - @settings(max_examples=25) - def _test(methods): + @given(_input=generate_methods(max_calldata_bytes)) + @settings(max_examples=125) + def _test(_input): + max_default_args, default_fn_mutability, methods = _input + func_defs = "\n".join( - generate_func_def(m, s, i, d) for i, (m, s, _, d) in enumerate(methods) + generate_func_def(func_id, mutability, calldata_words, n_default_args) + for (func_id, mutability, calldata_words, _, n_default_args) in (methods) ) if default_fn_mutability == "": @@ -571,8 +577,8 @@ def __default__(): c = get_contract(code, override_opt_level=opt_level) - for i, (mutability, n_calldata_words, n_strip_bytes, n_default_args) in enumerate(methods): - funcname = f"foo{seed + i}" + for func_id, mutability, n_calldata_words, n_strip_bytes, n_default_args in methods: + funcname = f"foo{func_id}" func = getattr(c, funcname) for j in range(n_default_args + 1): @@ -580,9 +586,9 @@ def __default__(): args.extend([1] * j) # check the function returns as expected - assert func(*args) == i + assert func(*args) == func_id - method_id = utils.method_id(abi_sig(n_calldata_words, i, j)) + method_id = utils.method_id(abi_sig(func_id, n_calldata_words, j)) argsdata = b"\x00" * (n_calldata_words * 32 + j * 32) @@ -590,7 +596,7 @@ def __default__(): if mutability == "@payable": tx = func(*args, transact={"value": 1}) (event,) = get_logs(tx, c, "_Return") - assert event.args.val == i + assert event.args.val == func_id else: hexstr = (method_id + argsdata).hex() txdata = {"to": c.address, "data": hexstr, "value": 1} diff --git a/tests/parser/test_selector_table_stability.py b/tests/functional/codegen/test_selector_table_stability.py similarity index 96% rename from tests/parser/test_selector_table_stability.py rename to tests/functional/codegen/test_selector_table_stability.py index abc2c17b8f..3302ff5009 100644 --- a/tests/parser/test_selector_table_stability.py +++ b/tests/functional/codegen/test_selector_table_stability.py @@ -8,7 +8,9 @@ def test_dense_jumptable_stability(): code = "\n".join(f"@external\ndef {name}():\n pass" for name in function_names) - output = compile_code(code, ["asm"], settings=Settings(optimize=OptimizationLevel.CODESIZE)) + output = compile_code( + code, output_formats=["asm"], settings=Settings(optimize=OptimizationLevel.CODESIZE) + ) # test that the selector table data is stable across different runs # (tox should provide different PYTHONHASHSEEDs). diff --git a/tests/parser/types/numbers/test_constants.py b/tests/functional/codegen/types/numbers/test_constants.py similarity index 96% rename from tests/parser/types/numbers/test_constants.py rename to tests/functional/codegen/types/numbers/test_constants.py index 0d5e386dad..25617651ec 100644 --- a/tests/parser/types/numbers/test_constants.py +++ b/tests/functional/codegen/types/numbers/test_constants.py @@ -12,12 +12,12 @@ def test_builtin_constants(get_contract_with_gas_estimation): code = """ @external def test_zaddress(a: address) -> bool: - return a == ZERO_ADDRESS + return a == empty(address) @external def test_empty_bytes32(a: bytes32) -> bool: - return a == EMPTY_BYTES32 + return a == empty(bytes32) @external @@ -81,12 +81,12 @@ def goo() -> int128: @external def hoo() -> bytes32: - bar: bytes32 = EMPTY_BYTES32 + bar: bytes32 = empty(bytes32) return bar @external def joo() -> address: - bar: address = ZERO_ADDRESS + bar: address = empty(address) return bar @external @@ -206,7 +206,7 @@ def test() -> uint256: return ret """ - ir = compile_code(code, ["ir"])["ir"] + ir = compile_code(code, output_formats=["ir"])["ir"] assert search_for_sublist( ir, ["mstore", [MemoryPositions.RESERVED_MEMORY], [2**12 * some_prime]] ) diff --git a/tests/parser/types/numbers/test_decimals.py b/tests/functional/codegen/types/numbers/test_decimals.py similarity index 100% rename from tests/parser/types/numbers/test_decimals.py rename to tests/functional/codegen/types/numbers/test_decimals.py diff --git a/tests/parser/features/arithmetic/test_division.py b/tests/functional/codegen/types/numbers/test_division.py similarity index 100% rename from tests/parser/features/arithmetic/test_division.py rename to tests/functional/codegen/types/numbers/test_division.py diff --git a/tests/fuzzing/test_exponents.py b/tests/functional/codegen/types/numbers/test_exponents.py similarity index 97% rename from tests/fuzzing/test_exponents.py rename to tests/functional/codegen/types/numbers/test_exponents.py index 29c1f198ed..5726e4c1ca 100644 --- a/tests/fuzzing/test_exponents.py +++ b/tests/functional/codegen/types/numbers/test_exponents.py @@ -92,7 +92,7 @@ def foo(a: int16) -> int16: @example(a=2**127 - 1) # 256 bits @example(a=2**256 - 1) -@settings(max_examples=200, deadline=1000) +@settings(max_examples=200) def test_max_exp(get_contract, assert_tx_failed, a): code = f""" @external @@ -127,7 +127,7 @@ def foo(b: uint256) -> uint256: @example(a=2**63 - 1) # 128 bits @example(a=2**127 - 1) -@settings(max_examples=200, deadline=1000) +@settings(max_examples=200) def test_max_exp_int128(get_contract, assert_tx_failed, a): code = f""" @external diff --git a/tests/parser/types/numbers/test_isqrt.py b/tests/functional/codegen/types/numbers/test_isqrt.py similarity index 98% rename from tests/parser/types/numbers/test_isqrt.py rename to tests/functional/codegen/types/numbers/test_isqrt.py index ce26d24d06..b734323a6e 100644 --- a/tests/parser/types/numbers/test_isqrt.py +++ b/tests/functional/codegen/types/numbers/test_isqrt.py @@ -119,7 +119,6 @@ def test(a: uint256) -> (uint256, uint256, uint256, uint256, uint256, String[100 @hypothesis.example(2704) @hypothesis.example(110889) @hypothesis.example(32239684) -@hypothesis.settings(deadline=1000) def test_isqrt_valid_range(isqrt_contract, value): vyper_isqrt = isqrt_contract.test(value) actual_isqrt = math.isqrt(value) diff --git a/tests/parser/features/arithmetic/test_modulo.py b/tests/functional/codegen/types/numbers/test_modulo.py similarity index 100% rename from tests/parser/features/arithmetic/test_modulo.py rename to tests/functional/codegen/types/numbers/test_modulo.py diff --git a/tests/parser/types/numbers/test_signed_ints.py b/tests/functional/codegen/types/numbers/test_signed_ints.py similarity index 100% rename from tests/parser/types/numbers/test_signed_ints.py rename to tests/functional/codegen/types/numbers/test_signed_ints.py diff --git a/tests/parser/types/numbers/test_sqrt.py b/tests/functional/codegen/types/numbers/test_sqrt.py similarity index 98% rename from tests/parser/types/numbers/test_sqrt.py rename to tests/functional/codegen/types/numbers/test_sqrt.py index df1ed0539c..020a79e7ef 100644 --- a/tests/parser/types/numbers/test_sqrt.py +++ b/tests/functional/codegen/types/numbers/test_sqrt.py @@ -145,7 +145,6 @@ def test_sqrt_bounds(sqrt_contract, value): ) @hypothesis.example(value=Decimal(SizeLimits.MAX_INT128)) @hypothesis.example(value=Decimal(0)) -@hypothesis.settings(deadline=1000) def test_sqrt_valid_range(sqrt_contract, value): vyper_sqrt = sqrt_contract.test(value) actual_sqrt = decimal_sqrt(value) @@ -158,7 +157,6 @@ def test_sqrt_valid_range(sqrt_contract, value): min_value=Decimal(SizeLimits.MIN_INT128), max_value=Decimal("-1E10"), places=DECIMAL_PLACES ) ) -@hypothesis.settings(deadline=400) @hypothesis.example(value=Decimal(SizeLimits.MIN_INT128)) @hypothesis.example(value=Decimal("-1E10")) def test_sqrt_invalid_range(sqrt_contract, value): diff --git a/tests/parser/types/numbers/test_unsigned_ints.py b/tests/functional/codegen/types/numbers/test_unsigned_ints.py similarity index 100% rename from tests/parser/types/numbers/test_unsigned_ints.py rename to tests/functional/codegen/types/numbers/test_unsigned_ints.py diff --git a/tests/parser/types/test_bytes.py b/tests/functional/codegen/types/test_bytes.py similarity index 100% rename from tests/parser/types/test_bytes.py rename to tests/functional/codegen/types/test_bytes.py diff --git a/tests/parser/types/test_bytes_literal.py b/tests/functional/codegen/types/test_bytes_literal.py similarity index 100% rename from tests/parser/types/test_bytes_literal.py rename to tests/functional/codegen/types/test_bytes_literal.py diff --git a/tests/parser/types/test_bytes_zero_padding.py b/tests/functional/codegen/types/test_bytes_zero_padding.py similarity index 96% rename from tests/parser/types/test_bytes_zero_padding.py rename to tests/functional/codegen/types/test_bytes_zero_padding.py index ee938fdffb..f9fcf37b25 100644 --- a/tests/parser/types/test_bytes_zero_padding.py +++ b/tests/functional/codegen/types/test_bytes_zero_padding.py @@ -26,7 +26,6 @@ def get_count(counter: uint256) -> Bytes[24]: @pytest.mark.fuzzing @hypothesis.given(value=hypothesis.strategies.integers(min_value=0, max_value=2**64)) -@hypothesis.settings(deadline=400) def test_zero_pad_range(little_endian_contract, value): actual_bytes = value.to_bytes(8, byteorder="little") contract_bytes = little_endian_contract.get_count(value) diff --git a/tests/parser/types/test_dynamic_array.py b/tests/functional/codegen/types/test_dynamic_array.py similarity index 100% rename from tests/parser/types/test_dynamic_array.py rename to tests/functional/codegen/types/test_dynamic_array.py diff --git a/tests/parser/types/test_enum.py b/tests/functional/codegen/types/test_enum.py similarity index 100% rename from tests/parser/types/test_enum.py rename to tests/functional/codegen/types/test_enum.py diff --git a/tests/parser/types/test_identifier_naming.py b/tests/functional/codegen/types/test_identifier_naming.py old mode 100755 new mode 100644 similarity index 91% rename from tests/parser/types/test_identifier_naming.py rename to tests/functional/codegen/types/test_identifier_naming.py index 5cfc7e8ed7..0a93329848 --- a/tests/parser/types/test_identifier_naming.py +++ b/tests/functional/codegen/types/test_identifier_naming.py @@ -1,16 +1,12 @@ import pytest -from vyper.ast.folding import BUILTIN_CONSTANTS from vyper.ast.identifiers import RESERVED_KEYWORDS from vyper.builtins.functions import BUILTIN_FUNCTIONS from vyper.codegen.expr import ENVIRONMENT_VARIABLES from vyper.exceptions import NamespaceCollision, StructureException, SyntaxException from vyper.semantics.types.primitives import AddressT -BUILTIN_CONSTANTS = set(BUILTIN_CONSTANTS.keys()) -ALL_RESERVED_KEYWORDS = ( - BUILTIN_CONSTANTS | BUILTIN_FUNCTIONS | RESERVED_KEYWORDS | ENVIRONMENT_VARIABLES -) +ALL_RESERVED_KEYWORDS = BUILTIN_FUNCTIONS | RESERVED_KEYWORDS | ENVIRONMENT_VARIABLES @pytest.mark.parametrize("constant", sorted(ALL_RESERVED_KEYWORDS)) @@ -46,7 +42,7 @@ def test({constant}: int128): SELF_NAMESPACE_MEMBERS = set(AddressT._type_members.keys()) -DISALLOWED_FN_NAMES = SELF_NAMESPACE_MEMBERS | RESERVED_KEYWORDS | BUILTIN_CONSTANTS +DISALLOWED_FN_NAMES = SELF_NAMESPACE_MEMBERS | RESERVED_KEYWORDS ALLOWED_FN_NAMES = ALL_RESERVED_KEYWORDS - DISALLOWED_FN_NAMES diff --git a/tests/parser/types/test_lists.py b/tests/functional/codegen/types/test_lists.py similarity index 100% rename from tests/parser/types/test_lists.py rename to tests/functional/codegen/types/test_lists.py diff --git a/tests/parser/types/test_node_types.py b/tests/functional/codegen/types/test_node_types.py similarity index 100% rename from tests/parser/types/test_node_types.py rename to tests/functional/codegen/types/test_node_types.py diff --git a/tests/parser/types/test_string.py b/tests/functional/codegen/types/test_string.py similarity index 99% rename from tests/parser/types/test_string.py rename to tests/functional/codegen/types/test_string.py index a5eef66dae..7f1fa71329 100644 --- a/tests/parser/types/test_string.py +++ b/tests/functional/codegen/types/test_string.py @@ -139,7 +139,7 @@ def out_literals() -> (int128, address, String[10]) : view @external def test(addr: address) -> (int128, address, String[10]): a: int128 = 0 - b: address = ZERO_ADDRESS + b: address = empty(address) c: String[10] = "" (a, b, c) = Test(addr).out_literals() return a, b,c diff --git a/tests/parser/types/test_string_literal.py b/tests/functional/codegen/types/test_string_literal.py similarity index 100% rename from tests/parser/types/test_string_literal.py rename to tests/functional/codegen/types/test_string_literal.py diff --git a/tests/examples/auctions/test_blind_auction.py b/tests/functional/examples/auctions/test_blind_auction.py similarity index 100% rename from tests/examples/auctions/test_blind_auction.py rename to tests/functional/examples/auctions/test_blind_auction.py diff --git a/tests/examples/auctions/test_simple_open_auction.py b/tests/functional/examples/auctions/test_simple_open_auction.py similarity index 100% rename from tests/examples/auctions/test_simple_open_auction.py rename to tests/functional/examples/auctions/test_simple_open_auction.py diff --git a/tests/examples/company/test_company.py b/tests/functional/examples/company/test_company.py similarity index 100% rename from tests/examples/company/test_company.py rename to tests/functional/examples/company/test_company.py diff --git a/tests/examples/conftest.py b/tests/functional/examples/conftest.py similarity index 100% rename from tests/examples/conftest.py rename to tests/functional/examples/conftest.py diff --git a/tests/examples/crowdfund/test_crowdfund_example.py b/tests/functional/examples/crowdfund/test_crowdfund_example.py similarity index 100% rename from tests/examples/crowdfund/test_crowdfund_example.py rename to tests/functional/examples/crowdfund/test_crowdfund_example.py diff --git a/tests/examples/factory/test_factory.py b/tests/functional/examples/factory/test_factory.py similarity index 100% rename from tests/examples/factory/test_factory.py rename to tests/functional/examples/factory/test_factory.py diff --git a/tests/examples/market_maker/test_on_chain_market_maker.py b/tests/functional/examples/market_maker/test_on_chain_market_maker.py similarity index 100% rename from tests/examples/market_maker/test_on_chain_market_maker.py rename to tests/functional/examples/market_maker/test_on_chain_market_maker.py diff --git a/tests/examples/name_registry/test_name_registry.py b/tests/functional/examples/name_registry/test_name_registry.py similarity index 100% rename from tests/examples/name_registry/test_name_registry.py rename to tests/functional/examples/name_registry/test_name_registry.py diff --git a/tests/examples/safe_remote_purchase/test_safe_remote_purchase.py b/tests/functional/examples/safe_remote_purchase/test_safe_remote_purchase.py similarity index 100% rename from tests/examples/safe_remote_purchase/test_safe_remote_purchase.py rename to tests/functional/examples/safe_remote_purchase/test_safe_remote_purchase.py diff --git a/tests/examples/storage/test_advanced_storage.py b/tests/functional/examples/storage/test_advanced_storage.py similarity index 100% rename from tests/examples/storage/test_advanced_storage.py rename to tests/functional/examples/storage/test_advanced_storage.py diff --git a/tests/examples/storage/test_storage.py b/tests/functional/examples/storage/test_storage.py similarity index 100% rename from tests/examples/storage/test_storage.py rename to tests/functional/examples/storage/test_storage.py diff --git a/tests/examples/tokens/test_erc1155.py b/tests/functional/examples/tokens/test_erc1155.py similarity index 100% rename from tests/examples/tokens/test_erc1155.py rename to tests/functional/examples/tokens/test_erc1155.py diff --git a/tests/examples/tokens/test_erc20.py b/tests/functional/examples/tokens/test_erc20.py similarity index 100% rename from tests/examples/tokens/test_erc20.py rename to tests/functional/examples/tokens/test_erc20.py diff --git a/tests/examples/tokens/test_erc4626.py b/tests/functional/examples/tokens/test_erc4626.py similarity index 100% rename from tests/examples/tokens/test_erc4626.py rename to tests/functional/examples/tokens/test_erc4626.py diff --git a/tests/examples/tokens/test_erc721.py b/tests/functional/examples/tokens/test_erc721.py similarity index 100% rename from tests/examples/tokens/test_erc721.py rename to tests/functional/examples/tokens/test_erc721.py diff --git a/tests/examples/voting/test_ballot.py b/tests/functional/examples/voting/test_ballot.py similarity index 100% rename from tests/examples/voting/test_ballot.py rename to tests/functional/examples/voting/test_ballot.py diff --git a/tests/examples/wallet/test_wallet.py b/tests/functional/examples/wallet/test_wallet.py similarity index 100% rename from tests/examples/wallet/test_wallet.py rename to tests/functional/examples/wallet/test_wallet.py diff --git a/tests/grammar/test_grammar.py b/tests/functional/grammar/test_grammar.py similarity index 95% rename from tests/grammar/test_grammar.py rename to tests/functional/grammar/test_grammar.py index d665ca2544..aa0286cfa5 100644 --- a/tests/grammar/test_grammar.py +++ b/tests/functional/grammar/test_grammar.py @@ -4,7 +4,7 @@ import hypothesis import hypothesis.strategies as st import pytest -from hypothesis import HealthCheck, assume, given +from hypothesis import assume, given from hypothesis.extra.lark import LarkStrategy from vyper.ast import Module, parse_to_ast @@ -103,7 +103,7 @@ def has_no_docstrings(c): @pytest.mark.fuzzing @given(code=from_grammar().filter(lambda c: utf8_encodable(c))) -@hypothesis.settings(deadline=400, max_examples=500, suppress_health_check=(HealthCheck.too_slow,)) +@hypothesis.settings(max_examples=500) def test_grammar_bruteforce(code): if utf8_encodable(code): _, _, reformatted_code = pre_parse(code + "\n") diff --git a/tests/parser/syntax/__init__.py b/tests/functional/syntax/__init__.py similarity index 100% rename from tests/parser/syntax/__init__.py rename to tests/functional/syntax/__init__.py diff --git a/tests/parser/exceptions/test_argument_exception.py b/tests/functional/syntax/exceptions/test_argument_exception.py similarity index 100% rename from tests/parser/exceptions/test_argument_exception.py rename to tests/functional/syntax/exceptions/test_argument_exception.py diff --git a/tests/parser/exceptions/test_call_violation.py b/tests/functional/syntax/exceptions/test_call_violation.py similarity index 100% rename from tests/parser/exceptions/test_call_violation.py rename to tests/functional/syntax/exceptions/test_call_violation.py diff --git a/tests/parser/exceptions/test_constancy_exception.py b/tests/functional/syntax/exceptions/test_constancy_exception.py similarity index 100% rename from tests/parser/exceptions/test_constancy_exception.py rename to tests/functional/syntax/exceptions/test_constancy_exception.py diff --git a/tests/parser/exceptions/test_function_declaration_exception.py b/tests/functional/syntax/exceptions/test_function_declaration_exception.py similarity index 100% rename from tests/parser/exceptions/test_function_declaration_exception.py rename to tests/functional/syntax/exceptions/test_function_declaration_exception.py diff --git a/tests/parser/exceptions/test_instantiation_exception.py b/tests/functional/syntax/exceptions/test_instantiation_exception.py similarity index 100% rename from tests/parser/exceptions/test_instantiation_exception.py rename to tests/functional/syntax/exceptions/test_instantiation_exception.py diff --git a/tests/parser/exceptions/test_invalid_literal_exception.py b/tests/functional/syntax/exceptions/test_invalid_literal_exception.py similarity index 100% rename from tests/parser/exceptions/test_invalid_literal_exception.py rename to tests/functional/syntax/exceptions/test_invalid_literal_exception.py diff --git a/tests/parser/exceptions/test_invalid_payable.py b/tests/functional/syntax/exceptions/test_invalid_payable.py similarity index 100% rename from tests/parser/exceptions/test_invalid_payable.py rename to tests/functional/syntax/exceptions/test_invalid_payable.py diff --git a/tests/parser/exceptions/test_invalid_reference.py b/tests/functional/syntax/exceptions/test_invalid_reference.py similarity index 100% rename from tests/parser/exceptions/test_invalid_reference.py rename to tests/functional/syntax/exceptions/test_invalid_reference.py diff --git a/tests/parser/exceptions/test_invalid_type_exception.py b/tests/functional/syntax/exceptions/test_invalid_type_exception.py similarity index 100% rename from tests/parser/exceptions/test_invalid_type_exception.py rename to tests/functional/syntax/exceptions/test_invalid_type_exception.py diff --git a/tests/parser/exceptions/test_namespace_collision.py b/tests/functional/syntax/exceptions/test_namespace_collision.py similarity index 100% rename from tests/parser/exceptions/test_namespace_collision.py rename to tests/functional/syntax/exceptions/test_namespace_collision.py diff --git a/tests/parser/exceptions/test_overflow_exception.py b/tests/functional/syntax/exceptions/test_overflow_exception.py similarity index 100% rename from tests/parser/exceptions/test_overflow_exception.py rename to tests/functional/syntax/exceptions/test_overflow_exception.py diff --git a/tests/parser/exceptions/test_structure_exception.py b/tests/functional/syntax/exceptions/test_structure_exception.py similarity index 100% rename from tests/parser/exceptions/test_structure_exception.py rename to tests/functional/syntax/exceptions/test_structure_exception.py diff --git a/tests/parser/exceptions/test_syntax_exception.py b/tests/functional/syntax/exceptions/test_syntax_exception.py similarity index 100% rename from tests/parser/exceptions/test_syntax_exception.py rename to tests/functional/syntax/exceptions/test_syntax_exception.py diff --git a/tests/parser/exceptions/test_type_mismatch_exception.py b/tests/functional/syntax/exceptions/test_type_mismatch_exception.py similarity index 100% rename from tests/parser/exceptions/test_type_mismatch_exception.py rename to tests/functional/syntax/exceptions/test_type_mismatch_exception.py diff --git a/tests/parser/exceptions/test_undeclared_definition.py b/tests/functional/syntax/exceptions/test_undeclared_definition.py similarity index 100% rename from tests/parser/exceptions/test_undeclared_definition.py rename to tests/functional/syntax/exceptions/test_undeclared_definition.py diff --git a/tests/parser/exceptions/test_variable_declaration_exception.py b/tests/functional/syntax/exceptions/test_variable_declaration_exception.py similarity index 100% rename from tests/parser/exceptions/test_variable_declaration_exception.py rename to tests/functional/syntax/exceptions/test_variable_declaration_exception.py diff --git a/tests/parser/exceptions/test_vyper_exception_pos.py b/tests/functional/syntax/exceptions/test_vyper_exception_pos.py similarity index 100% rename from tests/parser/exceptions/test_vyper_exception_pos.py rename to tests/functional/syntax/exceptions/test_vyper_exception_pos.py diff --git a/tests/parser/syntax/utils/test_event_names.py b/tests/functional/syntax/names/test_event_names.py similarity index 100% rename from tests/parser/syntax/utils/test_event_names.py rename to tests/functional/syntax/names/test_event_names.py diff --git a/tests/parser/syntax/utils/test_function_names.py b/tests/functional/syntax/names/test_function_names.py similarity index 100% rename from tests/parser/syntax/utils/test_function_names.py rename to tests/functional/syntax/names/test_function_names.py diff --git a/tests/parser/syntax/utils/test_variable_names.py b/tests/functional/syntax/names/test_variable_names.py similarity index 100% rename from tests/parser/syntax/utils/test_variable_names.py rename to tests/functional/syntax/names/test_variable_names.py diff --git a/tests/signatures/test_invalid_function_decorators.py b/tests/functional/syntax/signatures/test_invalid_function_decorators.py similarity index 100% rename from tests/signatures/test_invalid_function_decorators.py rename to tests/functional/syntax/signatures/test_invalid_function_decorators.py diff --git a/tests/signatures/test_method_id_conflicts.py b/tests/functional/syntax/signatures/test_method_id_conflicts.py similarity index 100% rename from tests/signatures/test_method_id_conflicts.py rename to tests/functional/syntax/signatures/test_method_id_conflicts.py diff --git a/tests/functional/syntax/test_abi_decode.py b/tests/functional/syntax/test_abi_decode.py new file mode 100644 index 0000000000..f05ff429cd --- /dev/null +++ b/tests/functional/syntax/test_abi_decode.py @@ -0,0 +1,45 @@ +import pytest + +from vyper import compiler +from vyper.exceptions import TypeMismatch + +fail_list = [ + ( + """ +@external +def foo(j: uint256) -> bool: + s: bool = _abi_decode(j, bool, unwrap_tuple= False) + return s + """, + TypeMismatch, + ), + ( + """ +@external +def bar(j: String[32]) -> bool: + s: bool = _abi_decode(j, bool, unwrap_tuple= False) + return s + """, + TypeMismatch, + ), +] + + +@pytest.mark.parametrize("bad_code,exc", fail_list) +def test_abi_encode_fail(bad_code, exc): + with pytest.raises(exc): + compiler.compile_code(bad_code) + + +valid_list = [ + """ +@external +def foo(x: Bytes[32]) -> uint256: + return _abi_decode(x, uint256) + """ +] + + +@pytest.mark.parametrize("good_code", valid_list) +def test_abi_encode_success(good_code): + assert compiler.compile_code(good_code) is not None diff --git a/tests/parser/syntax/test_abi_encode.py b/tests/functional/syntax/test_abi_encode.py similarity index 100% rename from tests/parser/syntax/test_abi_encode.py rename to tests/functional/syntax/test_abi_encode.py diff --git a/tests/parser/syntax/test_addmulmod.py b/tests/functional/syntax/test_addmulmod.py similarity index 100% rename from tests/parser/syntax/test_addmulmod.py rename to tests/functional/syntax/test_addmulmod.py diff --git a/tests/parser/syntax/test_address_code.py b/tests/functional/syntax/test_address_code.py similarity index 100% rename from tests/parser/syntax/test_address_code.py rename to tests/functional/syntax/test_address_code.py diff --git a/tests/parser/syntax/test_ann_assign.py b/tests/functional/syntax/test_ann_assign.py similarity index 100% rename from tests/parser/syntax/test_ann_assign.py rename to tests/functional/syntax/test_ann_assign.py diff --git a/tests/parser/syntax/test_as_uint256.py b/tests/functional/syntax/test_as_uint256.py similarity index 100% rename from tests/parser/syntax/test_as_uint256.py rename to tests/functional/syntax/test_as_uint256.py diff --git a/tests/parser/syntax/test_as_wei_value.py b/tests/functional/syntax/test_as_wei_value.py similarity index 100% rename from tests/parser/syntax/test_as_wei_value.py rename to tests/functional/syntax/test_as_wei_value.py diff --git a/tests/parser/syntax/test_block.py b/tests/functional/syntax/test_block.py similarity index 100% rename from tests/parser/syntax/test_block.py rename to tests/functional/syntax/test_block.py diff --git a/tests/parser/syntax/test_blockscope.py b/tests/functional/syntax/test_blockscope.py similarity index 100% rename from tests/parser/syntax/test_blockscope.py rename to tests/functional/syntax/test_blockscope.py diff --git a/tests/parser/syntax/test_bool.py b/tests/functional/syntax/test_bool.py similarity index 98% rename from tests/parser/syntax/test_bool.py rename to tests/functional/syntax/test_bool.py index 09f799d91c..48ed37321a 100644 --- a/tests/parser/syntax/test_bool.py +++ b/tests/functional/syntax/test_bool.py @@ -52,7 +52,7 @@ def foo() -> bool: """ @external def foo() -> bool: - a: address = ZERO_ADDRESS + a: address = empty(address) return a == 1 """, ( @@ -137,7 +137,7 @@ def foo() -> bool: """ @external def foo2(a: address) -> bool: - return a != ZERO_ADDRESS + return a != empty(address) """, ] diff --git a/tests/parser/syntax/test_bool_ops.py b/tests/functional/syntax/test_bool_ops.py similarity index 100% rename from tests/parser/syntax/test_bool_ops.py rename to tests/functional/syntax/test_bool_ops.py diff --git a/tests/parser/syntax/test_bytes.py b/tests/functional/syntax/test_bytes.py similarity index 100% rename from tests/parser/syntax/test_bytes.py rename to tests/functional/syntax/test_bytes.py diff --git a/tests/parser/syntax/test_chainid.py b/tests/functional/syntax/test_chainid.py similarity index 100% rename from tests/parser/syntax/test_chainid.py rename to tests/functional/syntax/test_chainid.py diff --git a/tests/parser/syntax/test_code_size.py b/tests/functional/syntax/test_code_size.py similarity index 100% rename from tests/parser/syntax/test_code_size.py rename to tests/functional/syntax/test_code_size.py diff --git a/tests/parser/syntax/test_codehash.py b/tests/functional/syntax/test_codehash.py similarity index 92% rename from tests/parser/syntax/test_codehash.py rename to tests/functional/syntax/test_codehash.py index 5074d14636..c2d9a2e274 100644 --- a/tests/parser/syntax/test_codehash.py +++ b/tests/functional/syntax/test_codehash.py @@ -33,7 +33,7 @@ def foo4() -> bytes32: return self.a.codehash """ settings = Settings(evm_version=evm_version, optimize=optimize) - compiled = compile_code(code, ["bytecode_runtime"], settings=settings) + compiled = compile_code(code, output_formats=["bytecode_runtime"], settings=settings) bytecode = bytes.fromhex(compiled["bytecode_runtime"][2:]) hash_ = keccak256(bytecode) diff --git a/tests/parser/syntax/test_concat.py b/tests/functional/syntax/test_concat.py similarity index 100% rename from tests/parser/syntax/test_concat.py rename to tests/functional/syntax/test_concat.py diff --git a/tests/parser/syntax/test_conditionals.py b/tests/functional/syntax/test_conditionals.py similarity index 100% rename from tests/parser/syntax/test_conditionals.py rename to tests/functional/syntax/test_conditionals.py diff --git a/tests/parser/syntax/test_constants.py b/tests/functional/syntax/test_constants.py similarity index 100% rename from tests/parser/syntax/test_constants.py rename to tests/functional/syntax/test_constants.py diff --git a/tests/parser/syntax/test_create_with_code_of.py b/tests/functional/syntax/test_create_with_code_of.py similarity index 100% rename from tests/parser/syntax/test_create_with_code_of.py rename to tests/functional/syntax/test_create_with_code_of.py diff --git a/tests/parser/syntax/test_dynamic_array.py b/tests/functional/syntax/test_dynamic_array.py similarity index 100% rename from tests/parser/syntax/test_dynamic_array.py rename to tests/functional/syntax/test_dynamic_array.py diff --git a/tests/parser/syntax/test_enum.py b/tests/functional/syntax/test_enum.py similarity index 100% rename from tests/parser/syntax/test_enum.py rename to tests/functional/syntax/test_enum.py diff --git a/tests/parser/syntax/test_extract32.py b/tests/functional/syntax/test_extract32.py similarity index 100% rename from tests/parser/syntax/test_extract32.py rename to tests/functional/syntax/test_extract32.py diff --git a/tests/parser/syntax/test_for_range.py b/tests/functional/syntax/test_for_range.py similarity index 100% rename from tests/parser/syntax/test_for_range.py rename to tests/functional/syntax/test_for_range.py diff --git a/tests/parser/syntax/test_functions_call.py b/tests/functional/syntax/test_functions_call.py similarity index 100% rename from tests/parser/syntax/test_functions_call.py rename to tests/functional/syntax/test_functions_call.py diff --git a/tests/parser/syntax/test_immutables.py b/tests/functional/syntax/test_immutables.py similarity index 100% rename from tests/parser/syntax/test_immutables.py rename to tests/functional/syntax/test_immutables.py diff --git a/tests/parser/syntax/test_interfaces.py b/tests/functional/syntax/test_interfaces.py similarity index 95% rename from tests/parser/syntax/test_interfaces.py rename to tests/functional/syntax/test_interfaces.py index 5afb34e6bd..9100389dbd 100644 --- a/tests/parser/syntax/test_interfaces.py +++ b/tests/functional/syntax/test_interfaces.py @@ -47,7 +47,7 @@ def test(): @external def test(): - a: address(ERC20) = ZERO_ADDRESS + a: address(ERC20) = empty(address) """, InvalidType, ), @@ -306,7 +306,7 @@ def some_func(): nonpayable @external def __init__(): - self.my_interface[self.idx] = MyInterface(ZERO_ADDRESS) + self.my_interface[self.idx] = MyInterface(empty(address)) """, """ interface MyInterface: @@ -374,7 +374,7 @@ def test_interfaces_success(good_code): assert compiler.compile_code(good_code) is not None -def test_imports_and_implements_within_interface(): +def test_imports_and_implements_within_interface(make_input_bundle): interface_code = """ from vyper.interfaces import ERC20 import foo.bar as Baz @@ -386,6 +386,8 @@ def foobar(): pass """ + input_bundle = make_input_bundle({"foo.vy": interface_code}) + code = """ import foo as Foo @@ -396,9 +398,4 @@ def foobar(): pass """ - assert ( - compiler.compile_code( - code, interface_codes={"Foo": {"type": "vyper", "code": interface_code}} - ) - is not None - ) + assert compiler.compile_code(code, input_bundle=input_bundle) is not None diff --git a/tests/parser/syntax/test_invalids.py b/tests/functional/syntax/test_invalids.py similarity index 100% rename from tests/parser/syntax/test_invalids.py rename to tests/functional/syntax/test_invalids.py diff --git a/tests/parser/syntax/test_keccak256.py b/tests/functional/syntax/test_keccak256.py similarity index 100% rename from tests/parser/syntax/test_keccak256.py rename to tests/functional/syntax/test_keccak256.py diff --git a/tests/parser/syntax/test_len.py b/tests/functional/syntax/test_len.py similarity index 100% rename from tests/parser/syntax/test_len.py rename to tests/functional/syntax/test_len.py diff --git a/tests/parser/syntax/test_list.py b/tests/functional/syntax/test_list.py similarity index 98% rename from tests/parser/syntax/test_list.py rename to tests/functional/syntax/test_list.py index 3f81b911c8..db41de5526 100644 --- a/tests/parser/syntax/test_list.py +++ b/tests/functional/syntax/test_list.py @@ -305,8 +305,9 @@ def foo(): """ @external def foo(): + x: DynArray[uint256, 3] = [1, 2, 3] for i in [[], []]: - pass + x = i """, ] diff --git a/tests/parser/syntax/test_logging.py b/tests/functional/syntax/test_logging.py similarity index 100% rename from tests/parser/syntax/test_logging.py rename to tests/functional/syntax/test_logging.py diff --git a/tests/parser/syntax/test_minmax.py b/tests/functional/syntax/test_minmax.py similarity index 100% rename from tests/parser/syntax/test_minmax.py rename to tests/functional/syntax/test_minmax.py diff --git a/tests/parser/syntax/test_minmax_value.py b/tests/functional/syntax/test_minmax_value.py similarity index 100% rename from tests/parser/syntax/test_minmax_value.py rename to tests/functional/syntax/test_minmax_value.py diff --git a/tests/parser/syntax/test_msg_data.py b/tests/functional/syntax/test_msg_data.py similarity index 100% rename from tests/parser/syntax/test_msg_data.py rename to tests/functional/syntax/test_msg_data.py diff --git a/tests/parser/syntax/test_nested_list.py b/tests/functional/syntax/test_nested_list.py similarity index 100% rename from tests/parser/syntax/test_nested_list.py rename to tests/functional/syntax/test_nested_list.py diff --git a/tests/parser/syntax/test_no_none.py b/tests/functional/syntax/test_no_none.py similarity index 93% rename from tests/parser/syntax/test_no_none.py rename to tests/functional/syntax/test_no_none.py index 7030a56b18..24c32a46a4 100644 --- a/tests/parser/syntax/test_no_none.py +++ b/tests/functional/syntax/test_no_none.py @@ -30,13 +30,13 @@ def foo(): """ @external def foo(): - bar: bytes32 = EMPTY_BYTES32 + bar: bytes32 = empty(bytes32) bar = None """, """ @external def foo(): - bar: address = ZERO_ADDRESS + bar: address = empty(address) bar = None """, """ @@ -104,13 +104,13 @@ def foo(): """ @external def foo(): - bar: bytes32 = EMPTY_BYTES32 + bar: bytes32 = empty(bytes32) assert bar is None """, """ @external def foo(): - bar: address = ZERO_ADDRESS + bar: address = empty(address) assert bar is None """, ] @@ -148,13 +148,13 @@ def foo(): """ @external def foo(): - bar: bytes32 = EMPTY_BYTES32 + bar: bytes32 = empty(bytes32) assert bar == None """, """ @external def foo(): - bar: address = ZERO_ADDRESS + bar: address = empty(address) assert bar == None """, ] diff --git a/tests/parser/syntax/test_print.py b/tests/functional/syntax/test_print.py similarity index 100% rename from tests/parser/syntax/test_print.py rename to tests/functional/syntax/test_print.py diff --git a/tests/parser/syntax/test_public.py b/tests/functional/syntax/test_public.py similarity index 100% rename from tests/parser/syntax/test_public.py rename to tests/functional/syntax/test_public.py diff --git a/tests/parser/syntax/test_raw_call.py b/tests/functional/syntax/test_raw_call.py similarity index 100% rename from tests/parser/syntax/test_raw_call.py rename to tests/functional/syntax/test_raw_call.py diff --git a/tests/parser/syntax/test_return_tuple.py b/tests/functional/syntax/test_return_tuple.py similarity index 100% rename from tests/parser/syntax/test_return_tuple.py rename to tests/functional/syntax/test_return_tuple.py diff --git a/tests/parser/syntax/test_self_balance.py b/tests/functional/syntax/test_self_balance.py similarity index 88% rename from tests/parser/syntax/test_self_balance.py rename to tests/functional/syntax/test_self_balance.py index 63db58e347..d22d8a2750 100644 --- a/tests/parser/syntax/test_self_balance.py +++ b/tests/functional/syntax/test_self_balance.py @@ -20,7 +20,7 @@ def __default__(): pass """ settings = Settings(evm_version=evm_version) - opcodes = compiler.compile_code(code, ["opcodes"], settings=settings)["opcodes"] + opcodes = compiler.compile_code(code, output_formats=["opcodes"], settings=settings)["opcodes"] if EVM_VERSIONS[evm_version] >= EVM_VERSIONS["istanbul"]: assert "SELFBALANCE" in opcodes else: diff --git a/tests/parser/syntax/test_selfdestruct.py b/tests/functional/syntax/test_selfdestruct.py similarity index 100% rename from tests/parser/syntax/test_selfdestruct.py rename to tests/functional/syntax/test_selfdestruct.py diff --git a/tests/parser/syntax/test_send.py b/tests/functional/syntax/test_send.py similarity index 100% rename from tests/parser/syntax/test_send.py rename to tests/functional/syntax/test_send.py diff --git a/tests/parser/syntax/test_slice.py b/tests/functional/syntax/test_slice.py similarity index 100% rename from tests/parser/syntax/test_slice.py rename to tests/functional/syntax/test_slice.py diff --git a/tests/parser/syntax/test_string.py b/tests/functional/syntax/test_string.py similarity index 100% rename from tests/parser/syntax/test_string.py rename to tests/functional/syntax/test_string.py diff --git a/tests/parser/syntax/test_structs.py b/tests/functional/syntax/test_structs.py similarity index 100% rename from tests/parser/syntax/test_structs.py rename to tests/functional/syntax/test_structs.py diff --git a/tests/parser/syntax/test_ternary.py b/tests/functional/syntax/test_ternary.py similarity index 100% rename from tests/parser/syntax/test_ternary.py rename to tests/functional/syntax/test_ternary.py diff --git a/tests/parser/syntax/test_tuple_assign.py b/tests/functional/syntax/test_tuple_assign.py similarity index 98% rename from tests/parser/syntax/test_tuple_assign.py rename to tests/functional/syntax/test_tuple_assign.py index 115499ce8b..49b63ee614 100644 --- a/tests/parser/syntax/test_tuple_assign.py +++ b/tests/functional/syntax/test_tuple_assign.py @@ -41,7 +41,7 @@ def out_literals() -> (int128, int128, Bytes[10]): @external def test() -> (int128, address, Bytes[10]): a: int128 = 0 - b: address = ZERO_ADDRESS + b: address = empty(address) a, b = self.out_literals() # tuple count mismatch return """, diff --git a/tests/parser/syntax/test_unbalanced_return.py b/tests/functional/syntax/test_unbalanced_return.py similarity index 97% rename from tests/parser/syntax/test_unbalanced_return.py rename to tests/functional/syntax/test_unbalanced_return.py index 5337b4b677..d1d9732777 100644 --- a/tests/parser/syntax/test_unbalanced_return.py +++ b/tests/functional/syntax/test_unbalanced_return.py @@ -56,7 +56,7 @@ def valid_address(sender: address) -> bool: """ @internal def valid_address(sender: address) -> bool: - if sender == ZERO_ADDRESS: + if sender == empty(address): selfdestruct(sender) _sender: address = sender else: @@ -144,7 +144,7 @@ def test() -> int128: """ @external def test() -> int128: - x: bytes32 = EMPTY_BYTES32 + x: bytes32 = empty(bytes32) if False: if False: return 0 diff --git a/tests/parser/functions/test_as_wei_value.py b/tests/parser/functions/test_as_wei_value.py deleted file mode 100644 index bab0aed616..0000000000 --- a/tests/parser/functions/test_as_wei_value.py +++ /dev/null @@ -1,31 +0,0 @@ -def test_ext_call(w3, side_effects_contract, assert_side_effects_invoked, get_contract): - code = """ -@external -def foo(a: Foo) -> uint256: - return as_wei_value(a.foo(7), "ether") - -interface Foo: - def foo(x: uint8) -> uint8: nonpayable - """ - - c1 = side_effects_contract("uint8") - c2 = get_contract(code) - - assert c2.foo(c1.address) == w3.to_wei(7, "ether") - assert_side_effects_invoked(c1, lambda: c2.foo(c1.address, transact={})) - - -def test_internal_call(w3, get_contract_with_gas_estimation): - code = """ -@external -def foo() -> uint256: - return as_wei_value(self.bar(), "ether") - -@internal -def bar() -> uint8: - return 7 - """ - - c = get_contract_with_gas_estimation(code) - - assert c.foo() == w3.to_wei(7, "ether") diff --git a/vyper/builtins/interfaces/__init__.py b/tests/unit/__init__.py similarity index 100% rename from vyper/builtins/interfaces/__init__.py rename to tests/unit/__init__.py diff --git a/tests/unit/abi_types/test_invalid_abi_types.py b/tests/unit/abi_types/test_invalid_abi_types.py new file mode 100644 index 0000000000..c8566e066f --- /dev/null +++ b/tests/unit/abi_types/test_invalid_abi_types.py @@ -0,0 +1,26 @@ +import pytest + +from vyper.abi_types import ( + ABI_Bytes, + ABI_BytesM, + ABI_DynamicArray, + ABI_FixedMxN, + ABI_GIntM, + ABI_String, +) +from vyper.exceptions import InvalidABIType + +cases_invalid_types = [ + (ABI_GIntM, ((0, False), (7, False), (300, True), (300, False))), + (ABI_FixedMxN, ((0, 0, False), (8, 0, False), (256, 81, True), (300, 80, False))), + (ABI_BytesM, ((0,), (33,), (-10,))), + (ABI_Bytes, ((-1,), (-69,))), + (ABI_DynamicArray, ((ABI_GIntM(256, False), -1), (ABI_String(256), -10))), +] + + +@pytest.mark.parametrize("typ,params_variants", cases_invalid_types) +def test_invalid_abi_types(assert_compile_failed, typ, params_variants): + # double parametrization cannot work because the 2nd dimension is variable + for params in params_variants: + assert_compile_failed(lambda: typ(*params), InvalidABIType) diff --git a/tests/ast/nodes/test_binary.py b/tests/unit/ast/nodes/test_binary.py similarity index 100% rename from tests/ast/nodes/test_binary.py rename to tests/unit/ast/nodes/test_binary.py diff --git a/tests/ast/nodes/test_compare_nodes.py b/tests/unit/ast/nodes/test_compare_nodes.py similarity index 100% rename from tests/ast/nodes/test_compare_nodes.py rename to tests/unit/ast/nodes/test_compare_nodes.py diff --git a/tests/ast/nodes/test_evaluate_binop_decimal.py b/tests/unit/ast/nodes/test_evaluate_binop_decimal.py similarity index 97% rename from tests/ast/nodes/test_evaluate_binop_decimal.py rename to tests/unit/ast/nodes/test_evaluate_binop_decimal.py index c6c69626b8..5c9956caba 100644 --- a/tests/ast/nodes/test_evaluate_binop_decimal.py +++ b/tests/unit/ast/nodes/test_evaluate_binop_decimal.py @@ -13,7 +13,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_decimals, right=st_decimals) @example(left=Decimal("0.9999999999"), right=Decimal("0.0000000001")) @example(left=Decimal("0.0000000001"), right=Decimal("0.9999999999")) @@ -52,7 +52,7 @@ def test_binop_pow(): @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given( values=st.lists(st_decimals, min_size=2, max_size=10), ops=st.lists(st.sampled_from("+-*/%"), min_size=11, max_size=11), diff --git a/tests/ast/nodes/test_evaluate_binop_int.py b/tests/unit/ast/nodes/test_evaluate_binop_int.py similarity index 95% rename from tests/ast/nodes/test_evaluate_binop_int.py rename to tests/unit/ast/nodes/test_evaluate_binop_int.py index d632a95461..80c9381c0f 100644 --- a/tests/ast/nodes/test_evaluate_binop_int.py +++ b/tests/unit/ast/nodes/test_evaluate_binop_int.py @@ -9,7 +9,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_int32, right=st_int32) @example(left=1, right=1) @example(left=1, right=-1) @@ -42,7 +42,7 @@ def foo(a: int128, b: int128) -> int128: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st_uint64, right=st_uint64) @pytest.mark.parametrize("op", "+-*/%") def test_binop_uint256(get_contract, assert_tx_failed, op, left, right): @@ -69,7 +69,7 @@ def foo(a: uint256, b: uint256) -> uint256: @pytest.mark.xfail(reason="need to implement safe exponentiation logic") @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st.integers(min_value=2, max_value=245), right=st.integers(min_value=0, max_value=16)) @example(left=0, right=0) @example(left=0, right=1) @@ -89,7 +89,7 @@ def foo(a: uint256, b: uint256) -> uint256: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given( values=st.lists(st.integers(min_value=-256, max_value=256), min_size=2, max_size=10), ops=st.lists(st.sampled_from("+-*/%"), min_size=11, max_size=11), diff --git a/tests/ast/nodes/test_evaluate_boolop.py b/tests/unit/ast/nodes/test_evaluate_boolop.py similarity index 95% rename from tests/ast/nodes/test_evaluate_boolop.py rename to tests/unit/ast/nodes/test_evaluate_boolop.py index 6bd9ecc6cb..8b70537c39 100644 --- a/tests/ast/nodes/test_evaluate_boolop.py +++ b/tests/unit/ast/nodes/test_evaluate_boolop.py @@ -8,7 +8,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(values=st.lists(st.booleans(), min_size=2, max_size=10)) @pytest.mark.parametrize("comparator", ["and", "or"]) def test_boolop_simple(get_contract, values, comparator): @@ -32,7 +32,7 @@ def foo({input_value}) -> bool: @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given( values=st.lists(st.booleans(), min_size=2, max_size=10), comparators=st.lists(st.sampled_from(["and", "or"]), min_size=11, max_size=11), diff --git a/tests/ast/nodes/test_evaluate_compare.py b/tests/unit/ast/nodes/test_evaluate_compare.py similarity index 95% rename from tests/ast/nodes/test_evaluate_compare.py rename to tests/unit/ast/nodes/test_evaluate_compare.py index 9ff5cea338..07f8e70de6 100644 --- a/tests/ast/nodes/test_evaluate_compare.py +++ b/tests/unit/ast/nodes/test_evaluate_compare.py @@ -8,7 +8,7 @@ # TODO expand to all signed types @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st.integers(), right=st.integers()) @pytest.mark.parametrize("op", ["==", "!=", "<", "<=", ">=", ">"]) def test_compare_eq_signed(get_contract, op, left, right): @@ -28,7 +28,7 @@ def foo(a: int128, b: int128) -> bool: # TODO expand to all unsigned types @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given(left=st.integers(min_value=0), right=st.integers(min_value=0)) @pytest.mark.parametrize("op", ["==", "!=", "<", "<=", ">=", ">"]) def test_compare_eq_unsigned(get_contract, op, left, right): @@ -47,7 +47,7 @@ def foo(a: uint128, b: uint128) -> bool: @pytest.mark.fuzzing -@settings(max_examples=20, deadline=1000) +@settings(max_examples=20) @given(left=st.integers(), right=st.lists(st.integers(), min_size=1, max_size=16)) def test_compare_in(left, right, get_contract): source = f""" @@ -76,7 +76,7 @@ def bar(a: int128) -> bool: @pytest.mark.fuzzing -@settings(max_examples=20, deadline=1000) +@settings(max_examples=20) @given(left=st.integers(), right=st.lists(st.integers(), min_size=1, max_size=16)) def test_compare_not_in(left, right, get_contract): source = f""" diff --git a/tests/ast/nodes/test_evaluate_subscript.py b/tests/unit/ast/nodes/test_evaluate_subscript.py similarity index 93% rename from tests/ast/nodes/test_evaluate_subscript.py rename to tests/unit/ast/nodes/test_evaluate_subscript.py index 3c0fa5d16d..ca50a076a5 100644 --- a/tests/ast/nodes/test_evaluate_subscript.py +++ b/tests/unit/ast/nodes/test_evaluate_subscript.py @@ -6,7 +6,7 @@ @pytest.mark.fuzzing -@settings(max_examples=50, deadline=1000) +@settings(max_examples=50) @given( idx=st.integers(min_value=0, max_value=9), array=st.lists(st.integers(), min_size=10, max_size=10), diff --git a/tests/ast/nodes/test_evaluate_unaryop.py b/tests/unit/ast/nodes/test_evaluate_unaryop.py similarity index 100% rename from tests/ast/nodes/test_evaluate_unaryop.py rename to tests/unit/ast/nodes/test_evaluate_unaryop.py diff --git a/tests/ast/nodes/test_from_node.py b/tests/unit/ast/nodes/test_from_node.py similarity index 100% rename from tests/ast/nodes/test_from_node.py rename to tests/unit/ast/nodes/test_from_node.py diff --git a/tests/ast/nodes/test_get_children.py b/tests/unit/ast/nodes/test_get_children.py similarity index 100% rename from tests/ast/nodes/test_get_children.py rename to tests/unit/ast/nodes/test_get_children.py diff --git a/tests/ast/nodes/test_get_descendants.py b/tests/unit/ast/nodes/test_get_descendants.py similarity index 100% rename from tests/ast/nodes/test_get_descendants.py rename to tests/unit/ast/nodes/test_get_descendants.py diff --git a/tests/ast/nodes/test_hex.py b/tests/unit/ast/nodes/test_hex.py similarity index 100% rename from tests/ast/nodes/test_hex.py rename to tests/unit/ast/nodes/test_hex.py diff --git a/tests/ast/nodes/test_replace_in_tree.py b/tests/unit/ast/nodes/test_replace_in_tree.py similarity index 100% rename from tests/ast/nodes/test_replace_in_tree.py rename to tests/unit/ast/nodes/test_replace_in_tree.py diff --git a/tests/parser/parser_utils/test_annotate_and_optimize_ast.py b/tests/unit/ast/test_annotate_and_optimize_ast.py similarity index 100% rename from tests/parser/parser_utils/test_annotate_and_optimize_ast.py rename to tests/unit/ast/test_annotate_and_optimize_ast.py diff --git a/tests/parser/ast_utils/test_ast_dict.py b/tests/unit/ast/test_ast_dict.py similarity index 91% rename from tests/parser/ast_utils/test_ast_dict.py rename to tests/unit/ast/test_ast_dict.py index f483d0cbe8..1f60c9ac8b 100644 --- a/tests/parser/ast_utils/test_ast_dict.py +++ b/tests/unit/ast/test_ast_dict.py @@ -19,7 +19,7 @@ def get_node_ids(ast_struct, ids=None): elif v is None or isinstance(v, (str, int)): continue else: - raise Exception("Unknown ast_struct provided.") + raise Exception(f"Unknown ast_struct provided. {k}, {v}") return ids @@ -30,7 +30,7 @@ def test() -> int128: a: uint256 = 100 return 123 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) node_ids = get_node_ids(dict_out) assert len(node_ids) == len(set(node_ids)) @@ -40,7 +40,7 @@ def test_basic_ast(): code = """ a: int128 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) assert dict_out["ast_dict"]["ast"]["body"][0] == { "annotation": { "ast_type": "Name", @@ -89,7 +89,7 @@ def foo() -> uint256: view def foo() -> uint256: return 1 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) assert dict_out["ast_dict"]["ast"]["body"][1] == { "col_offset": 0, "annotation": { diff --git a/tests/ast/test_folding.py b/tests/unit/ast/test_folding.py similarity index 86% rename from tests/ast/test_folding.py rename to tests/unit/ast/test_folding.py index 22d5f58222..62a7140e97 100644 --- a/tests/ast/test_folding.py +++ b/tests/unit/ast/test_folding.py @@ -132,49 +132,6 @@ def test_replace_constant_no(source): assert vy_ast.compare_nodes(unmodified_ast, folded_ast) -builtins_modified = [ - "ZERO_ADDRESS", - "foo = ZERO_ADDRESS", - "foo: int128[ZERO_ADDRESS] = 42", - "foo = [ZERO_ADDRESS]", - "def foo(bar: address = ZERO_ADDRESS): pass", - "def foo(): bar = ZERO_ADDRESS", - "def foo(): return ZERO_ADDRESS", - "log foo(ZERO_ADDRESS)", - "log foo(42, ZERO_ADDRESS)", -] - - -@pytest.mark.parametrize("source", builtins_modified) -def test_replace_builtin_constant(source): - unmodified_ast = vy_ast.parse_to_ast(source) - folded_ast = vy_ast.parse_to_ast(source) - - folding.replace_builtin_constants(folded_ast) - - assert not vy_ast.compare_nodes(unmodified_ast, folded_ast) - - -builtins_unmodified = [ - "ZERO_ADDRESS = 2", - "ZERO_ADDRESS()", - "def foo(ZERO_ADDRESS: int128 = 42): pass", - "def foo(): ZERO_ADDRESS = 42", - "def ZERO_ADDRESS(): pass", - "log ZERO_ADDRESS(42)", -] - - -@pytest.mark.parametrize("source", builtins_unmodified) -def test_replace_builtin_constant_no(source): - unmodified_ast = vy_ast.parse_to_ast(source) - folded_ast = vy_ast.parse_to_ast(source) - - folding.replace_builtin_constants(folded_ast) - - assert vy_ast.compare_nodes(unmodified_ast, folded_ast) - - userdefined_modified = [ "FOO", "foo = FOO", diff --git a/tests/ast/test_metadata_journal.py b/tests/unit/ast/test_metadata_journal.py similarity index 100% rename from tests/ast/test_metadata_journal.py rename to tests/unit/ast/test_metadata_journal.py diff --git a/tests/ast/test_natspec.py b/tests/unit/ast/test_natspec.py similarity index 100% rename from tests/ast/test_natspec.py rename to tests/unit/ast/test_natspec.py diff --git a/tests/parser/ast_utils/test_ast.py b/tests/unit/ast/test_parser.py similarity index 100% rename from tests/parser/ast_utils/test_ast.py rename to tests/unit/ast/test_parser.py diff --git a/tests/ast/test_pre_parser.py b/tests/unit/ast/test_pre_parser.py similarity index 66% rename from tests/ast/test_pre_parser.py rename to tests/unit/ast/test_pre_parser.py index 5427532c16..3d072674f6 100644 --- a/tests/ast/test_pre_parser.py +++ b/tests/unit/ast/test_pre_parser.py @@ -1,8 +1,9 @@ import pytest from vyper.ast.pre_parser import pre_parse, validate_version_pragma +from vyper.compiler.phases import CompilerData from vyper.compiler.settings import OptimizationLevel, Settings -from vyper.exceptions import VersionException +from vyper.exceptions import StructureException, VersionException SRC_LINE = (1, 0) # Dummy source line COMPILER_VERSION = "0.1.1" @@ -96,43 +97,50 @@ def test_prerelease_invalid_version_pragma(file_version, mock_version): """ """, Settings(), + Settings(optimize=OptimizationLevel.GAS), ), ( """ #pragma optimize codesize """, Settings(optimize=OptimizationLevel.CODESIZE), + None, ), ( """ #pragma optimize none """, Settings(optimize=OptimizationLevel.NONE), + None, ), ( """ #pragma optimize gas """, Settings(optimize=OptimizationLevel.GAS), + None, ), ( """ #pragma version 0.3.10 """, Settings(compiler_version="0.3.10"), + Settings(optimize=OptimizationLevel.GAS), ), ( """ #pragma evm-version shanghai """, Settings(evm_version="shanghai"), + Settings(evm_version="shanghai", optimize=OptimizationLevel.GAS), ), ( """ #pragma optimize codesize #pragma evm-version shanghai """, - Settings(evm_version="shanghai", optimize=OptimizationLevel.GAS), + Settings(evm_version="shanghai", optimize=OptimizationLevel.CODESIZE), + None, ), ( """ @@ -140,6 +148,7 @@ def test_prerelease_invalid_version_pragma(file_version, mock_version): #pragma evm-version shanghai """, Settings(evm_version="shanghai", compiler_version="0.3.10"), + Settings(evm_version="shanghai", optimize=OptimizationLevel.GAS), ), ( """ @@ -147,6 +156,7 @@ def test_prerelease_invalid_version_pragma(file_version, mock_version): #pragma optimize gas """, Settings(compiler_version="0.3.10", optimize=OptimizationLevel.GAS), + Settings(optimize=OptimizationLevel.GAS), ), ( """ @@ -155,11 +165,59 @@ def test_prerelease_invalid_version_pragma(file_version, mock_version): #pragma optimize gas """, Settings(compiler_version="0.3.10", optimize=OptimizationLevel.GAS, evm_version="shanghai"), + Settings(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 +@pytest.mark.parametrize("code, pre_parse_settings, compiler_data_settings", pragma_examples) +def test_parse_pragmas(code, pre_parse_settings, compiler_data_settings, mock_version): + mock_version("0.3.10") + settings, _, _ = pre_parse(code) + + assert settings == pre_parse_settings + + compiler_data = CompilerData(code) + + # check what happens after CompilerData constructor + if compiler_data_settings is None: + # None is sentinel here meaning that nothing changed + compiler_data_settings = pre_parse_settings + + assert compiler_data.settings == compiler_data_settings + + +invalid_pragmas = [ + # evm-versionnn + """ +# pragma evm-versionnn cancun + """, + # bad fork name + """ +# pragma evm-version cancunn + """, + # oppptimize + """ +# pragma oppptimize codesize + """, + # ggas + """ +# pragma optimize ggas + """, + # double specified + """ +# pragma optimize gas +# pragma optimize codesize + """, + # double specified + """ +# pragma evm-version cancun +# pragma evm-version shanghai + """, +] + + +@pytest.mark.parametrize("code", invalid_pragmas) +def test_invalid_pragma(code): + with pytest.raises(StructureException): + pre_parse(code) diff --git a/tests/test_utils.py b/tests/unit/ast/test_source_annotation.py similarity index 100% rename from tests/test_utils.py rename to tests/unit/ast/test_source_annotation.py diff --git a/tests/cli/outputs/test_storage_layout.py b/tests/unit/cli/outputs/test_storage_layout.py similarity index 100% rename from tests/cli/outputs/test_storage_layout.py rename to tests/unit/cli/outputs/test_storage_layout.py diff --git a/tests/cli/outputs/test_storage_layout_overrides.py b/tests/unit/cli/outputs/test_storage_layout_overrides.py similarity index 100% rename from tests/cli/outputs/test_storage_layout_overrides.py rename to tests/unit/cli/outputs/test_storage_layout_overrides.py diff --git a/tests/unit/cli/vyper_compile/test_compile_files.py b/tests/unit/cli/vyper_compile/test_compile_files.py new file mode 100644 index 0000000000..2a16efa777 --- /dev/null +++ b/tests/unit/cli/vyper_compile/test_compile_files.py @@ -0,0 +1,221 @@ +from pathlib import Path + +import pytest + +from vyper.cli.vyper_compile import compile_files + + +def test_combined_json_keys(tmp_path, make_file): + make_file("bar.vy", "") + + combined_keys = { + "bytecode", + "bytecode_runtime", + "blueprint_bytecode", + "abi", + "source_map", + "layout", + "method_identifiers", + "userdoc", + "devdoc", + } + compile_data = compile_files(["bar.vy"], ["combined_json"], root_folder=tmp_path) + + assert set(compile_data.keys()) == {Path("bar.vy"), "version"} + assert set(compile_data[Path("bar.vy")].keys()) == combined_keys + + +def test_invalid_root_path(): + with pytest.raises(FileNotFoundError): + compile_files([], [], root_folder="path/that/does/not/exist") + + +FOO_CODE = """ +{} + +struct FooStruct: + foo_: uint256 + +@external +def foo() -> FooStruct: + return FooStruct({{foo_: 13}}) + +@external +def bar(a: address) -> FooStruct: + return {}(a).bar() +""" + +BAR_CODE = """ +struct FooStruct: + foo_: uint256 +@external +def bar() -> FooStruct: + return FooStruct({foo_: 13}) +""" + + +SAME_FOLDER_IMPORT_STMT = [ + ("import Bar as Bar", "Bar"), + ("import contracts.Bar as Bar", "Bar"), + ("from . import Bar", "Bar"), + ("from contracts import Bar", "Bar"), + ("from ..contracts import Bar", "Bar"), + ("from . import Bar as FooBar", "FooBar"), + ("from contracts import Bar as FooBar", "FooBar"), + ("from ..contracts import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt,alias", SAME_FOLDER_IMPORT_STMT) +def test_import_same_folder(import_stmt, alias, tmp_path, make_file): + foo = "contracts/foo.vy" + make_file("contracts/foo.vy", FOO_CODE.format(import_stmt, alias)) + make_file("contracts/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +SUBFOLDER_IMPORT_STMT = [ + ("import other.Bar as Bar", "Bar"), + ("import contracts.other.Bar as Bar", "Bar"), + ("from other import Bar", "Bar"), + ("from contracts.other import Bar", "Bar"), + ("from .other import Bar", "Bar"), + ("from ..contracts.other import Bar", "Bar"), + ("from other import Bar as FooBar", "FooBar"), + ("from contracts.other import Bar as FooBar", "FooBar"), + ("from .other import Bar as FooBar", "FooBar"), + ("from ..contracts.other import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt, alias", SUBFOLDER_IMPORT_STMT) +def test_import_subfolder(import_stmt, alias, tmp_path, make_file): + foo = make_file("contracts/foo.vy", (FOO_CODE.format(import_stmt, alias))) + make_file("contracts/other/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +OTHER_FOLDER_IMPORT_STMT = [ + ("import interfaces.Bar as Bar", "Bar"), + ("from interfaces import Bar", "Bar"), + ("from ..interfaces import Bar", "Bar"), + ("from interfaces import Bar as FooBar", "FooBar"), + ("from ..interfaces import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt, alias", OTHER_FOLDER_IMPORT_STMT) +def test_import_other_folder(import_stmt, alias, tmp_path, make_file): + foo = make_file("contracts/foo.vy", FOO_CODE.format(import_stmt, alias)) + make_file("interfaces/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +def test_import_parent_folder(tmp_path, make_file): + foo = make_file("contracts/baz/foo.vy", FOO_CODE.format("from ... import Bar", "Bar")) + make_file("Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + # perform relative import outside of base folder + compile_files([foo], ["combined_json"], root_folder=tmp_path / "contracts") + + +META_IMPORT_STMT = [ + "import Meta as Meta", + "import contracts.Meta as Meta", + "from . import Meta", + "from contracts import Meta", +] + + +@pytest.mark.parametrize("import_stmt", META_IMPORT_STMT) +def test_import_self_interface(import_stmt, tmp_path, make_file): + # a contract can access its derived interface by importing itself + code = f""" +{import_stmt} + +struct FooStruct: + foo_: uint256 + +@external +def know_thyself(a: address) -> FooStruct: + return Meta(a).be_known() + +@external +def be_known() -> FooStruct: + return FooStruct({{foo_: 42}}) + """ + meta = make_file("contracts/Meta.vy", code) + + assert compile_files([meta], ["combined_json"], root_folder=tmp_path) + + +DERIVED_IMPORT_STMT_BAZ = ["import Foo as Foo", "from . import Foo"] + +DERIVED_IMPORT_STMT_FOO = ["import Bar as Bar", "from . import Bar"] + + +@pytest.mark.parametrize("import_stmt_baz", DERIVED_IMPORT_STMT_BAZ) +@pytest.mark.parametrize("import_stmt_foo", DERIVED_IMPORT_STMT_FOO) +def test_derived_interface_imports(import_stmt_baz, import_stmt_foo, tmp_path, make_file): + # contracts-as-interfaces should be able to contain import statements + baz_code = f""" +{import_stmt_baz} + +struct FooStruct: + foo_: uint256 + +@external +def foo(a: address) -> FooStruct: + return Foo(a).foo() + +@external +def bar(_foo: address, _bar: address) -> FooStruct: + return Foo(_foo).bar(_bar) + """ + + make_file("Foo.vy", FOO_CODE.format(import_stmt_foo, "Bar")) + make_file("Bar.vy", BAR_CODE) + baz = make_file("Baz.vy", baz_code) + + assert compile_files([baz], ["combined_json"], root_folder=tmp_path) + + +def test_local_namespace(make_file, tmp_path): + # interface code namespaces should be isolated + # all of these contract should be able to compile together + codes = [ + "import foo as FooBar", + "import bar as FooBar", + "import foo as BarFoo", + "import bar as BarFoo", + ] + struct_def = """ +struct FooStruct: + foo_: uint256 + + """ + + paths = [] + for i, code in enumerate(codes): + code += struct_def + filename = f"code{i}.vy" + make_file(filename, code) + paths.append(filename) + + for file_name in ("foo.vy", "bar.vy"): + make_file(file_name, BAR_CODE) + + assert compile_files(paths, ["combined_json"], root_folder=tmp_path) + + +def test_compile_outside_root_path(tmp_path, make_file): + # absolute paths relative to "." + foo = make_file("foo.vy", FOO_CODE.format("import bar as Bar", "Bar")) + bar = make_file("bar.vy", BAR_CODE) + + assert compile_files([foo, bar], ["combined_json"], root_folder=".") diff --git a/tests/cli/vyper_compile/test_parse_args.py b/tests/unit/cli/vyper_compile/test_parse_args.py similarity index 99% rename from tests/cli/vyper_compile/test_parse_args.py rename to tests/unit/cli/vyper_compile/test_parse_args.py index a676a7836b..0e8c4e9605 100644 --- a/tests/cli/vyper_compile/test_parse_args.py +++ b/tests/unit/cli/vyper_compile/test_parse_args.py @@ -21,7 +21,9 @@ def foo() -> bool: bar_path = chdir_path.joinpath("bar.vy") with bar_path.open("w") as fp: fp.write(code) + _parse_args([str(bar_path)]) # absolute path os.chdir(chdir_path.parent) + _parse_args([str(bar_path)]) # absolute path, subfolder of cwd _parse_args([str(bar_path.relative_to(chdir_path.parent))]) # relative path diff --git a/tests/unit/cli/vyper_json/test_compile_json.py b/tests/unit/cli/vyper_json/test_compile_json.py new file mode 100644 index 0000000000..732762d72b --- /dev/null +++ b/tests/unit/cli/vyper_json/test_compile_json.py @@ -0,0 +1,214 @@ +import json + +import pytest + +import vyper +from vyper.cli.vyper_json import compile_from_input_dict, compile_json, exc_handler_to_dict +from vyper.compiler import OUTPUT_FORMATS, compile_code +from vyper.exceptions import InvalidType, JSONError, SyntaxException + +FOO_CODE = """ +import contracts.bar as Bar + +@external +def foo(a: address) -> bool: + return Bar(a).bar(1) + +@external +def baz() -> uint256: + return self.balance +""" + +BAR_CODE = """ +@external +def bar(a: uint256) -> bool: + return True +""" + +BAD_SYNTAX_CODE = """ +def bar()>: +""" + +BAD_COMPILER_CODE = """ +@external +def oopsie(a: uint256) -> bool: + return 42 +""" + +BAR_ABI = [ + { + "name": "bar", + "outputs": [{"type": "bool", "name": "out"}], + "inputs": [{"type": "uint256", "name": "a"}], + "stateMutability": "nonpayable", + "type": "function", + } +] + + +@pytest.fixture(scope="function") +def input_json(): + return { + "language": "Vyper", + "sources": { + "contracts/foo.vy": {"content": FOO_CODE}, + "contracts/bar.vy": {"content": BAR_CODE}, + }, + "interfaces": {"contracts/ibar.json": {"abi": BAR_ABI}}, + "settings": {"outputSelection": {"*": ["*"]}}, + } + + +# test string and dict inputs both work +def test_string_input(input_json): + assert compile_json(input_json) == compile_json(json.dumps(input_json)) + + +def test_bad_json(): + with pytest.raises(JSONError): + compile_json("this probably isn't valid JSON, is it") + + +def test_keyerror_becomes_jsonerror(input_json): + del input_json["sources"] + with pytest.raises(KeyError): + compile_from_input_dict(input_json) + with pytest.raises(JSONError): + compile_json(input_json) + + +def test_compile_json(input_json, make_input_bundle): + input_bundle = make_input_bundle({"contracts/bar.vy": BAR_CODE}) + + foo = compile_code( + FOO_CODE, + source_id=0, + contract_name="contracts/foo.vy", + output_formats=OUTPUT_FORMATS, + input_bundle=input_bundle, + ) + bar = compile_code( + BAR_CODE, source_id=1, contract_name="contracts/bar.vy", output_formats=OUTPUT_FORMATS + ) + + compile_code_results = {"contracts/bar.vy": bar, "contracts/foo.vy": foo} + + output_json = compile_json(input_json) + assert list(output_json["contracts"].keys()) == ["contracts/foo.vy", "contracts/bar.vy"] + + assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] + assert output_json["compiler"] == f"vyper-{vyper.__version__}" + + for source_id, contract_name in enumerate(["foo", "bar"]): + path = f"contracts/{contract_name}.vy" + data = compile_code_results[path] + assert output_json["sources"][path] == {"id": source_id, "ast": data["ast_dict"]["ast"]} + assert output_json["contracts"][path][contract_name] == { + "abi": data["abi"], + "devdoc": data["devdoc"], + "interface": data["interface"], + "ir": data["ir_dict"], + "userdoc": data["userdoc"], + "metadata": data["metadata"], + "evm": { + "bytecode": {"object": data["bytecode"], "opcodes": data["opcodes"]}, + "deployedBytecode": { + "object": data["bytecode_runtime"], + "opcodes": data["opcodes_runtime"], + "sourceMap": data["source_map"]["pc_pos_map_compressed"], + "sourceMapFull": data["source_map_full"], + }, + "methodIdentifiers": data["method_identifiers"], + }, + } + + +def test_different_outputs(make_input_bundle, input_json): + input_json["settings"]["outputSelection"] = { + "contracts/bar.vy": "*", + "contracts/foo.vy": ["evm.methodIdentifiers"], + } + output_json = compile_json(input_json) + assert list(output_json["contracts"].keys()) == ["contracts/foo.vy", "contracts/bar.vy"] + + assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] + assert output_json["compiler"] == f"vyper-{vyper.__version__}" + + contracts = output_json["contracts"] + + foo = contracts["contracts/foo.vy"]["foo"] + bar = contracts["contracts/bar.vy"]["bar"] + assert sorted(bar.keys()) == ["abi", "devdoc", "evm", "interface", "ir", "metadata", "userdoc"] + + assert sorted(foo.keys()) == ["evm"] + + # check method_identifiers + input_bundle = make_input_bundle({"contracts/bar.vy": BAR_CODE}) + method_identifiers = compile_code( + FOO_CODE, + contract_name="contracts/foo.vy", + output_formats=["method_identifiers"], + input_bundle=input_bundle, + )["method_identifiers"] + assert foo["evm"]["methodIdentifiers"] == method_identifiers + + +def test_root_folder_not_exists(input_json): + with pytest.raises(FileNotFoundError): + compile_json(input_json, root_folder="/path/that/does/not/exist") + + +def test_wrong_language(): + with pytest.raises(JSONError): + compile_json({"language": "Solidity"}) + + +def test_exc_handler_raises_syntax(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} + with pytest.raises(SyntaxException): + compile_json(input_json) + + +def test_exc_handler_to_dict_syntax(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} + result = compile_json(input_json, exc_handler_to_dict) + assert "errors" in result + assert len(result["errors"]) == 1 + error = result["errors"][0] + assert error["component"] == "compiler", error + assert error["type"] == "SyntaxException" + + +def test_exc_handler_raises_compiler(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} + with pytest.raises(InvalidType): + compile_json(input_json) + + +def test_exc_handler_to_dict_compiler(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} + result = compile_json(input_json, exc_handler_to_dict) + assert sorted(result.keys()) == ["compiler", "errors"] + assert result["compiler"] == f"vyper-{vyper.__version__}" + assert len(result["errors"]) == 1 + error = result["errors"][0] + assert error["component"] == "compiler" + assert error["type"] == "InvalidType" + + +def test_source_ids_increment(input_json): + input_json["settings"]["outputSelection"] = {"*": ["evm.deployedBytecode.sourceMap"]} + result = compile_json(input_json) + + def get(filename, contractname): + return result["contracts"][filename][contractname]["evm"]["deployedBytecode"]["sourceMap"] + + assert get("contracts/foo.vy", "foo").startswith("-1:-1:0") + assert get("contracts/bar.vy", "bar").startswith("-1:-1:1") + + +def test_relative_import_paths(input_json): + input_json["sources"]["contracts/potato/baz/baz.vy"] = {"content": """from ... import foo"""} + input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": """from . import baz"""} + input_json["sources"]["contracts/potato/footato.vy"] = {"content": """from baz import baz"""} + compile_from_input_dict(input_json) diff --git a/tests/unit/cli/vyper_json/test_get_inputs.py b/tests/unit/cli/vyper_json/test_get_inputs.py new file mode 100644 index 0000000000..6e323a91bd --- /dev/null +++ b/tests/unit/cli/vyper_json/test_get_inputs.py @@ -0,0 +1,142 @@ +from pathlib import PurePath + +import pytest + +from vyper.cli.vyper_json import get_compilation_targets, get_inputs +from vyper.exceptions import JSONError +from vyper.utils import keccak256 + +FOO_CODE = """ +import contracts.bar as Bar + +@external +def foo(a: address) -> bool: + return Bar(a).bar(1) +""" + +BAR_CODE = """ +@external +def bar(a: uint256) -> bool: + return True +""" + + +def test_no_sources(): + with pytest.raises(KeyError): + get_inputs({}) + + +def test_contracts_urls(): + with pytest.raises(JSONError): + get_inputs({"sources": {"foo.vy": {"urls": ["https://foo.code.com/"]}}}) + + +def test_contracts_no_content_key(): + with pytest.raises(JSONError): + get_inputs({"sources": {"foo.vy": FOO_CODE}}) + + +def test_contracts_keccak(): + hash_ = keccak256(FOO_CODE.encode()).hex() + + input_json = {"sources": {"foo.vy": {"content": FOO_CODE, "keccak256": hash_}}} + get_inputs(input_json) + + input_json["sources"]["foo.vy"]["keccak256"] = "0x" + hash_ + get_inputs(input_json) + + input_json["sources"]["foo.vy"]["keccak256"] = "0x1234567890" + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_contracts_outside_pwd(): + input_json = {"sources": {"../foo.vy": {"content": FOO_CODE}}} + get_inputs(input_json) + + +def test_contract_collision(): + # ./foo.vy and foo.vy will resolve to the same path + input_json = {"sources": {"./foo.vy": {"content": FOO_CODE}, "foo.vy": {"content": FOO_CODE}}} + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_contracts_return_value(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}} + } + result = get_inputs(input_json) + assert result == { + PurePath("foo.vy"): {"content": FOO_CODE}, + PurePath("contracts/bar.vy"): {"content": BAR_CODE}, + } + + +BAR_ABI = [ + { + "name": "bar", + "outputs": [{"type": "bool", "name": "out"}], + "inputs": [{"type": "uint256", "name": "a"}], + "stateMutability": "nonpayable", + "type": "function", + } +] + + +# tests to get interfaces from input dicts + + +def test_interface_collision(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.json": {"abi": BAR_ABI}, "bar.vy": {"content": BAR_CODE}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_json_no_abi(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.json": {"content": BAR_ABI}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_vy_no_content(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.vy": {"abi": BAR_CODE}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_interfaces_output(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": { + "bar.json": {"abi": BAR_ABI}, + "interface.folder/bar2.vy": {"content": BAR_CODE}, + }, + } + targets = get_compilation_targets(input_json) + assert targets == [PurePath("foo.vy")] + + result = get_inputs(input_json) + assert result == { + PurePath("foo.vy"): {"content": FOO_CODE}, + PurePath("bar.json"): {"abi": BAR_ABI}, + PurePath("interface.folder/bar2.vy"): {"content": BAR_CODE}, + } + + +# EIP-2678 -- not currently supported +@pytest.mark.xfail +def test_manifest_output(): + input_json = {"interfaces": {"bar.json": {"contractTypes": {"Bar": {"abi": BAR_ABI}}}}} + result = get_inputs(input_json) + assert isinstance(result, dict) + assert result == {"Bar": {"type": "json", "code": BAR_ABI}} diff --git a/tests/cli/vyper_json/test_get_settings.py b/tests/unit/cli/vyper_json/test_get_settings.py similarity index 97% rename from tests/cli/vyper_json/test_get_settings.py rename to tests/unit/cli/vyper_json/test_get_settings.py index bbe5dab113..989d4565cd 100644 --- a/tests/cli/vyper_json/test_get_settings.py +++ b/tests/unit/cli/vyper_json/test_get_settings.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import pytest from vyper.cli.vyper_json import get_evm_version diff --git a/tests/unit/cli/vyper_json/test_output_selection.py b/tests/unit/cli/vyper_json/test_output_selection.py new file mode 100644 index 0000000000..78ad7404f2 --- /dev/null +++ b/tests/unit/cli/vyper_json/test_output_selection.py @@ -0,0 +1,60 @@ +from pathlib import PurePath + +import pytest + +from vyper.cli.vyper_json import TRANSLATE_MAP, get_output_formats +from vyper.exceptions import JSONError + + +def test_no_outputs(): + with pytest.raises(KeyError): + get_output_formats({}, {}) + + +def test_invalid_output(): + input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "foobar"]}}} + targets = [PurePath("foo.vy")] + with pytest.raises(JSONError): + get_output_formats(input_json, targets) + + +def test_unknown_contract(): + input_json = {"settings": {"outputSelection": {"bar.vy": ["abi"]}}} + targets = [PurePath("foo.vy")] + with pytest.raises(JSONError): + get_output_formats(input_json, targets) + + +@pytest.mark.parametrize("output", TRANSLATE_MAP.items()) +def test_translate_map(output): + input_json = {"settings": {"outputSelection": {"foo.vy": [output[0]]}}} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): [output[1]]} + + +def test_star(): + input_json = {"settings": {"outputSelection": {"*": ["*"]}}} + targets = [PurePath("foo.vy"), PurePath("bar.vy")] + expected = sorted(set(TRANSLATE_MAP.values())) + result = get_output_formats(input_json, targets) + assert result == {PurePath("foo.vy"): expected, PurePath("bar.vy"): expected} + + +def test_evm(): + input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "evm"]}}} + targets = [PurePath("foo.vy")] + expected = ["abi"] + sorted(v for k, v in TRANSLATE_MAP.items() if k.startswith("evm")) + result = get_output_formats(input_json, targets) + assert result == {PurePath("foo.vy"): expected} + + +def test_solc_style(): + input_json = {"settings": {"outputSelection": {"foo.vy": {"": ["abi"], "foo.vy": ["ir"]}}}} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): ["abi", "ir_dict"]} + + +def test_metadata(): + input_json = {"settings": {"outputSelection": {"*": ["metadata"]}}} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): ["metadata"]} diff --git a/tests/cli/vyper_json/test_parse_args_vyperjson.py b/tests/unit/cli/vyper_json/test_parse_args_vyperjson.py similarity index 97% rename from tests/cli/vyper_json/test_parse_args_vyperjson.py rename to tests/unit/cli/vyper_json/test_parse_args_vyperjson.py index 11e527843a..3b0f700c7e 100644 --- a/tests/cli/vyper_json/test_parse_args_vyperjson.py +++ b/tests/unit/cli/vyper_json/test_parse_args_vyperjson.py @@ -29,7 +29,6 @@ def bar(a: uint256) -> bool: "inputs": [{"type": "uint256", "name": "a"}], "stateMutability": "nonpayable", "type": "function", - "gas": 313, } ] @@ -39,7 +38,7 @@ def bar(a: uint256) -> bool: "contracts/foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}, }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, + "interfaces": {"contracts/ibar.json": {"abi": BAR_ABI}}, "settings": {"outputSelection": {"*": ["*"]}}, } diff --git a/tests/compiler/__init__.py b/tests/unit/compiler/__init__.py similarity index 100% rename from tests/compiler/__init__.py rename to tests/unit/compiler/__init__.py diff --git a/tests/compiler/asm/test_asm_optimizer.py b/tests/unit/compiler/asm/test_asm_optimizer.py similarity index 100% rename from tests/compiler/asm/test_asm_optimizer.py rename to tests/unit/compiler/asm/test_asm_optimizer.py diff --git a/tests/unit/compiler/ir/__init__.py b/tests/unit/compiler/ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/compiler/test_calldatacopy.py b/tests/unit/compiler/ir/test_calldatacopy.py similarity index 100% rename from tests/compiler/test_calldatacopy.py rename to tests/unit/compiler/ir/test_calldatacopy.py diff --git a/tests/compiler/ir/test_compile_ir.py b/tests/unit/compiler/ir/test_compile_ir.py similarity index 100% rename from tests/compiler/ir/test_compile_ir.py rename to tests/unit/compiler/ir/test_compile_ir.py diff --git a/tests/compiler/ir/test_optimize_ir.py b/tests/unit/compiler/ir/test_optimize_ir.py similarity index 70% rename from tests/compiler/ir/test_optimize_ir.py rename to tests/unit/compiler/ir/test_optimize_ir.py index b679e55453..cb46ba238d 100644 --- a/tests/compiler/ir/test_optimize_ir.py +++ b/tests/unit/compiler/ir/test_optimize_ir.py @@ -1,9 +1,13 @@ import pytest from vyper.codegen.ir_node import IRnode +from vyper.evm.opcodes import EVM_VERSIONS, anchor_evm_version from vyper.exceptions import StaticAssertionException from vyper.ir import optimizer +POST_CANCUN = {k: v for k, v in EVM_VERSIONS.items() if v >= EVM_VERSIONS["cancun"]} + + optimize_list = [ (["eq", 1, 2], [0]), (["lt", 1, 2], [1]), @@ -143,7 +147,9 @@ (["sub", "x", 0], ["x"]), (["sub", "x", "x"], [0]), (["sub", ["sload", 0], ["sload", 0]], None), - (["sub", ["callvalue"], ["callvalue"]], None), + (["sub", ["callvalue"], ["callvalue"]], [0]), + (["sub", ["msize"], ["msize"]], None), + (["sub", ["gas"], ["gas"]], None), (["sub", -1, ["sload", 0]], ["not", ["sload", 0]]), (["mul", "x", 1], ["x"]), (["div", "x", 1], ["x"]), @@ -210,7 +216,9 @@ (["eq", -1, ["add", -(2**255), 2**255 - 1]], [1]), # test compile-time wrapping (["eq", -2, ["add", 2**256 - 1, 2**256 - 1]], [1]), # test compile-time wrapping (["eq", "x", "x"], [1]), - (["eq", "callvalue", "callvalue"], None), + (["eq", "gas", "gas"], None), + (["eq", "msize", "msize"], None), + (["eq", "callvalue", "callvalue"], [1]), (["ne", "x", "x"], [0]), ] @@ -268,3 +276,106 @@ def test_operator_set_values(): assert optimizer.COMPARISON_OPS == {"lt", "gt", "le", "ge", "slt", "sgt", "sle", "sge"} assert optimizer.STRICT_COMPARISON_OPS == {"lt", "gt", "slt", "sgt"} assert optimizer.UNSTRICT_COMPARISON_OPS == {"le", "ge", "sle", "sge"} + + +mload_merge_list = [ + # copy "backward" with no overlap between src and dst buffers, + # OK to become mcopy + ( + ["seq", ["mstore", 32, ["mload", 128]], ["mstore", 64, ["mload", 160]]], + ["mcopy", 32, 128, 64], + ), + # copy with overlap "backwards", OK to become mcopy + (["seq", ["mstore", 32, ["mload", 64]], ["mstore", 64, ["mload", 96]]], ["mcopy", 32, 64, 64]), + # "stationary" overlap (i.e. a no-op mcopy), OK to become mcopy + (["seq", ["mstore", 32, ["mload", 32]], ["mstore", 64, ["mload", 64]]], ["mcopy", 32, 32, 64]), + # copy "forward" with no overlap, OK to become mcopy + (["seq", ["mstore", 64, ["mload", 0]], ["mstore", 96, ["mload", 32]]], ["mcopy", 64, 0, 64]), + # copy "forwards" with overlap by one word, must NOT become mcopy + (["seq", ["mstore", 64, ["mload", 32]], ["mstore", 96, ["mload", 64]]], None), + # check "forward" overlap by one byte, must NOT become mcopy + (["seq", ["mstore", 64, ["mload", 1]], ["mstore", 96, ["mload", 33]]], None), + # check "forward" overlap by one byte again, must NOT become mcopy + (["seq", ["mstore", 63, ["mload", 0]], ["mstore", 95, ["mload", 32]]], None), + # copy 3 words with partial overlap "forwards", partially becomes mcopy + # (2 words are mcopied and 1 word is mload/mstored + ( + [ + "seq", + ["mstore", 96, ["mload", 32]], + ["mstore", 128, ["mload", 64]], + ["mstore", 160, ["mload", 96]], + ], + ["seq", ["mcopy", 96, 32, 64], ["mstore", 160, ["mload", 96]]], + ), + # copy 4 words with partial overlap "forwards", becomes 2 mcopies of 2 words each + ( + [ + "seq", + ["mstore", 96, ["mload", 32]], + ["mstore", 128, ["mload", 64]], + ["mstore", 160, ["mload", 96]], + ["mstore", 192, ["mload", 128]], + ], + ["seq", ["mcopy", 96, 32, 64], ["mcopy", 160, 96, 64]], + ), + # copy 4 words with 1 byte of overlap, must NOT become mcopy + ( + [ + "seq", + ["mstore", 96, ["mload", 33]], + ["mstore", 128, ["mload", 65]], + ["mstore", 160, ["mload", 97]], + ["mstore", 192, ["mload", 129]], + ], + None, + ), + # Ensure only sequential mstore + mload sequences are optimized + ( + [ + "seq", + ["mstore", 0, ["mload", 32]], + ["sstore", 0, ["calldataload", 4]], + ["mstore", 32, ["mload", 64]], + ], + None, + ), + # not-word aligned optimizations (not overlap) + (["seq", ["mstore", 0, ["mload", 1]], ["mstore", 32, ["mload", 33]]], ["mcopy", 0, 1, 64]), + # not-word aligned optimizations (overlap) + (["seq", ["mstore", 1, ["mload", 0]], ["mstore", 33, ["mload", 32]]], None), + # not-word aligned optimizations (overlap and not-overlap) + ( + [ + "seq", + ["mstore", 0, ["mload", 1]], + ["mstore", 32, ["mload", 33]], + ["mstore", 1, ["mload", 0]], + ["mstore", 33, ["mload", 32]], + ], + ["seq", ["mcopy", 0, 1, 64], ["mstore", 1, ["mload", 0]], ["mstore", 33, ["mload", 32]]], + ), + # overflow test + ( + [ + "seq", + ["mstore", 2**256 - 1 - 31 - 32, ["mload", 0]], + ["mstore", 2**256 - 1 - 31, ["mload", 32]], + ], + ["mcopy", 2**256 - 1 - 31 - 32, 0, 64], + ), +] + + +@pytest.mark.parametrize("ir", mload_merge_list) +@pytest.mark.parametrize("evm_version", list(POST_CANCUN.keys())) +def test_mload_merge(ir, evm_version): + with anchor_evm_version(evm_version): + optimized = optimizer.optimize(IRnode.from_list(ir[0])) + if ir[1] is None: + # no-op, assert optimizer does nothing + expected = IRnode.from_list(ir[0]) + else: + expected = IRnode.from_list(ir[1]) + + assert optimized == expected diff --git a/tests/compiler/ir/test_repeat.py b/tests/unit/compiler/ir/test_repeat.py similarity index 100% rename from tests/compiler/ir/test_repeat.py rename to tests/unit/compiler/ir/test_repeat.py diff --git a/tests/compiler/ir/test_with.py b/tests/unit/compiler/ir/test_with.py similarity index 100% rename from tests/compiler/ir/test_with.py rename to tests/unit/compiler/ir/test_with.py diff --git a/tests/compiler/test_bytecode_runtime.py b/tests/unit/compiler/test_bytecode_runtime.py similarity index 86% rename from tests/compiler/test_bytecode_runtime.py rename to tests/unit/compiler/test_bytecode_runtime.py index 9519b03772..613ee4d2b8 100644 --- a/tests/compiler/test_bytecode_runtime.py +++ b/tests/unit/compiler/test_bytecode_runtime.py @@ -48,14 +48,14 @@ def _parse_cbor_metadata(initcode): def test_bytecode_runtime(): - out = vyper.compile_code(simple_contract_code, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(simple_contract_code, output_formats=["bytecode_runtime", "bytecode"]) assert len(out["bytecode"]) > len(out["bytecode_runtime"]) assert out["bytecode_runtime"].removeprefix("0x") in out["bytecode"].removeprefix("0x") def test_bytecode_signature(): - out = vyper.compile_code(simple_contract_code, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(simple_contract_code, output_formats=["bytecode_runtime", "bytecode"]) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -72,7 +72,9 @@ def test_bytecode_signature(): def test_bytecode_signature_dense_jumptable(): settings = Settings(optimize=OptimizationLevel.CODESIZE) - out = vyper.compile_code(many_functions, ["bytecode_runtime", "bytecode"], settings=settings) + out = vyper.compile_code( + many_functions, output_formats=["bytecode_runtime", "bytecode"], settings=settings + ) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -89,7 +91,9 @@ def test_bytecode_signature_dense_jumptable(): def test_bytecode_signature_sparse_jumptable(): settings = Settings(optimize=OptimizationLevel.GAS) - out = vyper.compile_code(many_functions, ["bytecode_runtime", "bytecode"], settings=settings) + out = vyper.compile_code( + many_functions, output_formats=["bytecode_runtime", "bytecode"], settings=settings + ) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -104,7 +108,7 @@ def test_bytecode_signature_sparse_jumptable(): def test_bytecode_signature_immutables(): - out = vyper.compile_code(has_immutables, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(has_immutables, output_formats=["bytecode_runtime", "bytecode"]) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) diff --git a/tests/compiler/test_compile_code.py b/tests/unit/compiler/test_compile_code.py similarity index 99% rename from tests/compiler/test_compile_code.py rename to tests/unit/compiler/test_compile_code.py index cdbf9d1f52..7af133e362 100644 --- a/tests/compiler/test_compile_code.py +++ b/tests/unit/compiler/test_compile_code.py @@ -11,4 +11,4 @@ def a() -> bool: return True """ with pytest.warns(vyper.warnings.ContractSizeLimitWarning): - vyper.compile_code(code, ["bytecode_runtime"]) + vyper.compile_code(code, output_formats=["bytecode_runtime"]) diff --git a/tests/compiler/test_default_settings.py b/tests/unit/compiler/test_default_settings.py similarity index 100% rename from tests/compiler/test_default_settings.py rename to tests/unit/compiler/test_default_settings.py diff --git a/tests/unit/compiler/test_input_bundle.py b/tests/unit/compiler/test_input_bundle.py new file mode 100644 index 0000000000..c49c81219b --- /dev/null +++ b/tests/unit/compiler/test_input_bundle.py @@ -0,0 +1,208 @@ +import json +from pathlib import Path, PurePath + +import pytest + +from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, JSONInputBundle + + +# FilesystemInputBundle which uses same search path as make_file +@pytest.fixture +def input_bundle(tmp_path): + return FilesystemInputBundle([tmp_path]) + + +def test_load_file(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + + file = input_bundle.load_file(Path("foo.vy")) + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmp_path / Path("foo.vy"), "contents") + + +def test_search_path_context_manager(make_file, tmp_path): + ib = FilesystemInputBundle([]) + + make_file("foo.vy", "contents") + + with pytest.raises(FileNotFoundError): + # no search path given + ib.load_file(Path("foo.vy")) + + with ib.search_path(tmp_path): + file = ib.load_file(Path("foo.vy")) + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmp_path / Path("foo.vy"), "contents") + + +def test_search_path_precedence(make_file, tmp_path, tmp_path_factory, input_bundle): + # test search path precedence. + # most recent search path is the highest precedence + tmpdir = tmp_path_factory.mktemp("some_directory") + tmpdir2 = tmp_path_factory.mktemp("some_other_directory") + + for i, directory in enumerate([tmp_path, tmpdir, tmpdir2]): + with (directory / "foo.vy").open("w") as f: + f.write(f"contents {i}") + + ib = FilesystemInputBundle([tmp_path, tmpdir, tmpdir2]) + + file = ib.load_file("foo.vy") + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmpdir2 / "foo.vy", "contents 2") + + with ib.search_path(tmpdir): + file = ib.load_file("foo.vy") + + assert isinstance(file, FileInput) + assert file == FileInput(1, tmpdir / "foo.vy", "contents 1") + + +# special rules for handling json files +def test_load_abi(make_file, input_bundle, tmp_path): + contents = json.dumps("some string") + + make_file("foo.json", contents) + + file = input_bundle.load_file("foo.json") + assert isinstance(file, ABIInput) + assert file == ABIInput(0, tmp_path / "foo.json", "some string") + + # suffix doesn't matter + make_file("foo.txt", contents) + + file = input_bundle.load_file("foo.txt") + assert isinstance(file, ABIInput) + assert file == ABIInput(1, tmp_path / "foo.txt", "some string") + + +# check that unique paths give unique source ids +def test_source_id_file_input(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + make_file("bar.vy", "contents 2") + + file = input_bundle.load_file("foo.vy") + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "contents") + + file2 = input_bundle.load_file("bar.vy") + # source id increments + assert file2.source_id == 1 + assert file2 == FileInput(1, tmp_path / "bar.vy", "contents 2") + + file3 = input_bundle.load_file("foo.vy") + assert file3.source_id == 0 + assert file3 == FileInput(0, tmp_path / "foo.vy", "contents") + + +# check that unique paths give unique source ids +def test_source_id_json_input(make_file, input_bundle, tmp_path): + contents = json.dumps("some string") + contents2 = json.dumps(["some list"]) + + make_file("foo.json", contents) + + make_file("bar.json", contents2) + + file = input_bundle.load_file("foo.json") + assert isinstance(file, ABIInput) + assert file == ABIInput(0, tmp_path / "foo.json", "some string") + + file2 = input_bundle.load_file("bar.json") + assert isinstance(file2, ABIInput) + assert file2 == ABIInput(1, tmp_path / "bar.json", ["some list"]) + + file3 = input_bundle.load_file("foo.json") + assert isinstance(file3, ABIInput) + assert file3 == ABIInput(0, tmp_path / "foo.json", "some string") + + +# test some pathological case where the file changes underneath +def test_mutating_file_source_id(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + + file = input_bundle.load_file("foo.vy") + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "contents") + + make_file("foo.vy", "new contents") + + file = input_bundle.load_file("foo.vy") + # source id hasn't changed, even though contents have + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "new contents") + + +# test the os.normpath behavior of symlink +# (slightly pathological, for illustration's sake) +def test_load_file_symlink(make_file, input_bundle, tmp_path, tmp_path_factory): + dir1 = tmp_path / "first" + dir2 = tmp_path / "second" + symlink = tmp_path / "symlink" + + dir1.mkdir() + dir2.mkdir() + symlink.symlink_to(dir2, target_is_directory=True) + + with (tmp_path / "foo.vy").open("w") as f: + f.write("contents of the upper directory") + + with (dir1 / "foo.vy").open("w") as f: + f.write("contents of the inner directory") + + # symlink rules would be: + # base/symlink/../foo.vy => + # base/first/second/../foo.vy => + # base/first/foo.vy + # normpath would be base/symlink/../foo.vy => + # base/foo.vy + file = input_bundle.load_file(symlink / ".." / "foo.vy") + + assert file == FileInput(0, tmp_path / "foo.vy", "contents of the upper directory") + + +def test_json_input_bundle_basic(): + files = {PurePath("foo.vy"): {"content": "some text"}} + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + file = input_bundle.load_file(PurePath("foo.vy")) + assert file == FileInput(0, PurePath("foo.vy"), "some text") + + +def test_json_input_bundle_normpath(): + files = {PurePath("foo/../bar.vy"): {"content": "some text"}} + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + expected = FileInput(0, PurePath("bar.vy"), "some text") + + file = input_bundle.load_file(PurePath("bar.vy")) + assert file == expected + + file = input_bundle.load_file(PurePath("baz/../bar.vy")) + assert file == expected + + file = input_bundle.load_file(PurePath("./bar.vy")) + assert file == expected + + with input_bundle.search_path(PurePath("foo")): + file = input_bundle.load_file(PurePath("../bar.vy")) + assert file == expected + + +def test_json_input_abi(): + some_abi = ["some abi"] + some_abi_str = json.dumps(some_abi) + files = { + PurePath("foo.json"): {"abi": some_abi}, + PurePath("bar.txt"): {"content": some_abi_str}, + } + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + file = input_bundle.load_file(PurePath("foo.json")) + assert file == ABIInput(0, PurePath("foo.json"), some_abi) + + file = input_bundle.load_file(PurePath("bar.txt")) + assert file == ABIInput(1, PurePath("bar.txt"), some_abi) diff --git a/tests/compiler/test_opcodes.py b/tests/unit/compiler/test_opcodes.py similarity index 95% rename from tests/compiler/test_opcodes.py rename to tests/unit/compiler/test_opcodes.py index 20f45ced6b..15d2a617ba 100644 --- a/tests/compiler/test_opcodes.py +++ b/tests/unit/compiler/test_opcodes.py @@ -22,7 +22,7 @@ def a() -> bool: return True """ - out = vyper.compile_code(code, ["opcodes_runtime", "opcodes"]) + out = vyper.compile_code(code, output_formats=["opcodes_runtime", "opcodes"]) assert len(out["opcodes"]) > len(out["opcodes_runtime"]) assert out["opcodes_runtime"] in out["opcodes"] diff --git a/tests/compiler/test_pre_parser.py b/tests/unit/compiler/test_pre_parser.py similarity index 100% rename from tests/compiler/test_pre_parser.py rename to tests/unit/compiler/test_pre_parser.py diff --git a/tests/compiler/test_sha3_32.py b/tests/unit/compiler/test_sha3_32.py similarity index 100% rename from tests/compiler/test_sha3_32.py rename to tests/unit/compiler/test_sha3_32.py diff --git a/tests/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py similarity index 91% rename from tests/compiler/test_source_map.py rename to tests/unit/compiler/test_source_map.py index 886596bb80..c9a152b09c 100644 --- a/tests/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -28,7 +28,7 @@ def foo(a: uint256) -> int128: def test_jump_map(): - source_map = compile_code(TEST_CODE, ["source_map"])["source_map"] + source_map = compile_code(TEST_CODE, output_formats=["source_map"])["source_map"] pos_map = source_map["pc_pos_map"] jump_map = source_map["pc_jump_map"] @@ -46,7 +46,7 @@ def test_jump_map(): def test_pos_map_offsets(): - source_map = compile_code(TEST_CODE, ["source_map"])["source_map"] + source_map = compile_code(TEST_CODE, output_formats=["source_map"])["source_map"] expanded = expand_source_map(source_map["pc_pos_map_compressed"]) pc_iter = iter(source_map["pc_pos_map"][i] for i in sorted(source_map["pc_pos_map"])) @@ -76,7 +76,7 @@ def test_error_map(): def update_foo(): self.foo += 1 """ - error_map = compile_code(code, ["source_map"])["source_map"]["error_map"] + error_map = compile_code(code, output_formats=["source_map"])["source_map"]["error_map"] assert "safeadd" in list(error_map.values()) assert "fallback function" in list(error_map.values()) diff --git a/tests/functional/semantics/analysis/test_array_index.py b/tests/unit/semantics/analysis/test_array_index.py similarity index 100% rename from tests/functional/semantics/analysis/test_array_index.py rename to tests/unit/semantics/analysis/test_array_index.py diff --git a/tests/functional/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py similarity index 100% rename from tests/functional/semantics/analysis/test_cyclic_function_calls.py rename to tests/unit/semantics/analysis/test_cyclic_function_calls.py diff --git a/tests/functional/semantics/analysis/test_for_loop.py b/tests/unit/semantics/analysis/test_for_loop.py similarity index 100% rename from tests/functional/semantics/analysis/test_for_loop.py rename to tests/unit/semantics/analysis/test_for_loop.py diff --git a/tests/functional/semantics/analysis/test_potential_types.py b/tests/unit/semantics/analysis/test_potential_types.py similarity index 100% rename from tests/functional/semantics/analysis/test_potential_types.py rename to tests/unit/semantics/analysis/test_potential_types.py diff --git a/tests/functional/semantics/conftest.py b/tests/unit/semantics/conftest.py similarity index 100% rename from tests/functional/semantics/conftest.py rename to tests/unit/semantics/conftest.py diff --git a/tests/functional/semantics/test_namespace.py b/tests/unit/semantics/test_namespace.py similarity index 100% rename from tests/functional/semantics/test_namespace.py rename to tests/unit/semantics/test_namespace.py diff --git a/tests/functional/test_storage_slots.py b/tests/unit/semantics/test_storage_slots.py similarity index 100% rename from tests/functional/test_storage_slots.py rename to tests/unit/semantics/test_storage_slots.py diff --git a/tests/functional/semantics/types/test_event.py b/tests/unit/semantics/types/test_event.py similarity index 100% rename from tests/functional/semantics/types/test_event.py rename to tests/unit/semantics/types/test_event.py diff --git a/tests/functional/semantics/types/test_pure_types.py b/tests/unit/semantics/types/test_pure_types.py similarity index 100% rename from tests/functional/semantics/types/test_pure_types.py rename to tests/unit/semantics/types/test_pure_types.py diff --git a/tests/functional/semantics/types/test_size_in_bytes.py b/tests/unit/semantics/types/test_size_in_bytes.py similarity index 100% rename from tests/functional/semantics/types/test_size_in_bytes.py rename to tests/unit/semantics/types/test_size_in_bytes.py diff --git a/tests/functional/semantics/types/test_type_from_abi.py b/tests/unit/semantics/types/test_type_from_abi.py similarity index 100% rename from tests/functional/semantics/types/test_type_from_abi.py rename to tests/unit/semantics/types/test_type_from_abi.py diff --git a/tests/functional/semantics/types/test_type_from_annotation.py b/tests/unit/semantics/types/test_type_from_annotation.py similarity index 100% rename from tests/functional/semantics/types/test_type_from_annotation.py rename to tests/unit/semantics/types/test_type_from_annotation.py diff --git a/vyper/__init__.py b/vyper/__init__.py index 35237bd044..482d5c3a60 100644 --- a/vyper/__init__.py +++ b/vyper/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path as _Path -from vyper.compiler import compile_code, compile_codes # noqa: F401 +from vyper.compiler import compile_code # noqa: F401 try: from importlib.metadata import PackageNotFoundError # type: ignore diff --git a/vyper/abi_types.py b/vyper/abi_types.py index b272996aed..051f8db19f 100644 --- a/vyper/abi_types.py +++ b/vyper/abi_types.py @@ -1,4 +1,4 @@ -from vyper.exceptions import CompilerPanic +from vyper.exceptions import InvalidABIType from vyper.utils import ceil32 @@ -69,7 +69,7 @@ def __repr__(self): class ABI_GIntM(ABIType): def __init__(self, m_bits, signed): if not (0 < m_bits <= 256 and 0 == m_bits % 8): - raise CompilerPanic("Invalid M provided for GIntM") + raise InvalidABIType("Invalid M provided for GIntM") self.m_bits = m_bits self.signed = signed @@ -117,9 +117,9 @@ def selector_name(self): class ABI_FixedMxN(ABIType): def __init__(self, m_bits, n_places, signed): if not (0 < m_bits <= 256 and 0 == m_bits % 8): - raise CompilerPanic("Invalid M for FixedMxN") + raise InvalidABIType("Invalid M for FixedMxN") if not (0 < n_places and n_places <= 80): - raise CompilerPanic("Invalid N for FixedMxN") + raise InvalidABIType("Invalid N for FixedMxN") self.m_bits = m_bits self.n_places = n_places @@ -142,7 +142,7 @@ def is_complex_type(self): class ABI_BytesM(ABIType): def __init__(self, m_bytes): if not 0 < m_bytes <= 32: - raise CompilerPanic("Invalid M for BytesM") + raise InvalidABIType("Invalid M for BytesM") self.m_bytes = m_bytes @@ -173,7 +173,7 @@ def selector_name(self): class ABI_StaticArray(ABIType): def __init__(self, subtyp, m_elems): if not m_elems >= 0: - raise CompilerPanic("Invalid M") + raise InvalidABIType("Invalid M") self.subtyp = subtyp self.m_elems = m_elems @@ -200,7 +200,7 @@ def is_complex_type(self): class ABI_Bytes(ABIType): def __init__(self, bytes_bound): if not bytes_bound >= 0: - raise CompilerPanic("Negative bytes_bound provided to ABI_Bytes") + raise InvalidABIType("Negative bytes_bound provided to ABI_Bytes") self.bytes_bound = bytes_bound @@ -234,7 +234,7 @@ def selector_name(self): class ABI_DynamicArray(ABIType): def __init__(self, subtyp, elems_bound): if not elems_bound >= 0: - raise CompilerPanic("Negative bound provided to DynamicArray") + raise InvalidABIType("Negative bound provided to DynamicArray") self.subtyp = subtyp self.elems_bound = elems_bound diff --git a/vyper/ast/folding.py b/vyper/ast/folding.py index fbd1dfc2f4..38d58f6fd0 100644 --- a/vyper/ast/folding.py +++ b/vyper/ast/folding.py @@ -1,4 +1,3 @@ -import warnings from typing import Optional, Union from vyper.ast import nodes as vy_ast @@ -6,21 +5,6 @@ from vyper.exceptions import UnfoldableNode, UnknownType from vyper.semantics.types.base import VyperType from vyper.semantics.types.utils import type_from_annotation -from vyper.utils import SizeLimits - -BUILTIN_CONSTANTS = { - "EMPTY_BYTES32": ( - vy_ast.Hex, - "0x0000000000000000000000000000000000000000000000000000000000000000", - "empty(bytes32)", - ), # NOQA: E501 - "ZERO_ADDRESS": (vy_ast.Hex, "0x0000000000000000000000000000000000000000", "empty(address)"), - "MAX_INT128": (vy_ast.Int, 2**127 - 1, "max_value(int128)"), - "MIN_INT128": (vy_ast.Int, -(2**127), "min_value(int128)"), - "MAX_DECIMAL": (vy_ast.Decimal, SizeLimits.MAX_AST_DECIMAL, "max_value(decimal)"), - "MIN_DECIMAL": (vy_ast.Decimal, SizeLimits.MIN_AST_DECIMAL, "min_value(decimal)"), - "MAX_UINT256": (vy_ast.Int, 2**256 - 1, "max_value(uint256)"), -} def fold(vyper_module: vy_ast.Module) -> None: @@ -32,8 +16,6 @@ def fold(vyper_module: vy_ast.Module) -> None: vyper_module : Module Top-level Vyper AST node. """ - replace_builtin_constants(vyper_module) - changed_nodes = 1 while changed_nodes: changed_nodes = 0 @@ -138,21 +120,6 @@ def replace_builtin_functions(vyper_module: vy_ast.Module) -> int: return changed_nodes -def replace_builtin_constants(vyper_module: vy_ast.Module) -> None: - """ - Replace references to builtin constants with their literal values. - - Arguments - --------- - vyper_module : Module - Top-level Vyper AST node. - """ - for name, (node, value, replacement) in BUILTIN_CONSTANTS.items(): - found = replace_constant(vyper_module, name, node(value=value), True) - if found > 0: - warnings.warn(f"{name} is deprecated. Please use `{replacement}` instead.") - - def replace_user_defined_constants(vyper_module: vy_ast.Module) -> int: """ Find user-defined constant assignments, and replace references diff --git a/vyper/ast/nodes.pyi b/vyper/ast/nodes.pyi index 0d59a2fa63..47c9af8526 100644 --- a/vyper/ast/nodes.pyi +++ b/vyper/ast/nodes.pyi @@ -142,6 +142,7 @@ class Expr(VyperNode): class UnaryOp(ExprNode): op: VyperNode = ... + operand: VyperNode = ... class USub(VyperNode): ... class Not(VyperNode): ... @@ -165,12 +166,15 @@ class BitXor(VyperNode): ... class BoolOp(ExprNode): op: VyperNode = ... + values: list[VyperNode] = ... class And(VyperNode): ... class Or(VyperNode): ... class Compare(ExprNode): op: VyperNode = ... + left: VyperNode = ... + right: VyperNode = ... class Eq(VyperNode): ... class NotEq(VyperNode): ... @@ -179,6 +183,7 @@ class LtE(VyperNode): ... class Gt(VyperNode): ... class GtE(VyperNode): ... class In(VyperNode): ... +class NotIn(VyperNode): ... class Call(ExprNode): args: list = ... diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index 0ead889787..9d96efea5e 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -111,7 +111,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, str]: validate_version_pragma(compiler_version, start) settings.compiler_version = compiler_version - if pragma.startswith("optimize "): + elif pragma.startswith("optimize "): if settings.optimize is not None: raise StructureException("pragma optimize specified twice!", start) try: @@ -119,7 +119,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, str]: settings.optimize = OptimizationLevel.from_string(mode) except ValueError: raise StructureException(f"Invalid optimization mode `{mode}`", start) - if pragma.startswith("evm-version "): + elif 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() @@ -127,6 +127,9 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, str]: raise StructureException("Invalid evm version: `{evm_version}`", start) settings.evm_version = evm_version + else: + raise StructureException(f"Unknown pragma `{pragma.split()[0]}`") + if typ == NAME and string in ("class", "yield"): raise SyntaxException( f"The `{string}` keyword is not allowed. ", code, start[0], start[1] diff --git a/vyper/builtins/_signatures.py b/vyper/builtins/_signatures.py index d39a4a085f..2802421129 100644 --- a/vyper/builtins/_signatures.py +++ b/vyper/builtins/_signatures.py @@ -74,7 +74,7 @@ def decorator_fn(self, node, context): return decorator_fn -class BuiltinFunction: +class BuiltinFunction(VyperType): _has_varargs = False _kwargs: Dict[str, KwargSettings] = {} diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 3ec8f69934..001939638b 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -21,6 +21,7 @@ clamp_basetype, clamp_nonzero, copy_bytes, + dummy_node_for_type, ensure_in_memory, eval_once_check, eval_seq, @@ -28,7 +29,6 @@ get_type_for_exact_size, ir_tuple_from_args, make_setter, - needs_external_call_wrap, promote_signed_int, sar, shl, @@ -36,7 +36,7 @@ unwrap_location, ) from vyper.codegen.expr import Expr -from vyper.codegen.ir_node import Encoding +from vyper.codegen.ir_node import Encoding, scope_multi from vyper.codegen.keccak256_helper import keccak256_helper from vyper.evm.address_space import MEMORY, STORAGE from vyper.exceptions import ( @@ -475,6 +475,12 @@ def evaluate(self, node): return vy_ast.Int.from_node(node, value=length) + def infer_arg_types(self, node): + self._validate_arg_types(node) + # return a concrete type + typ = get_possible_types_from_node(node.args[0]).pop() + return [typ] + def build_IR(self, node, context): arg = Expr(node.args[0], context).ir_node if arg.value == "~calldata": @@ -1155,14 +1161,17 @@ def build_IR(self, expr, args, kwargs, context): outsize, ] - if delegate_call: - call_op = ["delegatecall", gas, to, *common_call_args] - elif static_call: - call_op = ["staticcall", gas, to, *common_call_args] - else: - call_op = ["call", gas, to, value, *common_call_args] + gas, value = IRnode.from_list(gas), IRnode.from_list(value) + with scope_multi((to, value, gas), ("_to", "_value", "_gas")) as (b1, (to, value, gas)): + if delegate_call: + call_op = ["delegatecall", gas, to, *common_call_args] + elif static_call: + call_op = ["staticcall", gas, to, *common_call_args] + else: + call_op = ["call", gas, to, value, *common_call_args] - call_ir += [call_op] + call_ir += [call_op] + call_ir = b1.resolve(call_ir) # build sequence IR if outsize: @@ -1414,7 +1423,7 @@ class BitwiseNot(BuiltinFunction): def evaluate(self, node): if not self.__class__._warned: - vyper_warn("`bitwise_not()` is deprecated! Please use the ^ operator instead.") + vyper_warn("`bitwise_not()` is deprecated! Please use the ~ operator instead.") self.__class__._warned = True validate_call_args(node, 1) @@ -1589,13 +1598,15 @@ def build_IR(self, expr, context): # CREATE* functions +CREATE2_SENTINEL = dummy_node_for_type(BYTES32_T) + # create helper functions # generates CREATE op sequence + zero check for result -def _create_ir(value, buf, length, salt=None, checked=True): +def _create_ir(value, buf, length, salt, checked=True): args = [value, buf, length] create_op = "create" - if salt is not None: + if salt is not CREATE2_SENTINEL: create_op = "create2" args.append(salt) @@ -1713,8 +1724,9 @@ def build_IR(self, expr, args, kwargs, context): context.check_is_not_constant("use {self._id}", expr) should_use_create2 = "salt" in [kwarg.arg for kwarg in expr.keywords] + if not should_use_create2: - kwargs["salt"] = None + kwargs["salt"] = CREATE2_SENTINEL ir_builder = self._build_create_IR(expr, args, context, **kwargs) @@ -1794,13 +1806,16 @@ def _add_gas_estimate(self, args, should_use_create2): def _build_create_IR(self, expr, args, context, value, salt): target = args[0] - with target.cache_when_complex("create_target") as (b1, target): + # something we can pass to scope_multi + with scope_multi( + (target, value, salt), ("create_target", "create_value", "create_salt") + ) as (b1, (target, value, salt)): codesize = IRnode.from_list(["extcodesize", target]) msize = IRnode.from_list(["msize"]) - with codesize.cache_when_complex("target_codesize") as ( + with scope_multi((codesize, msize), ("target_codesize", "mem_ofst")) as ( b2, - codesize, - ), msize.cache_when_complex("mem_ofst") as (b3, mem_ofst): + (codesize, mem_ofst), + ): ir = ["seq"] # make sure there is actually code at the target @@ -1824,7 +1839,7 @@ def _build_create_IR(self, expr, args, context, value, salt): ir.append(_create_ir(value, buf, buf_len, salt)) - return b1.resolve(b2.resolve(b3.resolve(ir))) + return b1.resolve(b2.resolve(ir)) class CreateFromBlueprint(_CreateBase): @@ -1877,17 +1892,18 @@ def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_ar # (since the abi encoder could write to fresh memory). # it would be good to not require the memory copy, but need # to evaluate memory safety. - with target.cache_when_complex("create_target") as (b1, target), argslen.cache_when_complex( - "encoded_args_len" - ) as (b2, encoded_args_len), code_offset.cache_when_complex("code_ofst") as (b3, codeofst): - codesize = IRnode.from_list(["sub", ["extcodesize", target], codeofst]) + with scope_multi( + (target, value, salt, argslen, code_offset), + ("create_target", "create_value", "create_salt", "encoded_args_len", "code_offset"), + ) as (b1, (target, value, salt, encoded_args_len, code_offset)): + codesize = IRnode.from_list(["sub", ["extcodesize", target], code_offset]) # copy code to memory starting from msize. we are clobbering # unused memory so it's safe. msize = IRnode.from_list(["msize"], location=MEMORY) - with codesize.cache_when_complex("target_codesize") as ( - b4, - codesize, - ), msize.cache_when_complex("mem_ofst") as (b5, mem_ofst): + with scope_multi((codesize, msize), ("target_codesize", "mem_ofst")) as ( + b2, + (codesize, mem_ofst), + ): ir = ["seq"] # make sure there is code at the target, and that @@ -1906,9 +1922,8 @@ def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_ar # copy the target code into memory. # layout starting from mem_ofst: - # 00...00 (22 0's) | preamble | bytecode - ir.append(["extcodecopy", target, mem_ofst, codeofst, codesize]) - + # | + ir.append(["extcodecopy", target, mem_ofst, code_offset, codesize]) ir.append(copy_bytes(add_ofst(mem_ofst, codesize), argbuf, encoded_args_len, bufsz)) # theoretically, dst = "msize", but just be safe. @@ -1922,7 +1937,7 @@ def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_ar ir.append(_create_ir(value, mem_ofst, length, salt)) - return b1.resolve(b2.resolve(b3.resolve(b4.resolve(b5.resolve(ir))))) + return b1.resolve(b2.resolve(ir)) class _UnsafeMath(BuiltinFunction): @@ -2357,8 +2372,6 @@ def build_IR(self, expr, args, kwargs, context): class ABIEncode(BuiltinFunction): _id = "_abi_encode" # TODO prettier to rename this to abi.encode # signature: *, ensure_tuple= -> Bytes[] - # (check the signature manually since we have no utility methods - # to handle varargs.) # explanation of ensure_tuple: # default is to force even a single value into a tuple, # e.g. _abi_encode(bytes) -> _abi_encode((bytes,)) @@ -2483,6 +2496,8 @@ def fetch_call_return(self, node): return output_type.typedef def infer_arg_types(self, node): + self._validate_arg_types(node) + validate_call_args(node, 2, ["unwrap_tuple"]) data_type = get_exact_type_from_node(node.args[0]) @@ -2519,24 +2534,11 @@ def build_IR(self, expr, args, kwargs, context): ) data = ensure_in_memory(data, context) + with data.cache_when_complex("to_decode") as (b1, data): data_ptr = bytes_data_ptr(data) data_len = get_bytearray_length(data) - # Normally, ABI-encoded data assumes the argument is a tuple - # (See comments for `wrap_value_for_external_return`) - # However, we do not want to use `wrap_value_for_external_return` - # technique as used in external call codegen because in order to be - # type-safe we would need an extra memory copy. To avoid a copy, - # we manually add the ABI-dynamic offset so that it is - # re-interpreted in-place. - if ( - unwrap_tuple is True - and needs_external_call_wrap(output_typ) - and output_typ.abi_type.is_dynamic() - ): - data_ptr = add_ofst(data_ptr, 32) - ret = ["seq"] if abi_min_size == abi_size_bound: @@ -2545,18 +2547,30 @@ def build_IR(self, expr, args, kwargs, context): # runtime assert: abi_min_size <= data_len <= abi_size_bound ret.append(clamp2(abi_min_size, data_len, abi_size_bound, signed=False)) - # return pointer to the buffer - ret.append(data_ptr) - - return b1.resolve( - IRnode.from_list( - ret, - typ=output_typ, - location=data.location, - encoding=Encoding.ABI, - annotation=f"abi_decode({output_typ})", - ) + to_decode = IRnode.from_list( + data_ptr, + typ=wrapped_typ, + location=data.location, + encoding=Encoding.ABI, + annotation=f"abi_decode({output_typ})", ) + to_decode.encoding = Encoding.ABI + + # TODO optimization: skip make_setter when we don't need + # input validation + + output_buf = context.new_internal_variable(wrapped_typ) + output = IRnode.from_list(output_buf, typ=wrapped_typ, location=MEMORY) + + # sanity check buffer size for wrapped output type will not buffer overflow + assert wrapped_typ.memory_bytes_required == output_typ.memory_bytes_required + ret.append(make_setter(output, to_decode)) + + ret.append(output) + # finalize. set the type and location for the return buffer. + # (note: unwraps the tuple type if necessary) + ret = IRnode.from_list(ret, typ=output_typ, location=MEMORY) + return b1.resolve(ret) class _MinMaxValue(TypenameFoldedFunction): @@ -2575,7 +2589,6 @@ def evaluate(self, node): if isinstance(input_type, IntegerT): ret = vy_ast.Int.from_node(node, value=val) - # TODO: to change to known_type once #3213 is merged ret._metadata["type"] = input_type return ret diff --git a/vyper/builtins/interfaces/ERC165.py b/vyper/builtins/interfaces/ERC165.vy similarity index 75% rename from vyper/builtins/interfaces/ERC165.py rename to vyper/builtins/interfaces/ERC165.vy index 0a75431f3c..a4ca451abd 100644 --- a/vyper/builtins/interfaces/ERC165.py +++ b/vyper/builtins/interfaces/ERC165.vy @@ -1,6 +1,4 @@ -interface_code = """ @view @external def supportsInterface(interface_id: bytes4) -> bool: pass -""" diff --git a/vyper/builtins/interfaces/ERC20.py b/vyper/builtins/interfaces/ERC20.vy similarity index 96% rename from vyper/builtins/interfaces/ERC20.py rename to vyper/builtins/interfaces/ERC20.vy index a63408672b..065ca97a9b 100644 --- a/vyper/builtins/interfaces/ERC20.py +++ b/vyper/builtins/interfaces/ERC20.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Transfer: _from: indexed(address) @@ -37,4 +36,3 @@ def transferFrom(_from: address, _to: address, _value: uint256) -> bool: @external def approve(_spender: address, _value: uint256) -> bool: pass -""" diff --git a/vyper/builtins/interfaces/ERC20Detailed.py b/vyper/builtins/interfaces/ERC20Detailed.py deleted file mode 100644 index 03dd597e8a..0000000000 --- a/vyper/builtins/interfaces/ERC20Detailed.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -NOTE: interface uses `String[1]` where 1 is the lower bound of the string returned by the function. - For end-users this means they can't use `implements: ERC20Detailed` unless their implementation - uses a value n >= 1. Regardless this is fine as one can't do String[0] where n == 0. -""" - -interface_code = """ -@view -@external -def name() -> String[1]: - pass - -@view -@external -def symbol() -> String[1]: - pass - -@view -@external -def decimals() -> uint8: - pass -""" diff --git a/vyper/builtins/interfaces/ERC20Detailed.vy b/vyper/builtins/interfaces/ERC20Detailed.vy new file mode 100644 index 0000000000..7c4f546d45 --- /dev/null +++ b/vyper/builtins/interfaces/ERC20Detailed.vy @@ -0,0 +1,18 @@ +#NOTE: interface uses `String[1]` where 1 is the lower bound of the string returned by the function. +# For end-users this means they can't use `implements: ERC20Detailed` unless their implementation +# uses a value n >= 1. Regardless this is fine as one can't do String[0] where n == 0. + +@view +@external +def name() -> String[1]: + pass + +@view +@external +def symbol() -> String[1]: + pass + +@view +@external +def decimals() -> uint8: + pass diff --git a/vyper/builtins/interfaces/ERC4626.py b/vyper/builtins/interfaces/ERC4626.vy similarity index 98% rename from vyper/builtins/interfaces/ERC4626.py rename to vyper/builtins/interfaces/ERC4626.vy index 21a9ce723a..05865406cf 100644 --- a/vyper/builtins/interfaces/ERC4626.py +++ b/vyper/builtins/interfaces/ERC4626.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Deposit: sender: indexed(address) @@ -89,4 +88,3 @@ def previewRedeem(shares: uint256) -> uint256: @external def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256: pass -""" diff --git a/vyper/builtins/interfaces/ERC721.py b/vyper/builtins/interfaces/ERC721.vy similarity index 97% rename from vyper/builtins/interfaces/ERC721.py rename to vyper/builtins/interfaces/ERC721.vy index 8dea4e4976..464c0e255b 100644 --- a/vyper/builtins/interfaces/ERC721.py +++ b/vyper/builtins/interfaces/ERC721.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Transfer: @@ -66,5 +65,3 @@ def approve(_approved: address, _tokenId: uint256): @external def setApprovalForAll(_operator: address, _approved: bool): pass - -""" diff --git a/vyper/cli/utils.py b/vyper/cli/utils.py deleted file mode 100644 index 1110ecdfdd..0000000000 --- a/vyper/cli/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path -from typing import Sequence - -from vyper import ast as vy_ast -from vyper.exceptions import StructureException -from vyper.typing import InterfaceImports, SourceCode - - -def get_interface_file_path(base_paths: Sequence, import_path: str) -> Path: - relative_path = Path(import_path) - for path in base_paths: - # Find ABI JSON files - file_path = path.joinpath(relative_path) - suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None) - if suffix: - return file_path.with_suffix(suffix) - - # Find ethPM Manifest files (`from path.to.Manifest import InterfaceName`) - # NOTE: Use file parent because this assumes that `file_path` - # coincides with an ABI interface file - file_path = file_path.parent - suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None) - if suffix: - return file_path.with_suffix(suffix) - - raise FileNotFoundError(f" Cannot locate interface '{import_path}{{.vy,.json}}'") - - -def extract_file_interface_imports(code: SourceCode) -> InterfaceImports: - ast_tree = vy_ast.parse_to_ast(code) - - imports_dict: InterfaceImports = {} - for node in ast_tree.get_children((vy_ast.Import, vy_ast.ImportFrom)): - if isinstance(node, vy_ast.Import): # type: ignore - if not node.alias: - raise StructureException("Import requires an accompanying `as` statement", node) - if node.alias in imports_dict: - raise StructureException(f"Interface with alias {node.alias} already exists", node) - imports_dict[node.alias] = node.name.replace(".", "/") - elif isinstance(node, vy_ast.ImportFrom): # type: ignore - level = node.level # type: ignore - module = node.module or "" # type: ignore - if not level and module == "vyper.interfaces": - # uses a builtin interface, so skip adding to imports - continue - - base_path = "" - if level > 1: - base_path = "../" * (level - 1) - elif level == 1: - base_path = "./" - base_path = f"{base_path}{module.replace('.','/')}/" - - if node.name in imports_dict and imports_dict[node.name] != f"{base_path}{node.name}": - raise StructureException(f"Interface with name {node.name} already exists", node) - imports_dict[node.name] = f"{base_path}{node.name}" - - return imports_dict diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index bdd01eebbe..82eba63f32 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -3,14 +3,13 @@ import json import sys import warnings -from collections import OrderedDict from pathlib import Path -from typing import Dict, Iterable, Iterator, Optional, Set, TypeVar +from typing import Any, 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.input_bundle import FileInput, FilesystemInputBundle from vyper.compiler.settings import ( VYPER_TRACEBACK_LIMIT, OptimizationLevel, @@ -18,7 +17,7 @@ _set_debug_mode, ) from vyper.evm.opcodes import DEFAULT_EVM_VERSION, EVM_VERSIONS -from vyper.typing import ContractCodes, ContractPath, OutputFormats +from vyper.typing import ContractPath, OutputFormats T = TypeVar("T") @@ -43,7 +42,6 @@ ir_json - Intermediate representation in JSON format ir_runtime - Intermediate representation of runtime bytecode in list format asm - Output the EVM assembly of the deployable bytecode -hex-ir - Output IR and assembly constants in hex instead of decimal """ combined_json_outputs = [ @@ -219,94 +217,20 @@ def exc_handler(contract_path: ContractPath, exception: Exception) -> None: raise exception -def get_interface_codes(root_path: Path, contract_sources: ContractCodes) -> Dict: - interface_codes: Dict = {} - interfaces: Dict = {} - - for file_path, code in contract_sources.items(): - interfaces[file_path] = {} - parent_path = root_path.joinpath(file_path).parent - - interface_codes = extract_file_interface_imports(code) - for interface_name, interface_path in interface_codes.items(): - base_paths = [parent_path] - if not interface_path.startswith(".") and root_path.joinpath(file_path).exists(): - base_paths.append(root_path) - elif interface_path.startswith("../") and len(Path(file_path).parent.parts) < Path( - interface_path - ).parts.count(".."): - raise FileNotFoundError( - f"{file_path} - Cannot perform relative import outside of base folder" - ) - - valid_path = get_interface_file_path(base_paths, interface_path) - with valid_path.open() as fh: - code = fh.read() - if valid_path.suffix == ".json": - contents = json.loads(code.encode()) - - # EthPM Manifest (EIP-2678) - if "contractTypes" in contents: - if ( - interface_name not in contents["contractTypes"] - or "abi" not in contents["contractTypes"][interface_name] - ): - raise ValueError( - f"Could not find interface '{interface_name}'" - f" in manifest '{valid_path}'." - ) - - interfaces[file_path][interface_name] = { - "type": "json", - "code": contents["contractTypes"][interface_name]["abi"], - } - - # ABI JSON file (either `List[ABI]` or `{"abi": List[ABI]}`) - elif isinstance(contents, list) or ( - "abi" in contents and isinstance(contents["abi"], list) - ): - interfaces[file_path][interface_name] = {"type": "json", "code": contents} - - else: - raise ValueError(f"Corrupted file: '{valid_path}'") - - else: - interfaces[file_path][interface_name] = {"type": "vyper", "code": code} - - return interfaces - - def compile_files( - input_files: Iterable[str], + input_files: list[str], output_formats: OutputFormats, root_folder: str = ".", show_gas_estimates: bool = False, settings: Optional[Settings] = None, - storage_layout: Optional[Iterable[str]] = None, + storage_layout_paths: list[str] = None, no_bytecode_metadata: bool = False, -) -> OrderedDict: +) -> dict: root_path = Path(root_folder).resolve() if not root_path.exists(): raise FileNotFoundError(f"Invalid root path - '{root_path.as_posix()}' does not exist") - contract_sources: ContractCodes = OrderedDict() - for file_name in input_files: - file_path = Path(file_name) - try: - file_str = file_path.resolve().relative_to(root_path).as_posix() - except ValueError: - file_str = file_path.as_posix() - with file_path.open() as fh: - # trailing newline fixes python parsing bug when source ends in a comment - # https://bugs.python.org/issue35107 - contract_sources[file_str] = fh.read() + "\n" - - storage_layouts = OrderedDict() - if storage_layout: - for storage_file_name, contract_name in zip(storage_layout, contract_sources.keys()): - storage_file_path = Path(storage_file_name) - with storage_file_path.open() as sfh: - storage_layouts[contract_name] = json.load(sfh) + input_bundle = FilesystemInputBundle([root_path]) show_version = False if "combined_json" in output_formats: @@ -318,20 +242,44 @@ def compile_files( translate_map = {"abi_python": "abi", "json": "abi", "ast": "ast_dict", "ir_json": "ir_dict"} final_formats = [translate_map.get(i, i) for i in output_formats] - compiler_data = vyper.compile_codes( - contract_sources, - final_formats, - exc_handler=exc_handler, - interface_codes=get_interface_codes(root_path, contract_sources), - settings=settings, - storage_layouts=storage_layouts, - show_gas_estimates=show_gas_estimates, - no_bytecode_metadata=no_bytecode_metadata, - ) + if storage_layout_paths: + if len(storage_layout_paths) != len(input_files): + raise ValueError( + "provided {len(storage_layout_paths)} storage " + "layouts, but {len(input_files)} source files" + ) + + ret: dict[Any, Any] = {} if show_version: - compiler_data["version"] = vyper.__version__ + ret["version"] = vyper.__version__ - return compiler_data + for file_name in input_files: + file_path = Path(file_name) + file = input_bundle.load_file(file_path) + assert isinstance(file, FileInput) # mypy hint + + storage_layout_override = None + if storage_layout_paths: + storage_file_path = storage_layout_paths.pop(0) + with open(storage_file_path) as sfh: + storage_layout_override = json.load(sfh) + + output = vyper.compile_code( + file.source_code, + contract_name=str(file.path), + source_id=file.source_id, + input_bundle=input_bundle, + output_formats=final_formats, + exc_handler=exc_handler, + settings=settings, + storage_layout_override=storage_layout_override, + show_gas_estimates=show_gas_estimates, + no_bytecode_metadata=no_bytecode_metadata, + ) + + ret[file_path] = output + + return ret if __name__ == "__main__": diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 4a1c91550e..2720f20d23 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -4,15 +4,14 @@ import json import sys import warnings -from pathlib import Path -from typing import Any, Callable, Dict, Hashable, List, Optional, Tuple, Union +from pathlib import Path, PurePath +from typing import Any, Callable, Hashable, Optional import vyper -from vyper.cli.utils import extract_file_interface_imports, get_interface_file_path +from vyper.compiler.input_bundle import FileInput, JSONInputBundle 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 TRANSLATE_MAP = { @@ -29,7 +28,7 @@ "interface": "interface", "ir": "ir_dict", "ir_runtime": "ir_runtime_dict", - # "metadata": "metadata", # don't include in "*" output for now + "metadata": "metadata", "layout": "layout", "userdoc": "userdoc", } @@ -97,15 +96,15 @@ def _parse_args(argv): print(output_json) -def exc_handler_raises(file_path: Union[str, None], exception: Exception, component: str) -> None: +def exc_handler_raises(file_path: Optional[str], exception: Exception, component: str) -> None: if file_path: print(f"Unhandled exception in '{file_path}':") exception._exc_handler = True # type: ignore raise exception -def exc_handler_to_dict(file_path: Union[str, None], exception: Exception, component: str) -> Dict: - err_dict: Dict = { +def exc_handler_to_dict(file_path: Optional[str], exception: Exception, component: str) -> dict: + err_dict: dict = { "type": type(exception).__name__, "component": component, "severity": "error", @@ -129,23 +128,7 @@ def exc_handler_to_dict(file_path: Union[str, None], exception: Exception, compo return output_json -def _standardize_path(path_str: str) -> str: - try: - path = Path(path_str) - - if path.is_absolute(): - path = path.resolve() - else: - pwd = Path(".").resolve() - path = path.resolve().relative_to(pwd) - - except ValueError: - raise JSONError(f"{path_str} - path exists outside base folder") - - return path.as_posix() - - -def get_evm_version(input_dict: Dict) -> Optional[str]: +def get_evm_version(input_dict: dict) -> Optional[str]: if "settings" not in input_dict: return None @@ -168,76 +151,75 @@ def get_evm_version(input_dict: Dict) -> Optional[str]: return evm_version -def get_input_dict_contracts(input_dict: Dict) -> ContractCodes: - contract_sources: ContractCodes = {} +def get_compilation_targets(input_dict: dict) -> list[PurePath]: + # TODO: once we have modules, add optional "compilation_targets" key + # which specifies which sources we actually want to compile. + + return [PurePath(p) for p in input_dict["sources"].keys()] + + +def get_inputs(input_dict: dict) -> dict[PurePath, Any]: + ret = {} + seen = {} + for path, value in input_dict["sources"].items(): + path = PurePath(path) if "urls" in value: raise JSONError(f"{path} - 'urls' is not a supported field, use 'content' instead") if "content" not in value: raise JSONError(f"{path} missing required field - 'content'") if "keccak256" in value: - hash_ = value["keccak256"].lower() - if hash_.startswith("0x"): - hash_ = hash_[2:] + hash_ = value["keccak256"].lower().removeprefix("0x") if hash_ != keccak256(value["content"].encode("utf-8")).hex(): raise JSONError( f"Calculated keccak of '{path}' does not match keccak given in input JSON" ) - key = _standardize_path(path) - if key in contract_sources: - raise JSONError(f"Contract namespace collision: {key}") - contract_sources[key] = value["content"] - return contract_sources + if path.stem in seen: + raise JSONError(f"Contract namespace collision: {path}") - -def get_input_dict_interfaces(input_dict: Dict) -> Dict: - interface_sources: Dict = {} + # value looks like {"content": } + # this will be interpreted by JSONInputBundle later + ret[path] = value + seen[path.stem] = True for path, value in input_dict.get("interfaces", {}).items(): - key = _standardize_path(path) - - if key.endswith(".json"): - # EthPM Manifest v3 (EIP-2678) - if "contractTypes" in value: - for name, ct in value["contractTypes"].items(): - if name in interface_sources: - raise JSONError(f"Interface namespace collision: {name}") - - interface_sources[name] = {"type": "json", "code": ct["abi"]} - - continue # Skip to next interface - - # ABI JSON file (`{"abi": List[ABI]}`) - elif "abi" in value: - interface = {"type": "json", "code": value["abi"]} - - # ABI JSON file (`List[ABI]`) - elif isinstance(value, list): - interface = {"type": "json", "code": value} - - else: - raise JSONError(f"Interface '{path}' must have 'abi' field") - - elif key.endswith(".vy"): - if "content" not in value: - raise JSONError(f"Interface '{path}' must have 'content' field") - - interface = {"type": "vyper", "code": value["content"]} - + path = PurePath(path) + if path.stem in seen: + raise JSONError(f"Interface namespace collision: {path}") + + if isinstance(value, list): + # backwards compatibility - straight ABI with no "abi" key. + # (should probably just reject these) + value = {"abi": value} + + # some validation + if not isinstance(value, dict): + raise JSONError("invalid interface (must be a dictionary):\n{json.dumps(value)}") + if "content" in value: + if not isinstance(value["content"], str): + raise JSONError(f"invalid 'content' (expected string):\n{json.dumps(value)}") + elif "abi" in value: + if not isinstance(value["abi"], list): + raise JSONError(f"invalid 'abi' (expected list):\n{json.dumps(value)}") else: - raise JSONError(f"Interface '{path}' must have suffix '.vy' or '.json'") - - key = key.rsplit(".", maxsplit=1)[0] - if key in interface_sources: - raise JSONError(f"Interface namespace collision: {key}") + raise JSONError( + "invalid interface (must contain either 'content' or 'abi'):\n{json.dumps(value)}" + ) + if "content" in value and "abi" in value: + raise JSONError( + "invalid interface (found both 'content' and 'abi'):\n{json.dumps(value)}" + ) - interface_sources[key] = interface + ret[path] = value + seen[path.stem] = True - return interface_sources + return ret -def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCodes) -> Dict: - output_formats = {} +# get unique output formats for each contract, given the input_dict +# NOTE: would maybe be nice to raise on duplicated output formats +def get_output_formats(input_dict: dict, targets: list[PurePath]) -> dict[PurePath, list[str]]: + output_formats: dict[PurePath, list[str]] = {} for path, outputs in input_dict["settings"]["outputSelection"].items(): if isinstance(outputs, dict): # if outputs are given in solc json format, collapse them into a single list @@ -248,6 +230,7 @@ def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCo for key in [i for i in ("evm", "evm.bytecode", "evm.deployedBytecode") if i in outputs]: outputs.remove(key) outputs.update([i for i in TRANSLATE_MAP if i.startswith(key)]) + if "*" in outputs: outputs = TRANSLATE_MAP.values() else: @@ -259,107 +242,23 @@ def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCo outputs = sorted(set(outputs)) if path == "*": - output_keys = list(contract_sources.keys()) + output_paths = targets else: - output_keys = [_standardize_path(path)] - if output_keys[0] not in contract_sources: - raise JSONError(f"outputSelection references unknown contract '{output_keys[0]}'") + output_paths = [PurePath(path)] + if output_paths[0] not in targets: + raise JSONError(f"outputSelection references unknown contract '{output_paths[0]}'") - for key in output_keys: - output_formats[key] = outputs + for output_path in output_paths: + output_formats[output_path] = outputs return output_formats -def get_interface_codes( - root_path: Union[Path, None], - contract_path: ContractPath, - contract_sources: ContractCodes, - interface_sources: Dict, -) -> Dict: - interface_codes: Dict = {} - interfaces: Dict = {} - - code = contract_sources[contract_path] - interface_codes = extract_file_interface_imports(code) - for interface_name, interface_path in interface_codes.items(): - # If we know the interfaces already (e.g. EthPM Manifest file) - if interface_name in interface_sources: - interfaces[interface_name] = interface_sources[interface_name] - continue - - path = Path(contract_path).parent.joinpath(interface_path).as_posix() - keys = [_standardize_path(path)] - if not interface_path.startswith("."): - keys.append(interface_path) - - key = next((i for i in keys if i in interface_sources), None) - if key: - interfaces[interface_name] = interface_sources[key] - continue - - key = next((i + ".vy" for i in keys if i + ".vy" in contract_sources), None) - if key: - interfaces[interface_name] = {"type": "vyper", "code": contract_sources[key]} - continue - - if root_path is None: - raise FileNotFoundError(f"Cannot locate interface '{interface_path}{{.vy,.json}}'") - - parent_path = root_path.joinpath(contract_path).parent - base_paths = [parent_path] - if not interface_path.startswith("."): - base_paths.append(root_path) - elif interface_path.startswith("../") and len(Path(contract_path).parent.parts) < Path( - interface_path - ).parts.count(".."): - raise FileNotFoundError( - f"{contract_path} - Cannot perform relative import outside of base folder" - ) - - valid_path = get_interface_file_path(base_paths, interface_path) - with valid_path.open() as fh: - code = fh.read() - if valid_path.suffix == ".json": - code_dict = json.loads(code.encode()) - # EthPM Manifest v3 (EIP-2678) - if "contractTypes" in code_dict: - if interface_name not in code_dict["contractTypes"]: - raise JSONError(f"'{interface_name}' not found in '{valid_path}'") - - if "abi" not in code_dict["contractTypes"][interface_name]: - raise JSONError(f"Missing abi for '{interface_name}' in '{valid_path}'") - - abi = code_dict["contractTypes"][interface_name]["abi"] - interfaces[interface_name] = {"type": "json", "code": abi} - - # ABI JSON (`{"abi": List[ABI]}`) - elif "abi" in code_dict: - interfaces[interface_name] = {"type": "json", "code": code_dict["abi"]} - - # ABI JSON (`List[ABI]`) - elif isinstance(code_dict, list): - interfaces[interface_name] = {"type": "json", "code": code_dict} - - else: - raise JSONError(f"Unexpected type in file: '{valid_path}'") - - else: - interfaces[interface_name] = {"type": "vyper", "code": code} - - return interfaces - - def compile_from_input_dict( - input_dict: Dict, - exc_handler: Callable = exc_handler_raises, - root_folder: Union[str, None] = None, -) -> Tuple[Dict, Dict]: - root_path = None - if root_folder is not None: - root_path = Path(root_folder).resolve() - if not root_path.exists(): - raise FileNotFoundError(f"Invalid root path - '{root_path.as_posix()}' does not exist") + input_dict: dict, exc_handler: Callable = exc_handler_raises, root_folder: Optional[str] = None +) -> tuple[dict, dict]: + if root_folder is None: + root_folder = "." if input_dict["language"] != "Vyper": raise JSONError(f"Invalid language '{input_dict['language']}' - Only Vyper is supported.") @@ -382,46 +281,50 @@ def compile_from_input_dict( no_bytecode_metadata = not input_dict["settings"].get("bytecodeMetadata", True) - contract_sources: ContractCodes = get_input_dict_contracts(input_dict) - interface_sources = get_input_dict_interfaces(input_dict) - output_formats = get_input_dict_output_formats(input_dict, contract_sources) + compilation_targets = get_compilation_targets(input_dict) + sources = get_inputs(input_dict) + output_formats = get_output_formats(input_dict, compilation_targets) - compiler_data, warning_data = {}, {} + input_bundle = JSONInputBundle(sources, search_paths=[Path(root_folder)]) + + res, warnings_dict = {}, {} warnings.simplefilter("always") - for id_, contract_path in enumerate(sorted(contract_sources)): + for contract_path in compilation_targets: with warnings.catch_warnings(record=True) as caught_warnings: try: - interface_codes = get_interface_codes( - root_path, contract_path, contract_sources, interface_sources - ) - except Exception as exc: - return exc_handler(contract_path, exc, "parser"), {} - try: - data = vyper.compile_codes( - {contract_path: contract_sources[contract_path]}, - output_formats[contract_path], - interface_codes=interface_codes, - initial_id=id_, + # use load_file to get a unique source_id + file = input_bundle.load_file(contract_path) + assert isinstance(file, FileInput) # mypy hint + data = vyper.compile_code( + file.source_code, + contract_name=str(file.path), + input_bundle=input_bundle, + output_formats=output_formats[contract_path], + source_id=file.source_id, settings=settings, no_bytecode_metadata=no_bytecode_metadata, ) + assert isinstance(data, dict) + data["source_id"] = file.source_id except Exception as exc: return exc_handler(contract_path, exc, "compiler"), {} - compiler_data[contract_path] = data[contract_path] + res[contract_path] = data if caught_warnings: - warning_data[contract_path] = caught_warnings + warnings_dict[contract_path] = caught_warnings - return compiler_data, warning_data + return res, warnings_dict -def format_to_output_dict(compiler_data: Dict) -> Dict: - output_dict: Dict = {"compiler": f"vyper-{vyper.__version__}", "contracts": {}, "sources": {}} - for id_, (path, data) in enumerate(compiler_data.items()): - output_dict["sources"][path] = {"id": id_} +# convert output of compile_input_dict to final output format +def format_to_output_dict(compiler_data: dict) -> dict: + output_dict: dict = {"compiler": f"vyper-{vyper.__version__}", "contracts": {}, "sources": {}} + for path, data in compiler_data.items(): + path = str(path) # Path breaks json serializability + output_dict["sources"][path] = {"id": data["source_id"]} if "ast_dict" in data: output_dict["sources"][path]["ast"] = data["ast_dict"]["ast"] - name = Path(path).stem + name = PurePath(path).stem output_dict["contracts"][path] = {name: {}} output_contracts = output_dict["contracts"][path][name] @@ -459,7 +362,7 @@ def format_to_output_dict(compiler_data: Dict) -> Dict: # https://stackoverflow.com/a/49518779 -def _raise_on_duplicate_keys(ordered_pairs: List[Tuple[Hashable, Any]]) -> Dict: +def _raise_on_duplicate_keys(ordered_pairs: list[tuple[Hashable, Any]]) -> dict: """ Raise JSONError if a duplicate key exists in provided ordered list of pairs, otherwise return a dict. @@ -474,17 +377,15 @@ def _raise_on_duplicate_keys(ordered_pairs: List[Tuple[Hashable, Any]]) -> Dict: def compile_json( - input_json: Union[Dict, str], + input_json: dict | str, exc_handler: Callable = exc_handler_raises, - root_path: Union[str, None] = None, - json_path: Union[str, None] = None, -) -> Dict: + root_folder: Optional[str] = None, + json_path: Optional[str] = None, +) -> dict: try: if isinstance(input_json, str): try: - input_dict: Dict = json.loads( - input_json, object_pairs_hook=_raise_on_duplicate_keys - ) + input_dict = json.loads(input_json, object_pairs_hook=_raise_on_duplicate_keys) except json.decoder.JSONDecodeError as exc: new_exc = JSONError(str(exc), exc.lineno, exc.colno) return exc_handler(json_path, new_exc, "json") @@ -492,7 +393,7 @@ def compile_json( input_dict = input_json try: - compiler_data, warn_data = compile_from_input_dict(input_dict, exc_handler, root_path) + compiler_data, warn_data = compile_from_input_dict(input_dict, exc_handler, root_folder) if "errors" in compiler_data: return compiler_data except KeyError as exc: diff --git a/vyper/cli/vyper_serve.py b/vyper/cli/vyper_serve.py index 401e59e7ba..9771dc922d 100755 --- a/vyper/cli/vyper_serve.py +++ b/vyper/cli/vyper_serve.py @@ -91,11 +91,11 @@ def _compile(self, data): try: code = data["code"] - out_dict = vyper.compile_codes( - {"": code}, + out_dict = vyper.compile_code( + code, list(vyper.compiler.OUTPUT_FORMATS.keys()), evm_version=data.get("evm_version", DEFAULT_EVM_VERSION), - )[""] + ) out_dict["ir"] = str(out_dict["ir"]) out_dict["ir_runtime"] = str(out_dict["ir_runtime"]) except VyperException as e: diff --git a/vyper/codegen/function_definitions/common.py b/vyper/codegen/function_definitions/common.py index 3fd5ce0b29..1d24b6c6dd 100644 --- a/vyper/codegen/function_definitions/common.py +++ b/vyper/codegen/function_definitions/common.py @@ -73,6 +73,13 @@ class EntryPointInfo: min_calldatasize: int # the min calldata required for this entry point ir_node: IRnode # the ir for this entry point + def __post_init__(self): + # ABI v2 property guaranteed by the spec. + # https://docs.soliditylang.org/en/v0.8.21/abi-spec.html#formal-specification-of-the-encoding states: # noqa: E501 + # > Note that for any X, len(enc(X)) is a multiple of 32. + assert self.min_calldatasize >= 4 + assert (self.min_calldatasize - 4) % 32 == 0 + @dataclass class ExternalFuncIR(FuncIR): diff --git a/vyper/codegen/function_definitions/external_function.py b/vyper/codegen/function_definitions/external_function.py index 32236e9aad..65276469e7 100644 --- a/vyper/codegen/function_definitions/external_function.py +++ b/vyper/codegen/function_definitions/external_function.py @@ -135,20 +135,17 @@ def handler_for(calldata_kwargs, default_kwargs): return ret -# TODO it would be nice if this returned a data structure which were -# amenable to generating a jump table instead of the linear search for -# method_id we have now. def generate_ir_for_external_function(code, func_t, context): # TODO type hints: # def generate_ir_for_external_function( # code: vy_ast.FunctionDef, # func_t: ContractFunctionT, # context: Context, - # check_nonpayable: bool, # ) -> IRnode: - """Return the IR for an external function. Includes code to inspect the method_id, - enter the function (nonpayable and reentrancy checks), handle kwargs and exit - the function (clean up reentrancy storage variables) + """ + Return the IR for an external function. Returns IR for the body + of the function, handle kwargs and exit the function. Also returns + metadata required for `module.py` to construct the selector table. """ nonreentrant_pre, nonreentrant_post = get_nonreentrant_lock(func_t) diff --git a/vyper/codegen/ir_node.py b/vyper/codegen/ir_node.py index 4513a61e0a..e17ef47c8f 100644 --- a/vyper/codegen/ir_node.py +++ b/vyper/codegen/ir_node.py @@ -1,3 +1,4 @@ +import contextlib import re from enum import Enum, auto from functools import cached_property @@ -46,6 +47,77 @@ class Encoding(Enum): # future: packed +# shortcut for chaining multiple cache_when_complex calls +# CMC 2023-08-10 remove this and scope_together _as soon as_ we have +# real variables in IR (that we can declare without explicit scoping - +# needs liveness analysis). +@contextlib.contextmanager +def scope_multi(ir_nodes, names): + assert len(ir_nodes) == len(names) + + builders = [] + scoped_ir_nodes = [] + + class _MultiBuilder: + def resolve(self, body): + # sanity check that it's initialized properly + assert len(builders) == len(ir_nodes) + ret = body + for b in reversed(builders): + ret = b.resolve(ret) + return ret + + mb = _MultiBuilder() + + with contextlib.ExitStack() as stack: + for arg, name in zip(ir_nodes, names): + b, ir_node = stack.enter_context(arg.cache_when_complex(name)) + + builders.append(b) + scoped_ir_nodes.append(ir_node) + + yield mb, scoped_ir_nodes + + +# create multiple with scopes if any of the items are complex, to force +# ordering of side effects. +@contextlib.contextmanager +def scope_together(ir_nodes, names): + assert len(ir_nodes) == len(names) + + should_scope = any(s._optimized.is_complex_ir for s in ir_nodes) + + class _Builder: + def resolve(self, body): + if not should_scope: + # uses of the variable have already been inlined + return body + + ret = body + # build with scopes from inside-out (hence reversed) + for arg, name in reversed(list(zip(ir_nodes, names))): + ret = ["with", name, arg, ret] + + if isinstance(body, IRnode): + return IRnode.from_list( + ret, typ=body.typ, location=body.location, encoding=body.encoding + ) + else: + return ret + + b = _Builder() + + if should_scope: + ir_vars = tuple( + IRnode.from_list(name, typ=arg.typ, location=arg.location, encoding=arg.encoding) + for (arg, name) in zip(ir_nodes, names) + ) + yield b, ir_vars + else: + # inline them + yield b, ir_nodes + + # this creates a magical block which maps to IR `with` class _WithBuilder: def __init__(self, ir_node, name, should_inline=False): @@ -315,14 +387,15 @@ def __init__( def gas(self): return self._gas + self.add_gas_estimate - # the IR should be cached. - # TODO make this private. turns out usages are all for the caching - # idiom that cache_when_complex addresses + # the IR should be cached and/or evaluated exactly once @property def is_complex_ir(self): # list of items not to cache. note can add other env variables # which do not change, e.g. calldatasize, coinbase, etc. - do_not_cache = {"~empty", "calldatasize"} + # reads (from memory or storage) should not be cached because + # they can have or be affected by side effects. + do_not_cache = {"~empty", "calldatasize", "callvalue"} + return ( isinstance(self.value, str) and (self.value.lower() in VALID_IR_MACROS or self.value.upper() in get_ir_opcodes()) diff --git a/vyper/codegen/memory_allocator.py b/vyper/codegen/memory_allocator.py index 582d4b9c54..b5e1212917 100644 --- a/vyper/codegen/memory_allocator.py +++ b/vyper/codegen/memory_allocator.py @@ -1,6 +1,6 @@ from typing import List -from vyper.exceptions import CompilerPanic +from vyper.exceptions import CompilerPanic, MemoryAllocationException from vyper.utils import MemoryPositions @@ -46,6 +46,8 @@ class MemoryAllocator: next_mem: int + _ALLOCATION_LIMIT: int = 2**64 + def __init__(self, start_position: int = MemoryPositions.RESERVED_MEMORY): """ Initializer. @@ -110,6 +112,14 @@ def _expand_memory(self, size: int) -> int: before_value = self.next_mem self.next_mem += size self.size_of_mem = max(self.size_of_mem, self.next_mem) + + if self.size_of_mem >= self._ALLOCATION_LIMIT: + # this should not be caught + raise MemoryAllocationException( + f"Tried to allocate {self.size_of_mem} bytes! " + f"(limit is {self._ALLOCATION_LIMIT} (2**64) bytes)" + ) + return before_value def deallocate_memory(self, pos: int, size: int) -> None: diff --git a/vyper/codegen/module.py b/vyper/codegen/module.py index 6445a5e1e0..bfdafa8ba9 100644 --- a/vyper/codegen/module.py +++ b/vyper/codegen/module.py @@ -93,9 +93,12 @@ def _generate_external_entry_points(external_functions, global_ctx): for code in external_functions: func_ir = generate_ir_for_function(code, global_ctx) for abi_sig, entry_point in func_ir.entry_points.items(): + method_id = method_id_int(abi_sig) assert abi_sig not in entry_points + assert method_id not in sig_of + entry_points[abi_sig] = entry_point - sig_of[method_id_int(abi_sig)] = abi_sig + sig_of[method_id] = abi_sig # stick function common body into final entry point to save a jump ir_node = IRnode.from_list(["seq", entry_point.ir_node, func_ir.common_ir]) diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py index 3ecb0afdc3..254cad32e6 100644 --- a/vyper/codegen/stmt.py +++ b/vyper/codegen/stmt.py @@ -91,17 +91,15 @@ def parse_Assign(self): return IRnode.from_list(ret) def parse_If(self): + with self.context.block_scope(): + test_expr = Expr.parse_value_expr(self.stmt.test, self.context) + body = ["if", test_expr, parse_body(self.stmt.body, self.context)] + if self.stmt.orelse: with self.context.block_scope(): - add_on = [parse_body(self.stmt.orelse, self.context)] - else: - add_on = [] + body.extend([parse_body(self.stmt.orelse, self.context)]) - with self.context.block_scope(): - test_expr = Expr.parse_value_expr(self.stmt.test, self.context) - body = ["if", test_expr, parse_body(self.stmt.body, self.context)] + add_on - ir_node = IRnode.from_list(body) - return ir_node + return IRnode.from_list(body) def parse_Log(self): event = self.stmt._metadata["type"] @@ -302,7 +300,9 @@ def _parse_For_range(self): loop_body.append(["mstore", iptr, i]) loop_body.append(parse_body(self.stmt.body, self.context)) - # NOTE: codegen for `repeat` inserts an assertion that rounds <= rounds_bound. + # NOTE: codegen for `repeat` inserts an assertion that + # (gt rounds_bound rounds). note this also covers the case where + # rounds < 0. # if we ever want to remove that, we need to manually add the assertion # where it makes sense. ir_node = IRnode.from_list( diff --git a/vyper/compiler/README.md b/vyper/compiler/README.md index d6b55fdd82..eb70750a2b 100644 --- a/vyper/compiler/README.md +++ b/vyper/compiler/README.md @@ -51,11 +51,9 @@ for specific implementation details. [`vyper.compiler.compile_codes`](__init__.py) is the main user-facing function for generating compiler output from Vyper source. The process is as follows: -1. The `@evm_wrapper` decorator sets the target EVM version in -[`opcodes.py`](../evm/opcodes.py). -2. A [`CompilerData`](phases.py) object is created for each contract to be compiled. +1. A [`CompilerData`](phases.py) object is created for each contract to be compiled. This object uses `@property` methods to trigger phases of the compiler as required. -3. Functions in [`output.py`](output.py) generate the requested outputs from the +2. Functions in [`output.py`](output.py) generate the requested outputs from the compiler data. ## Design diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index 0b3c0d8191..62ea05b243 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -1,21 +1,15 @@ from collections import OrderedDict +from pathlib import Path from typing import Any, Callable, Dict, Optional, Sequence, Union import vyper.ast as vy_ast # break an import cycle import vyper.codegen.core as codegen import vyper.compiler.output as output +from vyper.compiler.input_bundle import InputBundle, PathLike from vyper.compiler.phases import CompilerData from vyper.compiler.settings import Settings from vyper.evm.opcodes import DEFAULT_EVM_VERSION, anchor_evm_version -from vyper.typing import ( - ContractCodes, - ContractPath, - InterfaceDict, - InterfaceImports, - OutputDict, - OutputFormats, - StorageLayout, -) +from vyper.typing import ContractPath, OutputFormats, StorageLayout OUTPUT_FORMATS = { # requires vyper_module @@ -47,119 +41,25 @@ } -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, - settings: Settings = None, - storage_layouts: Optional[dict[ContractPath, Optional[StorageLayout]]] = None, - show_gas_estimates: bool = False, - no_bytecode_metadata: bool = False, -) -> OrderedDict: - """ - Generate compiler output(s) from one or more contract source codes. - - Arguments - --------- - contract_sources: Dict[str, str] - Vyper source codes to be compiled. Formatted as `{"contract name": "source code"}` - output_formats: List, optional - List of compiler outputs to generate. Possible options are all the keys - in `OUTPUT_FORMATS`. If not given, the deployment bytecode is generated. - exc_handler: Callable, optional - Callable used to handle exceptions if the compilation fails. Should accept - 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. - settings: Settings, optional - Compiler settings - show_gas_estimates: bool, optional - Show gas estimates for abi and ir output modes - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - - * May be a singular dictionary shared across all sources to be compiled, - i.e. `{'interface name': "definition"}` - * or may be organized according to contracts that are being compiled, i.e. - `{'contract name': {'interface name': "definition"}` - - * Interface definitions are formatted as: `{'type': "json/vyper", 'code': "interface code"}` - * JSON interfaces are given as lists, vyper interfaces as strings - no_bytecode_metadata: bool, optional - Do not add metadata to bytecode. Defaults to False - - Returns - ------- - Dict - Compiler output as `{'contract name': {'output key': "output data"}}` - """ - settings = settings or Settings() - - if output_formats is None: - output_formats = ("bytecode",) - if isinstance(output_formats, Sequence): - output_formats = dict((k, output_formats) for k in contract_sources.keys()) - - out: OrderedDict = OrderedDict() - for source_id, contract_name in enumerate(sorted(contract_sources), start=initial_id): - source_code = contract_sources[contract_name] - interfaces: Any = interface_codes - storage_layout_override = None - if storage_layouts and contract_name in storage_layouts: - storage_layout_override = storage_layouts[contract_name] - - if ( - isinstance(interfaces, dict) - and contract_name in interfaces - and isinstance(interfaces[contract_name], dict) - ): - interfaces = interfaces[contract_name] - - # make IR output the same between runs - codegen.reset_names() - - 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 - - UNKNOWN_CONTRACT_NAME = "" def compile_code( contract_source: str, - output_formats: Optional[OutputFormats] = None, - interface_codes: Optional[InterfaceImports] = None, + contract_name: str = UNKNOWN_CONTRACT_NAME, + source_id: int = 0, + input_bundle: InputBundle = None, settings: Settings = None, + output_formats: Optional[OutputFormats] = None, storage_layout_override: Optional[StorageLayout] = None, + no_bytecode_metadata: bool = False, show_gas_estimates: bool = False, + exc_handler: Optional[Callable] = None, ) -> dict: """ - Generate compiler output(s) from a single contract source code. + Generate consumable compiler output(s) from a single contract source code. + Basically, a wrapper around CompilerData which munges the output + data into the requested output formats. Arguments --------- @@ -175,11 +75,11 @@ def compile_code( Compiler settings. show_gas_estimates: bool, optional Show gas estimates for abi and ir output modes - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - - * Formatted as as `{'interface name': {'type': "json/vyper", 'code': "interface code"}}` - * JSON interfaces are given as lists, vyper interfaces as strings + exc_handler: Callable, optional + Callable used to handle exceptions if the compilation fails. Should accept + two arguments - the name of the contract, and the exception that was raised + no_bytecode_metadata: bool, optional + Do not add metadata to bytecode. Defaults to False Returns ------- @@ -187,14 +87,37 @@ def compile_code( Compiler output as `{'output key': "output data"}` """ - contract_sources = {UNKNOWN_CONTRACT_NAME: contract_source} - storage_layouts = {UNKNOWN_CONTRACT_NAME: storage_layout_override} + settings = settings or Settings() + + if output_formats is None: + output_formats = ("bytecode",) - return compile_codes( - contract_sources, - output_formats, - interface_codes=interface_codes, - settings=settings, - storage_layouts=storage_layouts, - show_gas_estimates=show_gas_estimates, - )[UNKNOWN_CONTRACT_NAME] + # make IR output the same between runs + codegen.reset_names() + + compiler_data = CompilerData( + contract_source, + input_bundle, + Path(contract_name), + source_id, + settings, + storage_layout_override, + show_gas_estimates, + no_bytecode_metadata, + ) + + ret = {} + with anchor_evm_version(compiler_data.settings.evm_version): + for output_format in output_formats: + if output_format not in OUTPUT_FORMATS: + raise ValueError(f"Unsupported format type {repr(output_format)}") + try: + formatter = OUTPUT_FORMATS[output_format] + ret[output_format] = formatter(compiler_data) + except Exception as exc: + if exc_handler is not None: + exc_handler(contract_name, exc) + else: + raise exc + + return ret diff --git a/vyper/compiler/input_bundle.py b/vyper/compiler/input_bundle.py new file mode 100644 index 0000000000..1e41c3f137 --- /dev/null +++ b/vyper/compiler/input_bundle.py @@ -0,0 +1,180 @@ +import contextlib +import json +import os +from dataclasses import dataclass +from pathlib import Path, PurePath +from typing import Any, Iterator, Optional + +from vyper.exceptions import JSONError + +# a type to make mypy happy +PathLike = Path | PurePath + + +@dataclass +class CompilerInput: + # an input to the compiler, basically an abstraction for file contents + source_id: int + path: PathLike + + @staticmethod + def from_string(source_id: int, path: PathLike, file_contents: str) -> "CompilerInput": + try: + s = json.loads(file_contents) + return ABIInput(source_id, path, s) + except (ValueError, TypeError): + return FileInput(source_id, path, file_contents) + + +@dataclass +class FileInput(CompilerInput): + source_code: str + + +@dataclass +class ABIInput(CompilerInput): + # some json input, which has already been parsed into a dict or list + # this is needed because json inputs present json interfaces as json + # objects, not as strings. this class helps us avoid round-tripping + # back to a string to pretend it's a file. + abi: Any # something that json.load() returns + + +class _NotFound(Exception): + pass + + +# wrap os.path.normpath, but return the same type as the input +def _normpath(path): + return path.__class__(os.path.normpath(path)) + + +# an "input bundle" to the compiler, representing the files which are +# available to the compiler. it is useful because it parametrizes I/O +# operations over different possible input types. you can think of it +# as a virtual filesystem which models the compiler's interactions +# with the outside world. it exposes a "load_file" operation which +# searches for a file from a set of search paths, and also provides +# id generation service to get a unique source id per file. +class InputBundle: + # a list of search paths + search_paths: list[PathLike] + + def __init__(self, search_paths): + self.search_paths = search_paths + self._source_id_counter = 0 + self._source_ids: dict[PathLike, int] = {} + + def _load_from_path(self, path): + raise NotImplementedError(f"not implemented! {self.__class__}._load_from_path()") + + def _generate_source_id(self, path: PathLike) -> int: + if path not in self._source_ids: + self._source_ids[path] = self._source_id_counter + self._source_id_counter += 1 + + return self._source_ids[path] + + def load_file(self, path: PathLike | str) -> CompilerInput: + # search path precedence + tried = [] + for sp in reversed(self.search_paths): + # note from pathlib docs: + # > If the argument is an absolute path, the previous path is ignored. + # Path("/a") / Path("/b") => Path("/b") + to_try = sp / path + + # normalize the path with os.path.normpath, to break down + # things like "foo/bar/../x.vy" => "foo/x.vy", with all + # the caveats around symlinks that os.path.normpath comes with. + to_try = _normpath(to_try) + try: + res = self._load_from_path(to_try) + break + except _NotFound: + tried.append(to_try) + + else: + formatted_search_paths = "\n".join([" " + str(p) for p in tried]) + raise FileNotFoundError( + f"could not find {path} in any of the following locations:\n" + f"{formatted_search_paths}" + ) + + # try to parse from json, so that return types are consistent + # across FilesystemInputBundle and JSONInputBundle. + if isinstance(res, FileInput): + return CompilerInput.from_string(res.source_id, res.path, res.source_code) + + return res + + def add_search_path(self, path: PathLike) -> None: + self.search_paths.append(path) + + # temporarily add something to the search path (within the + # scope of the context manager) with highest precedence. + # if `path` is None, do nothing + @contextlib.contextmanager + def search_path(self, path: Optional[PathLike]) -> Iterator[None]: + if path is None: + yield # convenience, so caller does not have to handle null path + + else: + self.search_paths.append(path) + try: + yield + finally: + self.search_paths.pop() + + +# regular input. takes a search path(s), and `load_file()` will search all +# search paths for the file and read it from the filesystem +class FilesystemInputBundle(InputBundle): + def _load_from_path(self, path: Path) -> CompilerInput: + try: + with path.open() as f: + code = f.read() + except FileNotFoundError: + raise _NotFound(path) + + source_id = super()._generate_source_id(path) + + return FileInput(source_id, path, code) + + +# fake filesystem for JSON inputs. takes a base path, and `load_file()` +# "reads" the file from the JSON input. Note that this input bundle type +# never actually interacts with the filesystem -- it is guaranteed to be pure! +class JSONInputBundle(InputBundle): + input_json: dict[PurePath, Any] + + def __init__(self, input_json, search_paths): + super().__init__(search_paths) + self.input_json = {} + for path, item in input_json.items(): + path = _normpath(path) + + # should be checked by caller + assert path not in self.input_json + self.input_json[_normpath(path)] = item + + def _load_from_path(self, path: PurePath) -> CompilerInput: + try: + value = self.input_json[path] + except KeyError: + raise _NotFound(path) + + source_id = super()._generate_source_id(path) + + if "content" in value: + return FileInput(source_id, path, value["content"]) + + if "abi" in value: + return ABIInput(source_id, path, value["abi"]) + + # TODO: ethPM support + # if isinstance(contents, dict) and "contractTypes" in contents: + + # unreachable, based on how JSONInputBundle is constructed in + # the codebase. + raise JSONError(f"Unexpected type in file: '{path}'") # pragma: nocover diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index 334c5ba613..e47f300ba9 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -1,6 +1,5 @@ import warnings from collections import OrderedDict, deque -from pathlib import Path import asttokens @@ -17,7 +16,7 @@ def build_ast_dict(compiler_data: CompilerData) -> dict: ast_dict = { - "contract_name": compiler_data.contract_name, + "contract_name": str(compiler_data.contract_path), "ast": ast_to_dict(compiler_data.vyper_module), } return ast_dict @@ -35,7 +34,7 @@ def build_userdoc(compiler_data: CompilerData) -> dict: def build_external_interface_output(compiler_data: CompilerData) -> str: interface = compiler_data.vyper_module_folded._metadata["type"] - stem = Path(compiler_data.contract_name).stem + stem = compiler_data.contract_path.stem # capitalize words separated by '_' # ex: test_interface.vy -> TestInterface name = "".join([x.capitalize() for x in stem.split("_")]) @@ -104,11 +103,10 @@ def build_ir_runtime_dict_output(compiler_data: CompilerData) -> dict: def build_metadata_output(compiler_data: CompilerData) -> dict: - warnings.warn("metadata output format is unstable!") sigs = compiler_data.function_signatures def _var_rec_dict(variable_record): - ret = vars(variable_record) + ret = vars(variable_record).copy() ret["typ"] = str(ret["typ"]) if ret["data_offset"] is None: del ret["data_offset"] @@ -118,7 +116,7 @@ def _var_rec_dict(variable_record): return ret def _to_dict(func_t): - ret = vars(func_t) + ret = vars(func_t).copy() ret["return_type"] = str(ret["return_type"]) ret["_ir_identifier"] = func_t._ir_info.ir_identifier @@ -134,7 +132,7 @@ def _to_dict(func_t): args = ret[attr] ret[attr] = {arg.name: str(arg.typ) for arg in args} - ret["frame_info"] = vars(func_t._ir_info.frame_info) + ret["frame_info"] = vars(func_t._ir_info.frame_info).copy() del ret["frame_info"]["frame_vars"] # frame_var.pos might be IR, cannot serialize keep_keys = { diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index a1c7342320..bfbb336d54 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -1,6 +1,7 @@ import copy import warnings from functools import cached_property +from pathlib import Path, PurePath from typing import Optional, Tuple from vyper import ast as vy_ast @@ -8,12 +9,15 @@ 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.input_bundle import FilesystemInputBundle, InputBundle 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 -from vyper.typing import InterfaceImports, StorageLayout +from vyper.typing import StorageLayout + +DEFAULT_CONTRACT_NAME = PurePath("VyperContract.vy") class CompilerData: @@ -49,8 +53,8 @@ class CompilerData: def __init__( self, source_code: str, - contract_name: str = "VyperContract", - interface_codes: Optional[InterfaceImports] = None, + input_bundle: InputBundle = None, + contract_path: Path | PurePath = DEFAULT_CONTRACT_NAME, source_id: int = 0, settings: Settings = None, storage_layout: StorageLayout = None, @@ -62,15 +66,11 @@ def __init__( Arguments --------- - source_code : str + source_code: str Vyper source code. - contract_name : str, optional + contract_path: Path, optional The name of the contract being compiled. - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - * Formatted as as `{'interface name': {'type': "json/vyper", 'code': "interface code"}}` - * JSON interfaces are given as lists, vyper interfaces as strings - source_id : int, optional + source_id: int, optional ID number used to identify this contract in the source map. settings: Settings Set optimization mode. @@ -79,18 +79,23 @@ def __init__( no_bytecode_metadata: bool, optional Do not add metadata to bytecode. Defaults to False """ - self.contract_name = contract_name + self.contract_path = contract_path self.source_code = source_code - self.interface_codes = interface_codes self.source_id = source_id 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() + self.input_bundle = input_bundle or FilesystemInputBundle([Path(".")]) + + _ = self._generate_ast # force settings to be calculated @cached_property def _generate_ast(self): - settings, ast = generate_ast(self.source_code, self.source_id, self.contract_name) + contract_name = str(self.contract_path) + settings, ast = generate_ast(self.source_code, self.source_id, contract_name) + # validate the compiler settings # XXX: this is a bit ugly, clean up later if settings.evm_version is not None: @@ -117,6 +122,8 @@ def _generate_ast(self): if self.settings.optimize is None: self.settings.optimize = OptimizationLevel.default() + # note self.settings.compiler_version is erased here as it is + # not used after pre-parsing return ast @cached_property @@ -128,12 +135,12 @@ def vyper_module_unfolded(self) -> vy_ast.Module: # This phase is intended to generate an AST for tooling use, and is not # used in the compilation process. - return generate_unfolded_ast(self.vyper_module, self.interface_codes) + return generate_unfolded_ast(self.contract_path, self.vyper_module, self.input_bundle) @cached_property def _folded_module(self): return generate_folded_ast( - self.vyper_module, self.interface_codes, self.storage_layout_override + self.contract_path, self.vyper_module, self.input_bundle, self.storage_layout_override ) @property @@ -215,7 +222,7 @@ def generate_ast( Vyper source code. source_id : int ID number used to identify this contract in the source map. - contract_name : str + contract_name: str Name of the contract. Returns @@ -226,21 +233,24 @@ def generate_ast( return vy_ast.parse_to_ast_with_settings(source_code, source_id, contract_name) +# destructive -- mutates module in place! def generate_unfolded_ast( - vyper_module: vy_ast.Module, interface_codes: Optional[InterfaceImports] + contract_path: Path | PurePath, vyper_module: vy_ast.Module, input_bundle: InputBundle ) -> vy_ast.Module: vy_ast.validation.validate_literal_nodes(vyper_module) - vy_ast.folding.replace_builtin_constants(vyper_module) vy_ast.folding.replace_builtin_functions(vyper_module) - # note: validate_semantics does type inference on the AST - validate_semantics(vyper_module, interface_codes) + + with input_bundle.search_path(contract_path.parent): + # note: validate_semantics does type inference on the AST + validate_semantics(vyper_module, input_bundle) return vyper_module def generate_folded_ast( + contract_path: Path, vyper_module: vy_ast.Module, - interface_codes: Optional[InterfaceImports], + input_bundle: InputBundle, storage_layout_overrides: StorageLayout = None, ) -> Tuple[vy_ast.Module, StorageLayout]: """ @@ -258,11 +268,15 @@ def generate_folded_ast( StorageLayout Layout of variables in storage """ + vy_ast.validation.validate_literal_nodes(vyper_module) vyper_module_folded = copy.deepcopy(vyper_module) vy_ast.folding.fold(vyper_module_folded) - validate_semantics(vyper_module_folded, interface_codes) + + with input_bundle.search_path(contract_path.parent): + validate_semantics(vyper_module_folded, input_bundle) + symbol_tables = set_data_positions(vyper_module_folded, storage_layout_overrides) return vyper_module_folded, symbol_tables diff --git a/vyper/exceptions.py b/vyper/exceptions.py index defca7cc53..3bde20356e 100644 --- a/vyper/exceptions.py +++ b/vyper/exceptions.py @@ -269,6 +269,10 @@ class StorageLayoutException(VyperException): """Invalid slot for the storage layout overrides""" +class MemoryAllocationException(VyperException): + """Tried to allocate too much memory""" + + class JSONError(Exception): """Invalid compiler input JSON.""" @@ -332,3 +336,7 @@ class UnfoldableNode(VyperInternalException): class TypeCheckFailure(VyperInternalException): """An issue was not caught during type checking that should have been.""" + + +class InvalidABIType(VyperInternalException): + """An internal routine constructed an invalid ABI type""" diff --git a/vyper/ir/compile_ir.py b/vyper/ir/compile_ir.py index 7a3e97155b..1c4dc1ef7c 100644 --- a/vyper/ir/compile_ir.py +++ b/vyper/ir/compile_ir.py @@ -415,7 +415,7 @@ def _height_of(witharg): ) ) # stack: i, rounds, rounds_bound - # assert rounds <= rounds_bound + # assert 0 <= rounds <= rounds_bound (for rounds_bound < 2**255) # TODO this runtime assertion shouldn't fail for # internally generated repeats. o.extend(["DUP2", "GT"] + _assert_false()) diff --git a/vyper/ir/optimizer.py b/vyper/ir/optimizer.py index 08c2168381..8df4bbac2d 100644 --- a/vyper/ir/optimizer.py +++ b/vyper/ir/optimizer.py @@ -662,10 +662,10 @@ def _rewrite_mstore_dload(argz): def _merge_mload(argz): if not version_check(begin="cancun"): return False - return _merge_load(argz, "mload", "mcopy") + return _merge_load(argz, "mload", "mcopy", allow_overlap=False) -def _merge_load(argz, _LOAD, _COPY): +def _merge_load(argz, _LOAD, _COPY, allow_overlap=True): # look for sequential operations copying from X to Y # and merge them into a single copy operation changed = False @@ -689,9 +689,14 @@ def _merge_load(argz, _LOAD, _COPY): initial_dst_offset = dst_offset initial_src_offset = src_offset idx = i + + # dst and src overlap, discontinue the optimization + has_overlap = initial_src_offset < initial_dst_offset < src_offset + 32 + if ( initial_dst_offset + total_length == dst_offset and initial_src_offset + total_length == src_offset + and (allow_overlap or not has_overlap) ): mstore_nodes.append(ir_node) total_length += 32 diff --git a/vyper/semantics/analysis/__init__.py b/vyper/semantics/analysis/__init__.py index 9e987d1cd0..7db230167e 100644 --- a/vyper/semantics/analysis/__init__.py +++ b/vyper/semantics/analysis/__init__.py @@ -7,11 +7,11 @@ from .utils import _ExprAnalyser -def validate_semantics(vyper_ast, interface_codes): +def validate_semantics(vyper_ast, input_bundle): # validate semantics and annotate AST with type/semantics information namespace = get_namespace() with namespace.enter_scope(): - add_module_namespace(vyper_ast, interface_codes) + add_module_namespace(vyper_ast, input_bundle) vy_ast.expansion.expand_annotated_ast(vyper_ast) validate_functions(vyper_ast) diff --git a/vyper/semantics/analysis/annotation.py b/vyper/semantics/analysis/annotation.py deleted file mode 100644 index 01ca51d7f4..0000000000 --- a/vyper/semantics/analysis/annotation.py +++ /dev/null @@ -1,283 +0,0 @@ -from vyper import ast as vy_ast -from vyper.exceptions import StructureException, TypeCheckFailure -from vyper.semantics.analysis.utils import ( - get_common_types, - get_exact_type_from_node, - get_possible_types_from_node, -) -from vyper.semantics.types import TYPE_T, BoolT, EnumT, EventT, SArrayT, StructT, is_type_t -from vyper.semantics.types.function import ContractFunctionT, MemberFunctionT - - -class _AnnotationVisitorBase: - - """ - Annotation visitor base class. - - Annotation visitors apply metadata (such as type information) to vyper AST objects. - Immediately after type checking a statement-level node, that node is passed to - `StatementAnnotationVisitor`. Some expression nodes are then passed onward to - `ExpressionAnnotationVisitor` for additional annotation. - """ - - def visit(self, node, *args): - if isinstance(node, self.ignored_types): - return - # iterate over the MRO until we find a matching visitor function - # this lets us use a single function to broadly target several - # node types with a shared parent - for class_ in node.__class__.mro(): - ast_type = class_.__name__ - visitor_fn = getattr(self, f"visit_{ast_type}", None) - if visitor_fn: - visitor_fn(node, *args) - return - raise StructureException(f"Cannot annotate: {node.ast_type}", node) - - -class StatementAnnotationVisitor(_AnnotationVisitorBase): - ignored_types = (vy_ast.Break, vy_ast.Continue, vy_ast.Pass, vy_ast.Raise) - - def __init__(self, fn_node: vy_ast.FunctionDef, namespace: dict) -> None: - self.func = fn_node._metadata["type"] - self.namespace = namespace - self.expr_visitor = ExpressionAnnotationVisitor(self.func) - - assert self.func.n_keyword_args == len(fn_node.args.defaults) - for kwarg in self.func.keyword_args: - self.expr_visitor.visit(kwarg.default_value, kwarg.typ) - - def visit(self, node): - super().visit(node) - - def visit_AnnAssign(self, node): - type_ = get_exact_type_from_node(node.target) - self.expr_visitor.visit(node.target, type_) - self.expr_visitor.visit(node.value, type_) - - def visit_Assert(self, node): - self.expr_visitor.visit(node.test) - - def visit_Assign(self, node): - type_ = get_exact_type_from_node(node.target) - self.expr_visitor.visit(node.target, type_) - self.expr_visitor.visit(node.value, type_) - - def visit_AugAssign(self, node): - type_ = get_exact_type_from_node(node.target) - self.expr_visitor.visit(node.target, type_) - self.expr_visitor.visit(node.value, type_) - - def visit_Expr(self, node): - self.expr_visitor.visit(node.value) - - def visit_If(self, node): - self.expr_visitor.visit(node.test) - - def visit_Log(self, node): - node._metadata["type"] = self.namespace[node.value.func.id] - self.expr_visitor.visit(node.value) - - def visit_Return(self, node): - if node.value is not None: - self.expr_visitor.visit(node.value, self.func.return_type) - - def visit_For(self, node): - if isinstance(node.iter, (vy_ast.Name, vy_ast.Attribute)): - self.expr_visitor.visit(node.iter) - - iter_type = node.target._metadata["type"] - if isinstance(node.iter, vy_ast.List): - # typecheck list literal as static array - len_ = len(node.iter.elements) - self.expr_visitor.visit(node.iter, SArrayT(iter_type, len_)) - - if isinstance(node.iter, vy_ast.Call) and node.iter.func.id == "range": - for a in node.iter.args: - self.expr_visitor.visit(a, iter_type) - for a in node.iter.keywords: - if a.arg == "bound": - self.expr_visitor.visit(a.value, iter_type) - - -class ExpressionAnnotationVisitor(_AnnotationVisitorBase): - ignored_types = () - - def __init__(self, fn_node: ContractFunctionT): - self.func = fn_node - - def visit(self, node, type_=None): - # the statement visitor sometimes passes type information about expressions - super().visit(node, type_) - - def visit_Attribute(self, node, type_): - type_ = get_exact_type_from_node(node) - node._metadata["type"] = type_ - self.visit(node.value, None) - - def visit_BinOp(self, node, type_): - if type_ is None: - type_ = get_common_types(node.left, node.right) - if len(type_) == 1: - type_ = type_.pop() - node._metadata["type"] = type_ - - self.visit(node.left, type_) - self.visit(node.right, type_) - - def visit_BoolOp(self, node, type_): - for value in node.values: - self.visit(value) - - def visit_Call(self, node, type_): - call_type = get_exact_type_from_node(node.func) - node_type = type_ or call_type.fetch_call_return(node) - node._metadata["type"] = node_type - self.visit(node.func) - - if isinstance(call_type, ContractFunctionT): - # function calls - if call_type.is_internal: - self.func.called_functions.add(call_type) - for arg, typ in zip(node.args, call_type.argument_types): - self.visit(arg, typ) - for kwarg in node.keywords: - # We should only see special kwargs - self.visit(kwarg.value, call_type.call_site_kwargs[kwarg.arg].typ) - - elif is_type_t(call_type, EventT): - # events have no kwargs - for arg, typ in zip(node.args, list(call_type.typedef.arguments.values())): - self.visit(arg, typ) - elif is_type_t(call_type, StructT): - # struct ctors - # ctors have no kwargs - for value, arg_type in zip( - node.args[0].values, list(call_type.typedef.members.values()) - ): - self.visit(value, arg_type) - elif isinstance(call_type, MemberFunctionT): - assert len(node.args) == len(call_type.arg_types) - for arg, arg_type in zip(node.args, call_type.arg_types): - self.visit(arg, arg_type) - else: - # builtin functions - arg_types = call_type.infer_arg_types(node) - for arg, arg_type in zip(node.args, arg_types): - self.visit(arg, arg_type) - kwarg_types = call_type.infer_kwarg_types(node) - for kwarg in node.keywords: - self.visit(kwarg.value, kwarg_types[kwarg.arg]) - - def visit_Compare(self, node, type_): - if isinstance(node.op, (vy_ast.In, vy_ast.NotIn)): - if isinstance(node.right, vy_ast.List): - type_ = get_common_types(node.left, *node.right.elements).pop() - self.visit(node.left, type_) - rlen = len(node.right.elements) - self.visit(node.right, SArrayT(type_, rlen)) - else: - type_ = get_exact_type_from_node(node.right) - self.visit(node.right, type_) - if isinstance(type_, EnumT): - self.visit(node.left, type_) - else: - # array membership - self.visit(node.left, type_.value_type) - else: - type_ = get_common_types(node.left, node.right).pop() - self.visit(node.left, type_) - self.visit(node.right, type_) - - def visit_Constant(self, node, type_): - if type_ is None: - possible_types = get_possible_types_from_node(node) - if len(possible_types) == 1: - type_ = possible_types.pop() - node._metadata["type"] = type_ - - def visit_Dict(self, node, type_): - node._metadata["type"] = type_ - - def visit_Index(self, node, type_): - self.visit(node.value, type_) - - def visit_List(self, node, type_): - if type_ is None: - type_ = get_possible_types_from_node(node) - # CMC 2022-04-14 this seems sus. try to only annotate - # if get_possible_types only returns 1 type - if len(type_) >= 1: - type_ = type_.pop() - node._metadata["type"] = type_ - for element in node.elements: - self.visit(element, type_.value_type) - - def visit_Name(self, node, type_): - if isinstance(type_, TYPE_T): - node._metadata["type"] = type_ - else: - node._metadata["type"] = get_exact_type_from_node(node) - - def visit_Subscript(self, node, type_): - node._metadata["type"] = type_ - - if isinstance(type_, TYPE_T): - # don't recurse; can't annotate AST children of type definition - return - - if isinstance(node.value, vy_ast.List): - possible_base_types = get_possible_types_from_node(node.value) - - if len(possible_base_types) == 1: - base_type = possible_base_types.pop() - - elif type_ is not None and len(possible_base_types) > 1: - for possible_type in possible_base_types: - if type_.compare_type(possible_type.value_type): - base_type = possible_type - break - else: - # this should have been caught in - # `get_possible_types_from_node` but wasn't. - raise TypeCheckFailure(f"Expected {type_} but it is not a possible type", node) - - else: - base_type = get_exact_type_from_node(node.value) - - # get the correct type for the index, it might - # not be base_type.key_type - index_types = get_possible_types_from_node(node.slice.value) - index_type = index_types.pop() - - self.visit(node.slice, index_type) - self.visit(node.value, base_type) - - def visit_Tuple(self, node, type_): - node._metadata["type"] = type_ - - if isinstance(type_, TYPE_T): - # don't recurse; can't annotate AST children of type definition - return - - for element, subtype in zip(node.elements, type_.member_types): - self.visit(element, subtype) - - def visit_UnaryOp(self, node, type_): - if type_ is None: - type_ = get_possible_types_from_node(node.operand) - if len(type_) == 1: - type_ = type_.pop() - node._metadata["type"] = type_ - self.visit(node.operand, type_) - - def visit_IfExp(self, node, type_): - if type_ is None: - ts = get_common_types(node.body, node.orelse) - if len(ts) == 1: - type_ = ts.pop() - - node._metadata["type"] = type_ - self.visit(node.test, BoolT()) - self.visit(node.body, type_) - self.visit(node.orelse, type_) diff --git a/vyper/semantics/analysis/common.py b/vyper/semantics/analysis/common.py index 193d1892e1..507eb0a570 100644 --- a/vyper/semantics/analysis/common.py +++ b/vyper/semantics/analysis/common.py @@ -10,10 +10,17 @@ class VyperNodeVisitorBase: def visit(self, node, *args): if isinstance(node, self.ignored_types): return + + # iterate over the MRO until we find a matching visitor function + # this lets us use a single function to broadly target several + # node types with a shared parent + for class_ in node.__class__.mro(): + ast_type = class_.__name__ + visitor_fn = getattr(self, f"visit_{ast_type}", None) + if visitor_fn: + return visitor_fn(node, *args) + node_type = type(node).__name__ - visitor_fn = getattr(self, f"visit_{node_type}", None) - if visitor_fn is None: - raise StructureException( - f"Unsupported syntax for {self.scope_name} namespace: {node_type}", node - ) - visitor_fn(node, *args) + raise StructureException( + f"Unsupported syntax for {self.scope_name} namespace: {node_type}", node + ) diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index c10df3b8fd..647f01c299 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -14,11 +14,11 @@ NonPayableViolation, StateAccessViolation, StructureException, + TypeCheckFailure, TypeMismatch, VariableDeclarationException, VyperException, ) -from vyper.semantics.analysis.annotation import StatementAnnotationVisitor from vyper.semantics.analysis.base import VarInfo from vyper.semantics.analysis.common import VyperNodeVisitorBase from vyper.semantics.analysis.utils import ( @@ -34,9 +34,11 @@ from vyper.semantics.environment import CONSTANT_ENVIRONMENT_VARS, MUTABLE_ENVIRONMENT_VARS from vyper.semantics.namespace import get_namespace from vyper.semantics.types import ( + TYPE_T, AddressT, BoolT, DArrayT, + EnumT, EventT, HashMapT, IntegerT, @@ -44,6 +46,8 @@ StringT, StructT, TupleT, + VyperType, + _BytestringT, is_type_t, ) from vyper.semantics.types.function import ContractFunctionT, MemberFunctionT, StateMutability @@ -117,20 +121,8 @@ def _check_iterator_modification( return None -def _validate_revert_reason(msg_node: vy_ast.VyperNode) -> None: - if msg_node: - if isinstance(msg_node, vy_ast.Str): - if not msg_node.value.strip(): - raise StructureException("Reason string cannot be empty", msg_node) - elif not (isinstance(msg_node, vy_ast.Name) and msg_node.id == "UNREACHABLE"): - try: - validate_expected_type(msg_node, StringT(1024)) - except TypeMismatch as e: - raise InvalidType("revert reason must fit within String[1024]") from e - - -def _validate_address_code_attribute(node: vy_ast.Attribute) -> None: - value_type = get_exact_type_from_node(node.value) +# helpers +def _validate_address_code(node: vy_ast.Attribute, value_type: VyperType) -> None: if isinstance(value_type, AddressT) and node.attr == "code": # Validate `slice(
.code, start, length)` where `length` is constant parent = node.get_ancestor() @@ -139,6 +131,7 @@ def _validate_address_code_attribute(node: vy_ast.Attribute) -> None: ok_args = len(parent.args) == 3 and isinstance(parent.args[2], vy_ast.Int) if ok_func and ok_args: return + raise StructureException( "(address).code is only allowed inside of a slice function with a constant length", node ) @@ -150,7 +143,7 @@ def _validate_msg_data_attribute(node: vy_ast.Attribute) -> None: allowed_builtins = ("slice", "len", "raw_call") if not isinstance(parent, vy_ast.Call) or parent.get("func.id") not in allowed_builtins: raise StructureException( - "msg.data is only allowed inside of the slice or len functions", node + "msg.data is only allowed inside of the slice, len or raw_call functions", node ) if parent.get("func.id") == "slice": ok_args = len(parent.args) == 3 and isinstance(parent.args[2], vy_ast.Int) @@ -160,8 +153,30 @@ def _validate_msg_data_attribute(node: vy_ast.Attribute) -> None: ) +def _validate_msg_value_access(node: vy_ast.Attribute) -> None: + if isinstance(node.value, vy_ast.Name) and node.attr == "value" and node.value.id == "msg": + raise NonPayableViolation("msg.value is not allowed in non-payable functions", node) + + +def _validate_pure_access(node: vy_ast.Attribute, typ: VyperType) -> None: + env_vars = set(CONSTANT_ENVIRONMENT_VARS.keys()) | set(MUTABLE_ENVIRONMENT_VARS.keys()) + if isinstance(node.value, vy_ast.Name) and node.value.id in env_vars: + if isinstance(typ, ContractFunctionT) and typ.mutability == StateMutability.PURE: + return + + raise StateAccessViolation( + "not allowed to query contract or environment variables in pure functions", node + ) + + +def _validate_self_reference(node: vy_ast.Name) -> None: + # CMC 2023-10-19 this detector seems sus, things like `a.b(self)` could slip through + if node.id == "self" and not isinstance(node.get_ancestor(), vy_ast.Attribute): + raise StateAccessViolation("not allowed to query self in pure functions", node) + + class FunctionNodeVisitor(VyperNodeVisitorBase): - ignored_types = (vy_ast.Constant, vy_ast.Pass) + ignored_types = (vy_ast.Pass,) scope_name = "function" def __init__( @@ -171,8 +186,7 @@ def __init__( self.fn_node = fn_node self.namespace = namespace self.func = fn_node._metadata["type"] - self.annotation_visitor = StatementAnnotationVisitor(fn_node, namespace) - self.expr_visitor = _LocalExpressionVisitor() + self.expr_visitor = _ExprVisitor(self.func) # allow internal function params to be mutable location, is_immutable = ( @@ -189,44 +203,13 @@ def __init__( f"Missing or unmatched return statements in function '{fn_node.name}'", fn_node ) - if self.func.mutability == StateMutability.PURE: - node_list = fn_node.get_descendants( - vy_ast.Attribute, - { - "value.id": set(CONSTANT_ENVIRONMENT_VARS.keys()).union( - set(MUTABLE_ENVIRONMENT_VARS.keys()) - ) - }, - ) - - # Add references to `self` as standalone address - self_references = fn_node.get_descendants(vy_ast.Name, {"id": "self"}) - standalone_self = [ - n for n in self_references if not isinstance(n.get_ancestor(), vy_ast.Attribute) - ] - node_list.extend(standalone_self) # type: ignore - - for node in node_list: - t = node._metadata.get("type") - if isinstance(t, ContractFunctionT) and t.mutability == StateMutability.PURE: - # allowed - continue - raise StateAccessViolation( - "not allowed to query contract or environment variables in pure functions", - node_list[0], - ) - if self.func.mutability is not StateMutability.PAYABLE: - node_list = fn_node.get_descendants( - vy_ast.Attribute, {"value.id": "msg", "attr": "value"} - ) - if node_list: - raise NonPayableViolation( - "msg.value is not allowed in non-payable functions", node_list[0] - ) + # visit default args + assert self.func.n_keyword_args == len(fn_node.args.defaults) + for kwarg in self.func.keyword_args: + self.expr_visitor.visit(kwarg.default_value, kwarg.typ) def visit(self, node): super().visit(node) - self.annotation_visitor.visit(node) def visit_AnnAssign(self, node): name = node.get("target.id") @@ -238,16 +221,42 @@ def visit_AnnAssign(self, node): "Memory variables must be declared with an initial value", node ) - type_ = type_from_annotation(node.annotation, DataLocation.MEMORY) - validate_expected_type(node.value, type_) + typ = type_from_annotation(node.annotation, DataLocation.MEMORY) + validate_expected_type(node.value, typ) try: - self.namespace[name] = VarInfo(type_, location=DataLocation.MEMORY) + self.namespace[name] = VarInfo(typ, location=DataLocation.MEMORY) except VyperException as exc: raise exc.with_annotation(node) from None - self.expr_visitor.visit(node.value) - def visit_Assign(self, node): + self.expr_visitor.visit(node.target, typ) + self.expr_visitor.visit(node.value, typ) + + def _validate_revert_reason(self, msg_node: vy_ast.VyperNode) -> None: + if isinstance(msg_node, vy_ast.Str): + if not msg_node.value.strip(): + raise StructureException("Reason string cannot be empty", msg_node) + self.expr_visitor.visit(msg_node, get_exact_type_from_node(msg_node)) + elif not (isinstance(msg_node, vy_ast.Name) and msg_node.id == "UNREACHABLE"): + try: + validate_expected_type(msg_node, StringT(1024)) + except TypeMismatch as e: + raise InvalidType("revert reason must fit within String[1024]") from e + self.expr_visitor.visit(msg_node, get_exact_type_from_node(msg_node)) + # CMC 2023-10-19 nice to have: tag UNREACHABLE nodes with a special type + + def visit_Assert(self, node): + if node.msg: + self._validate_revert_reason(node.msg) + + try: + validate_expected_type(node.test, BoolT()) + except InvalidType: + raise InvalidType("Assertion test value must be a boolean", node.test) + self.expr_visitor.visit(node.test, BoolT()) + + # repeated code for Assign and AugAssign + def _assign_helper(self, node): if isinstance(node.value, vy_ast.Tuple): raise StructureException("Right-hand side of assignment cannot be a tuple", node.value) @@ -260,81 +269,71 @@ def visit_Assign(self, node): validate_expected_type(node.value, target.typ) target.validate_modification(node, self.func.mutability) - self.expr_visitor.visit(node.value) - self.expr_visitor.visit(node.target) + self.expr_visitor.visit(node.value, target.typ) + self.expr_visitor.visit(node.target, target.typ) - def visit_AugAssign(self, node): - if isinstance(node.value, vy_ast.Tuple): - raise StructureException("Right-hand side of assignment cannot be a tuple", node.value) - - lhs_info = get_expr_info(node.target) - - validate_expected_type(node.value, lhs_info.typ) - lhs_info.validate_modification(node, self.func.mutability) - - self.expr_visitor.visit(node.value) - self.expr_visitor.visit(node.target) - - def visit_Raise(self, node): - if node.exc: - _validate_revert_reason(node.exc) - self.expr_visitor.visit(node.exc) + def visit_Assign(self, node): + self._assign_helper(node) - def visit_Assert(self, node): - if node.msg: - _validate_revert_reason(node.msg) - self.expr_visitor.visit(node.msg) + def visit_AugAssign(self, node): + self._assign_helper(node) - try: - validate_expected_type(node.test, BoolT()) - except InvalidType: - raise InvalidType("Assertion test value must be a boolean", node.test) - self.expr_visitor.visit(node.test) + def visit_Break(self, node): + for_node = node.get_ancestor(vy_ast.For) + if for_node is None: + raise StructureException("`break` must be enclosed in a `for` loop", node) def visit_Continue(self, node): + # TODO: use context/state instead of ast search for_node = node.get_ancestor(vy_ast.For) if for_node is None: raise StructureException("`continue` must be enclosed in a `for` loop", node) - def visit_Break(self, node): - for_node = node.get_ancestor(vy_ast.For) - if for_node is None: - raise StructureException("`break` must be enclosed in a `for` loop", node) + def visit_Expr(self, node): + if not isinstance(node.value, vy_ast.Call): + raise StructureException("Expressions without assignment are disallowed", node) - def visit_Return(self, node): - values = node.value - if values is None: - if self.func.return_type: - raise FunctionDeclarationException("Return statement is missing a value", node) - return - elif self.func.return_type is None: - raise FunctionDeclarationException("Function does not return any values", node) + fn_type = get_exact_type_from_node(node.value.func) + if is_type_t(fn_type, EventT): + raise StructureException("To call an event you must use the `log` statement", node) - if isinstance(values, vy_ast.Tuple): - values = values.elements - if not isinstance(self.func.return_type, TupleT): - raise FunctionDeclarationException("Function only returns a single value", node) - if self.func.return_type.length != len(values): - raise FunctionDeclarationException( - f"Incorrect number of return values: " - f"expected {self.func.return_type.length}, got {len(values)}", + if is_type_t(fn_type, StructT): + raise StructureException("Struct creation without assignment is disallowed", node) + + if isinstance(fn_type, ContractFunctionT): + if ( + fn_type.mutability > StateMutability.VIEW + and self.func.mutability <= StateMutability.VIEW + ): + raise StateAccessViolation( + f"Cannot call a mutating function from a {self.func.mutability.value} function", node, ) - for given, expected in zip(values, self.func.return_type.member_types): - validate_expected_type(given, expected) - else: - validate_expected_type(values, self.func.return_type) - self.expr_visitor.visit(node.value) - def visit_If(self, node): - validate_expected_type(node.test, BoolT()) - self.expr_visitor.visit(node.test) - with self.namespace.enter_scope(): - for n in node.body: - self.visit(n) - with self.namespace.enter_scope(): - for n in node.orelse: - self.visit(n) + if ( + self.func.mutability == StateMutability.PURE + and fn_type.mutability != StateMutability.PURE + ): + raise StateAccessViolation( + "Cannot call non-pure function from a pure function", node + ) + + if isinstance(fn_type, MemberFunctionT) and fn_type.is_modifying: + # it's a dotted function call like dynarray.pop() + expr_info = get_expr_info(node.value.func.value) + expr_info.validate_modification(node, self.func.mutability) + + # NOTE: fetch_call_return validates call args. + return_value = fn_type.fetch_call_return(node.value) + if ( + return_value + and not isinstance(fn_type, MemberFunctionT) + and not isinstance(fn_type, ContractFunctionT) + ): + raise StructureException( + f"Function '{fn_type._id}' cannot be called without assigning the result", node + ) + self.expr_visitor.visit(node.value, fn_type) def visit_For(self, node): if isinstance(node.iter, vy_ast.Subscript): @@ -463,19 +462,18 @@ def visit_For(self, node): f"which potentially modifies iterated storage variable '{iter_name}'", call_node, ) - self.expr_visitor.visit(node.iter) if not isinstance(node.target, vy_ast.Name): raise StructureException("Invalid syntax for loop iterator", node.target) for_loop_exceptions = [] iter_name = node.target.id - for type_ in type_list: + for possible_target_type in type_list: # type check the for loop body using each possible type for iterator value with self.namespace.enter_scope(): try: - self.namespace[iter_name] = VarInfo(type_, is_constant=True) + self.namespace[iter_name] = VarInfo(possible_target_type, is_constant=True) except VyperException as exc: raise exc.with_annotation(node) from None @@ -486,17 +484,27 @@ def visit_For(self, node): except (TypeMismatch, InvalidOperation) as exc: for_loop_exceptions.append(exc) else: - # type information is applied directly here because the - # scope is closed prior to the call to - # `StatementAnnotationVisitor` - node.target._metadata["type"] = type_ - - # success -- bail out instead of error handling. + self.expr_visitor.visit(node.target, possible_target_type) + + if isinstance(node.iter, (vy_ast.Name, vy_ast.Attribute)): + iter_type = get_exact_type_from_node(node.iter) + # note CMC 2023-10-23: slightly redundant with how type_list is computed + validate_expected_type(node.target, iter_type.value_type) + self.expr_visitor.visit(node.iter, iter_type) + if isinstance(node.iter, vy_ast.List): + len_ = len(node.iter.elements) + self.expr_visitor.visit(node.iter, SArrayT(possible_target_type, len_)) + if isinstance(node.iter, vy_ast.Call) and node.iter.func.id == "range": + for a in node.iter.args: + self.expr_visitor.visit(a, possible_target_type) + for a in node.iter.keywords: + if a.arg == "bound": + self.expr_visitor.visit(a.value, possible_target_type) + + # success -- do not enter error handling section return - # if we have gotten here, there was an error for - # every type tried for the iterator - + # failed to find a good type. bail out if len(set(str(i) for i in for_loop_exceptions)) == 1: # if every attempt at type checking raised the same exception raise for_loop_exceptions[0] @@ -510,56 +518,20 @@ def visit_For(self, node): "but type checking fails with all possible types:", node, *( - (f"Casting '{iter_name}' as {type_}: {exc.message}", exc.annotations[0]) - for type_, exc in zip(type_list, for_loop_exceptions) + (f"Casting '{iter_name}' as {typ}: {exc.message}", exc.annotations[0]) + for typ, exc in zip(type_list, for_loop_exceptions) ), ) - def visit_Expr(self, node): - if not isinstance(node.value, vy_ast.Call): - raise StructureException("Expressions without assignment are disallowed", node) - - fn_type = get_exact_type_from_node(node.value.func) - if is_type_t(fn_type, EventT): - raise StructureException("To call an event you must use the `log` statement", node) - - if is_type_t(fn_type, StructT): - raise StructureException("Struct creation without assignment is disallowed", node) - - if isinstance(fn_type, ContractFunctionT): - if ( - fn_type.mutability > StateMutability.VIEW - and self.func.mutability <= StateMutability.VIEW - ): - raise StateAccessViolation( - f"Cannot call a mutating function from a {self.func.mutability.value} function", - node, - ) - - if ( - self.func.mutability == StateMutability.PURE - and fn_type.mutability != StateMutability.PURE - ): - raise StateAccessViolation( - "Cannot call non-pure function from a pure function", node - ) - - if isinstance(fn_type, MemberFunctionT) and fn_type.is_modifying: - # it's a dotted function call like dynarray.pop() - expr_info = get_expr_info(node.value.func.value) - expr_info.validate_modification(node, self.func.mutability) - - # NOTE: fetch_call_return validates call args. - return_value = fn_type.fetch_call_return(node.value) - if ( - return_value - and not isinstance(fn_type, MemberFunctionT) - and not isinstance(fn_type, ContractFunctionT) - ): - raise StructureException( - f"Function '{fn_type._id}' cannot be called without assigning the result", node - ) - self.expr_visitor.visit(node.value) + def visit_If(self, node): + validate_expected_type(node.test, BoolT()) + self.expr_visitor.visit(node.test, BoolT()) + with self.namespace.enter_scope(): + for n in node.body: + self.visit(n) + with self.namespace.enter_scope(): + for n in node.orelse: + self.visit(n) def visit_Log(self, node): if not isinstance(node.value, vy_ast.Call): @@ -572,62 +544,249 @@ def visit_Log(self, node): f"Cannot emit logs from {self.func.mutability.value.lower()} functions", node ) f.fetch_call_return(node.value) - self.expr_visitor.visit(node.value) + node._metadata["type"] = f.typedef + self.expr_visitor.visit(node.value, f.typedef) + + def visit_Raise(self, node): + if node.exc: + self._validate_revert_reason(node.exc) + def visit_Return(self, node): + values = node.value + if values is None: + if self.func.return_type: + raise FunctionDeclarationException("Return statement is missing a value", node) + return + elif self.func.return_type is None: + raise FunctionDeclarationException("Function does not return any values", node) -class _LocalExpressionVisitor(VyperNodeVisitorBase): - ignored_types = (vy_ast.Constant, vy_ast.Name) + if isinstance(values, vy_ast.Tuple): + values = values.elements + if not isinstance(self.func.return_type, TupleT): + raise FunctionDeclarationException("Function only returns a single value", node) + if self.func.return_type.length != len(values): + raise FunctionDeclarationException( + f"Incorrect number of return values: " + f"expected {self.func.return_type.length}, got {len(values)}", + node, + ) + for given, expected in zip(values, self.func.return_type.member_types): + validate_expected_type(given, expected) + else: + validate_expected_type(values, self.func.return_type) + self.expr_visitor.visit(node.value, self.func.return_type) + + +class _ExprVisitor(VyperNodeVisitorBase): scope_name = "function" - def visit_Attribute(self, node: vy_ast.Attribute) -> None: - self.visit(node.value) + def __init__(self, fn_node: ContractFunctionT): + self.func = fn_node + + def visit(self, node, typ): + # recurse and typecheck in case we are being fed the wrong type for + # some reason. note that `validate_expected_type` is unnecessary + # for nodes that already call `get_exact_type_from_node` and + # `get_possible_types_from_node` because `validate_expected_type` + # would be calling the same function again. + # CMC 2023-06-27 would be cleanest to call validate_expected_type() + # before recursing but maybe needs some refactoring before that + # can happen. + super().visit(node, typ) + + # annotate + node._metadata["type"] = typ + + def visit_Attribute(self, node: vy_ast.Attribute, typ: VyperType) -> None: _validate_msg_data_attribute(node) - _validate_address_code_attribute(node) - - def visit_BinOp(self, node: vy_ast.BinOp) -> None: - self.visit(node.left) - self.visit(node.right) - - def visit_BoolOp(self, node: vy_ast.BoolOp) -> None: - for value in node.values: # type: ignore[attr-defined] - self.visit(value) - - def visit_Call(self, node: vy_ast.Call) -> None: - self.visit(node.func) - for arg in node.args: - self.visit(arg) - for kwarg in node.keywords: - self.visit(kwarg.value) - - def visit_Compare(self, node: vy_ast.Compare) -> None: - self.visit(node.left) # type: ignore[attr-defined] - self.visit(node.right) # type: ignore[attr-defined] - - def visit_Dict(self, node: vy_ast.Dict) -> None: - for key in node.keys: - self.visit(key) + + # CMC 2023-10-19 TODO generalize this to mutability check on every node. + # something like, + # if self.func.mutability < expr_info.mutability: + # raise ... + + if self.func.mutability != StateMutability.PAYABLE: + _validate_msg_value_access(node) + + if self.func.mutability == StateMutability.PURE: + _validate_pure_access(node, typ) + + value_type = get_exact_type_from_node(node.value) + _validate_address_code(node, value_type) + + self.visit(node.value, value_type) + + def visit_BinOp(self, node: vy_ast.BinOp, typ: VyperType) -> None: + validate_expected_type(node.left, typ) + self.visit(node.left, typ) + + rtyp = typ + if isinstance(node.op, (vy_ast.LShift, vy_ast.RShift)): + rtyp = get_possible_types_from_node(node.right).pop() + + validate_expected_type(node.right, rtyp) + + self.visit(node.right, rtyp) + + def visit_BoolOp(self, node: vy_ast.BoolOp, typ: VyperType) -> None: + assert typ == BoolT() # sanity check for value in node.values: - self.visit(value) + validate_expected_type(value, BoolT()) + self.visit(value, BoolT()) + + def visit_Call(self, node: vy_ast.Call, typ: VyperType) -> None: + call_type = get_exact_type_from_node(node.func) + # except for builtin functions, `get_exact_type_from_node` + # already calls `validate_expected_type` on the call args + # and kwargs via `call_type.fetch_call_return` + self.visit(node.func, call_type) + + if isinstance(call_type, ContractFunctionT): + # function calls + if call_type.is_internal: + self.func.called_functions.add(call_type) + for arg, typ in zip(node.args, call_type.argument_types): + self.visit(arg, typ) + for kwarg in node.keywords: + # We should only see special kwargs + typ = call_type.call_site_kwargs[kwarg.arg].typ + self.visit(kwarg.value, typ) + + elif is_type_t(call_type, EventT): + # events have no kwargs + expected_types = call_type.typedef.arguments.values() + for arg, typ in zip(node.args, expected_types): + self.visit(arg, typ) + elif is_type_t(call_type, StructT): + # struct ctors + # ctors have no kwargs + expected_types = call_type.typedef.members.values() + for value, arg_type in zip(node.args[0].values, expected_types): + self.visit(value, arg_type) + elif isinstance(call_type, MemberFunctionT): + assert len(node.args) == len(call_type.arg_types) + for arg, arg_type in zip(node.args, call_type.arg_types): + self.visit(arg, arg_type) + else: + # builtin functions + arg_types = call_type.infer_arg_types(node) + # `infer_arg_types` already calls `validate_expected_type` + for arg, arg_type in zip(node.args, arg_types): + self.visit(arg, arg_type) + kwarg_types = call_type.infer_kwarg_types(node) + for kwarg in node.keywords: + self.visit(kwarg.value, kwarg_types[kwarg.arg]) + + def visit_Compare(self, node: vy_ast.Compare, typ: VyperType) -> None: + if isinstance(node.op, (vy_ast.In, vy_ast.NotIn)): + # membership in list literal - `x in [a, b, c]` + # needle: ltyp, haystack: rtyp + if isinstance(node.right, vy_ast.List): + ltyp = get_common_types(node.left, *node.right.elements).pop() + + rlen = len(node.right.elements) + rtyp = SArrayT(ltyp, rlen) + validate_expected_type(node.right, rtyp) + else: + rtyp = get_exact_type_from_node(node.right) + if isinstance(rtyp, EnumT): + # enum membership - `some_enum in other_enum` + ltyp = rtyp + else: + # array membership - `x in my_list_variable` + assert isinstance(rtyp, (SArrayT, DArrayT)) + ltyp = rtyp.value_type - def visit_Index(self, node: vy_ast.Index) -> None: - self.visit(node.value) + validate_expected_type(node.left, ltyp) - def visit_List(self, node: vy_ast.List) -> None: - for element in node.elements: - self.visit(element) + self.visit(node.left, ltyp) + self.visit(node.right, rtyp) + + else: + # ex. a < b + cmp_typ = get_common_types(node.left, node.right).pop() + if isinstance(cmp_typ, _BytestringT): + # for bytestrings, get_common_types automatically downcasts + # to the smaller common type - that will annotate with the + # wrong type, instead use get_exact_type_from_node (which + # resolves to the right type for bytestrings anyways). + ltyp = get_exact_type_from_node(node.left) + rtyp = get_exact_type_from_node(node.right) + else: + ltyp = rtyp = cmp_typ + validate_expected_type(node.left, ltyp) + validate_expected_type(node.right, rtyp) + + self.visit(node.left, ltyp) + self.visit(node.right, rtyp) + + def visit_Constant(self, node: vy_ast.Constant, typ: VyperType) -> None: + validate_expected_type(node, typ) - def visit_Subscript(self, node: vy_ast.Subscript) -> None: - self.visit(node.value) - self.visit(node.slice) + def visit_Index(self, node: vy_ast.Index, typ: VyperType) -> None: + validate_expected_type(node.value, typ) + self.visit(node.value, typ) - def visit_Tuple(self, node: vy_ast.Tuple) -> None: + def visit_List(self, node: vy_ast.List, typ: VyperType) -> None: + assert isinstance(typ, (SArrayT, DArrayT)) for element in node.elements: - self.visit(element) + validate_expected_type(element, typ.value_type) + self.visit(element, typ.value_type) - def visit_UnaryOp(self, node: vy_ast.UnaryOp) -> None: - self.visit(node.operand) # type: ignore[attr-defined] + def visit_Name(self, node: vy_ast.Name, typ: VyperType) -> None: + if self.func.mutability == StateMutability.PURE: + _validate_self_reference(node) + + if not isinstance(typ, TYPE_T): + validate_expected_type(node, typ) + + def visit_Subscript(self, node: vy_ast.Subscript, typ: VyperType) -> None: + if isinstance(typ, TYPE_T): + # don't recurse; can't annotate AST children of type definition + return + + if isinstance(node.value, vy_ast.List): + possible_base_types = get_possible_types_from_node(node.value) + + for possible_type in possible_base_types: + if typ.compare_type(possible_type.value_type): + base_type = possible_type + break + else: + # this should have been caught in + # `get_possible_types_from_node` but wasn't. + raise TypeCheckFailure(f"Expected {typ} but it is not a possible type", node) + + else: + base_type = get_exact_type_from_node(node.value) + + # get the correct type for the index, it might + # not be exactly base_type.key_type + # note: index_type is validated in types_from_Subscript + index_types = get_possible_types_from_node(node.slice.value) + index_type = index_types.pop() + + self.visit(node.slice, index_type) + self.visit(node.value, base_type) + + def visit_Tuple(self, node: vy_ast.Tuple, typ: VyperType) -> None: + if isinstance(typ, TYPE_T): + # don't recurse; can't annotate AST children of type definition + return + + assert isinstance(typ, TupleT) + for element, subtype in zip(node.elements, typ.member_types): + validate_expected_type(element, subtype) + self.visit(element, subtype) - def visit_IfExp(self, node: vy_ast.IfExp) -> None: - self.visit(node.test) - self.visit(node.body) - self.visit(node.orelse) + def visit_UnaryOp(self, node: vy_ast.UnaryOp, typ: VyperType) -> None: + validate_expected_type(node.operand, typ) + self.visit(node.operand, typ) + + def visit_IfExp(self, node: vy_ast.IfExp, typ: VyperType) -> None: + validate_expected_type(node.test, BoolT()) + self.visit(node.test, BoolT()) + validate_expected_type(node.body, typ) + self.visit(node.body, typ) + validate_expected_type(node.orelse, typ) + self.visit(node.orelse, typ) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 02ae82faac..239438f35b 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -1,13 +1,13 @@ -import importlib -import pkgutil -from typing import Optional, Union +import os +from pathlib import Path, PurePath +from typing import Optional import vyper.builtins.interfaces from vyper import ast as vy_ast +from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, InputBundle from vyper.evm.opcodes import version_check from vyper.exceptions import ( CallViolation, - CompilerPanic, ExceptionList, InvalidLiteral, InvalidType, @@ -15,30 +15,27 @@ StateAccessViolation, StructureException, SyntaxException, - UndeclaredDefinition, VariableDeclarationException, VyperException, ) from vyper.semantics.analysis.base import VarInfo from vyper.semantics.analysis.common import VyperNodeVisitorBase -from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions from vyper.semantics.analysis.utils import check_constant, validate_expected_type from vyper.semantics.data_locations import DataLocation from vyper.semantics.namespace import Namespace, get_namespace from vyper.semantics.types import EnumT, EventT, InterfaceT, StructT from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.utils import type_from_annotation -from vyper.typing import InterfaceDict -def add_module_namespace(vy_module: vy_ast.Module, interface_codes: InterfaceDict) -> None: +def add_module_namespace(vy_module: vy_ast.Module, input_bundle: InputBundle) -> None: """ Analyze a Vyper module AST node, add all module-level objects to the namespace and validate top-level correctness """ namespace = get_namespace() - ModuleAnalyzer(vy_module, interface_codes, namespace) + ModuleAnalyzer(vy_module, input_bundle, namespace) def _find_cyclic_call(fn_names: list, self_members: dict) -> Optional[list]: @@ -58,10 +55,10 @@ class ModuleAnalyzer(VyperNodeVisitorBase): scope_name = "module" def __init__( - self, module_node: vy_ast.Module, interface_codes: InterfaceDict, namespace: Namespace + self, module_node: vy_ast.Module, input_bundle: InputBundle, namespace: Namespace ) -> None: self.ast = module_node - self.interface_codes = interface_codes or {} + self.input_bundle = input_bundle self.namespace = namespace # TODO: Move computation out of constructor @@ -98,7 +95,6 @@ def __init__( _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 self_members = namespace["self"].typ.members # get list of internal function calls made by each function @@ -288,17 +284,19 @@ def visit_FunctionDef(self, node): def visit_Import(self, node): if not node.alias: raise StructureException("Import requires an accompanying `as` statement", node) - _add_import(node, node.name, node.alias, node.alias, self.interface_codes, self.namespace) + # import x.y[name] as y[alias] + self._add_import(node, 0, node.name, node.alias) def visit_ImportFrom(self, node): - _add_import( - node, - node.module, - node.name, - node.alias or node.name, - self.interface_codes, - self.namespace, - ) + # from m.n[module] import x[name] as y[alias] + alias = node.alias or node.name + + module = node.module or "" + if module: + module += "." + + qualified_module_name = module + node.name + self._add_import(node, node.level, qualified_module_name, alias) def visit_InterfaceDef(self, node): obj = InterfaceT.from_ast(node) @@ -314,41 +312,87 @@ def visit_StructDef(self, node): except VyperException as exc: raise exc.with_annotation(node) from None + def _add_import( + self, node: vy_ast.VyperNode, level: int, qualified_module_name: str, alias: str + ) -> None: + type_ = self._load_import(level, qualified_module_name) + + try: + self.namespace[alias] = type_ + except VyperException as exc: + raise exc.with_annotation(node) from None + + # load an InterfaceT from an import. + # raises FileNotFoundError + def _load_import(self, level: int, module_str: str) -> InterfaceT: + if _is_builtin(module_str): + return _load_builtin_import(level, module_str) + + path = _import_to_path(level, module_str) + + try: + file = self.input_bundle.load_file(path.with_suffix(".vy")) + assert isinstance(file, FileInput) # mypy hint + interface_ast = vy_ast.parse_to_ast(file.source_code, contract_name=str(file.path)) + return InterfaceT.from_ast(interface_ast) + except FileNotFoundError: + pass + + try: + file = self.input_bundle.load_file(path.with_suffix(".json")) + assert isinstance(file, ABIInput) # mypy hint + return InterfaceT.from_json_abi(str(file.path), file.abi) + except FileNotFoundError: + raise ModuleNotFoundError(module_str) + + +# convert an import to a path (without suffix) +def _import_to_path(level: int, module_str: str) -> PurePath: + base_path = "" + if level > 1: + base_path = "../" * (level - 1) + elif level == 1: + base_path = "./" + return PurePath(f"{base_path}{module_str.replace('.','/')}/") + + +# can add more, e.g. "vyper.builtins.interfaces", etc. +BUILTIN_PREFIXES = ["vyper.interfaces"] + + +def _is_builtin(module_str): + return any(module_str.startswith(prefix) for prefix in BUILTIN_PREFIXES) + + +def _load_builtin_import(level: int, module_str: str) -> InterfaceT: + if not _is_builtin(module_str): + raise ModuleNotFoundError(f"Not a builtin: {module_str}") from None + + builtins_path = vyper.builtins.interfaces.__path__[0] + # hygiene: convert to relpath to avoid leaking user directory info + # (note Path.relative_to cannot handle absolute to relative path + # conversion, so we must use the `os` module). + builtins_path = os.path.relpath(builtins_path) + + search_path = Path(builtins_path).parent.parent.parent + # generate an input bundle just because it knows how to build paths. + input_bundle = FilesystemInputBundle([search_path]) + + # remap builtins directory -- + # vyper/interfaces => vyper/builtins/interfaces + remapped_module = module_str + if remapped_module.startswith("vyper.interfaces"): + remapped_module = remapped_module.removeprefix("vyper.interfaces") + remapped_module = vyper.builtins.interfaces.__package__ + remapped_module -def _add_import( - node: Union[vy_ast.Import, vy_ast.ImportFrom], - module: str, - name: str, - alias: str, - interface_codes: InterfaceDict, - namespace: dict, -) -> None: - if module == "vyper.interfaces": - interface_codes = _get_builtin_interfaces() - if name not in interface_codes: - suggestions_str = get_levenshtein_error_suggestions(name, _get_builtin_interfaces(), 1.0) - raise UndeclaredDefinition(f"Unknown interface: {name}. {suggestions_str}", node) - - if interface_codes[name]["type"] == "vyper": - interface_ast = vy_ast.parse_to_ast(interface_codes[name]["code"], contract_name=name) - type_ = InterfaceT.from_ast(interface_ast) - elif interface_codes[name]["type"] == "json": - type_ = InterfaceT.from_json_abi(name, interface_codes[name]["code"]) # type: ignore - else: - raise CompilerPanic(f"Unknown interface format: {interface_codes[name]['type']}") + path = _import_to_path(level, remapped_module).with_suffix(".vy") try: - namespace[alias] = type_ - except VyperException as exc: - raise exc.with_annotation(node) from None - - -def _get_builtin_interfaces(): - interface_names = [i.name for i in pkgutil.iter_modules(vyper.builtins.interfaces.__path__)] - return { - name: { - "type": "vyper", - "code": importlib.import_module(f"vyper.builtins.interfaces.{name}").interface_code, - } - for name in interface_names - } + file = input_bundle.load_file(path) + assert isinstance(file, FileInput) # mypy hint + except FileNotFoundError: + raise ModuleNotFoundError(f"Not a builtin: {module_str}") from None + + # TODO: it might be good to cache this computation + interface_ast = vy_ast.parse_to_ast(file.source_code, contract_name=module_str) + return InterfaceT.from_ast(interface_ast) diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 4f911764e0..afa6b56838 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -312,10 +312,17 @@ def types_from_Constant(self, node): def types_from_List(self, node): # literal array if _is_empty_list(node): - # empty list literal `[]` ret = [] - # subtype can be anything - for t in types.PRIMITIVE_TYPES.values(): + + if len(node.elements) > 0: + # empty nested list literals `[[], []]` + subtypes = self.get_possible_types_from_node(node.elements[0]) + else: + # empty list literal `[]` + # subtype can be anything + subtypes = types.PRIMITIVE_TYPES.values() + + for t in subtypes: # 1 is minimum possible length for dynarray, # can be assigned to anything if isinstance(t, VyperType): diff --git a/vyper/typing.py b/vyper/typing.py index 18e201e814..ad3964dff9 100644 --- a/vyper/typing.py +++ b/vyper/typing.py @@ -7,17 +7,9 @@ # Compiler ContractPath = str SourceCode = str -ContractCodes = Dict[ContractPath, SourceCode] OutputFormats = Sequence[str] -OutputDict = Dict[ContractPath, OutputFormats] StorageLayout = Dict -# Interfaces -InterfaceAsName = str -InterfaceImportPath = str -InterfaceImports = Dict[InterfaceAsName, InterfaceImportPath] -InterfaceDict = Dict[ContractPath, InterfaceImports] - # Opcodes OpcodeGasCost = Union[int, Tuple] OpcodeValue = Tuple[Optional[int], int, int, OpcodeGasCost]