Skip to content

Commit

Permalink
feat[lang]: implement function exports (#3786)
Browse files Browse the repository at this point in the history
add `exports:` declaration to the language. this allows users to
directly export functions marked `@external` from libraries instead of
writing `@external` wrapper functions (and consequently, for library
authors to define the external interface for their modules).

important refactors:
- remove expanded getters from module AST, change them to annotation on
  public VariablDecls.
- redo order of node visitation in ModuleAnalyzer
- refactor `InterfaceT.validate_implements()` to handle exported
  functions
- add `exposed_functions` property to `ModuleT` which reflects the
  runtime functions exposed in the selector table
- refactor IR generation to use new reachability analysis

misc:
- remove Module.add_to_body, Module.remove_from_body as they are dead
  now
- move VariableDecl validation to vyper/ast/nodes.py
- improve call-site annotations for error messages
- improve annotations for exceptions which reference a previously-
  declared node.
  • Loading branch information
charles-cooper authored Feb 25, 2024
1 parent 9bab114 commit a91af13
Show file tree
Hide file tree
Showing 25 changed files with 1,146 additions and 351 deletions.
111 changes: 111 additions & 0 deletions tests/functional/builtins/codegen/test_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,114 @@ def bar(x: {type}):
"type": "function",
}
]


def test_exports_abi(make_input_bundle):
lib1 = """
@external
def foo():
pass
@external
def bar():
pass
"""

main = """
import lib1
initializes: lib1
exports: lib1.foo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
out = compile_code(main, input_bundle=input_bundle, output_formats=["abi"])

# just for clarity -- check bar() is not in the output
for fn in out["abi"]:
assert fn["name"] != "bar"

expected = [
{
"inputs": [],
"name": "foo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
}
]

assert out["abi"] == expected


def test_exports_variable(make_input_bundle):
lib1 = """
@external
def foo():
pass
private_storage_variable: uint256
private_immutable_variable: immutable(uint256)
private_constant_variable: constant(uint256) = 3
public_storage_variable: public(uint256)
public_immutable_variable: public(immutable(uint256))
public_constant_variable: public(constant(uint256)) = 10
@deploy
def __init__(a: uint256, b: uint256):
public_immutable_variable = a
private_immutable_variable = b
"""

main = """
import lib1
initializes: lib1
exports: (
lib1.foo,
lib1.public_storage_variable,
lib1.public_immutable_variable,
lib1.public_constant_variable,
)
@deploy
def __init__():
lib1.__init__(5, 6)
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
out = compile_code(main, input_bundle=input_bundle, output_formats=["abi"])
expected = [
{
"inputs": [],
"name": "foo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [],
"name": "public_storage_variable",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "public_immutable_variable",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "public_constant_variable",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{"inputs": [], "outputs": [], "stateMutability": "nonpayable", "type": "constructor"},
]

assert out["abi"] == expected
Empty file.
154 changes: 154 additions & 0 deletions tests/functional/codegen/modules/test_exports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import pytest


def test_simple_export(make_input_bundle, get_contract):
lib1 = """
@external
def foo() -> uint256:
return 5
"""
main = """
import lib1
exports: lib1.foo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 5


def test_export_with_state(make_input_bundle, get_contract):
lib1 = """
counter: uint256
@external
def foo() -> uint256:
return self.counter
"""
main = """
import lib1
initializes: lib1
exports: lib1.foo
@deploy
def __init__():
lib1.counter = 99
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 99


def test_variable_decl_exports(make_input_bundle, get_contract):
lib1 = """
counter: public(uint256)
FOO: public(immutable(uint256))
BAR: public(constant(uint256)) = 3
@deploy
def __init__():
self.counter = 1
FOO = 2
"""
main = """
import lib1
initializes: lib1
exports: (
lib1.counter,
lib1.FOO,
lib1.BAR,
)
@deploy
def __init__():
lib1.__init__()
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)

assert c.counter() == 1
assert c.FOO() == 2
assert c.BAR() == 3


def test_not_exported(make_input_bundle, get_contract):
# test that non-exported functions are not in the selector table
lib1 = """
@external
def foo() -> uint256:
return 100
@external
def bar() -> uint256:
return 101
"""
main = """
import lib1
exports: lib1.foo
@external
def __default__() -> uint256:
return 127
"""
caller_code = """
interface Foo:
def foo() -> uint256: nonpayable
def bar() -> uint256: nonpayable
@external
def call_bar(foo: Foo) -> uint256:
return foo.bar()
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
caller = get_contract(caller_code)

assert caller.call_bar(c.address) == 127 # default return value


def test_nested_export(make_input_bundle, get_contract):
lib1 = """
@external
def foo() -> uint256:
return 5
"""
lib2 = """
import lib1
"""
main = """
import lib2
exports: lib2.lib1.foo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 5


# not sure if this one should work
@pytest.mark.xfail(reason="ambiguous spec")
def test_recursive_export(make_input_bundle, get_contract):
lib1 = """
@external
def foo() -> uint256:
return 5
"""
lib2 = """
import lib1
exports: lib1.foo
"""
main = """
import lib2
exports: lib2.foo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 5
9 changes: 6 additions & 3 deletions tests/functional/codegen/test_call_graph_stability.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ def foo():
r = d.args[0].args[0].value
if isinstance(r, str) and r.startswith("internal"):
ir_funcs.append(r)
assert ir_funcs == [
f._ir_info.internal_function_label(is_ctor_context=False) for f in sigs.values()
]

expected = []
for f in foo_t.called_functions:
expected.append(f._ir_info.internal_function_label(is_ctor_context=False))

assert ir_funcs == expected
Empty file.
Loading

0 comments on commit a91af13

Please sign in to comment.