Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[lang]: add module.__at__() to cast to interface #4090

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3daf88b
feat[lang]: allow module intrinsic interface call
charles-cooper Jun 1, 2024
dad60ac
add codegen test
charles-cooper Jun 1, 2024
08c652a
add export test for unimplemented _inline_ interfaces
charles-cooper Jun 1, 2024
fe1cfa4
add codegen test for inline interface export
charles-cooper Jun 1, 2024
15d0fde
fix lint
charles-cooper Jun 1, 2024
a2c0ea8
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Jun 2, 2024
feac371
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Jun 2, 2024
2d5f67d
add sanity check
charles-cooper Aug 8, 2024
9621397
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Oct 11, 2024
8504d4d
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Oct 13, 2024
15e810e
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Oct 19, 2024
4986b50
fix lint
charles-cooper Oct 19, 2024
24ac428
reject empty interfaces
charles-cooper Oct 19, 2024
c0f37ac
fix a test
charles-cooper Oct 19, 2024
59e298e
fix again
charles-cooper Oct 19, 2024
e05fabf
remove init function from interfaces
charles-cooper Oct 20, 2024
7a2d36d
fix for windows tests
charles-cooper Oct 20, 2024
ac43beb
reject weird exports of value types
charles-cooper Oct 22, 2024
df217e8
add `module.__at__`
charles-cooper Nov 10, 2024
dfb3eb3
fix exports
charles-cooper Nov 10, 2024
236e11a
add invalid at exports
cyberthirst Nov 13, 2024
2b5c4ad
parametrize over interface accessor
cyberthirst Nov 13, 2024
3db8f42
add intrinsic interface instantiation test
cyberthirst Nov 13, 2024
5b91458
add intrinsic interface convert test
cyberthirst Nov 13, 2024
7ebf244
add intrinsic interfaces have different types
cyberthirst Nov 13, 2024
272d4d5
extend interface init function test
cyberthirst Nov 13, 2024
6860897
add test accesing default function in interface
cyberthirst Nov 13, 2024
73aeacc
add more interface tests
cyberthirst Nov 13, 2024
4e282b4
lint
cyberthirst Nov 13, 2024
9bac423
Merge pull request #50 from cyberthirst/fork/charles-cooper/fix/inter…
charles-cooper Nov 19, 2024
341d4b6
mark xfail
charles-cooper Nov 20, 2024
9b29806
fix lint
charles-cooper Nov 25, 2024
4c463ac
leave a linter hint
charles-cooper Nov 25, 2024
3aaf044
Merge branch 'master' into fix/interface-intrinsic
charles-cooper Nov 25, 2024
307d70e
add docs for `module.__at__`
charles-cooper Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions tests/functional/codegen/modules/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,26 @@ def __init__():
# call `c.__default__()`
env.message_call(c.address)
assert c.counter() == 6


def test_inline_interface_export(make_input_bundle, get_contract):
lib1 = """
interface IAsset:
def asset() -> address: view

implements: IAsset

@external
@view
def asset() -> address:
return self
"""
main = """
import lib1

exports: lib1.IAsset
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)

assert c.asset() == c.address
31 changes: 30 additions & 1 deletion tests/functional/codegen/modules/test_interface_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,45 @@ def foo() -> bool:
# check that this typechecks both directions
a: lib1.IERC20 = IERC20(msg.sender)
b: lib2.IERC20 = IERC20(msg.sender)
c: IERC20 = lib1.IERC20(msg.sender) # allowed in call position

# return the equality so we can sanity check it
return a == b
return a == b and b == c
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() is True


def test_intrinsic_interface(get_contract, make_input_bundle):
lib = """
@external
@view
def foo() -> uint256:
# detect self call
if msg.sender == self:
return 4
else:
return 5
"""
main = """
import lib

exports: lib.__interface__

@external
@view
def bar() -> uint256:
return staticcall lib.__interface__(self).foo()
"""
input_bundle = make_input_bundle({"lib.vy": lib})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 5
assert c.bar() == 4


def test_import_interface_flags(make_input_bundle, get_contract):
ifaces = """
flag Foo:
Expand Down
33 changes: 32 additions & 1 deletion tests/functional/syntax/modules/test_deploy_visibility.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from vyper.compiler import compile_code
from vyper.exceptions import CallViolation
from vyper.exceptions import CallViolation, UnknownAttribute


def test_call_deploy_from_external(make_input_bundle):
Expand All @@ -25,3 +25,34 @@ def foo():
compile_code(main, input_bundle=input_bundle)

assert e.value.message == "Cannot call an @deploy function from an @external function!"


def test_module_interface_init(make_input_bundle, tmp_path):
lib1 = """
#lib1.vy
k: uint256

@external
def bar():
pass

@deploy
def __init__():
self.k = 10
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})

code = """
import lib1

@deploy
def __init__():
lib1.__interface__(self).__init__()
"""

with pytest.raises(UnknownAttribute) as e:
compile_code(code, input_bundle=input_bundle)

# as_posix() for windows tests
lib1_path = (tmp_path / "lib1.vy").as_posix()
assert e.value.message == f"interface {lib1_path} has no member '__init__'."
74 changes: 74 additions & 0 deletions tests/functional/syntax/modules/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,28 @@ def do_xyz():
assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!"


def test_no_export_unimplemented_inline_interface(make_input_bundle):
lib1 = """
interface ifoo:
def do_xyz(): nonpayable

# technically implements ifoo, but missing `implements: ifoo`

@external
def do_xyz():
pass
"""
main = """
import lib1

exports: lib1.ifoo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
with pytest.raises(InterfaceViolation) as e:
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!"


def test_export_selector_conflict(make_input_bundle):
ifoo = """
@external
Expand Down Expand Up @@ -444,3 +466,55 @@ def __init__():
with pytest.raises(InterfaceViolation) as e:
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!"


def test_export_empty_interface(make_input_bundle, tmp_path):
lib1 = """
def an_internal_function():
pass
"""
main = """
import lib1

exports: lib1.__interface__
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
with pytest.raises(StructureException) as e:
compile_code(main, input_bundle=input_bundle)

# as_posix() for windows
lib1_path = (tmp_path / "lib1.vy").as_posix()
assert e.value._message == f"lib1 (located at `{lib1_path}`) has no external functions!"


def test_invalid_export(make_input_bundle):
lib1 = """
@external
def foo():
pass
"""
main = """
import lib1
a: address

exports: lib1.__interface__(self.a).foo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})

with pytest.raises(StructureException) as e:
compile_code(main, input_bundle=input_bundle)

assert e.value._message == "invalid export of a value"
assert e.value._hint == "exports should look like <module>.<function | interface>"

main = """
interface Foo:
def foo(): nonpayable

exports: Foo
"""
with pytest.raises(StructureException) as e:
compile_code(main)

assert e.value._message == "invalid export"
assert e.value._hint == "exports should look like <module>.<function | interface>"
24 changes: 17 additions & 7 deletions vyper/codegen/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
FlagT,
HashMapT,
InterfaceT,
ModuleT,
SArrayT,
StringT,
StructT,
Expand Down Expand Up @@ -680,7 +681,8 @@ def parse_Call(self):
# TODO fix cyclic import
from vyper.builtins._signatures import BuiltinFunctionT

func_t = self.expr.func._metadata["type"]
func = self.expr.func
func_t = func._metadata["type"]

if isinstance(func_t, BuiltinFunctionT):
return func_t.build_IR(self.expr, self.context)
Expand All @@ -691,8 +693,14 @@ def parse_Call(self):
return self.handle_struct_literal()

# Interface constructor. Bar(<address>).
if is_type_t(func_t, InterfaceT):
if is_type_t(func_t, InterfaceT) or func.get("attr") == "__at__":
assert not self.is_stmt # sanity check typechecker

# magic: do sanity checks for module.__at__
if func.get("attr") == "__at__":
assert isinstance(func_t, MemberFunctionT)
assert isinstance(func.value._metadata["type"], ModuleT)

(arg0,) = self.expr.args
arg_ir = Expr(arg0, self.context).ir_node

Expand All @@ -702,16 +710,16 @@ def parse_Call(self):
return arg_ir

if isinstance(func_t, MemberFunctionT):
darray = Expr(self.expr.func.value, self.context).ir_node
# TODO consider moving these to builtins or a dedicated file
darray = Expr(func.value, self.context).ir_node
assert isinstance(darray.typ, DArrayT)
args = [Expr(x, self.context).ir_node for x in self.expr.args]
if self.expr.func.attr == "pop":
# TODO consider moving this to builtins
darray = Expr(self.expr.func.value, self.context).ir_node
if func.attr == "pop":
darray = Expr(func.value, self.context).ir_node
assert len(self.expr.args) == 0
return_item = not self.is_stmt
return pop_dyn_array(darray, return_popped_item=return_item)
elif self.expr.func.attr == "append":
elif func.attr == "append":
(arg,) = args
check_assign(
dummy_node_for_type(darray.typ.value_type), dummy_node_for_type(arg.typ)
Expand All @@ -726,6 +734,8 @@ def parse_Call(self):
ret.append(append_dyn_array(darray, arg))
return IRnode.from_list(ret)

raise CompilerPanic("unreachable!") # pragma: nocover

assert isinstance(func_t, ContractFunctionT)
assert func_t.is_internal or func_t.is_constructor

Expand Down
3 changes: 3 additions & 0 deletions vyper/compiler/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ def build_abi_output(compiler_data: CompilerData) -> list:
_ = compiler_data.ir_runtime # ensure _ir_info is generated

abi = module_t.interface.to_toplevel_abi_dict()
if module_t.init_function:
abi += module_t.init_function.to_toplevel_abi_dict()

if compiler_data.show_gas_estimates:
# Add gas estimates for each function to ABI
gas_estimates = build_gas_estimates(compiler_data.function_signatures)
Expand Down
1 change: 1 addition & 0 deletions vyper/semantics/analysis/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

if TYPE_CHECKING:
from vyper.semantics.types.function import ContractFunctionT
from vyper.semantics.types.module import ModuleT

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'ModuleT' may not be defined if module
vyper.semantics.types.module
is imported before module
vyper.semantics.analysis.base
, as the
definition
of ModuleT occurs after the cyclic
import
of vyper.semantics.analysis.base.
'ModuleT' may not be defined if module
vyper.semantics.types.module
is imported before module
vyper.semantics.analysis.base
, as the
definition
of ModuleT occurs after the cyclic
import
of vyper.semantics.analysis.base.
'ModuleT' may not be defined if module
vyper.semantics.types.module
is imported before module
vyper.semantics.analysis.base
, as the
definition
of ModuleT occurs after the cyclic
import
of vyper.semantics.analysis.base.
'ModuleT' may not be defined if module
vyper.semantics.types.module
is imported before module
vyper.semantics.analysis.base
, as the
definition
of ModuleT occurs after the cyclic
import
of vyper.semantics.analysis.base.
'ModuleT' may not be defined if module
vyper.semantics.types.module
is imported before module
vyper.semantics.analysis.base
, as the
definition
of ModuleT occurs after the cyclic
import
of vyper.semantics.analysis.base.


class FunctionVisibility(StringEnum):
Expand Down Expand Up @@ -96,6 +96,7 @@
class ModuleInfo(AnalysisResult):
module_t: "ModuleT"
alias: str
# import_node: vy_ast._ImportStmt # maybe could be useful
ownership: ModuleOwnership = ModuleOwnership.NO_OWNERSHIP
ownership_decl: Optional[vy_ast.VyperNode] = None

Expand Down
24 changes: 20 additions & 4 deletions vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
)
from vyper.semantics.data_locations import DataLocation
from vyper.semantics.namespace import Namespace, get_namespace, override_global_namespace
from vyper.semantics.types import EventT, FlagT, InterfaceT, StructT
from vyper.semantics.types import TYPE_T, EventT, FlagT, InterfaceT, StructT, is_type_t
from vyper.semantics.types.function import ContractFunctionT
from vyper.semantics.types.module import ModuleT
from vyper.semantics.types.utils import type_from_annotation
Expand Down Expand Up @@ -499,9 +499,19 @@ def visit_ExportsDecl(self, node):
raise StructureException("not a public variable!", decl, item)
funcs = [decl._expanded_getter._metadata["func_type"]]
elif isinstance(info.typ, ContractFunctionT):
# e.g. lib1.__interface__(self._addr).foo
if not isinstance(get_expr_info(item.value).typ, (ModuleT, TYPE_T)):
raise StructureException(
"invalid export of a value",
item.value,
hint="exports should look like <module>.<function | interface>",
)

# regular function
funcs = [info.typ]
elif isinstance(info.typ, InterfaceT):
elif is_type_t(info.typ, InterfaceT):
interface_t = info.typ.typedef

if not isinstance(item, vy_ast.Attribute):
raise StructureException(
"invalid export",
Expand All @@ -512,7 +522,7 @@ def visit_ExportsDecl(self, node):
if module_info is None:
raise StructureException("not a valid module!", item.value)

if info.typ not in module_info.typ.implemented_interfaces:
if interface_t not in module_info.typ.implemented_interfaces:
iface_str = item.node_source_code
module_str = item.value.node_source_code
msg = f"requested `{iface_str}` but `{module_str}`"
Expand All @@ -523,9 +533,15 @@ def visit_ExportsDecl(self, node):
# find the specific implementation of the function in the module
funcs = [
module_exposed_fns[fn.name]
for fn in info.typ.functions.values()
for fn in interface_t.functions.values()
if fn.is_external
]

if len(funcs) == 0:
path = module_info.module_node.path
msg = f"{module_info.alias} (located at `{path}`) has no external functions!"
raise StructureException(msg, item)

else:
raise StructureException(
f"not a function or interface: `{info.typ}`", info.typ.decl_node, item
Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/analysis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def _raise_invalid_reference(name, node):
try:
s = t.get_member(name, node)

if isinstance(s, (VyperType, TYPE_T)):
if isinstance(s, VyperType):
# ex. foo.bar(). bar() is a ContractFunctionT
return [s]

Expand Down
4 changes: 2 additions & 2 deletions vyper/semantics/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from . import primitives, subscriptable, user
from .base import TYPE_T, VOID_TYPE, KwargSettings, VyperType, is_type_t, map_void
from .bytestrings import BytesT, StringT, _BytestringT
from .function import MemberFunctionT
from .module import InterfaceT
from .function import ContractFunctionT, MemberFunctionT
from .module import InterfaceT, ModuleT
from .primitives import AddressT, BoolT, BytesM_T, DecimalT, IntegerT, SelfT
from .subscriptable import DArrayT, HashMapT, SArrayT, TupleT
from .user import EventT, FlagT, StructT
Expand Down
10 changes: 6 additions & 4 deletions vyper/semantics/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,13 @@ def __eq__(self, other):
)

def __lt__(self, other):
# CMC 2024-10-20 what is this for?
return self.abi_type.selector_name() < other.abi_type.selector_name()

def __repr__(self):
# TODO: add `pretty()` to the VyperType API?
return self._id

# return a dict suitable for serializing in the AST
def to_dict(self):
ret = {"name": self._id}
Expand Down Expand Up @@ -362,10 +367,7 @@ def get_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType":
raise StructureException(f"{self} instance does not have members", node)

hint = get_levenshtein_error_suggestions(key, self.members, 0.3)
raise UnknownAttribute(f"{self} has no member '{key}'.", node, hint=hint)

def __repr__(self):
return self._id
raise UnknownAttribute(f"{repr(self)} has no member '{key}'.", node, hint=hint)


class KwargSettings:
Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/types/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ def _id(self):
return self.name

def __repr__(self):
return f"{self.underlying_type._id} member function '{self.name}'"
return f"{self.underlying_type} member function '{self.name}'"

def fetch_call_return(self, node: vy_ast.Call) -> Optional[VyperType]:
validate_call_args(node, len(self.arg_types))
Expand Down
Loading
Loading