diff --git a/docs/compiling-a-contract.rst b/docs/compiling-a-contract.rst index c839e1e81d..f59d8bf786 100644 --- a/docs/compiling-a-contract.rst +++ b/docs/compiling-a-contract.rst @@ -198,6 +198,23 @@ The following is a list of supported EVM versions, and changes in the compiler i - 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. + +.. _warnings: + +Controlling Warnings +==================== + +Vyper allows suppression of warnings via the CLI flag ``-Wnone``, or promotion of (all) warnings to errors via the ``-Werror`` flag. + +.. code:: shell + + $ vyper -Wnone foo.vy # suppress warnings + +.. code:: shell + + $ vyper -Werror foo.vy # promote warnings to errors + + .. _integrity-hash: Integrity Hash diff --git a/examples/auctions/blind_auction.vy b/examples/auctions/blind_auction.vy index 143206ccb4..f102bdcfbc 100644 --- a/examples/auctions/blind_auction.vy +++ b/examples/auctions/blind_auction.vy @@ -172,7 +172,7 @@ def auctionEnd(): assert not self.ended # Log auction ending and set flag - log AuctionEnded(self.highestBidder, self.highestBid) + log AuctionEnded(highestBidder=self.highestBidder, highestBid=self.highestBid) self.ended = True # Transfer funds to beneficiary diff --git a/examples/stock/company.vy b/examples/stock/company.vy index 7739959e92..dda88c9798 100644 --- a/examples/stock/company.vy +++ b/examples/stock/company.vy @@ -63,7 +63,7 @@ def buyStock(): self.holdings[msg.sender] += buy_order # Log the buy event. - log Buy(msg.sender, buy_order) + log Buy(buyer=msg.sender, buy_order=buy_order) # Public function to allow external access to _getHolding @view @@ -94,7 +94,7 @@ def sellStock(sell_order: uint256): send(msg.sender, sell_order * self.price) # Log the sell event. - log Sell(msg.sender, sell_order) + log Sell(seller=msg.sender, sell_order=sell_order) # Transfer stock from one stockholder to another. (Assume that the # receiver is given some compensation, but this is not enforced.) @@ -109,7 +109,7 @@ def transferStock(receiver: address, transfer_order: uint256): self.holdings[receiver] += transfer_order # Log the transfer event. - log Transfer(msg.sender, receiver, transfer_order) + log Transfer(sender=msg.sender, receiver=receiver, value=transfer_order) # Allow the company to pay someone for services rendered. @external @@ -123,7 +123,8 @@ def payBill(vendor: address, amount: uint256): send(vendor, amount) # Log the payment event. - log Pay(vendor, amount) + log Pay(vendor=vendor, amount=amount) + # Public function to allow external access to _debt @view diff --git a/examples/storage/advanced_storage.vy b/examples/storage/advanced_storage.vy index 42a455cbf1..397bf6b97c 100644 --- a/examples/storage/advanced_storage.vy +++ b/examples/storage/advanced_storage.vy @@ -15,7 +15,7 @@ def set(_x: int128): assert _x >= 0, "No negative values" assert self.storedData < 100, "Storage is locked when 100 or more is stored" self.storedData = _x - log DataChange(msg.sender, _x) + log DataChange(setter=msg.sender, value=_x) @external def reset(): diff --git a/examples/tokens/ERC1155ownable.vy b/examples/tokens/ERC1155ownable.vy index b8513243d4..a8e9b4c327 100644 --- a/examples/tokens/ERC1155ownable.vy +++ b/examples/tokens/ERC1155ownable.vy @@ -69,7 +69,7 @@ event unPaused: event OwnershipTransferred: # Emits smart contract ownership transfer from current to new owner - previouwOwner: address + previousOwner: address newOwner: address event TransferSingle: @@ -150,7 +150,7 @@ def pause(): assert self.owner == msg.sender, "Ownable: caller is not the owner" assert not self.paused, "the contract is already paused" self.paused = True - log Paused(msg.sender) + log Paused(account=msg.sender) @external def unpause(): @@ -162,7 +162,7 @@ def unpause(): assert self.owner == msg.sender, "Ownable: caller is not the owner" assert self.paused, "the contract is not paused" self.paused = False - log unPaused(msg.sender) + log unPaused(account=msg.sender) ## ownership ## @external @@ -179,7 +179,7 @@ def transferOwnership(newOwner: address): assert newOwner != empty(address), "Transfer to the zero address not allowed. Use renounceOwnership() instead." oldOwner: address = self.owner self.owner = newOwner - log OwnershipTransferred(oldOwner, newOwner) + log OwnershipTransferred(previousOwner=oldOwner, newOwner=newOwner) @external def renounceOwnership(): @@ -191,7 +191,7 @@ def renounceOwnership(): assert self.owner == msg.sender, "Ownable: caller is not the owner" oldOwner: address = self.owner self.owner = empty(address) - log OwnershipTransferred(oldOwner, empty(address)) + log OwnershipTransferred(previousOwner=oldOwner, newOwner=empty(address)) @external @view @@ -226,7 +226,7 @@ def mint(receiver: address, id: uint256, amount:uint256): assert receiver != empty(address), "Can not mint to ZERO ADDRESS" operator: address = msg.sender self.balanceOf[receiver][id] += amount - log TransferSingle(operator, empty(address), receiver, id, amount) + log TransferSingle(operator=operator, fromAddress=empty(address), to=receiver, id=id, value=amount) @external @@ -249,7 +249,7 @@ def mintBatch(receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: Dy break self.balanceOf[receiver][ids[i]] += amounts[i] - log TransferBatch(operator, empty(address), receiver, ids, amounts) + log TransferBatch(operator=operator, fromAddress=empty(address), to=receiver, ids=ids, values=amounts) ## burn ## @external @@ -263,7 +263,7 @@ def burn(id: uint256, amount: uint256): assert not self.paused, "The contract has been paused" assert self.balanceOf[msg.sender][id] > 0 , "caller does not own this ID" self.balanceOf[msg.sender][id] -= amount - log TransferSingle(msg.sender, msg.sender, empty(address), id, amount) + log TransferSingle(operator=msg.sender, fromAddress=msg.sender, to=empty(address), id=id, value=amount) @external def burnBatch(ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE]): @@ -283,7 +283,7 @@ def burnBatch(ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BAT break self.balanceOf[msg.sender][ids[i]] -= amounts[i] - log TransferBatch(msg.sender, msg.sender, empty(address), ids, amounts) + log TransferBatch(operator=msg.sender, fromAddress=msg.sender, to=empty(address), ids=ids, values=amounts) ## approval ## @external @@ -298,7 +298,7 @@ def setApprovalForAll(owner: address, operator: address, approved: bool): assert not self.paused, "The contract has been paused" assert owner != operator, "ERC1155: setting approval status for self" self.isApprovedForAll[owner][operator] = approved - log ApprovalForAll(owner, operator, approved) + log ApprovalForAll(account=owner, operator=operator, approved=approved) @external def safeTransferFrom(sender: address, receiver: address, id: uint256, amount: uint256, bytes: bytes32): @@ -317,7 +317,7 @@ def safeTransferFrom(sender: address, receiver: address, id: uint256, amount: ui operator: address = msg.sender self.balanceOf[sender][id] -= amount self.balanceOf[receiver][id] += amount - log TransferSingle(operator, sender, receiver, id, amount) + log TransferSingle(operator=operator, fromAddress=sender, to=receiver, id=id, value=amount) @external def safeBatchTransferFrom(sender: address, receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE], _bytes: bytes32): @@ -342,7 +342,7 @@ def safeBatchTransferFrom(sender: address, receiver: address, ids: DynArray[uint self.balanceOf[sender][id] -= amount self.balanceOf[receiver][id] += amount - log TransferBatch(operator, sender, receiver, ids, amounts) + log TransferBatch(operator=operator, fromAddress=sender, to=receiver, ids=ids, values=amounts) # URI # @external @@ -355,7 +355,7 @@ def setURI(uri: String[MAX_URI_LENGTH]): assert self.baseuri != uri, "new and current URI are identical" assert msg.sender == self.owner, "Only the contract owner can update the URI" self.baseuri = uri - log URI(uri, 0) + log URI(value=uri, id=0) @external def toggleDynUri(status: bool): @@ -391,7 +391,7 @@ def setContractURI(contractUri: String[MAX_URI_LENGTH]): assert self.contractURI != contractUri, "new and current URI are identical" assert msg.sender == self.owner, "Only the contract owner can update the URI" self.contractURI = contractUri - log URI(contractUri, 0) + log URI(value=contractUri, id=0) @view @external diff --git a/examples/tokens/ERC20.vy b/examples/tokens/ERC20.vy index 2d70fd670a..f16cbb73f2 100644 --- a/examples/tokens/ERC20.vy +++ b/examples/tokens/ERC20.vy @@ -39,8 +39,7 @@ def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: self.balanceOf[msg.sender] = init_supply self.totalSupply = init_supply self.minter = msg.sender - log IERC20.Transfer(empty(address), msg.sender, init_supply) - + log IERC20.Transfer(sender=empty(address), receiver=msg.sender, value=init_supply) @external @@ -54,7 +53,7 @@ def transfer(_to : address, _value : uint256) -> bool: # so the following subtraction would revert on insufficient balance self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value - log IERC20.Transfer(msg.sender, _to, _value) + log IERC20.Transfer(sender=msg.sender, receiver=_to, value=_value) return True @@ -73,7 +72,7 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: # NOTE: vyper does not allow underflows # so the following subtraction would revert on insufficient allowance self.allowance[_from][msg.sender] -= _value - log IERC20.Transfer(_from, _to, _value) + log IERC20.Transfer(sender=_from, receiver=_to, value=_value) return True @@ -89,7 +88,7 @@ def approve(_spender : address, _value : uint256) -> bool: @param _value The amount of tokens to be spent. """ self.allowance[msg.sender][_spender] = _value - log IERC20.Approval(msg.sender, _spender, _value) + log IERC20.Approval(owner=msg.sender, spender=_spender, value=_value) return True @@ -106,7 +105,7 @@ def mint(_to: address, _value: uint256): assert _to != empty(address) self.totalSupply += _value self.balanceOf[_to] += _value - log IERC20.Transfer(empty(address), _to, _value) + log IERC20.Transfer(sender=empty(address), receiver=_to, value=_value) @internal @@ -120,7 +119,7 @@ def _burn(_to: address, _value: uint256): assert _to != empty(address) self.totalSupply -= _value self.balanceOf[_to] -= _value - log IERC20.Transfer(_to, empty(address), _value) + log IERC20.Transfer(sender=_to, receiver=empty(address), value=_value) @external diff --git a/examples/tokens/ERC4626.vy b/examples/tokens/ERC4626.vy index 2dc4656746..313d99ed4c 100644 --- a/examples/tokens/ERC4626.vy +++ b/examples/tokens/ERC4626.vy @@ -56,14 +56,14 @@ def decimals() -> uint8: def transfer(receiver: address, amount: uint256) -> bool: self.balanceOf[msg.sender] -= amount self.balanceOf[receiver] += amount - log IERC20.Transfer(msg.sender, receiver, amount) + log IERC20.Transfer(sender=msg.sender, receiver=receiver, value=amount) return True @external def approve(spender: address, amount: uint256) -> bool: self.allowance[msg.sender][spender] = amount - log IERC20.Approval(msg.sender, spender, amount) + log IERC20.Approval(owner=msg.sender, spender=spender, value=amount) return True @@ -72,7 +72,7 @@ def transferFrom(sender: address, receiver: address, amount: uint256) -> bool: self.allowance[sender][msg.sender] -= amount self.balanceOf[sender] -= amount self.balanceOf[receiver] += amount - log IERC20.Transfer(sender, receiver, amount) + log IERC20.Transfer(sender=sender, receiver=receiver, value=amount) return True @@ -137,7 +137,7 @@ def deposit(assets: uint256, receiver: address=msg.sender) -> uint256: self.totalSupply += shares self.balanceOf[receiver] += shares - log IERC4626.Deposit(msg.sender, receiver, assets, shares) + log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares) return shares @@ -170,7 +170,7 @@ def mint(shares: uint256, receiver: address=msg.sender) -> uint256: self.totalSupply += shares self.balanceOf[receiver] += shares - log IERC4626.Deposit(msg.sender, receiver, assets, shares) + log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares) return assets @@ -207,7 +207,7 @@ def withdraw(assets: uint256, receiver: address=msg.sender, owner: address=msg.s self.balanceOf[owner] -= shares extcall self.asset.transfer(receiver, assets) - log IERC4626.Withdraw(msg.sender, receiver, owner, assets, shares) + log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares) return shares @@ -233,7 +233,7 @@ def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sen self.balanceOf[owner] -= shares extcall self.asset.transfer(receiver, assets) - log IERC4626.Withdraw(msg.sender, receiver, owner, assets, shares) + log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares) return assets diff --git a/examples/tokens/ERC721.vy b/examples/tokens/ERC721.vy index d57088b5aa..acb445bd57 100644 --- a/examples/tokens/ERC721.vy +++ b/examples/tokens/ERC721.vy @@ -201,7 +201,7 @@ def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: addr # Add NFT self._addTokenTo(_to, _tokenId) # Log the transfer - log IERC721.Transfer(_from, _to, _tokenId) + log IERC721.Transfer(sender=_from, receiver=_to, token_id=_tokenId) ### TRANSFER FUNCTIONS ### @@ -275,7 +275,7 @@ def approve(_approved: address, _tokenId: uint256): assert (senderIsOwner or senderIsApprovedForAll) # Set the approval self.idToApprovals[_tokenId] = _approved - log IERC721.Approval(owner, _approved, _tokenId) + log IERC721.Approval(owner=owner, approved=_approved, token_id=_tokenId) @external @@ -291,7 +291,7 @@ def setApprovalForAll(_operator: address, _approved: bool): # Throws if `_operator` is the `msg.sender` assert _operator != msg.sender self.ownerToOperators[msg.sender][_operator] = _approved - log IERC721.ApprovalForAll(msg.sender, _operator, _approved) + log IERC721.ApprovalForAll(owner=msg.sender, operator=_operator, approved=_approved) ### MINT & BURN FUNCTIONS ### @@ -313,7 +313,7 @@ def mint(_to: address, _tokenId: uint256) -> bool: assert _to != empty(address) # Add NFT. Throws if `_tokenId` is owned by someone self._addTokenTo(_to, _tokenId) - log IERC721.Transfer(empty(address), _to, _tokenId) + log IERC721.Transfer(sender=empty(address), receiver=_to, token_id=_tokenId) return True @@ -333,7 +333,7 @@ def burn(_tokenId: uint256): assert owner != empty(address) self._clearApproval(owner, _tokenId) self._removeTokenFrom(owner, _tokenId) - log IERC721.Transfer(owner, empty(address), _tokenId) + log IERC721.Transfer(sender=owner, receiver=empty(address), token_id=_tokenId) @view diff --git a/tests/functional/codegen/calling_convention/test_inlineable_functions.py b/tests/functional/codegen/calling_convention/test_inlineable_functions.py new file mode 100644 index 0000000000..eae8a84bcd --- /dev/null +++ b/tests/functional/codegen/calling_convention/test_inlineable_functions.py @@ -0,0 +1,66 @@ +""" +Test functionality of internal functions which may be inlined +""" +# note for refactor: this may be able to be merged with +# calling_convention/test_internal_call.py + + +def test_call_in_call(get_contract): + code = """ +@internal +def _foo(a: uint256,) -> uint256: + return 1 + a + +@internal +def _foo2() -> uint256: + return 4 + +@external +def foo() -> uint256: + return self._foo(self._foo2()) + """ + + c = get_contract(code) + assert c.foo() == 5 + + +def test_call_in_call_with_raise(get_contract, tx_failed): + code = """ +@internal +def sum(a: uint256) -> uint256: + if a > 1: + return a + 1 + raise + +@internal +def middle(a: uint256) -> uint256: + return self.sum(a) + +@external +def test(a: uint256) -> uint256: + return self.middle(a) + """ + + c = get_contract(code) + + assert c.test(2) == 3 + + with tx_failed(): + c.test(0) + + +def test_inliner_with_unused_param(get_contract): + code = """ +data: public(uint256) + +@internal +def _foo(start: uint256, length: uint256): + self.data = start + +@external +def foo(x: uint256, y: uint256): + self._foo(x, y) +""" + + c = get_contract(code) + c.foo(1, 2) diff --git a/tests/functional/codegen/features/iteration/test_for_in_list.py b/tests/functional/codegen/features/iteration/test_for_in_list.py index 2c624e2856..9079fdfe3e 100644 --- a/tests/functional/codegen/features/iteration/test_for_in_list.py +++ b/tests/functional/codegen/features/iteration/test_for_in_list.py @@ -214,6 +214,21 @@ def data() -> int128: assert c.data() == sum(xs) +def test_constant_list_iter(get_contract): + code = """ +MY_LIST: constant(uint24[4]) = [1, 2, 3, 4] + +@external +def foo() -> uint24: + x: uint24 = 0 + for s: uint24 in MY_LIST: + x += s + return x + """ + c = get_contract(code) + assert c.foo() == sum([1, 2, 3, 4]) + + def test_basic_for_list_storage_address(get_contract): code = """ addresses: address[3] diff --git a/tests/functional/syntax/test_event_kwarg_hint.py b/tests/functional/syntax/test_event_kwarg_hint.py new file mode 100644 index 0000000000..6bbd01f23e --- /dev/null +++ b/tests/functional/syntax/test_event_kwarg_hint.py @@ -0,0 +1,43 @@ +import warnings + +from vyper.compiler import compile_code + + +def test_event_kwarg_hint(): + code = """ +from ethereum.ercs import IERC20 + +def foo(): + log IERC20.Transfer(msg.sender, msg.sender, 123) + """ + + with warnings.catch_warnings(record=True) as w: + assert compile_code(code) is not None + + expected = "Instantiating events with positional arguments is deprecated " + expected += "as of v0.4.1 and will be disallowed in a future release. " + expected += "Use kwargs instead e.g.:\n" + expected += "```\nlog IERC20.Transfer(sender=msg.sender, receiver=msg.sender, value=123)\n```" + + assert len(w) == 1, [s.message for s in w] + assert str(w[0].message).startswith(expected) + + +def test_event_hint_single_char_argument(): + code = """ +from ethereum.ercs import IERC20 + +def foo(): + log IERC20.Transfer(msg.sender, msg.sender, 1) + """ + + with warnings.catch_warnings(record=True) as w: + assert compile_code(code) is not None + + expected = "Instantiating events with positional arguments is deprecated " + expected += "as of v0.4.1 and will be disallowed in a future release. " + expected += "Use kwargs instead e.g.:\n" + expected += "```\nlog IERC20.Transfer(sender=msg.sender, receiver=msg.sender, value=1)\n```" + + assert len(w) == 1, [s.message for s in w] + assert str(w[0].message).startswith(expected) diff --git a/tests/functional/syntax/warnings/test_contract_size_limit_warning.py b/tests/functional/syntax/warnings/test_contract_size_limit_warning.py new file mode 100644 index 0000000000..3e27304266 --- /dev/null +++ b/tests/functional/syntax/warnings/test_contract_size_limit_warning.py @@ -0,0 +1,23 @@ +import random + +import pytest + +import vyper + + +@pytest.fixture +def huge_bytestring(): + r = random.Random(b"vyper") + + return bytes([r.getrandbits(8) for _ in range(0x6001)]) + + +def test_contract_size_exceeded(huge_bytestring): + code = f""" +@external +def a() -> bool: + q: Bytes[24577] = {huge_bytestring} + return True +""" + with pytest.warns(vyper.warnings.ContractSizeLimit): + vyper.compile_code(code, output_formats=["bytecode_runtime"]) diff --git a/tests/functional/syntax/warnings/test_deprecation_warning.py b/tests/functional/syntax/warnings/test_deprecation_warning.py new file mode 100644 index 0000000000..6b30974e97 --- /dev/null +++ b/tests/functional/syntax/warnings/test_deprecation_warning.py @@ -0,0 +1,50 @@ +import json + +import pytest + +import vyper +from vyper.cli.vyper_json import compile_json + +deprecated = [ + """ +struct Foo: + a: uint256 + b: uint256 + +@external +def foo(): + f: Foo = Foo({a: 128, b: 256}) + """, + """ +event Foo: + a: uint256 + b: uint256 + +@external +def foo(): + log Foo({a: 128, b: 256}) + """, +] + + +@pytest.mark.parametrize("code", deprecated) +def test_deprecated_warning(code): + with pytest.warns(vyper.warnings.Deprecation): + vyper.compile_code(code) + + +def test_deprecated_optimize_boolean_flag(): + code = """ +@external +def foo(): + pass + """ + + input_json = { + "language": "Vyper", + "sources": {"contracts/foo.vy": {"content": code}}, + "settings": {"outputSelection": {"*": ["*"]}, "optimize": True}, + } + + with pytest.warns(vyper.warnings.Deprecation): + compile_json(json.dumps(input_json)) diff --git a/tests/functional/syntax/warnings/test_enum_usage_warning.py b/tests/functional/syntax/warnings/test_enum_usage_warning.py new file mode 100644 index 0000000000..887b1808af --- /dev/null +++ b/tests/functional/syntax/warnings/test_enum_usage_warning.py @@ -0,0 +1,18 @@ +import pytest + +import vyper + + +def test_enum_usage_warning(): + code = """ +enum Foo: + Fe + Fi + Fo + +@external +def foo() -> Foo: + return Foo.Fe + """ + with pytest.warns(vyper.warnings.EnumUsage): + vyper.compile_code(code) diff --git a/tests/unit/cli/vyper_compile/test_parse_args.py b/tests/unit/cli/vyper_compile/test_parse_args.py index 0e8c4e9605..77e6a2be0b 100644 --- a/tests/unit/cli/vyper_compile/test_parse_args.py +++ b/tests/unit/cli/vyper_compile/test_parse_args.py @@ -1,8 +1,10 @@ import os +import warnings import pytest from vyper.cli.vyper_compile import _parse_args +from vyper.warnings import VyperWarning @pytest.fixture @@ -27,3 +29,33 @@ def foo() -> bool: _parse_args([str(bar_path)]) # absolute path, subfolder of cwd _parse_args([str(bar_path.relative_to(chdir_path.parent))]) # relative path + + +def test_warnings(make_file): + """ + test -Werror and -Wnone + """ + # test code which emits warnings + code = """ +x: public(uint256[2**64]) + """ + path = make_file("foo.vy", code) + path_str = str(path) + + with warnings.catch_warnings(record=True) as w: + _parse_args([str(path)]) + + with pytest.raises(VyperWarning) as e: + _parse_args([path_str, "-Werror"]) + + assert len(w) == 1 + warning_message = w[0].message.message + + assert e.value.message == warning_message + + # test squashing warnings + with warnings.catch_warnings(record=True) as w: + _parse_args([path_str, "-Wnone"]) + assert len(w) == 0 + + warnings.resetwarnings() diff --git a/tests/unit/compiler/asm/test_asm_optimizer.py b/tests/unit/compiler/asm/test_asm_optimizer.py index ee6b1653b0..dfbb53ad5a 100644 --- a/tests/unit/compiler/asm/test_asm_optimizer.py +++ b/tests/unit/compiler/asm/test_asm_optimizer.py @@ -120,7 +120,9 @@ def foo(): input_bundle = make_input_bundle({"library.vy": library}) res = compile_code(code, input_bundle=input_bundle, output_formats=["asm"]) asm = res["asm"] - assert "some_function()" in asm + + if not experimental_codegen: + assert "some_function()" in asm # Venom function inliner will remove this assert "unused1()" not in asm assert "unused2()" not in asm diff --git a/tests/unit/compiler/test_compile_code.py b/tests/unit/compiler/test_compile_code.py index dc5a743e72..8d3bd53433 100644 --- a/tests/unit/compiler/test_compile_code.py +++ b/tests/unit/compiler/test_compile_code.py @@ -1,28 +1,6 @@ -import random - -import pytest - import vyper -@pytest.fixture -def huge_bytestring(): - r = random.Random(b"vyper") - - return bytes([r.getrandbits(8) for _ in range(0x6001)]) - - -def test_contract_size_exceeded(huge_bytestring): - code = f""" -@external -def a() -> bool: - q: Bytes[24577] = {huge_bytestring} - return True -""" - with pytest.warns(vyper.warnings.ContractSizeLimitWarning): - vyper.compile_code(code, output_formats=["bytecode_runtime"]) - - # test that each compilation run gets a fresh analysis and storage allocator def test_shared_modules_allocation(make_input_bundle): lib1 = """ diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index ae1999a26e..6f0b37317d 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -37,17 +37,25 @@ def test_jump_map(optimize, experimental_codegen): pos_map = source_map["pc_pos_map"] jump_map = source_map["pc_jump_map"] - expected_jumps = 1 if optimize == OptimizationLevel.NONE: # some jumps which don't get optimized out when optimizer is off # (slightly different behavior depending if venom pipeline is enabled): - if not experimental_codegen: + if experimental_codegen: + expected_jumps = 0 + expected_internals = 0 + else: expected_jumps = 3 + expected_internals = 2 + else: + if experimental_codegen: + expected_jumps = 0 + expected_internals = 0 else: - expected_jumps = 2 + expected_jumps = 1 + expected_internals = 2 assert len([v for v in jump_map.values() if v == "o"]) == expected_jumps - assert len([v for v in jump_map.values() if v == "i"]) == 2 + assert len([v for v in jump_map.values() if v == "i"]) == expected_internals code_lines = [i + "\n" for i in TEST_CODE.split("\n")] for pc in [k for k, v in jump_map.items() if v == "o"]: diff --git a/tests/unit/compiler/venom/test_memmerging.py b/tests/unit/compiler/venom/test_memmerging.py index d309752621..3da09f76fb 100644 --- a/tests/unit/compiler/venom/test_memmerging.py +++ b/tests/unit/compiler/venom/test_memmerging.py @@ -3,7 +3,7 @@ from tests.venom_utils import assert_ctx_eq, parse_from_basic_block, parse_venom from vyper.evm.opcodes import version_check from vyper.venom.analysis import IRAnalysesCache -from vyper.venom.passes import SCCP, MemMergePass +from vyper.venom.passes import SCCP, MemMergePass, RemoveUnusedVariablesPass def _check_pre_post(pre, post): @@ -11,6 +11,7 @@ def _check_pre_post(pre, post): for fn in ctx.functions.values(): ac = IRAnalysesCache(fn) MemMergePass(ac, fn).run_pass() + RemoveUnusedVariablesPass(ac, fn).run_pass() assert_ctx_eq(ctx, parse_from_basic_block(post)) @@ -853,8 +854,6 @@ def test_memzeroing_2(): post = """ _global: - %1 = calldatasize - %2 = calldatasize %3 = calldatasize calldatacopy 64, %3, 256 stop @@ -881,8 +880,6 @@ def test_memzeroing_3(): post = """ _global: - %1 = calldatasize - %2 = calldatasize %3 = calldatasize calldatacopy 0, %3, 264 stop @@ -905,7 +902,6 @@ def test_memzeroing_small_calldatacopy(): post = """ _global: - %1 = calldatasize mstore 0, 0 stop """ @@ -935,13 +931,8 @@ def test_memzeroing_smaller_calldatacopy(): post = """ _global: - %1 = calldatasize - %2 = calldatasize %6 = calldatasize calldatacopy 0, %6, 24 - %3 = calldatasize - %4 = calldatasize - %5 = calldatasize mstore 100, 0 stop """ @@ -966,7 +957,6 @@ def test_memzeroing_overlap(): post = """ _global: - %1 = calldatasize %2 = calldatasize calldatacopy 100, %2, 64 stop diff --git a/tests/unit/compiler/venom/test_variable_equivalence.py b/tests/unit/compiler/venom/test_variable_equivalence.py new file mode 100644 index 0000000000..50732ddda9 --- /dev/null +++ b/tests/unit/compiler/venom/test_variable_equivalence.py @@ -0,0 +1,32 @@ +import itertools + +from tests.venom_utils import parse_from_basic_block +from vyper.venom.analysis import DFGAnalysis, IRAnalysesCache +from vyper.venom.basicblock import IRVariable + + +def test_variable_equivalence_dfg_order(): + a_code = """ + main: + %1 = 1 + %2 = %1 + %3 = %2 + """ + # technically invalid code, but variable equivalence should handle + # it either way + b_code = """ + main: + %3 = %2 + %2 = %1 + %1 = 1 + """ + fn1 = parse_from_basic_block(a_code).entry_function + fn2 = parse_from_basic_block(b_code).entry_function + + dfg1 = IRAnalysesCache(fn1).request_analysis(DFGAnalysis) + dfg2 = IRAnalysesCache(fn2).request_analysis(DFGAnalysis) + + vars_ = map(IRVariable, ("%1", "%2", "%3")) + for var1, var2 in itertools.combinations(vars_, 2): + assert dfg1.are_equivalent(var1, var2) + assert dfg2.are_equivalent(var1, var2) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index 24a0f9ade3..f0bcea21e9 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -7,7 +7,6 @@ import operator import pickle import sys -import warnings from typing import Any, Optional, Union from vyper.ast.metadata import NodeMetadata @@ -23,7 +22,6 @@ TypeMismatch, UnfoldableNode, VariableDeclarationException, - VyperException, ZeroDivisionException, ) from vyper.utils import ( @@ -34,6 +32,7 @@ quantize, sha256sum, ) +from vyper.warnings import EnumUsage, vyper_warn NODE_BASE_ATTRIBUTES = ( "_children", @@ -133,13 +132,9 @@ def get_node( node = vy_class(parent=parent, **ast_struct) - # TODO: Putting this after node creation to pretty print, remove after enum deprecation if enum_warn: - # TODO: hack to pretty print, logic should be factored out of exception - pretty_printed_node = str(VyperException("", node)) - warnings.warn( - f"enum will be deprecated in a future release, use flag instead. {pretty_printed_node}", - stacklevel=2, + vyper_warn( + EnumUsage("enum will be deprecated in a future release, use flag instead.", node) ) node.validate() diff --git a/vyper/ast/parse.py b/vyper/ast/parse.py index a7cd0464ed..21a86682f5 100644 --- a/vyper/ast/parse.py +++ b/vyper/ast/parse.py @@ -9,7 +9,8 @@ from vyper.ast.pre_parser import PreParser from vyper.compiler.settings import Settings from vyper.exceptions import CompilerPanic, ParserException, SyntaxException -from vyper.utils import sha256sum, vyper_warn +from vyper.utils import sha256sum +from vyper.warnings import Deprecation, vyper_warn def parse_to_ast(*args: Any, **kwargs: Any) -> vy_ast.Module: @@ -442,7 +443,7 @@ def visit_Call(self, node): # add full_source_code so that str(VyperException(msg, node)) works node.full_source_code = self._source_code - vyper_warn(msg, node) + vyper_warn(Deprecation(msg, node)) dict_ = node.args[0] kw_list = [] diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 55d5443a8f..bfd342853f 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -96,8 +96,8 @@ keccak256, method_id, method_id_int, - vyper_warn, ) +from vyper.warnings import vyper_warn from ._convert import convert from ._signatures import BuiltinFunctionT, process_inputs @@ -2293,19 +2293,12 @@ def build_IR(self, expr, args, kwargs, context): else: method_id = method_id_int("log(string,bytes)") - schema = args_abi_t.selector_name().encode("utf-8") - if len(schema) > 32: - raise CompilerPanic(f"print signature too long: {schema}") + schema = args_abi_t.selector_name().encode("utf-8") schema_t = StringT(len(schema)) - schema_buf = context.new_internal_variable(schema_t) - ret = ["seq"] - ret.append(["mstore", schema_buf, len(schema)]) + schema_buf = Expr._make_bytelike(context, StringT, schema) - # TODO use Expr.make_bytelike, or better have a `bytestring` IRnode type - ret.append( - ["mstore", add_ofst(schema_buf, 32), bytes_to_int(schema.ljust(32, b"\x00"))] - ) + ret = ["seq"] payload_buflen = args_abi_t.size_bound() payload_t = BytesT(payload_buflen) diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 9b5a67160d..7ec813085f 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 import argparse +import functools +import inspect import json import os import sys -import warnings from pathlib import Path from typing import Any, Optional @@ -16,6 +17,7 @@ from vyper.compiler.settings import VYPER_TRACEBACK_LIMIT, OptimizationLevel, Settings from vyper.typing import ContractPath, OutputFormats from vyper.utils import uniq +from vyper.warnings import warnings_filter format_options_help = """Format to print, one or more of (comma-separated): bytecode (default) - Deployable bytecode @@ -97,8 +99,6 @@ def _cli_helper(f, output_formats, compiled): def _parse_args(argv): - warnings.simplefilter("always") - if "--standard-json" in argv: argv.remove("--standard-json") vyper_json._parse_args(argv) @@ -186,6 +186,10 @@ def _parse_args(argv): ) parser.add_argument("--enable-decimals", help="Enable decimals", action="store_true") + parser.add_argument( + "-W", help="Control warnings", dest="warnings_control", choices=["error", "none"] + ) + args = parser.parse_args(argv) if args.traceback_limit is not None: @@ -247,6 +251,7 @@ def _parse_args(argv): settings, args.storage_layout, args.no_bytecode_metadata, + args.warnings_control, ) mode = "w" @@ -290,6 +295,21 @@ def get_search_paths(paths: list[str] = None, include_sys_path=True) -> list[Pat return search_paths +def _apply_warnings_filter(func): + @functools.wraps(func) + def inner(*args, **kwargs): + # find "warnings_control" argument + ba = inspect.signature(func).bind(*args, **kwargs) + ba.apply_defaults() + + warnings_control = ba.arguments["warnings_control"] + with warnings_filter(warnings_control): + return func(*args, **kwargs) + + return inner + + +@_apply_warnings_filter def compile_files( input_files: list[str], output_formats: OutputFormats, @@ -299,6 +319,7 @@ def compile_files( settings: Optional[Settings] = None, storage_layout_paths: list[str] = None, no_bytecode_metadata: bool = False, + warnings_control: Optional[str] = None, ) -> dict: search_paths = get_search_paths(paths, include_sys_path) input_bundle = FilesystemInputBundle(search_paths) diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 5f632f4167..cdcf65b59f 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -14,6 +14,7 @@ from vyper.exceptions import JSONError from vyper.typing import StorageLayout from vyper.utils import OrderedSet, keccak256 +from vyper.warnings import Deprecation, vyper_warn TRANSLATE_MAP = { "abi": "abi", @@ -276,9 +277,10 @@ def get_settings(input_dict: dict) -> Settings: if isinstance(optimize, bool): # bool optimization level for backwards compatibility - warnings.warn( - "optimize: is deprecated! please use one of 'gas', 'codesize', 'none'.", - stacklevel=2, + vyper_warn( + Deprecation( + "optimize: is deprecated! please use one of 'gas', 'codesize', 'none'." + ) ) optimize = OptimizationLevel.default() if optimize else OptimizationLevel.NONE elif isinstance(optimize, str): diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index aaf6f35047..789cc77524 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -560,7 +560,7 @@ def _get_element_ptr_array(parent, key, array_bounds_check): return IRnode.from_list("~empty", subtype) if parent.value == "multi": - assert isinstance(key.value, int) + assert isinstance(key.value, int), key return parent.args[key.value] ix = unwrap_location(key) diff --git a/vyper/codegen/expr.py b/vyper/codegen/expr.py index d3059e4245..427530d5d1 100644 --- a/vyper/codegen/expr.py +++ b/vyper/codegen/expr.py @@ -61,7 +61,8 @@ from vyper.semantics.types.bytestrings import _BytestringT from vyper.semantics.types.function import ContractFunctionT, MemberFunctionT from vyper.semantics.types.shortcuts import BYTES32_T, UINT256_T -from vyper.utils import DECIMAL_DIVISOR, bytes_to_int, is_checksum_encoded, vyper_warn +from vyper.utils import DECIMAL_DIVISOR, bytes_to_int, is_checksum_encoded +from vyper.warnings import VyperWarning, vyper_warn ENVIRONMENT_VARIABLES = {"block", "msg", "tx", "chain"} @@ -130,21 +131,22 @@ def parse_Hex(self): # String literals def parse_Str(self): bytez = self.expr.value.encode("utf-8") - return self._make_bytelike(StringT, bytez) + return self._make_bytelike(self.context, StringT, bytez) # Byte literals def parse_Bytes(self): - return self._make_bytelike(BytesT, self.expr.value) + return self._make_bytelike(self.context, BytesT, self.expr.value) def parse_HexBytes(self): # HexBytes already has value as bytes assert isinstance(self.expr.value, bytes) - return self._make_bytelike(BytesT, self.expr.value) + return self._make_bytelike(self.context, BytesT, self.expr.value) - def _make_bytelike(self, typeclass, bytez): + @classmethod + def _make_bytelike(cls, context, typeclass, bytez): bytez_length = len(bytez) btype = typeclass(bytez_length) - placeholder = self.context.new_internal_variable(btype) + placeholder = context.new_internal_variable(btype) seq = [] seq.append(["mstore", placeholder, bytez_length]) for i in range(0, len(bytez), 32): @@ -274,13 +276,13 @@ def parse_Attribute(self): if not version_check(begin="paris"): warning = "tried to use block.prevrandao in pre-Paris " warning += "environment! Suggest using block.difficulty instead." - vyper_warn(warning, self.expr) + vyper_warn(VyperWarning(warning, self.expr)) return IRnode.from_list(["prevrandao"], typ=BYTES32_T) elif key == "block.difficulty": if version_check(begin="paris"): warning = "tried to use block.difficulty in post-Paris " warning += "environment! Suggest using block.prevrandao instead." - vyper_warn(warning, self.expr) + vyper_warn(VyperWarning(warning, self.expr)) return IRnode.from_list(["difficulty"], typ=UINT256_T) elif key == "block.timestamp": return IRnode.from_list(["timestamp"], typ=UINT256_T) diff --git a/vyper/codegen/function_definitions/internal_function.py b/vyper/codegen/function_definitions/internal_function.py index e98c8a5632..6d6cf8d12e 100644 --- a/vyper/codegen/function_definitions/internal_function.py +++ b/vyper/codegen/function_definitions/internal_function.py @@ -78,6 +78,11 @@ def generate_ir_for_internal_function( ir_node = IRnode.from_list(["seq", body, cleanup_routine]) + # add function signature to passthru metadata so that venom + # has more information to work with + ir_node.passthrough_metadata["func_t"] = func_t + ir_node.passthrough_metadata["context"] = context + # tag gas estimate and frame info func_t._ir_info.gas_estimate = ir_node.gas tag_frame_info(func_t, context) diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index fef6070d14..a7182f6eba 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -109,4 +109,6 @@ def ir_for_self_call(stmt_expr, context): ) o.is_self_call = True o.invoked_function_ir = func_t._ir_info.func_ir + o.passthrough_metadata["func_t"] = func_t + o.passthrough_metadata["args_ir"] = args_ir return o diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py index 187d4ce295..bd474fcda6 100644 --- a/vyper/codegen/stmt.py +++ b/vyper/codegen/stmt.py @@ -258,7 +258,7 @@ def _parse_For_list(self): ret = ["seq"] # list literal, force it to memory first - if isinstance(self.stmt.iter, vy_ast.List): + if iter_list.is_literal: tmp_list = self.context.new_internal_variable(iter_list.typ) ret.append(make_setter(tmp_list, iter_list)) iter_list = tmp_list diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index b6a0e8ac8c..3ccab4869f 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -1,5 +1,4 @@ import base64 -import warnings from collections import deque from pathlib import PurePath @@ -16,8 +15,8 @@ from vyper.semantics.types.function import ContractFunctionT, FunctionVisibility, StateMutability from vyper.semantics.types.module import InterfaceT from vyper.typing import StorageLayout -from vyper.utils import safe_relpath, vyper_warn -from vyper.warnings import ContractSizeLimitWarning +from vyper.utils import safe_relpath +from vyper.warnings import ContractSizeLimit, vyper_warn def build_ast_dict(compiler_data: CompilerData) -> dict: @@ -440,13 +439,14 @@ def build_blueprint_bytecode_output(compiler_data: CompilerData) -> str: def build_bytecode_runtime_output(compiler_data: CompilerData) -> str: compiled_bytecode_runtime_length = len(compiler_data.bytecode_runtime) + # NOTE: we should actually add the size of the immutables section to this. if compiled_bytecode_runtime_length > EIP170_CONTRACT_SIZE_LIMIT: - warnings.warn( - f"Length of compiled bytecode is bigger than Ethereum contract size limit " - "(see EIP-170: https://eips.ethereum.org/EIPS/eip-170): " - f"{compiled_bytecode_runtime_length}b > {EIP170_CONTRACT_SIZE_LIMIT}b", - ContractSizeLimitWarning, - stacklevel=2, + vyper_warn( + ContractSizeLimit( + f"Length of compiled bytecode is bigger than Ethereum contract size limit " + "(see EIP-170: https://eips.ethereum.org/EIPS/eip-170): " + f"{compiled_bytecode_runtime_length}b > {EIP170_CONTRACT_SIZE_LIMIT}b" + ) ) return f"0x{compiler_data.bytecode_runtime.hex()}" diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 17812ee535..fa67a4bc13 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -1,6 +1,5 @@ import copy import json -import warnings from functools import cached_property from pathlib import Path, PurePath from typing import Any, Optional @@ -20,8 +19,9 @@ from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.module import ModuleT from vyper.typing import StorageLayout -from vyper.utils import ERC5202_PREFIX, sha256sum, vyper_warn +from vyper.utils import ERC5202_PREFIX, sha256sum from vyper.venom import generate_assembly_experimental, generate_ir +from vyper.warnings import VyperWarning, vyper_warn DEFAULT_CONTRACT_PATH = PurePath("VyperContract.vy") @@ -256,8 +256,8 @@ def function_signatures(self) -> dict[str, ContractFunctionT]: @cached_property def venom_functions(self): deploy_ir, runtime_ir = self._ir_output - deploy_venom = generate_ir(deploy_ir, self.settings.optimize) - runtime_venom = generate_ir(runtime_ir, self.settings.optimize) + deploy_venom = generate_ir(deploy_ir, self.settings) + runtime_venom = generate_ir(runtime_ir, self.settings) return deploy_venom, runtime_venom @cached_property @@ -352,10 +352,11 @@ def generate_assembly(ir_nodes: IRnode, optimize: Optional[OptimizationLevel] = assembly = compile_ir.compile_to_assembly(ir_nodes, optimize=optimize) if _find_nested_opcode(assembly, "DEBUG"): - warnings.warn( - "This code contains DEBUG opcodes! The DEBUG opcode will only work in " - "a supported EVM! It will FAIL on all other nodes!", - stacklevel=2, + vyper_warn( + VyperWarning( + "This code contains DEBUG opcodes! The DEBUG opcode will only work in " + "a supported EVM! It will FAIL on all other nodes!" + ) ) return assembly diff --git a/vyper/semantics/types/function.py b/vyper/semantics/types/function.py index ffeb5b7299..9f9a9043c9 100644 --- a/vyper/semantics/types/function.py +++ b/vyper/semantics/types/function.py @@ -1,5 +1,4 @@ import re -import warnings from dataclasses import dataclass from functools import cached_property from typing import Any, Dict, List, Optional, Tuple @@ -766,13 +765,6 @@ def _parse_decorators( state_mutability = StateMutability(decorator.id) else: - if decorator.id == "constant": - warnings.warn( - "'@constant' decorator has been removed (see VIP2040). " - "Use `@view` instead.", - DeprecationWarning, - stacklevel=2, - ) raise FunctionDeclarationException(f"Unknown decorator: {decorator.id}", decorator) else: diff --git a/vyper/semantics/types/subscriptable.py b/vyper/semantics/types/subscriptable.py index 4068d815d2..572da4948b 100644 --- a/vyper/semantics/types/subscriptable.py +++ b/vyper/semantics/types/subscriptable.py @@ -1,4 +1,3 @@ -import warnings from typing import Any, Dict, Optional, Tuple from vyper import ast as vy_ast @@ -9,6 +8,7 @@ from vyper.semantics.types.primitives import IntegerT from vyper.semantics.types.shortcuts import UINT256_T from vyper.semantics.types.utils import get_index_value, type_from_annotation +from vyper.warnings import VyperWarning, vyper_warn class _SubscriptableT(VyperType): @@ -113,7 +113,7 @@ def __init__(self, value_type: VyperType, length: int): raise InvalidType("Array length is invalid") if length >= 2**64: - warnings.warn("Use of large arrays can be unsafe!", stacklevel=2) + vyper_warn(VyperWarning("Use of large arrays can be unsafe!")) super().__init__(UINT256_T, value_type) self.length = length diff --git a/vyper/semantics/types/user.py b/vyper/semantics/types/user.py index d01ab23299..37ad7e4bff 100644 --- a/vyper/semantics/types/user.py +++ b/vyper/semantics/types/user.py @@ -23,7 +23,8 @@ from vyper.semantics.types.base import VyperType from vyper.semantics.types.subscriptable import HashMapT from vyper.semantics.types.utils import type_from_abi, type_from_annotation -from vyper.utils import keccak256, vyper_warn +from vyper.utils import keccak256 +from vyper.warnings import Deprecation, vyper_warn # user defined type @@ -298,12 +299,17 @@ def _ctor_call_return(self, node: vy_ast.Call) -> None: return validate_kwargs(node, self.arguments, self.typeclass) # warn about positional argument depreciation - msg = "Instantiating events with positional arguments is " - msg += "deprecated as of v0.4.1 and will be disallowed " - msg += "in a future release. Use kwargs instead eg. " - msg += "Foo(a=1, b=2)" - - vyper_warn(msg, node) + rec0 = ", ".join( + f"{argname}={val.node_source_code}" + for argname, val in zip(self.arguments.keys(), node.args) + ) + recommendation = f"log {node.func.node_source_code}({rec0})" + msg = "Instantiating events with positional arguments is" + msg += " deprecated as of v0.4.1 and will be disallowed" + msg += " in a future release. Use kwargs instead e.g.:" + msg += f"\n```\n{recommendation}\n```" + + vyper_warn(Deprecation(msg, node)) validate_call_args(node, len(self.arguments)) for arg, expected in zip(node.args, self.arguments.values()): diff --git a/vyper/utils.py b/vyper/utils.py index 39d3093478..d132747549 100644 --- a/vyper/utils.py +++ b/vyper/utils.py @@ -11,7 +11,7 @@ import warnings from typing import Generic, Iterable, Iterator, List, Set, TypeVar, Union -from vyper.exceptions import CompilerPanic, DecimalOverrideException, VyperException +from vyper.exceptions import CompilerPanic, DecimalOverrideException _T = TypeVar("_T") @@ -305,14 +305,6 @@ def trace(n=5, out=sys.stderr): print("END TRACE", file=out) -# print a warning -def vyper_warn(msg, node=None): - if node is not None: - # use VyperException for its formatting abilities - msg = str(VyperException(msg, node)) - warnings.warn(msg, stacklevel=2) - - # converts a signature like Func(bool,uint256,address) to its 4 byte method ID # TODO replace manual calculations in codebase with this def method_id_int(method_sig: str) -> int: @@ -693,3 +685,17 @@ def safe_relpath(path): # on Windows, if path and curdir are on different drives, an exception # can be thrown return path + + +def all2(iterator): + """ + This function checks if all elements in the given `iterable` are truthy, + similar to Python's built-in `all()` function. However, `all2` differs + in the case where there are no elements in the iterable. `all()` returns + `True` for the empty iterable, but `all2()` returns False. + """ + try: + s = next(iterator) + except StopIteration: + return False + return bool(s) and all(iterator) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index bb3fe58a8d..af9a39683e 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -4,7 +4,7 @@ from typing import Optional from vyper.codegen.ir_node import IRnode -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, Settings from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.context import IRContext from vyper.venom.function import IRFunction @@ -15,6 +15,7 @@ BranchOptimizationPass, DFTPass, FloatAllocas, + FunctionInlinerPass, LoadElimination, LowerDloadPass, MakeSSA, @@ -46,12 +47,10 @@ def generate_assembly_experimental( return compiler.generate_evm(optimize == OptimizationLevel.NONE) -def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: +def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache) -> None: # Run passes on Venom IR # TODO: Add support for optimization levels - ac = IRAnalysesCache(fn) - FloatAllocas(ac, fn).run_pass() SimplifyCFGPass(ac, fn).run_pass() @@ -94,14 +93,31 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: DFTPass(ac, fn).run_pass() -def run_passes_on(ctx: IRContext, optimize: OptimizationLevel): +def _run_global_passes(ctx: IRContext, optimize: OptimizationLevel, ir_analyses: dict) -> None: + FunctionInlinerPass(ir_analyses, ctx, optimize).run_pass() + + +def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: + ir_analyses = {} for fn in ctx.functions.values(): - _run_passes(fn, optimize) + ir_analyses[fn] = IRAnalysesCache(fn) + _run_global_passes(ctx, optimize, ir_analyses) + + ir_analyses = {} + for fn in ctx.functions.values(): + ir_analyses[fn] = IRAnalysesCache(fn) -def generate_ir(ir: IRnode, optimize: OptimizationLevel) -> IRContext: + for fn in ctx.functions.values(): + _run_passes(fn, optimize, ir_analyses[fn]) + + +def generate_ir(ir: IRnode, settings: Settings) -> IRContext: # Convert "old" IR to "new" IR ctx = ir_node_to_venom(ir) + + optimize = settings.optimize + assert optimize is not None # help mypy run_passes_on(ctx, optimize) return ctx diff --git a/vyper/venom/analysis/__init__.py b/vyper/venom/analysis/__init__.py index 4870de3fb7..c7cbb3dec8 100644 --- a/vyper/venom/analysis/__init__.py +++ b/vyper/venom/analysis/__init__.py @@ -2,5 +2,5 @@ from .cfg import CFGAnalysis from .dfg import DFGAnalysis from .dominators import DominatorTreeAnalysis -from .equivalent_vars import VarEquivalenceAnalysis +from .fcg import FCGAnalysis from .liveness import LivenessAnalysis diff --git a/vyper/venom/analysis/analysis.py b/vyper/venom/analysis/analysis.py index 7bff6ba555..fcab3b4dd0 100644 --- a/vyper/venom/analysis/analysis.py +++ b/vyper/venom/analysis/analysis.py @@ -72,4 +72,5 @@ def force_analysis(self, analysis_cls: Type[IRAnalysis], *args, **kwargs): assert issubclass(analysis_cls, IRAnalysis), f"{analysis_cls} is not an IRAnalysis" if analysis_cls in self.analyses_cache: self.invalidate_analysis(analysis_cls) + return self.request_analysis(analysis_cls, *args, **kwargs) diff --git a/vyper/venom/analysis/cfg.py b/vyper/venom/analysis/cfg.py index 2f90410cd5..77aa2cb362 100644 --- a/vyper/venom/analysis/cfg.py +++ b/vyper/venom/analysis/cfg.py @@ -34,29 +34,48 @@ def analyze(self) -> None: bb.add_cfg_out(next_bb) next_bb.add_cfg_in(bb) - self._compute_dfs_r(self.function.entry) + self._compute_dfs_post_r(self.function.entry) - def _compute_dfs_r(self, bb): + def _compute_dfs_post_r(self, bb): if bb.is_reachable: return bb.is_reachable = True for out_bb in bb.cfg_out: - self._compute_dfs_r(out_bb) + self._compute_dfs_post_r(out_bb) self._dfs.add(bb) @property - def dfs_walk(self) -> Iterator[IRBasicBlock]: + def dfs_pre_walk(self) -> Iterator[IRBasicBlock]: + visited: OrderedSet[IRBasicBlock] = OrderedSet() + + def _visit_dfs_pre_r(bb: IRBasicBlock): + if bb in visited: + return + visited.add(bb) + + yield bb + + for out_bb in bb.cfg_out: + yield from _visit_dfs_pre_r(out_bb) + + yield from _visit_dfs_pre_r(self.function.entry) + + @property + def dfs_post_walk(self) -> Iterator[IRBasicBlock]: return iter(self._dfs) def invalidate(self): from vyper.venom.analysis import DFGAnalysis, DominatorTreeAnalysis, LivenessAnalysis + fn = self.function + for bb in fn.get_basic_blocks(): + bb.cfg_in = OrderedSet() + bb.cfg_out = OrderedSet() + bb.out_vars = OrderedSet() + self.analyses_cache.invalidate_analysis(DominatorTreeAnalysis) self.analyses_cache.invalidate_analysis(LivenessAnalysis) - - self._dfs = None - - # be conservative - assume cfg invalidation invalidates dfg self.analyses_cache.invalidate_analysis(DFGAnalysis) + self._dfs = None diff --git a/vyper/venom/analysis/dfg.py b/vyper/venom/analysis/dfg.py index e528284422..fec8f65e2a 100644 --- a/vyper/venom/analysis/dfg.py +++ b/vyper/venom/analysis/dfg.py @@ -3,7 +3,7 @@ from vyper.utils import OrderedSet from vyper.venom.analysis.analysis import IRAnalysesCache, IRAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRVariable +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IROperand, IRVariable from vyper.venom.function import IRFunction @@ -41,6 +41,23 @@ def remove_use(self, op: IRVariable, inst: IRInstruction): uses: OrderedSet = self._dfg_inputs.get(op, OrderedSet()) uses.remove(inst) + def are_equivalent(self, var1: IROperand, var2: IROperand) -> bool: + if var1 == var2: + return True + + if isinstance(var1, IRVariable) and isinstance(var2, IRVariable): + var1 = self._traverse_store_chain(var1) + var2 = self._traverse_store_chain(var2) + + return var1 == var2 + + def _traverse_store_chain(self, var: IRVariable) -> IRVariable: + while True: + inst = self.get_producing_instruction(var) + if inst is None or inst.opcode != "store": + return var + var = inst.operands[0] # type: ignore + @property def outputs(self) -> dict[IRVariable, IRInstruction]: return self._dfg_outputs diff --git a/vyper/venom/analysis/dominators.py b/vyper/venom/analysis/dominators.py index b60f9bdab9..3e60a74213 100644 --- a/vyper/venom/analysis/dominators.py +++ b/vyper/venom/analysis/dominators.py @@ -54,7 +54,7 @@ def _compute_dominators(self): """ Compute dominators """ - basic_blocks = list(self.dfs_order.keys()) + basic_blocks = list(self.dfs_post_order.keys()) self.dominators = {bb: OrderedSet(basic_blocks) for bb in basic_blocks} self.dominators[self.entry_block] = OrderedSet([self.entry_block]) changed = True @@ -80,15 +80,15 @@ def _compute_idoms(self): """ Compute immediate dominators """ - self.immediate_dominators = {bb: None for bb in self.dfs_order.keys()} + self.immediate_dominators = {bb: None for bb in self.dfs_post_order.keys()} self.immediate_dominators[self.entry_block] = self.entry_block - for bb in self.dfs_walk: + for bb in self.dfs_post_walk: if bb == self.entry_block: continue - doms = sorted(self.dominators[bb], key=lambda x: self.dfs_order[x]) + doms = sorted(self.dominators[bb], key=lambda x: self.dfs_post_order[x]) self.immediate_dominators[bb] = doms[1] - self.dominated = {bb: OrderedSet() for bb in self.dfs_walk} + self.dominated = {bb: OrderedSet() for bb in self.dfs_post_walk} for dom, target in self.immediate_dominators.items(): self.dominated[target].add(dom) @@ -96,10 +96,10 @@ def _compute_df(self): """ Compute dominance frontier """ - basic_blocks = self.dfs_walk + basic_blocks = self.dfs_post_walk self.dominator_frontiers = {bb: OrderedSet() for bb in basic_blocks} - for bb in self.dfs_walk: + for bb in self.dfs_post_walk: if len(bb.cfg_in) > 1: for pred in bb.cfg_in: runner = pred @@ -129,12 +129,12 @@ def _intersect(self, bb1, bb2): return bb1 @cached_property - def dfs_walk(self) -> list[IRBasicBlock]: - return list(self.cfg.dfs_walk) + def dfs_post_walk(self) -> list[IRBasicBlock]: + return list(self.cfg.dfs_post_walk) @cached_property - def dfs_order(self) -> dict[IRBasicBlock, int]: - return {bb: idx for idx, bb in enumerate(self.dfs_walk)} + def dfs_post_order(self) -> dict[IRBasicBlock, int]: + return {bb: idx for idx, bb in enumerate(self.dfs_post_walk)} def as_graph(self) -> str: """ diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py deleted file mode 100644 index 895895651a..0000000000 --- a/vyper/venom/analysis/equivalent_vars.py +++ /dev/null @@ -1,40 +0,0 @@ -from vyper.venom.analysis import DFGAnalysis, IRAnalysis -from vyper.venom.basicblock import IRVariable - - -class VarEquivalenceAnalysis(IRAnalysis): - """ - Generate equivalence sets of variables. This is used to avoid swapping - variables which are the same during venom_to_assembly. Theoretically, - the DFTPass should order variable declarations optimally, but, it is - not aware of the "pickaxe" heuristic in venom_to_assembly, so they can - interfere. - """ - - def analyze(self): - dfg = self.analyses_cache.request_analysis(DFGAnalysis) - - equivalence_set: dict[IRVariable, int] = {} - - for bag, (var, inst) in enumerate(dfg._dfg_outputs.items()): - if inst.opcode != "store": - continue - - source = inst.operands[0] - - assert var not in equivalence_set # invariant - if source in equivalence_set: - equivalence_set[var] = equivalence_set[source] - continue - else: - equivalence_set[var] = bag - equivalence_set[source] = bag - - self._equivalence_set = equivalence_set - - def equivalent(self, var1, var2): - if var1 not in self._equivalence_set: - return False - if var2 not in self._equivalence_set: - return False - return self._equivalence_set[var1] == self._equivalence_set[var2] diff --git a/vyper/venom/analysis/fcg.py b/vyper/venom/analysis/fcg.py new file mode 100644 index 0000000000..98e2d51914 --- /dev/null +++ b/vyper/venom/analysis/fcg.py @@ -0,0 +1,49 @@ +from vyper.utils import OrderedSet +from vyper.venom.analysis.analysis import IRAnalysesCache, IRAnalysis +from vyper.venom.basicblock import IRInstruction, IRLabel +from vyper.venom.context import IRContext +from vyper.venom.function import IRFunction + + +class FCGAnalysis(IRAnalysis): + """ + Compute the function call graph for the context. + """ + + ctx: IRContext + call_sites: dict[IRFunction, OrderedSet[IRInstruction]] + callees: dict[IRFunction, OrderedSet[IRFunction]] + + def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): + super().__init__(analyses_cache, function) + self.ctx = function.ctx + self.call_sites = dict() + self.callees = dict() + + def analyze(self) -> None: + ctx = self.ctx + for func in ctx.get_functions(): + self.call_sites[func] = OrderedSet() + self.callees[func] = OrderedSet() + + for fn in ctx.get_functions(): + self._analyze_function(fn) + + def get_call_sites(self, fn: IRFunction) -> OrderedSet[IRInstruction]: + return self.call_sites.get(fn, OrderedSet()) + + def get_callees(self, fn: IRFunction) -> OrderedSet[IRFunction]: + return self.callees[fn] + + def _analyze_function(self, fn: IRFunction) -> None: + for bb in fn.get_basic_blocks(): + for inst in bb.instructions: + if inst.opcode == "invoke": + label = inst.operands[0] + assert isinstance(label, IRLabel) # mypy help + callee = self.ctx.get_function(label) + self.callees[fn].add(callee) + self.call_sites[callee].add(inst) + + def invalidate(self): + pass diff --git a/vyper/venom/analysis/liveness.py b/vyper/venom/analysis/liveness.py index 0ccda3de2c..e97f6fea03 100644 --- a/vyper/venom/analysis/liveness.py +++ b/vyper/venom/analysis/liveness.py @@ -15,7 +15,7 @@ def analyze(self): cfg = self.analyses_cache.request_analysis(CFGAnalysis) self._reset_liveness() - worklist = deque(cfg.dfs_walk) + worklist = deque(cfg.dfs_post_walk) while len(worklist) > 0: changed = False diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 0cdf849d51..19e24e5a1e 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -194,6 +194,10 @@ def __init__(self, name: str, version: int = 0) -> None: def name(self) -> str: return self._name + @property + def plain_name(self) -> str: + return self.name.strip("%") + class IRLabel(IROperand): """ @@ -387,6 +391,12 @@ def remove_phi_operand(self, label: IRLabel) -> None: del self.operands[i : i + 2] return + @property + def code_size_cost(self) -> int: + if self.opcode == "store": + return 1 + return 2 + def get_ast_source(self) -> Optional[IRnode]: if self.ast_source: return self.ast_source @@ -396,6 +406,25 @@ def get_ast_source(self) -> Optional[IRnode]: return inst.ast_source return self.parent.parent.ast_source + def copy(self) -> "IRInstruction": + ret = IRInstruction(self.opcode, self.operands.copy(), self.output) + ret.annotation = self.annotation + ret.ast_source = self.ast_source + ret.error_msg = self.error_msg + return ret + + def str_short(self) -> str: + s = "" + if self.output: + s += f"{self.output} = " + opcode = f"{self.opcode} " if self.opcode != "store" else "" + s += opcode + operands = self.operands + if opcode not in ["jmp", "jnz", "invoke"]: + operands = list(reversed(operands)) + s += ", ".join([(f"@{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]) + return s + def __repr__(self) -> str: s = "" if self.output: @@ -407,11 +436,14 @@ def __repr__(self) -> str: operands = [operands[0]] + list(reversed(operands[1:])) elif self.opcode not in ("jmp", "jnz", "phi"): operands = reversed(operands) # type: ignore - s += ", ".join([(f"@{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]) if self.annotation: - s += f" ; {self.annotation}" + s = f"{s: <30} ; {self.annotation}" + + # debug: + # if self.error_msg: + # s += f" ;>>> {self.error_msg}" return f"{s: <30}" @@ -469,8 +501,6 @@ def __init__(self, label: IRLabel, parent: "IRFunction") -> None: self.out_vars = OrderedSet() self.is_reachable = False - self._garbage_instructions: set[IRInstruction] = set() - def add_cfg_in(self, bb: "IRBasicBlock") -> None: self.cfg_in.add(bb) @@ -541,24 +571,43 @@ def insert_instruction(self, instruction: IRInstruction, index: Optional[int] = assert not self.is_terminated, (self, instruction) index = len(self.instructions) instruction.parent = self - instruction.ast_source = self.parent.ast_source - instruction.error_msg = self.parent.error_msg + fn = self.parent + if fn.ast_source is not None: + instruction.ast_source = fn.ast_source + if fn.error_msg is not None: + instruction.error_msg = fn.error_msg self.instructions.insert(index, instruction) - def mark_for_removal(self, instruction: IRInstruction) -> None: - self._garbage_instructions.add(instruction) - - def clear_dead_instructions(self) -> None: - if len(self._garbage_instructions) > 0: - self.instructions = [ - inst for inst in self.instructions if inst not in self._garbage_instructions - ] - self._garbage_instructions.clear() + def clear_nops(self) -> None: + if any(inst.opcode == "nop" for inst in self.instructions): + self.instructions = [inst for inst in self.instructions if inst.opcode != "nop"] def remove_instruction(self, instruction: IRInstruction) -> None: assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction" self.instructions.remove(instruction) + def remove_instructions_after(self, instruction: IRInstruction) -> None: + assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction" + assert instruction in self.instructions, "instruction must be in basic block" + self.instructions = self.instructions[: self.instructions.index(instruction) + 1] + + def ensure_well_formed(self): + for inst in self.instructions: + assert inst.parent == self # sanity + if inst.opcode == "revert": + self.remove_instructions_after(inst) + self.append_instruction("stop") # TODO: make revert a bb terminator? + break + + def key(inst): + if inst.opcode in ("phi", "param"): + return 0 + if inst.is_bb_terminator: + return 2 + return 1 + + self.instructions.sort(key=key) + @property def phi_instructions(self) -> Iterator[IRInstruction]: for inst in self.instructions: @@ -587,6 +636,10 @@ def pseudo_instructions(self) -> Iterator[IRInstruction]: def body_instructions(self) -> Iterator[IRInstruction]: return (inst for inst in self.instructions[:-1] if not inst.is_pseudo) + @property + def code_size_cost(self) -> int: + return sum(inst.code_size_cost for inst in self.instructions) + def replace_operands(self, replacements: dict) -> None: """ Update operands with replacements. @@ -665,12 +718,11 @@ def liveness_in_vars(self) -> OrderedSet[IRVariable]: return inst.liveness return OrderedSet() - def copy(self): + def copy(self) -> "IRBasicBlock": bb = IRBasicBlock(self.label, self.parent) - bb.instructions = self.instructions.copy() - bb.cfg_in = self.cfg_in.copy() - bb.cfg_out = self.cfg_out.copy() - bb.out_vars = self.out_vars.copy() + bb.instructions = [inst.copy() for inst in self.instructions] + for inst in bb.instructions: + inst.parent = bb return bb def __repr__(self) -> str: diff --git a/vyper/venom/context.py b/vyper/venom/context.py index 0c5cbc379c..55be7d2f61 100644 --- a/vyper/venom/context.py +++ b/vyper/venom/context.py @@ -1,8 +1,8 @@ import textwrap from dataclasses import dataclass, field -from typing import Optional +from typing import Iterator, Optional -from vyper.venom.basicblock import IRLabel +from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable from vyper.venom.function import IRFunction @@ -32,24 +32,37 @@ def __str__(self): class IRContext: functions: dict[IRLabel, IRFunction] + entry_function: Optional[IRFunction] ctor_mem_size: Optional[int] immutables_len: Optional[int] data_segment: list[DataSection] last_label: int + last_variable: int def __init__(self) -> None: self.functions = {} + self.entry_function = None self.ctor_mem_size = None self.immutables_len = None self.data_segment = [] self.last_label = 0 + self.last_variable = 0 + + def get_basic_blocks(self) -> Iterator[IRBasicBlock]: + for fn in self.functions.values(): + for bb in fn.get_basic_blocks(): + yield bb def add_function(self, fn: IRFunction) -> None: fn.ctx = self self.functions[fn.name] = fn + def remove_function(self, fn: IRFunction) -> None: + del self.functions[fn.name] + def create_function(self, name: str) -> IRFunction: label = IRLabel(name, True) + assert label not in self.functions, f"duplicate function {label}" fn = IRFunction(label, self) self.add_function(fn) return fn @@ -59,12 +72,22 @@ def get_function(self, name: IRLabel) -> IRFunction: return self.functions[name] raise Exception(f"Function {name} not found in context") + def get_functions(self) -> Iterator[IRFunction]: + return iter(self.functions.values()) + def get_next_label(self, suffix: str = "") -> IRLabel: if suffix != "": suffix = f"_{suffix}" self.last_label += 1 return IRLabel(f"{self.last_label}{suffix}") + def get_next_variable(self) -> IRVariable: + self.last_variable += 1 + return IRVariable(f"%{self.last_variable}") + + def get_last_variable(self) -> str: + return f"%{self.last_variable}" + def chain_basic_blocks(self) -> None: """ Chain basic blocks together. This is necessary for the IR to be valid, and is done after diff --git a/vyper/venom/function.py b/vyper/venom/function.py index f02da77fe3..d2148dee05 100644 --- a/vyper/venom/function.py +++ b/vyper/venom/function.py @@ -1,10 +1,22 @@ import textwrap +from dataclasses import dataclass from typing import Iterator, Optional from vyper.codegen.ir_node import IRnode from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable +@dataclass +class IRParameter: + name: str + index: int + offset: int + size: int + call_site_var: Optional[IRVariable] + func_var: Optional[IRVariable] + addr_var: Optional[IRVariable] + + class IRFunction: """ Function that contains basic blocks. @@ -49,6 +61,9 @@ def remove_basic_block(self, bb: IRBasicBlock): assert isinstance(bb, IRBasicBlock), bb del self._basic_block_dict[bb.label.name] + def has_basic_block(self, label: str) -> bool: + return label in self._basic_block_dict + def get_basic_block(self, label: Optional[str] = None) -> IRBasicBlock: """ Get basic block by label. @@ -72,6 +87,10 @@ def get_basic_blocks(self) -> Iterator[IRBasicBlock]: def num_basic_blocks(self) -> int: return len(self._basic_block_dict) + @property + def code_size_cost(self) -> int: + return sum(bb.code_size_cost for bb in self.get_basic_blocks()) + def get_terminal_basicblocks(self) -> Iterator[IRBasicBlock]: """ Get basic blocks that are terminal. @@ -149,6 +168,20 @@ def pop_source(self): assert len(self._error_msg_stack) > 0, "Empty error stack" self._error_msg_stack.pop() + def get_param_at_offset(self, offset: int) -> Optional[IRParameter]: + for param in self.args: + if param.offset == offset: + return param + return None + + def get_param_by_name(self, var: IRVariable | str) -> Optional[IRParameter]: + if isinstance(var, str): + var = IRVariable(var) + for param in self.args: + if f"%{param.name}" == var.name: + return param + return None + @property def ast_source(self) -> Optional[IRnode]: return self._ast_source_stack[-1] if len(self._ast_source_stack) > 0 else None @@ -181,8 +214,9 @@ def chain_basic_blocks(self) -> None: def copy(self): new = IRFunction(self.name) - new._basic_block_dict = self._basic_block_dict.copy() - new.last_variable = self.last_variable + for bb in self.get_basic_blocks(): + new_bb = bb.copy() + new.append_basic_block(new_bb) return new def as_graph(self, only_subgraph=False) -> str: diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index f46457b77f..accbafd2cf 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -107,8 +107,8 @@ NOOP_INSTRUCTIONS = frozenset(["pass", "cleanup_repeat", "var_list", "unique_symbol"]) -SymbolTable = dict[str, Optional[IROperand]] -_alloca_table: SymbolTable = None # type: ignore +SymbolTable = dict[str, IROperand] +_alloca_table: dict[int, IROperand] MAIN_ENTRY_LABEL_NAME = "__main_entry" @@ -121,11 +121,16 @@ def ir_node_to_venom(ir: IRnode) -> IRContext: ctx = IRContext() fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME) + ctx.entry_function = fn _convert_ir_bb(fn, ir, {}) ctx.chain_basic_blocks() + for fn in ctx.functions.values(): + for bb in fn.get_basic_blocks(): + bb.ensure_well_formed() + return ctx @@ -154,20 +159,29 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0): bb.append_instruction("store", size, ret=ret_size) -def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IRVariable]: +def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IROperand]: setup_ir = ir.args[1] goto_ir = [ir for ir in ir.args if ir.value == "goto"][0] target_label = goto_ir.args[0].value # goto - return_buf_ir = goto_ir.args[1] # return buffer ret_args: list[IROperand] = [IRLabel(target_label)] # type: ignore + func_t = ir.passthrough_metadata["func_t"] + assert func_t is not None, "func_t not found in passthrough metadata" if setup_ir != goto_ir: _convert_ir_bb(fn, setup_ir, symbols) - return_buf = _convert_ir_bb(fn, return_buf_ir, symbols) + converted_args = _convert_ir_bb_list(fn, goto_ir.args[1:], symbols) + + callsite_op = converted_args[-1] + assert isinstance(callsite_op, IRLabel), converted_args bb = fn.get_basic_block() - if len(goto_ir.args) > 2: + return_buf = None + + if len(converted_args) > 1: + return_buf = converted_args[0] + + if return_buf is not None: ret_args.append(return_buf) # type: ignore bb.append_invoke_instruction(ret_args, returns=False) # type: ignore @@ -176,22 +190,40 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio def _handle_internal_func( - fn: IRFunction, ir: IRnode, does_return_data: bool, symbols: SymbolTable + # TODO: remove does_return_data, replace with `func_t.return_type is not None` + fn: IRFunction, + ir: IRnode, + does_return_data: bool, + symbols: SymbolTable, ) -> IRFunction: + global _alloca_table + fn = fn.ctx.create_function(ir.args[0].args[0].value) + bb = fn.get_basic_block() + _saved_alloca_table = _alloca_table + _alloca_table = {} + # return buffer if does_return_data: - symbols["return_buffer"] = bb.append_instruction("param") + buf = bb.append_instruction("param") bb.instructions[-1].annotation = "return_buffer" + assert buf is not None # help mypy + symbols["return_buffer"] = buf + # return address - symbols["return_pc"] = bb.append_instruction("param") + return_pc = bb.append_instruction("param") + assert return_pc is not None # help mypy + symbols["return_pc"] = return_pc + bb.instructions[-1].annotation = "return_pc" _convert_ir_bb(fn, ir.args[0].args[2], symbols) + _alloca_table = _saved_alloca_table + return fn @@ -388,6 +420,7 @@ def _convert_ir_bb(fn, ir, symbols): elif ir.value == "exit_to": args = _convert_ir_bb_list(fn, ir.args[1:], symbols) var_list = args + # TODO: only append return args if the function is external _append_return_args(fn, *var_list) bb = fn.get_basic_block() if bb.is_terminated: @@ -398,6 +431,7 @@ def _convert_ir_bb(fn, ir, symbols): label = IRLabel(ir.args[0].value) if label.value == "return_pc": label = symbols.get("return_pc") + # return label should be top of stack bb.append_instruction("ret", label) else: bb.append_instruction("jmp", label) @@ -408,7 +442,6 @@ def _convert_ir_bb(fn, ir, symbols): val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols) return fn.get_basic_block().append_instruction("mstore", val, ptr) - elif ir.value == "ceil32": x = ir.args[0] expanded = IRnode.from_list(["and", ["add", x, 31], ["not", 31]]) diff --git a/vyper/venom/parser.py b/vyper/venom/parser.py index 5ccc29b7a4..91c030826c 100644 --- a/vyper/venom/parser.py +++ b/vyper/venom/parser.py @@ -124,6 +124,8 @@ def start(self, children) -> IRContext: funcs = children for fn_name, blocks in funcs: fn = ctx.create_function(fn_name) + if ctx.entry_function is None: + ctx.entry_function = fn fn._basic_block_dict.clear() for block_name, instructions in blocks: diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py index a3227dcf4b..c5e405242a 100644 --- a/vyper/venom/passes/__init__.py +++ b/vyper/venom/passes/__init__.py @@ -2,6 +2,7 @@ from .branch_optimization import BranchOptimizationPass from .dft import DFTPass from .float_allocas import FloatAllocas +from .function_inliner import FunctionInlinerPass from .literals_codesize import ReduceLiteralsCodesize from .load_elimination import LoadElimination from .lower_dload import LowerDloadPass diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py index 3951ac4455..eb8287e93b 100644 --- a/vyper/venom/passes/base_pass.py +++ b/vyper/venom/passes/base_pass.py @@ -1,4 +1,5 @@ from vyper.venom.analysis import IRAnalysesCache +from vyper.venom.context import IRContext from vyper.venom.function import IRFunction @@ -16,3 +17,19 @@ def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): def run_pass(self, *args, **kwargs): raise NotImplementedError(f"Not implemented! {self.__class__}.run_pass()") + + +class IRGlobalPass: + """ + Base class for all Venom IR passes. + """ + + ctx: IRContext + analyses_caches: dict[IRFunction, IRAnalysesCache] + + def __init__(self, analyses_caches: dict[IRFunction, IRAnalysesCache], ctx: IRContext): + self.analyses_caches = analyses_caches + self.ctx = ctx + + def run_pass(self, *args, **kwargs): + raise NotImplementedError(f"Not implemented! {self.__class__}.run_pass()") diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py new file mode 100644 index 0000000000..616c74c039 --- /dev/null +++ b/vyper/venom/passes/function_inliner.py @@ -0,0 +1,225 @@ +from typing import List, Optional + +from vyper.compiler.settings import OptimizationLevel +from vyper.exceptions import CompilerPanic +from vyper.utils import OrderedSet +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, FCGAnalysis, IRAnalysesCache +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IROperand, IRVariable +from vyper.venom.context import IRContext +from vyper.venom.function import IRFunction +from vyper.venom.passes import FloatAllocas +from vyper.venom.passes.base_pass import IRGlobalPass + + +class FunctionInlinerPass(IRGlobalPass): + """ + This pass inlines functions into their call sites to reduce function call overhead. + + Limitations: + - Does not handle recursive functions + + Side effects: + - Modifies the control flow graph + - Invalidates DFG and CFG + """ + + inline_count: int + fcg: FCGAnalysis + optimize: OptimizationLevel + + def __init__( + self, + analyses_caches: dict[IRFunction, IRAnalysesCache], + ctx: IRContext, + optimize: OptimizationLevel, + ): + super().__init__(analyses_caches, ctx) + self.optimize = optimize + + def run_pass(self): + entry = self.ctx.entry_function + self.inline_count = 0 + + function_count = len(self.ctx.functions) + self.fcg = self.analyses_caches[entry].request_analysis(FCGAnalysis) + self.walk = self._build_call_walk(entry) + + for _ in range(function_count): + candidate = self._select_inline_candidate() + if candidate is None: + return + + # print(f"Inlining function {candidate.name} with cost {candidate.code_size_cost}") + + calls = self.fcg.get_call_sites(candidate) + self._inline_function(candidate, calls) + self.ctx.remove_function(candidate) + self.walk.remove(candidate) + + # TODO: check if recomputing this is a perf issue or we should rather + # update it in-place. + self.fcg = self.analyses_caches[entry].force_analysis(FCGAnalysis) + + def _select_inline_candidate(self) -> Optional[IRFunction]: + for func in self.walk: + call_count = len(self.fcg.get_call_sites(func)) + if call_count == 0: + continue + + # Always inline if there is only one call site. + if call_count == 1: + return func + + # Decide whether to inline based on the optimization level. + if self.optimize == OptimizationLevel.CODESIZE: + continue + elif self.optimize == OptimizationLevel.GAS: + if func.code_size_cost <= 15: + return func + elif self.optimize == OptimizationLevel.NONE: + continue + else: # pragma: nocover + raise CompilerPanic(f"Unsupported inlining optimization level: {self.optimize}") + + return None + + def _inline_function(self, func: IRFunction, call_sites: List[IRInstruction]) -> None: + """ + Inline function into call sites. + """ + for call_site in call_sites: + FloatAllocas(self.analyses_caches[func], func).run_pass() + self._inline_call_site(func, call_site) + fn = call_site.parent.parent + self.analyses_caches[fn].invalidate_analysis(DFGAnalysis) + self.analyses_caches[fn].invalidate_analysis(CFGAnalysis) + + def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: + """ + Inline function into call site. + """ + # TODO: not allowed at all in Vyper at the moment + # but we could support it if we want to with Venom. + # (I think we should support tail call optimizable cases at least) + # if func == call_site.parent.parent: + # raise CompilerPanic("Recursive function inlining is not supported") + + if call_site.opcode != "invoke": + raise CompilerPanic(f"Expected invoke instruction, got {call_site.opcode}") + + prefix = f"inl{self.inline_count}_" + self.inline_count += 1 + call_site_bb = call_site.parent + call_site_func = call_site_bb.parent + + call_site_return = IRBasicBlock( + self.ctx.get_next_label(f"{prefix}inline_return"), call_site_bb.parent + ) + call_idx = call_site_bb.instructions.index(call_site) + + for inst in call_site_bb.instructions[call_idx + 1 :]: + call_site_return.insert_instruction(inst) + call_site_func.append_basic_block(call_site_return) + + func_copy = self._clone_function(func, prefix) + + for bb in func_copy.get_basic_blocks(): + bb.parent = call_site_func + call_site_func.append_basic_block(bb) + param_idx = 0 + for inst in bb.instructions: + if inst.opcode == "param": + # NOTE: one of these params is the return pc. technically assigning + # a variable to a label (e.g. %1 = @label) as we are doing here is + # not valid venom code, but it will get removed in store elimination + # (or unused variable elimination) + inst.opcode = "store" + val = call_site.operands[-param_idx - 1] + inst.operands = [val] + param_idx += 1 + elif inst.opcode == "palloca": + inst.opcode = "store" + inst.operands = [inst.operands[0]] + elif inst.opcode == "ret": + inst.opcode = "jmp" + inst.operands = [call_site_return.label] + elif inst.opcode == "revert": + bb.remove_instructions_after(inst) + bb.append_instruction("stop") + break + + for inst in bb.instructions: + if not inst.annotation: + inst.annotation = f"from {func.name}" + + call_site_bb.instructions = call_site_bb.instructions[:call_idx] + call_site_bb.append_instruction("jmp", func_copy.entry.label) + + def _build_call_walk(self, function: IRFunction) -> OrderedSet[IRFunction]: + """ + postorder DFS walk over the call graph. + """ + visited = set() + call_walk = [] + + def dfs(fn): + if fn in visited: + return + visited.add(fn) + + called_functions = self.fcg.get_callees(fn) + for func in called_functions: + dfs(func) + + call_walk.append(fn) + + dfs(function) + + return OrderedSet(call_walk) + + def _clone_function(self, func: IRFunction, prefix: str) -> IRFunction: + new_func_label = IRLabel(f"{prefix}{func.name.value}") + clone = IRFunction(new_func_label) + # clear the bb that is added by default + # consider using func.copy() intead? + clone._basic_block_dict.clear() + for bb in func.get_basic_blocks(): + clone.append_basic_block(self._clone_basic_block(bb, prefix)) + return clone + + def _clone_basic_block(self, bb: IRBasicBlock, prefix: str) -> IRBasicBlock: + new_bb_label = IRLabel(f"{prefix}{bb.label.value}") + new_bb = IRBasicBlock(new_bb_label, bb.parent) + new_bb.instructions = [self._clone_instruction(inst, prefix) for inst in bb.instructions] + for inst in new_bb.instructions: + inst.parent = new_bb + return new_bb + + def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: + func = inst.parent.parent + ops: list[IROperand] = [] + for op in inst.operands: + if isinstance(op, IRLabel): + if func.has_basic_block(op.name): + # it is a valid label inside of this function + label = IRLabel(f"{prefix}{op.name}") + else: + # otherwise it is something else (like a data label) + label = op + ops.append(label) + elif isinstance(op, IRVariable): + ops.append(IRVariable(f"{prefix}{op.plain_name}")) + else: + ops.append(op) + + output = None + if inst.output: + output = IRVariable(f"{prefix}{inst.output.plain_name}") + + clone = IRInstruction(inst.opcode, ops, output) + clone.parent = inst.parent + clone.annotation = inst.annotation + clone.ast_source = inst.ast_source + clone.error_msg = inst.error_msg + + return clone diff --git a/vyper/venom/passes/load_elimination.py b/vyper/venom/passes/load_elimination.py index f805f4c091..4575a1d93f 100644 --- a/vyper/venom/passes/load_elimination.py +++ b/vyper/venom/passes/load_elimination.py @@ -1,6 +1,6 @@ from typing import Optional -from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis, VarEquivalenceAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRLiteral from vyper.venom.effects import Effects from vyper.venom.passes.base_pass import IRPass @@ -33,7 +33,6 @@ def run_pass(self): self.analyses_cache.invalidate_analysis(LivenessAnalysis) self.analyses_cache.invalidate_analysis(DFGAnalysis) - self.analyses_cache.invalidate_analysis(VarEquivalenceAnalysis) def equivalent(self, op1, op2): return op1 == op2 diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py index ee013e0f1d..d2a0ab4408 100644 --- a/vyper/venom/passes/make_ssa.py +++ b/vyper/venom/passes/make_ssa.py @@ -35,8 +35,8 @@ def _add_phi_nodes(self): Add phi nodes to the function. """ self._compute_defs() - work = {bb: 0 for bb in self.dom.dfs_walk} - has_already = {bb: 0 for bb in self.dom.dfs_walk} + work = {bb: 0 for bb in self.dom.dfs_post_walk} + has_already = {bb: 0 for bb in self.dom.dfs_post_walk} i = 0 # Iterate over all variables @@ -148,7 +148,7 @@ def _compute_defs(self): Compute the definition points of variables in the function. """ self.defs = {} - for bb in self.dom.dfs_walk: + for bb in self.dom.dfs_post_walk: assignments = bb.get_assignments() for var in assignments: if var not in self.defs: diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index 9f985e2b0b..1ecf25c468 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -1,3 +1,4 @@ +from vyper.utils import all2 from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.function import IRFunction @@ -19,30 +20,31 @@ def run_pass(self): self.var_name_count = 0 for var, inst in dfg.outputs.items(): if inst.opcode == "alloca": - self._process_alloca_var(dfg, var) + self._process_alloca_var(dfg, inst, var) elif inst.opcode == "palloca": self._process_palloca_var(dfg, inst, var) self.analyses_cache.invalidate_analysis(DFGAnalysis) self.analyses_cache.invalidate_analysis(LivenessAnalysis) - def _mk_varname(self, varname: str): + def _mk_varname(self, varname: str, alloca_id: int): varname = varname.removeprefix("%") - varname = f"var{varname}_{self.var_name_count}" + varname = f"alloca_{alloca_id}_{varname}_{self.var_name_count}" self.var_name_count += 1 return varname - def _process_alloca_var(self, dfg: DFGAnalysis, var: IRVariable): + def _process_alloca_var(self, dfg: DFGAnalysis, alloca_inst, var: IRVariable): """ Process alloca allocated variable. If it is only used by mstore/mload/return instructions, it is promoted to a stack variable. Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if not all([inst.opcode in ["mstore", "mload", "return"] for inst in uses]): + if not all2(inst.opcode in ["mstore", "mload", "return"] for inst in uses): return - var_name = self._mk_varname(var.name) + alloca_id = alloca_inst.operands[2] + var_name = self._mk_varname(var.value, alloca_id.value) var = IRVariable(var_name) for inst in uses: if inst.opcode == "mstore": @@ -65,15 +67,16 @@ def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, va instructions, it is promoted to a stack variable. Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if not all(inst.opcode in ["mstore", "mload"] for inst in uses): + if not all2(inst.opcode in ["mstore", "mload"] for inst in uses): return - var_name = self._mk_varname(var.name) + ofst, _size, alloca_id = palloca_inst.operands + var_name = self._mk_varname(var.value, alloca_id.value) var = IRVariable(var_name) # some value given to us by the calling convention palloca_inst.opcode = "mload" - palloca_inst.operands = [palloca_inst.operands[0]] + palloca_inst.operands = [ofst] palloca_inst.output = var for inst in uses: diff --git a/vyper/venom/passes/memmerging.py b/vyper/venom/passes/memmerging.py index 2e5ee46b84..f9be679d74 100644 --- a/vyper/venom/passes/memmerging.py +++ b/vyper/venom/passes/memmerging.py @@ -155,7 +155,7 @@ def _optimize_copy(self, bb: IRBasicBlock, copy_opcode: str, load_opcode: str): if not all(use in copy.insts for use in uses): continue - bb.mark_for_removal(inst) + inst.make_nop() self._copies.clear() self._loads.clear() @@ -285,7 +285,6 @@ def _barrier(): _barrier() _barrier() - bb.clear_dead_instructions() # optimize memzeroing operations def _optimize_memzero(self, bb: IRBasicBlock): @@ -304,7 +303,7 @@ def _optimize_memzero(self, bb: IRBasicBlock): inst.operands = [IRLiteral(copy.length), calldatasize, IRLiteral(copy.dst)] for inst in copy.insts[:-1]: - bb.mark_for_removal(inst) + inst.make_nop() self._copies.clear() self._loads.clear() @@ -350,7 +349,6 @@ def _barrier(): continue _barrier() - bb.clear_dead_instructions() def _volatile_memory(inst): diff --git a/vyper/venom/passes/remove_unused_variables.py b/vyper/venom/passes/remove_unused_variables.py index 73fe2112d7..f6de08b156 100644 --- a/vyper/venom/passes/remove_unused_variables.py +++ b/vyper/venom/passes/remove_unused_variables.py @@ -25,6 +25,9 @@ def run_pass(self): inst = work_list.pop() self._process_instruction(inst) + for bb in self.function.get_basic_blocks(): + bb.clear_nops() + self.analyses_cache.invalidate_analysis(LivenessAnalysis) self.analyses_cache.invalidate_analysis(DFGAnalysis) diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index dbe5e607da..83af176bee 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -61,11 +61,12 @@ def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): super().__init__(analyses_cache, function) self.lattice = {} self.work_list: list[WorkListItem] = [] - self.cfg_dirty = False def run_pass(self): self.fn = self.function self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) # type: ignore + self.analyses_cache.request_analysis(CFGAnalysis) + self.cfg_dirty = False self._calculate_sccp(self.fn.entry) self._propagate_constants() @@ -177,8 +178,8 @@ def _visit_phi(self, inst: IRInstruction): def _visit_expr(self, inst: IRInstruction): opcode = inst.opcode - if opcode in ["store", "alloca", "palloca"]: - assert inst.output is not None, "Got store/alloca without output" + if opcode in ("store", "alloca", "palloca"): + assert inst.output is not None, inst out = self._eval_from_lattice(inst.operands[0]) self._set_lattice(inst.output, out) self._add_ssa_work_items(inst) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 37d2a58314..dd0d2d1f63 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -10,12 +10,7 @@ optimize_assembly, ) from vyper.utils import MemoryPositions, OrderedSet, wrap256 -from vyper.venom.analysis import ( - CFGAnalysis, - IRAnalysesCache, - LivenessAnalysis, - VarEquivalenceAnalysis, -) +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, IRAnalysesCache, LivenessAnalysis from vyper.venom.basicblock import ( TEST_INSTRUCTIONS, IRBasicBlock, @@ -142,6 +137,7 @@ class VenomCompiler: visited_instructions: OrderedSet # {IRInstruction} visited_basicblocks: OrderedSet # {IRBasicBlock} liveness_analysis: LivenessAnalysis + dfg: DFGAnalysis def __init__(self, ctxs: list[IRContext]): self.ctxs = ctxs @@ -163,7 +159,7 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]: NormalizationPass(ac, fn).run_pass() self.liveness_analysis = ac.request_analysis(LivenessAnalysis) - self.equivalence = ac.request_analysis(VarEquivalenceAnalysis) + self.dfg = ac.request_analysis(DFGAnalysis) ac.request_analysis(CFGAnalysis) assert fn.normalized, "Non-normalized CFG!" @@ -232,7 +228,7 @@ def _stack_reorder( continue to_swap = stack.peek(final_stack_depth) - if self.equivalence.equivalent(op, to_swap): + if self.dfg.are_equivalent(op, to_swap): # perform a "virtual" swap stack.poke(final_stack_depth, op) stack.poke(depth, to_swap) @@ -372,7 +368,8 @@ def _generate_evm_for_instruction( if opcode in ["jmp", "djmp", "jnz", "invoke"]: operands = list(inst.get_non_label_operands()) elif opcode in ("alloca", "palloca"): - offset, _size = inst.operands + assert len(inst.operands) == 3, f"alloca/palloca must have 3 operands, got {inst}" + offset, _size, _id = inst.operands operands = [offset] # iload and istore are special cases because they can take a literal @@ -582,7 +579,7 @@ def _generate_evm_for_instruction( next_scheduled = next_liveness.last() cost = 0 - if not self.equivalence.equivalent(inst.output, next_scheduled): + if not self.dfg.are_equivalent(inst.output, next_scheduled): cost = self.swap_op(assembly, stack, next_scheduled) if DEBUG_SHOW_COST and cost != 0: diff --git a/vyper/warnings.py b/vyper/warnings.py index 606cfeaf95..cec1d3403e 100644 --- a/vyper/warnings.py +++ b/vyper/warnings.py @@ -1,3 +1,68 @@ -# TODO: Create VyperWarning class similarly to what is being done with exceptinos? -class ContractSizeLimitWarning(Warning): +import contextlib +import warnings +from typing import Optional + +from vyper.exceptions import _BaseVyperException + + +class VyperWarning(_BaseVyperException, Warning): + pass + + +# print a warning +def vyper_warn(warning: VyperWarning | str, node=None): + if isinstance(warning, str): + warning = VyperWarning(warning, node) + warnings.warn(warning, stacklevel=2) + + +@contextlib.contextmanager +def warnings_filter(warnings_control: Optional[str]): + # note: using warnings.catch_warnings() since it saves and restores + # the warnings filter + with warnings.catch_warnings(): + set_warnings_filter(warnings_control) + yield + + +def set_warnings_filter(warnings_control: Optional[str]): + if warnings_control == "error": + warnings_filter = "error" + elif warnings_control == "none": + warnings_filter = "ignore" + else: + assert warnings_control is None # sanity + warnings_filter = "default" + + if warnings_control is not None: + # warnings.simplefilter only adds to the warnings filters, + # so we should clear warnings filter between calls to simplefilter() + warnings.resetwarnings() + + # NOTE: in the future we can do more fine-grained control by setting + # category to specific warning types + warnings.simplefilter(warnings_filter, category=VyperWarning) # type: ignore[arg-type] + + +class ContractSizeLimit(VyperWarning): + """ + Warn if past the EIP-170 size limit + """ + + pass + + +class EnumUsage(VyperWarning): + """ + Warn about using `enum` instead of `flag + """ + + pass + + +class Deprecation(VyperWarning): + """ + General deprecation warning + """ + pass