vyper
vyper copied to clipboard
feat[lang]: allow module intrinsic interface call
allow module.__interface__ to be used in call position by adding it to the module membership data structure.
additionally, fix a bug where interfaces defined inline could not be exported. this is simultaneously fixed as a related bug because previously, interfaces could come up in export analysis as InterfaceT or TYPE_T depending on their provenance. this commit fixes the bug by making them TYPE_T in both imported and inlined provenance.
refactor:
- wrap interfaces in TYPE_T
- streamline an
isinstance(t, (VyperType, TYPE_T))check.TYPE_Tnow inherits fromVyperType, so it doesn't need to be listed separately
What I did
fixes https://github.com/vyperlang/vyper/issues/3943
How I did it
How to verify it
Commit message
Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see our commit message style guide for what we would ideally like to see in a commit message.)
Description for the changelog
Cute Animal Picture
we allow for exporting a module with no external functions - i think we should raise in such case:
# main.vy
import lib1
def f(x: Bytes[32*5]) -> uint256:
k: uint256 = lib1.foo()
return k
exports: lib1.__interface__
# lib1.vy
@internal
def foo() -> uint256:
return 1
import lib1
uses: lib1
@deploy
def __init__():
lib1.__interface__(self).__init__()
exports: lib1.__interface__
#lib1.vy
k: uint256
@external
def bar():
pass
@deploy
def __init__():
self.k = 10
AttributeError: 'NoneType' object has no attribute 'module_t'
__interface__ can be used in "weird positions:
main.vy
import lib1
#uses: lib1
@deploy
def __init__():
log lib1.__interface__.Foo(1)
s: lib1.Structt = lib1.__interface__.Structt(i=1)
exports: lib1.__interface__
#lib1.vy
event Foo:
i: uint256
struct Structt:
i: uint256
k: uint256
@external
def bar():
pass
@deploy
def __init__():
self.k = 10
should address https://github.com/vyperlang/vyper/issues/3943, right?
with -f annotated_ast i get:
AttributeError: 'FunctionDef' object has no attribute 'node_id'
# main.vy
import lib1
@external
def foo() -> uint256:
return staticcall lib1.__interface__(self).d()
# lib1.vy
d: public(uint256)
would it be too much to ask for implicit cast of mod to mod.__interface__? For either v: Mod in argument (convert to Interface to use like v.method(...)) or extcall Mod(v).method(...)
.__interface__ is a little awkward to specify
with
-f annotated_asti get:AttributeError: 'FunctionDef' object has no attribute 'node_id'# main.vy import lib1 @external def foo() -> uint256: return staticcall lib1.__interface__(self).d() # lib1.vy d: public(uint256)
on latest (96213973ed07c584436ac9291b60061473649817) we get:
vyper.exceptions.UnknownAttribute: tmp/lib1.vy has no member 'd'.
contract "tmp/main.vy:6", function "foo", line 6:22
5 def foo() -> uint256:
---> 6 return staticcall lib1.__interface__(self).d()
-----------------------------^
7
would it be too much to ask for implicit cast of
modtomod.__interface__? For eitherv: Modin argument (convert to Interface to use likev.method(...)) orextcall Mod(v).method(...)
.__interface__is a little awkward to specify
yes but it's a bit hairy. it will feel ambiguous once we add deploy Mod(...) syntax
we allow for exporting a module with no external functions - i think we should raise in such case:
# main.vy import lib1 def f(x: Bytes[32*5]) -> uint256: k: uint256 = lib1.foo() return k exports: lib1.__interface__ # lib1.vy @internal def foo() -> uint256: return 1
24ac428539955f272a08e49301fbd8107d30b048
should address #3943, right?
yes. updated the PR description
with
-f annotated_asti get:AttributeError: 'FunctionDef' object has no attribute 'node_id'# main.vy import lib1 @external def foo() -> uint256: return staticcall lib1.__interface__(self).d() # lib1.vy d: public(uint256)
should be fixed as of latest (24ac428539955f272a08e49301fbd8107d30b048)
__interface__can be used in "weird positions:main.vy import lib1 #uses: lib1 @deploy def __init__(): log lib1.__interface__.Foo(1) s: lib1.Structt = lib1.__interface__.Structt(i=1) exports: lib1.__interface__ #lib1.vy event Foo: i: uint256 struct Structt: i: uint256 k: uint256 @external def bar(): pass @deploy def __init__(): self.k = 10
i'm not too bothered. it looks a bit weird, but technically Structt is a member of the interface. are you suggesting we should block it and only allow using lib1.__interface__.<external function>?
a bit oos for this PR, but we allow for implements: I where I is an empty .vyi file - which most likely is a bug in the user code
for code like
# main.vy
import lib1
implements: lib1. __interface__
i get:
vyper.exceptions.UnknownAttribute: tests/custom/lib1.vy has no member '__interface__'.
contract "tests/custom/test4.vy:4", line 4:12
3
---> 4 implements: lib1. __interface__
-------------------^
5
which is weird given we allow
# main.vy
import lib1
exports: lib1. __interface__
EDIT: i must have made a mistake during branch switching - i was cross-checking the behavior also against master and I can't repro this against this branch
# main.vy
import ITest2
exports: ITest2
# ITest2.vyi
def foo() -> uint8:
...
@view
def foobar() -> uint8:
...
yields:
Error compiling: tests/custom/test4.vy
vyper.exceptions.StructureException: not a function or interface: `type(tests/custom/ITest2.vyi)`
contract "tests/custom/test4.vy:4", line 4:9
3
---> 4 exports: ITest2
----------------^
5
EDIT: i must have made a mistake during branch switching - i was cross-checking the behavior also against master and I can't repro this against this branch
i'm not too bothered. it looks a bit weird, but technically
Structtis a member of the interface. are you suggesting we should block it and only allow usinglib1.__interface__.<external function>?
yes, that was the original intention
however, as i test it, the poc now doesn't work. is this intentional?
for code like
# main.vy import lib1 implements: lib1. __interface__i get:
vyper.exceptions.UnknownAttribute: tests/custom/lib1.vy has no member '__interface__'. contract "tests/custom/test4.vy:4", line 4:12 3 ---> 4 implements: lib1. __interface__ -------------------^ 5which is weird given we allow
# main.vy import lib1 exports: lib1. __interface__
i can't reproduce this. can you provide the contents you have for lib1.vy?
# main.vy import ITest2 exports: ITest2 # ITest2.vyi def foo() -> uint8: ... @view def foobar() -> uint8: ...yields:
Error compiling: tests/custom/test4.vy vyper.exceptions.StructureException: not a function or interface: `type(tests/custom/ITest2.vyi)` contract "tests/custom/test4.vy:4", line 4:9 3 ---> 4 exports: ITest2 ----------------^ 5
i can't reproduce this. when i run this i get the error:
Error compiling: tmp/main.vy
vyper.exceptions.StructureException: invalid export
contract "tmp/main.vy:6", line 6:0
5
---> 6 exports: ITest2
-------^
7
(hint: exports should look like <module>.<function | interface>)
a bit oos for this PR, but we allow for
implements: IwhereIis an empty.vyifile - which most likely is a bug in the user code
https://github.com/vyperlang/vyper/pull/4322
you can call __default__ through __interface__
# main.vy
import lib2
@external
def bar():
extcall lib2.__interface__(self).__default__()
# lib2.vy
@external
def __default__():
pass
i think you wanna be able to export it but not call it
exports of the following form are allowed:
# main.vy
import lib2
a: address
exports: lib2.__interface__(self.a).__default__
# lib2.vy
@external
def __default__():
pass
exports of the following form are allowed:
# main.vy import lib2 a: address exports: lib2.__interface__(self.a).__default__ # lib2.vy @external def __default__(): pass
ac43bebccc5a88e7a9b31e9ca5c5cbdc46f0fc5a
would it be too much to ask for implicit cast of
modtomod.__interface__? For eitherv: Modin argument (convert to Interface to use likev.method(...)) orextcall Mod(v).method(...).__interface__is a little awkward to specifyyes but it's a bit hairy. it will feel ambiguous once we add
deploy Mod(...)syntax
been thinking about this more and maybe it's feasible? since we already guard external calls with extcall or staticcall, you can kind of tell what type you get from the call AST context
x: Mod = deploy Mod(args...)
^^^ ^^^
type: interface
type: module ctor
extcall Mod(addr)
^^^
type: interface
it will be kind of confusing to have Mod be a module or interface depending on context, though.
another fun alternative that is slightly less verbose is:
extcall Mod.__at__(addr).foo()
Codecov Report
Attention: Patch coverage is 32.50000% with 27 lines in your changes missing coverage. Please review.
Project coverage is 47.70%. Comparing base (
b3ea663) to head (9bac423). Report is 10 commits behind head on master.
:exclamation: There is a different number of reports uploaded between BASE (b3ea663) and HEAD (9bac423). Click for more details.
HEAD has 137 uploads less than BASE
Flag BASE (b3ea663) HEAD (9bac423) 138 1
Additional details and impacted files
@@ Coverage Diff @@
## master #4090 +/- ##
===========================================
- Coverage 91.40% 47.70% -43.71%
===========================================
Files 112 112
Lines 15927 16028 +101
Branches 2694 2699 +5
===========================================
- Hits 14558 7646 -6912
- Misses 935 7749 +6814
- Partials 434 633 +199
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
🚨 Try these New Features:
- Flaky Tests Detection - Detect and resolve failed and flaky tests
# lib1.vy
def doo():
pass
# main.vy
import lib1
i: public(lib1.__interface__)
@external
def boo():
pass
exports: self.i
Error compiling: tests/custom/test.vy
AttributeError: 'NoneType' object has no attribute '_metadata'
During handling of the above exception, another exception occurred:
vyper.exceptions.CompilerPanic: unhandled exception 'NoneType' object has no attribute '_metadata'
contract "tests/custom/test.vy:9", line 9:0
8
---> 9 exports: self.i
-------^
This is an unhandled internal compiler error. Please create an issue on Github to notify the developers!
https://github.com/vyperlang/vyper/issues/new?template=bug.md