Skip to content

Commit

Permalink
Merge branch 'vyperlang:master' into refactor/cli-help
Browse files Browse the repository at this point in the history
  • Loading branch information
pcaversaccio authored Dec 31, 2024
2 parents e5ceb90 + a29b49d commit 9a071e3
Show file tree
Hide file tree
Showing 52 changed files with 3,236 additions and 452 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
# docs: documentation
# test: test suite
# lang: language changes
# stdlib: changes to the stdlib
# ux: language changes (UX)
# tool: integration
# ir: (old) IR/codegen changes
Expand All @@ -43,6 +44,7 @@ jobs:
docs
test
lang
stdlib
ux
tool
ir
Expand Down
24 changes: 22 additions & 2 deletions docs/control-structures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ External functions (marked with the ``@external`` decorator) are a part of the c
A Vyper contract cannot call directly between two external functions. If you must do this, you can use an :ref:`interface <interfaces>`.

.. note::
For external functions with default arguments like ``def my_function(x: uint256, b: uint256 = 1)`` the Vyper compiler will generate ``N+1`` overloaded function selectors based on ``N`` default arguments.
For external functions with default arguments like ``def my_function(x: uint256, b: uint256 = 1)`` the Vyper compiler will generate ``N+1`` overloaded function selectors based on ``N`` default arguments. Consequently, the ABI signature for a function (this includes interface functions) excludes optional arguments when their default values are used in the function call.

.. code-block:: vyper
from ethereum.ercs import IERC4626
@external
def foo(x: IERC4626):
extcall x.withdraw(0, self, self) # keccak256("withdraw(uint256,address,address)")[:4] = 0xb460af94
extcall x.withdraw(0) # keccak256("withdraw(uint256)")[:4] = 0x2e1a7d4d
.. _structure-functions-internal:

Expand All @@ -75,6 +84,14 @@ Or for internal functions which are defined in :ref:`imported modules <modules>`
def calculate(amount: uint256) -> uint256:
return calculator_library._times_two(amount)
Marking an internal function as ``payable`` specifies that the function can interact with ``msg.value``. A ``nonpayable`` internal function can be called from an external ``payable`` function, but it cannot access ``msg.value``.

.. code-block:: vyper
@payable
def _foo() -> uint256:
return msg.value % 2
.. note::
As of v0.4.0, the ``@internal`` decorator is optional. That is, functions with no visibility decorator default to being ``internal``.

Expand Down Expand Up @@ -110,7 +127,7 @@ You can optionally declare a function's mutability by using a :ref:`decorator <f
* ``@pure``: does not read from the contract state or any environment variables.
* ``@view``: may read from the contract state, but does not alter it.
* ``@nonpayable`` (default): may read from and write to the contract state, but cannot receive Ether.
* ``@payable``: may read from and write to the contract state, and can receive Ether.
* ``@payable``: may read from and write to the contract state, and can receive and access Ether via ``msg.value``.

.. code-block:: vyper
Expand All @@ -132,6 +149,9 @@ Functions marked with ``@view`` cannot call mutable (``payable`` or ``nonpayable

Functions marked with ``@pure`` cannot call non-``pure`` functions.

.. note::
The ``@nonpayable`` decorator is not strictly enforced on ``internal`` functions when they are invoked through an ``external`` ``payable`` function. As a result, an ``external`` ``payable`` function can invoke an ``internal`` ``nonpayable`` function. However, the ``nonpayable`` ``internal`` function cannot have access to ``msg.value``.

Re-entrancy Locks
-----------------

Expand Down
4 changes: 4 additions & 0 deletions docs/interfaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ This imports the defined interface from the vyper file at ``an_interface.vyi`` (

Prior to v0.4.0, ``implements`` required that events defined in an interface were re-defined in the "implementing" contract. As of v0.4.0, this is no longer required because events can be used just by importing them. Any events used in a contract will automatically be exported in the ABI output.

.. note::

An interface function with default parameters (e.g. ``deposit(assets: uint256, receiver: address = msg.sender)``) implies that the contract being interfaced with supports these default arguments via the ABI-encoded function signatures (e.g. ``keccak256("deposit(uint256,address)")[:4]`` and ``keccak256("deposit(uint256)")[:4]``). It is the responsibility of the callee to implement the behavior associated with these defaults.

Standalone Interfaces
=====================

Expand Down
134 changes: 126 additions & 8 deletions tests/functional/codegen/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from eth_utils import to_wei

from tests.utils import decimal_to_int
from vyper.compiler import compile_code
from vyper.compiler import compile_code, compile_from_file_input
from vyper.exceptions import (
ArgumentException,
DuplicateImport,
Expand Down Expand Up @@ -161,8 +161,6 @@ def bar() -> uint256:
("import Foo as Foo", "Foo.vyi"),
("from a import Foo", "a/Foo.vyi"),
("from b.a import Foo", "b/a/Foo.vyi"),
("from .a import Foo", "./a/Foo.vyi"),
("from ..a import Foo", "../a/Foo.vyi"),
]


Expand All @@ -174,6 +172,22 @@ def test_extract_file_interface_imports(code, filename, make_input_bundle):
assert compile_code(code, input_bundle=input_bundle) is not None


VALID_RELATIVE_IMPORT_CODE = [
# import statement, import path without suffix
("from .a import Foo", "mock.vy"),
("from ..a import Foo", "b/mock.vy"),
]


# TODO CMC 2024-10-13: should probably be in syntax tests
@pytest.mark.parametrize("code,filename", VALID_RELATIVE_IMPORT_CODE)
def test_extract_file_interface_relative_imports(code, filename, make_input_bundle):
input_bundle = make_input_bundle({"a/Foo.vyi": "", filename: code})

file_input = input_bundle.load_file(filename)
assert compile_from_file_input(file_input, input_bundle=input_bundle) is not None


BAD_IMPORT_CODE = [
("import a as A\nimport a as A", DuplicateImport),
("import a as A\nimport a as a", DuplicateImport),
Expand All @@ -186,12 +200,11 @@ def test_extract_file_interface_imports(code, filename, make_input_bundle):

# TODO CMC 2024-10-13: should probably be in syntax tests
@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.vyi": "", "b/a.vyi": "", "c.vyi": ""})
def test_extract_file_interface_imports_raises(code, exception_type, make_input_bundle):
input_bundle = make_input_bundle({"a.vyi": "", "b/a.vyi": "", "c.vyi": "", "mock.vy": code})
file_input = input_bundle.load_file("mock.vy")
with pytest.raises(exception_type):
compile_code(code, input_bundle=input_bundle)
compile_from_file_input(file_input, input_bundle=input_bundle)


def test_external_call_to_interface(env, get_contract, make_input_bundle):
Expand Down Expand Up @@ -863,3 +876,108 @@ def bar() -> uint256:
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
assert c.bar() == 1


def test_interface_with_flags():
code = """
struct MyStruct:
a: address
flag Foo:
BOO
MOO
POO
event Transfer:
sender: indexed(address)
@external
def bar():
pass
flag BAR:
BIZ
BAZ
BOO
@external
@view
def foo(s: MyStruct) -> MyStruct:
return s
"""

out = compile_code(code, contract_path="code.vy", output_formats=["interface"])["interface"]

assert "# Flags" in out
assert "flag Foo:" in out
assert "flag BAR" in out
assert "BOO" in out
assert "MOO" in out

compile_code(out, contract_path="code.vyi", output_formats=["interface"])


vyi_filenames = [
"test__test.vyi",
"test__t.vyi",
"t__test.vyi",
"t__t.vyi",
"t_t.vyi",
"test_test.vyi",
"t_test.vyi",
"test_t.vyi",
"_test_t__t_tt_.vyi",
"foo_bar_baz.vyi",
]


@pytest.mark.parametrize("vyi_filename", vyi_filenames)
def test_external_interface_names(vyi_filename):
code = """
@external
def foo():
...
"""

compile_code(code, contract_path=vyi_filename, output_formats=["external_interface"])


def test_external_interface_with_flag():
code = """
flag Foo:
Blah
@external
def foo() -> Foo:
...
"""

out = compile_code(code, contract_path="test__test.vyi", output_formats=["external_interface"])[
"external_interface"
]
assert "-> Foo:" in out


def test_external_interface_compiles_again():
code = """
@external
def foo() -> uint256:
...
@external
def bar(a:int32) -> uint256:
...
"""

out = compile_code(code, contract_path="test.vyi", output_formats=["external_interface"])[
"external_interface"
]
compile_code(out, contract_path="test.vyi", output_formats=["external_interface"])


@pytest.mark.xfail
def test_weird_interface_name():
# based on comment https://github.com/vyperlang/vyper/pull/4290#discussion_r1884137428
# we replace "_" for "" which results in an interface without name
out = compile_code("", contract_path="_.vyi", output_formats=["external_interface"])[
"external_interface"
]
assert "interface _:" in out
129 changes: 129 additions & 0 deletions tests/functional/syntax/test_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import pytest

from vyper import compiler
from vyper.exceptions import ModuleNotFound

CODE_TOP = """
import subdir0.lib0 as lib0
@external
def foo():
lib0.foo()
"""

CODE_LIB1 = """
def foo():
pass
"""


def test_implicitly_relative_import_crashes(make_input_bundle):
lib0 = """
import subdir1.lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": CODE_TOP, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": CODE_LIB1}
)

file_input = input_bundle.load_file("top.vy")
with pytest.raises(ModuleNotFound):
compiler.compile_from_file_input(file_input, input_bundle=input_bundle)


def test_implicitly_relative_import_crashes_2(make_input_bundle):
lib0 = """
from subdir1 import lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": CODE_TOP, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": CODE_LIB1}
)

file_input = input_bundle.load_file("top.vy")
with pytest.raises(ModuleNotFound):
compiler.compile_from_file_input(file_input, input_bundle=input_bundle)


def test_relative_import_searches_only_current_path(make_input_bundle):
top = """
from subdir import b as b
@external
def foo():
b.foo()
"""

a = """
def foo():
pass
"""

b = """
from . import a as a
def foo():
a.foo()
"""

input_bundle = make_input_bundle({"top.vy": top, "a.vy": a, "subdir/b.vy": b})
file_input = input_bundle.load_file("top.vy")

with pytest.raises(ModuleNotFound):
compiler.compile_from_file_input(file_input, input_bundle=input_bundle)


def test_absolute_import_within_relative_import(make_input_bundle):
top = """
import subdir0.subdir1.c as c
@external
def foo():
c.foo()
"""
a = """
import subdir0.b as b
def foo():
b.foo()
"""
b = """
def foo():
pass
"""

c = """
from .. import a as a
def foo():
a.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/a.vy": a, "subdir0/b.vy": b, "subdir0/subdir1/c.vy": c}
)
compiler.compile_code(top, input_bundle=input_bundle)


def test_absolute_path_passes(make_input_bundle):
lib0 = """
import subdir0.subdir1.lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": CODE_TOP, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": CODE_LIB1}
)
compiler.compile_code(CODE_TOP, input_bundle=input_bundle)


def test_absolute_path_passes_2(make_input_bundle):
lib0 = """
from .subdir1 import lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": CODE_TOP, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": CODE_LIB1}
)
compiler.compile_code(CODE_TOP, input_bundle=input_bundle)
Loading

0 comments on commit 9a071e3

Please sign in to comment.