From bfada55cb8ac53b7e629e2a2c17e3a07253241c9 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 28 Mar 2024 09:53:57 -0400 Subject: [PATCH 01/14] add __interface__ to module members --- vyper/semantics/types/module.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index 4557fc9612..44d08937bc 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -331,6 +331,8 @@ def __init__(self, module: vy_ast.Module, name: Optional[str] = None): # can access interfaces in type position self._helper.add_member(name, TYPE_T(interface_t)) + self.add_member("__interface__", self.interface) + # __eq__ is very strict on ModuleT - object equality! this is because we # don't want to reason about where a module came from (i.e. input bundle, # search path, symlinked vs normalized path, etc.) From 5ccd945f603abed49676695efa637f1376fedc45 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 7 Apr 2024 10:07:41 -0400 Subject: [PATCH 02/14] add a note --- vyper/semantics/analysis/module.py | 54 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index e0b7da0ce5..72a7c558b2 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -506,43 +506,49 @@ def visit_InitializesDecl(self, node): def visit_ExportsDecl(self, node): items = vy_ast.as_tuple(node.annotation) - funcs = [] + exported_funcs = [] used_modules = OrderedSet() for item in items: # set is_callable=True to give better error messages for imported # types, e.g. exports: some_module.MyEvent info = get_expr_info(item, is_callable=True) + if info.var_info is not None: - decl_node = info.var_info.decl_node + decl = info.var_info.decl_node if not info.var_info.is_public: - raise StructureException("not a public variable!", decl_node, item) - func_t = decl_node._expanded_getter._metadata["func_type"] - - else: + raise StructureException("not a public variable!", decl, item) + funcs = [decl._expanded_getter._metadata["func_type"]] + elif isinstance(info.typ, ContractFunctionT): # regular function - func_t = info.typ - decl_node = func_t.decl_node + funcs = [info.typ] + elif isinstance(info.typ, InterfaceT): + # TODO: disambiguate the module parent of expr and check that + # the interface is actually implemented by the module. (as + # written, i can `export: IERC20` even though those are just + # signatures). + funcs = [f for f in info.typ.functions.values() if f.is_external] + else: + raise StructureException(f"not a function or interface: `{info.typ}`", info.typ.decl_node, item) - if not isinstance(func_t, ContractFunctionT): - raise StructureException(f"not a function: `{func_t}`", decl_node, item) - if not func_t.is_external: - raise StructureException("can't export non-external functions!", decl_node, item) + for func_t in funcs: + if not func_t.is_external: + raise StructureException("can't export non-external functions!", func_t.decl_node, item) - self._add_exposed_function(func_t, item, relax=False) - with tag_exceptions(item): # tag with specific item - self._self_t.typ.add_member(func_t.name, func_t) + self._add_exposed_function(func_t, item, relax=False) + with tag_exceptions(item): # tag exceptions with specific item + self._self_t.typ.add_member(func_t.name, func_t) - funcs.append(func_t) + exported_funcs.append(func_t) - # check module uses - var_accesses = func_t.get_variable_accesses() - if any(s.variable.is_state_variable() for s in var_accesses): - module_info = check_module_uses(item) - assert module_info is not None # guaranteed by above checks - used_modules.add(module_info) + # check module uses + var_accesses = func_t.get_variable_accesses() + if any(s.variable.is_state_variable() for s in var_accesses): + module_info = check_module_uses(item) + assert module_info is not None # guaranteed by above checks + used_modules.add(module_info) - node._metadata["exports_info"] = ExportsInfo(funcs, used_modules) + node._metadata["exports_info"] = ExportsInfo(exported_funcs, used_modules) @property def _self_t(self): @@ -551,7 +557,7 @@ def _self_t(self): def _add_exposed_function(self, func_t, node, relax=True): # call this before self._self_t.typ.add_member() for exception raising # priority - if (prev_decl := self._exposed_functions.get(func_t)) is not None: + if not relax and (prev_decl := self._exposed_functions.get(func_t)) is not None: raise StructureException("already exported!", node, prev_decl=prev_decl) self._exposed_functions[func_t] = node From 5e692771d079b1a0bff9349d240e163a7ae0d38e Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 11 Apr 2024 12:42:07 -0400 Subject: [PATCH 03/14] fix interfaces --- vyper/semantics/analysis/module.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index c1511a9537..3c0b9743d4 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -547,17 +547,30 @@ def visit_ExportsDecl(self, node): # regular function funcs = [info.typ] elif isinstance(info.typ, InterfaceT): - # TODO: disambiguate the module parent of expr and check that - # the interface is actually implemented by the module. (as - # written, i can `export: IERC20` even though those are just - # signatures). - funcs = [f for f in info.typ.functions.values() if f.is_external] + if not isinstance(item, vy_ast.Attribute): + raise StructureException( + "invalid export", + hint="exports should look like .", + ) + + module_info = get_expr_info(item.value).module_info + if module_info is None: + raise StructureException("not a valid module!", item.value) + + module_exposed_fns = {fn.name: fn for fn in module_info.typ.exposed_functions} + funcs = [ + module_exposed_fns[f.name] for f in info.typ.functions.values() if f.is_external + ] else: - raise StructureException(f"not a function or interface: `{info.typ}`", info.typ.decl_node, item) + raise StructureException( + f"not a function or interface: `{info.typ}`", info.typ.decl_node, item + ) for func_t in funcs: if not func_t.is_external: - raise StructureException("can't export non-external functions!", func_t.decl_node, item) + raise StructureException( + "can't export non-external functions!", func_t.decl_node, item + ) self._add_exposed_function(func_t, item, relax=False) with tag_exceptions(item): # tag exceptions with specific item From 12281ba6ea474556246c833bb6fa819a266b55e5 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 12 Apr 2024 11:46:18 -0400 Subject: [PATCH 04/14] add some basic syntax tests --- .../functional/syntax/modules/test_exports.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index 24a233da9d..76f44eea7d 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -307,3 +307,61 @@ def bar(): assert e.value.prev_decl.col_offset == 9 assert e.value.prev_decl.node_source_code == "lib1.foo" assert e.value.prev_decl.module_node.path == "main.vy" + + +@pytest.fixture +def simple_library(make_input_bundle): + iface = """ +@external +def foo() -> uint256: + ... + +@external +def bar() -> uint256: + ... + """ + lib1 = """ +import ilib +implements: ilib + +@external +def foo() -> uint256: + return 1 + +@external +def bar() -> uint256: + return 2 + """ + return make_input_bundle({"lib1.vy": lib1, "ilib.vyi": iface}) + + +def test_exports_interface_simple(simple_library): + main = """ +import lib1 + +exports: lib1.__interface__ + """ + out = compile_code( + main, + output_formats=["abi", "ast_dict"], + contract_path="main.vy", + input_bundle=simple_library, + ) + fnames = [item["name"] for item in out["abi"]] + assert fnames == ["foo", "bar"] + + +def test_exports_interface2(simple_library): + main = """ +import lib1 + +exports: lib1.ilib + """ + out = compile_code( + main, + output_formats=["abi", "ast_dict"], + contract_path="main.vy", + input_bundle=simple_library, + ) + fnames = [item["name"] for item in out["abi"]] + assert fnames == ["foo", "bar"] From 87735190a8ce0e611b92a6926c515aa52731ee85 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 12 Apr 2024 18:18:51 -0400 Subject: [PATCH 05/14] codegen tests --- .../codegen/modules/test_exports.py | 78 +++++++++++++++++++ .../functional/syntax/modules/test_exports.py | 58 -------------- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 2dc90bfe74..4fe1ba7821 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -1,3 +1,8 @@ +import pytest + +from vyper.compiler import compile_code + + def test_simple_export(make_input_bundle, get_contract): lib1 = """ @external @@ -147,3 +152,76 @@ def foo() -> uint256: c = get_contract(main, input_bundle=input_bundle) assert c.foo() == 5 + + +@pytest.fixture +def simple_library(make_input_bundle): + ifoo = """ +@external +def foo() -> uint256: + ... + +@external +def bar() -> uint256: + ... + """ + ibar = """ +@external +def bar() -> uint256: + ... + +@external +def qux() -> uint256: + ... + """ + lib1 = """ +import ifoo +import ibar + +implements: ifoo +implements: ibar + +@external +def foo() -> uint256: + return 1 + +@external +def bar() -> uint256: + return 2 + +@external +def qux() -> uint256: + return 3 + """ + return make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo, "ibar.vyi": ibar}) + + +def test_exports_interface_simple(get_contract, simple_library): + main = """ +import lib1 + +exports: lib1.__interface__ + """ + c = get_contract(main, input_bundle=simple_library) + assert c.foo() == 1 + assert c.bar() == 2 + assert c.qux() == 3 + + +def test_exports_interface2(get_contract, simple_library): + main = """ +import lib1 + +exports: lib1.ifoo + """ + out = compile_code( + main, output_formats=["abi"], contract_path="main.vy", input_bundle=simple_library + ) + fnames = [item["name"] for item in out["abi"]] + assert fnames == ["foo", "bar"] + + c = get_contract(main, input_bundle=simple_library) + assert c.foo() == 1 + assert c.bar() == 2 + # TODO: check the selector table too + assert not hasattr(c, "qux") diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index 1b5d9ebed0..1edb99bc7f 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -309,61 +309,3 @@ def bar(): assert e.value.prev_decl.col_offset == 9 assert e.value.prev_decl.node_source_code == "lib1.foo" assert e.value.prev_decl.module_node.path == "main.vy" - - -@pytest.fixture -def simple_library(make_input_bundle): - iface = """ -@external -def foo() -> uint256: - ... - -@external -def bar() -> uint256: - ... - """ - lib1 = """ -import ilib -implements: ilib - -@external -def foo() -> uint256: - return 1 - -@external -def bar() -> uint256: - return 2 - """ - return make_input_bundle({"lib1.vy": lib1, "ilib.vyi": iface}) - - -def test_exports_interface_simple(simple_library): - main = """ -import lib1 - -exports: lib1.__interface__ - """ - out = compile_code( - main, - output_formats=["abi", "ast_dict"], - contract_path="main.vy", - input_bundle=simple_library, - ) - fnames = [item["name"] for item in out["abi"]] - assert fnames == ["foo", "bar"] - - -def test_exports_interface2(simple_library): - main = """ -import lib1 - -exports: lib1.ilib - """ - out = compile_code( - main, - output_formats=["abi", "ast_dict"], - contract_path="main.vy", - input_bundle=simple_library, - ) - fnames = [item["name"] for item in out["abi"]] - assert fnames == ["foo", "bar"] From e45cb4e86460c82009fc872e6bb7c0c4acdcca1a Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Sat, 13 Apr 2024 13:46:25 +0200 Subject: [PATCH 06/14] add exports tests --- .../codegen/modules/test_exports.py | 111 +++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 4fe1ba7821..f2f48318c5 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -1,6 +1,8 @@ import pytest from vyper.compiler import compile_code +from vyper.utils import method_id +from vyper.exceptions import StructureException def test_simple_export(make_input_bundle, get_contract): @@ -196,6 +198,15 @@ def qux() -> uint256: return make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo, "ibar.vyi": ibar}) +@pytest.fixture +def send_failing_tx_to_signature(w3, tx_failed): + def _send_transaction(c, method_sig): + data = method_id(method_sig) + with tx_failed(): + w3.eth.send_transaction({"to": c.address, "data": data}) + return _send_transaction + + def test_exports_interface_simple(get_contract, simple_library): main = """ import lib1 @@ -208,7 +219,7 @@ def test_exports_interface_simple(get_contract, simple_library): assert c.qux() == 3 -def test_exports_interface2(get_contract, simple_library): +def test_exports_interface2(get_contract, send_failing_tx_to_signature, simple_library): main = """ import lib1 @@ -223,5 +234,101 @@ def test_exports_interface2(get_contract, simple_library): c = get_contract(main, input_bundle=simple_library) assert c.foo() == 1 assert c.bar() == 2 - # TODO: check the selector table too assert not hasattr(c, "qux") + send_failing_tx_to_signature(c, "qux()") + + +def test_exported_fun_part_of_interface(get_contract, make_input_bundle): + main = """ +import lib2 + +exports: lib2.__interface__ + """ + lib1 = """ +@external +def bar() -> uint256: + return 1 + """ + lib2 = """ +import lib1 + +@external +def foo() -> uint256: + return 2 + +exports: lib1.bar + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) + c = get_contract(main, input_bundle=input_bundle) + assert c.bar() == 1 + assert c.foo() == 2 + + +def test_imported_module_not_part_of_interface(send_failing_tx_to_signature, get_contract, make_input_bundle): + main = """ +import lib2 + +exports: lib2.__interface__ + """ + lib1 = """ +@external +def bar() -> uint256: + return 1 + """ + lib2 = """ +import lib1 + +@external +def foo() -> uint256: + return 2 + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) + c = get_contract(main, input_bundle=input_bundle) + assert c.foo() == 2 + send_failing_tx_to_signature(c, "bar()") + + +def test_interface_export_collision(send_failing_tx_to_signature, get_contract, make_input_bundle): + main = """ +import lib1 + +exports: lib1.__interface__ +exports: lib1.bar + """ + lib1 = """ +@external +def bar() -> uint256: + return 1 + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + with pytest.raises(StructureException): + get_contract(main, input_bundle=input_bundle) + + +def test_export_unimplemented_interface(send_failing_tx_to_signature, get_contract, make_input_bundle): + ifoo = """ +@external +def foo() -> uint256: + ... + """ + lib1 = """ +import ifoo + +@external +def foo() -> uint256: + return 1 + +@external +def bar() -> uint256: + return 2 + """ + main = """ +import lib1 + +exports: lib1.ifoo + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo}) + c = get_contract(main, input_bundle=input_bundle) + assert c.foo() == 1 + send_failing_tx_to_signature(c, "bar()") + From 6650768d796dd9122b609945b4e7807f179b04f3 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 08:32:21 -0400 Subject: [PATCH 07/14] fix not-found functions, add tests --- .../codegen/modules/test_exports.py | 32 +++------ .../functional/syntax/modules/test_exports.py | 72 +++++++++++++++++++ vyper/semantics/analysis/module.py | 16 ++++- 3 files changed, 94 insertions(+), 26 deletions(-) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index f2f48318c5..1d6f0607f2 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -2,7 +2,6 @@ from vyper.compiler import compile_code from vyper.utils import method_id -from vyper.exceptions import StructureException def test_simple_export(make_input_bundle, get_contract): @@ -204,6 +203,7 @@ def _send_transaction(c, method_sig): data = method_id(method_sig) with tx_failed(): w3.eth.send_transaction({"to": c.address, "data": data}) + return _send_transaction @@ -247,7 +247,7 @@ def test_exported_fun_part_of_interface(get_contract, make_input_bundle): lib1 = """ @external def bar() -> uint256: - return 1 + return 1 """ lib2 = """ import lib1 @@ -264,7 +264,9 @@ def foo() -> uint256: assert c.foo() == 2 -def test_imported_module_not_part_of_interface(send_failing_tx_to_signature, get_contract, make_input_bundle): +def test_imported_module_not_part_of_interface( + send_failing_tx_to_signature, get_contract, make_input_bundle +): main = """ import lib2 @@ -273,7 +275,7 @@ def test_imported_module_not_part_of_interface(send_failing_tx_to_signature, get lib1 = """ @external def bar() -> uint256: - return 1 + return 1 """ lib2 = """ import lib1 @@ -288,24 +290,9 @@ def foo() -> uint256: send_failing_tx_to_signature(c, "bar()") -def test_interface_export_collision(send_failing_tx_to_signature, get_contract, make_input_bundle): - main = """ -import lib1 - -exports: lib1.__interface__ -exports: lib1.bar - """ - lib1 = """ -@external -def bar() -> uint256: - return 1 - """ - input_bundle = make_input_bundle({"lib1.vy": lib1}) - with pytest.raises(StructureException): - get_contract(main, input_bundle=input_bundle) - - -def test_export_unimplemented_interface(send_failing_tx_to_signature, get_contract, make_input_bundle): +def test_export_unimplemented_interface( + send_failing_tx_to_signature, get_contract, make_input_bundle +): ifoo = """ @external def foo() -> uint256: @@ -331,4 +318,3 @@ def bar() -> uint256: c = get_contract(main, input_bundle=input_bundle) assert c.foo() == 1 send_failing_tx_to_signature(c, "bar()") - diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index 1edb99bc7f..363a774f2d 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -309,3 +309,75 @@ def bar(): assert e.value.prev_decl.col_offset == 9 assert e.value.prev_decl.node_source_code == "lib1.foo" assert e.value.prev_decl.module_node.path == "main.vy" + + +def test_interface_export_collision(make_input_bundle): + main = """ +import lib1 + +exports: lib1.__interface__ +exports: lib1.bar + """ + lib1 = """ +@external +def bar() -> uint256: + return 1 + """ + 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 == "already exported!" + + +def test_export_missing_function(make_input_bundle): + ifoo = """ +@external +def do_xyz(): + ... + """ + lib1 = """ +import ifoo + +@external +@view +def bar() -> uint256: + return 1 + """ + main = """ +import lib1 + +exports: lib1.ifoo + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo}) + with pytest.raises(StructureException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "requested `lib1.ifoo` but `lib1.do_xyz` is not implemented" + + +def test_export_selector_conflict(make_input_bundle): + ifoo = """ +@external +def gsf(): + ... + """ + lib1 = """ +import ifoo + +@external +def gsf(): + pass + +@external +@view +def tgeo() -> uint256: + return 1 + """ + main = """ +import lib1 + +exports: (lib1.ifoo, lib1.tgeo) + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo}) + with pytest.raises(StructureException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "Methods produce colliding method ID `0x67e43e43`: gsf(), tgeo()" diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index d7ed814b2d..9211a3b815 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -558,9 +558,19 @@ def visit_ExportsDecl(self, node): raise StructureException("not a valid module!", item.value) module_exposed_fns = {fn.name: fn for fn in module_info.typ.exposed_functions} - funcs = [ - module_exposed_fns[f.name] for f in info.typ.functions.values() if f.is_external - ] + funcs = [] + for f in info.typ.functions.values(): + # find the implementation of the function in the specific module + impl = module_exposed_fns.get(f.name) + if impl is None: + msg = f"requested `{item.node_source_code}` but" + msg += f" `{item.value.node_source_code}.{f.name}`" + msg += " is not implemented" + raise StructureException(msg, item) + + # guaranteed by `.exposed_functions`: + assert isinstance(impl, ContractFunctionT) and impl.is_external + funcs.append(impl) else: raise StructureException( f"not a function or interface: `{info.typ}`", info.typ.decl_node, item From dbda0bb3d80580869846127dabbac3f63c17f48c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 08:51:23 -0400 Subject: [PATCH 08/14] fix from bad merge (dcc4449b4bc5) --- vyper/semantics/analysis/module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 9211a3b815..292ce23573 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -589,8 +589,7 @@ def visit_ExportsDecl(self, node): exported_funcs.append(func_t) # check module uses - var_accesses = func_t.get_variable_accesses() - if any(s.variable.is_state_variable() for s in var_accesses): + if func_t.uses_state(): module_info = check_module_uses(item) assert module_info is not None # guaranteed by above checks used_modules.add(module_info) From 690c7d20825b0b083297f1bc71dcf5de78675fb8 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 09:50:15 -0400 Subject: [PATCH 09/14] promote implements rejection only allow exporting `implement`ed interfaces --- .../codegen/modules/test_exports.py | 3 +- .../functional/syntax/modules/test_exports.py | 43 +++++++++++++++++-- vyper/semantics/analysis/module.py | 30 +++++++------ vyper/semantics/types/module.py | 9 ++++ 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 1d6f0607f2..59f6906d3b 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -290,7 +290,7 @@ def foo() -> uint256: send_failing_tx_to_signature(c, "bar()") -def test_export_unimplemented_interface( +def test_export_unimplemented_function( send_failing_tx_to_signature, get_contract, make_input_bundle ): ifoo = """ @@ -300,6 +300,7 @@ def foo() -> uint256: """ lib1 = """ import ifoo +implements: ifoo @external def foo() -> uint256: diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index 363a774f2d..fb66549831 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -1,7 +1,12 @@ import pytest from vyper.compiler import compile_code -from vyper.exceptions import ImmutableViolation, NamespaceCollision, StructureException +from vyper.exceptions import ( + ImmutableViolation, + InterfaceViolation, + NamespaceCollision, + StructureException, +) from .helpers import NONREENTRANT_NOTE @@ -349,9 +354,9 @@ def bar() -> uint256: exports: lib1.ifoo """ input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo}) - with pytest.raises(StructureException) as e: + with pytest.raises(InterfaceViolation) as e: compile_code(main, input_bundle=input_bundle) - assert e.value._message == "requested `lib1.ifoo` but `lib1.do_xyz` is not implemented" + assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!" def test_export_selector_conflict(make_input_bundle): @@ -381,3 +386,35 @@ def tgeo() -> uint256: with pytest.raises(StructureException) as e: compile_code(main, input_bundle=input_bundle) assert e.value._message == "Methods produce colliding method ID `0x67e43e43`: gsf(), tgeo()" + + +def test_export_different_return_type(make_input_bundle): + ifoo = """ +@external +def foo() -> uint256: + ... + """ + lib1 = """ +import ifoo + +foo: public(int256) + +@deploy +def __init__(): + self.foo = -1 + """ + main = """ +import lib1 + +initializes: lib1 + +exports: lib1.ifoo + +@deploy +def __init__(): + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo}) + 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`!" diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 292ce23573..349c569527 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -20,6 +20,7 @@ ExceptionList, ImmutableViolation, InitializerException, + InterfaceViolation, InvalidLiteral, InvalidType, ModuleNotFound, @@ -533,6 +534,8 @@ def visit_ExportsDecl(self, node): exported_funcs = [] used_modules = OrderedSet() + # CMC 2024-04-13 TODO: reduce nesting in this function + for item in items: # set is_callable=True to give better error messages for imported # types, e.g. exports: some_module.MyEvent @@ -557,20 +560,16 @@ 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: + iface_str = item.node_source_code + module_str = item.value.node_source_code + msg = f"requested `{iface_str}` but `{module_str}`" + msg += f" does not implement `{iface_str}`!" + raise InterfaceViolation(msg, item) + module_exposed_fns = {fn.name: fn for fn in module_info.typ.exposed_functions} - funcs = [] - for f in info.typ.functions.values(): - # find the implementation of the function in the specific module - impl = module_exposed_fns.get(f.name) - if impl is None: - msg = f"requested `{item.node_source_code}` but" - msg += f" `{item.value.node_source_code}.{f.name}`" - msg += " is not implemented" - raise StructureException(msg, item) - - # guaranteed by `.exposed_functions`: - assert isinstance(impl, ContractFunctionT) and impl.is_external - funcs.append(impl) + # find the specific implementation of the function in the module + funcs = [module_exposed_fns[fname] for fname in info.typ.functions.keys()] else: raise StructureException( f"not a function or interface: `{info.typ}`", info.typ.decl_node, item @@ -591,7 +590,10 @@ def visit_ExportsDecl(self, node): # check module uses if func_t.uses_state(): module_info = check_module_uses(item) - assert module_info is not None # guaranteed by above checks + + # guaranteed by above checks: + assert module_info is not None + used_modules.add(module_info) node._metadata["exports_info"] = ExportsInfo(exported_funcs, used_modules) diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index 44d08937bc..51d55a167e 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -373,6 +373,15 @@ def interface_defs(self): def implements_decls(self): return self._module.get_children(vy_ast.ImplementsDecl) + @cached_property + def implemented_interfaces(self): + ret = [node._metadata["interface_type"] for node in self.implements_decls] + + # a module implicitly implements module.__interface__. + ret.append(self.interface) + + return ret + @cached_property def interfaces(self) -> dict[str, InterfaceT]: ret = {} From 61b4edb010b0ccee5b21d1ae02fec96c036e097f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 09:53:03 -0400 Subject: [PATCH 10/14] add a test for missing `implements:` statement --- .../functional/syntax/modules/test_exports.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index fb66549831..7b00d29c98 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -334,7 +334,7 @@ def bar() -> uint256: assert e.value._message == "already exported!" -def test_export_missing_function(make_input_bundle): +def test_no_export_missing_function(make_input_bundle): ifoo = """ @external def do_xyz(): @@ -359,6 +359,32 @@ def bar() -> uint256: assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!" +def test_no_export_unimplemented_interface(make_input_bundle): + ifoo = """ +@external +def do_xyz(): + ... + """ + lib1 = """ +import ifoo + +# 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, "ifoo.vyi": ifoo}) + 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 From 4c10eaf1358b2a743d0d979fed19356e70d0685f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 12:07:49 -0400 Subject: [PATCH 11/14] add another test --- .../codegen/modules/test_exports.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 59f6906d3b..56af8464ab 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -319,3 +319,48 @@ def bar() -> uint256: c = get_contract(main, input_bundle=input_bundle) assert c.foo() == 1 send_failing_tx_to_signature(c, "bar()") + + +# sanity check that when multiple modules implement an interface, the +# correct one (specified by the user) gets selected for export. +def test_export_interface_multiple_choices(get_contract, make_input_bundle): + ifoo = """ +@external +def foo() -> uint256: + ... + """ + lib1 = """ +import ifoo +implements: ifoo + +@external +def foo() -> uint256: + return 1 + """ + lib2 = """ +import ifoo +implements: ifoo + +@external +def foo() -> uint256: + return 2 + """ + main = """ +import lib1 +import lib2 + +exports: lib1.ifoo + """ + main2 = """ +import lib1 +import lib2 + +exports: lib2.ifoo + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2, "ifoo.vyi": ifoo}) + + c = get_contract(main, input_bundle=input_bundle) + assert c.foo() == 1 + + c = get_contract(main2, input_bundle=input_bundle) + assert c.foo() == 2 From 0450173793975ac87852f3b7e09cb97a37bd764c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 13:19:18 -0400 Subject: [PATCH 12/14] fix - library has init function --- vyper/semantics/analysis/module.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 349c569527..cb1dc8430f 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -569,7 +569,11 @@ def visit_ExportsDecl(self, node): module_exposed_fns = {fn.name: fn for fn in module_info.typ.exposed_functions} # find the specific implementation of the function in the module - funcs = [module_exposed_fns[fname] for fname in info.typ.functions.keys()] + funcs = [ + module_exposed_fns[fn.name] + for fn in info.typ.functions.values() + if fn.is_external + ] else: raise StructureException( f"not a function or interface: `{info.typ}`", info.typ.decl_node, item From 2c40ada4958d10d5d12711ef825d7a9fe94dc487 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 14:01:25 -0400 Subject: [PATCH 13/14] add tests for getters, init and default --- .../codegen/modules/test_exports.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 56af8464ab..664298673a 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -364,3 +364,78 @@ def foo() -> uint256: c = get_contract(main2, input_bundle=input_bundle) assert c.foo() == 2 + + +def test_export_module_with_init(get_contract, make_input_bundle): + lib1 = """ +@deploy +def __init__(): + pass + +@external +def foo() -> uint256: + return 1 + """ + main = """ +import lib1 + +exports: lib1.__interface__ + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + assert c.foo() == 1 + + +def test_export_module_with_getter(get_contract, make_input_bundle): + lib1 = """ +counter: public(uint256) + +@external +def foo(): + self.counter += 1 + """ + main = """ +import lib1 + +initializes: lib1 +exports: lib1.__interface__ + +@deploy +def __init__(): + lib1.counter = 100 + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + assert c.counter() == 100 + c.foo(transact={}) + assert c.counter() == 101 + + +def test_export_module_with_default(w3, get_contract, make_input_bundle): + lib1 = """ +counter: public(uint256) + +@external +def foo() -> uint256: + return 1 + +@external +def __default__(): + self.counter += 1 + """ + main = """ +import lib1 +initializes: lib1 + +@deploy +def __init__(): + lib1.counter = 5 + +exports: lib1.__interface__ + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + assert c.foo() == 1 + assert c.counter() == 5 + w3.eth.send_transaction({"to": c.address}) + assert c.counter() == 6 From 8f50fcb72b39650fcb6421de09485af742d462f1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 13 Apr 2024 22:31:42 -0400 Subject: [PATCH 14/14] add a comment --- tests/functional/codegen/modules/test_exports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 664298673a..b02ed6ba9e 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -437,5 +437,6 @@ def __init__(): c = get_contract(main, input_bundle=input_bundle) assert c.foo() == 1 assert c.counter() == 5 + # call `c.__default__()` w3.eth.send_transaction({"to": c.address}) assert c.counter() == 6