Skip to content

Commit

Permalink
feat: implement export bundles
Browse files Browse the repository at this point in the history
export bundles are a way for library authors to group together external
functions in the ABI

example:
```vyper
bundle: IERC_X  # declare a bundle

@internal
def foo() -> uint256:
    return 10

@external
def extern1():
    pass

@external
@Bundle(IERC_X)
def extern2():
    pass

@external
@Bundle(IERC_X, bind=False)  # extern3 ok to export on its own
def extern3():
    pass

@external
@Bundle(IERC_X, bind=True)  # extern4 cannot be exported on its own!
def extern4():
    pass
```

it can be used like this:
```vyper
import library

exports: library.IERC_X

@external
def library_foo() -> uint256:
    return library.foo()
```

or like this:
```vyper
import library

exports: library.extern1, library.extern2

@external
def library_foo() -> uint256:
    return library.foo()
```

but not like this(!):

```vyper
import library

exports: library.extern4  # raises StructureException

@external
def library_foo() -> uint256:
    return library.foo()
```
  • Loading branch information
charles-cooper committed Dec 13, 2023
1 parent 8188d36 commit 6d6ab8b
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 59 deletions.
41 changes: 28 additions & 13 deletions vyper/ast/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@ def get_node(
ast_struct["ast_type"] = "ImplementsDecl"

# Replace `exports` AnnAssign nodes with `ExportsDecl`
if getattr(ast_struct["target"], "id", None) == "exports":
elif getattr(ast_struct["target"], "id", None) == "exports":
if ast_struct["value"] is not None:
_raise_syntax_exc("`exports` cannot have a value assigned", ast_struct["value"])
ast_struct["ast_type"] = "ExportsDecl"

# Replace `bundle` AnnAssign nodes with `BundleDecl`
elif getattr(ast_struct["target"], "id", None) == "bundle":
if ast_struct["value"] is not None:
_raise_syntax_exc("`exports` cannot have a value assigned", ast_struct["value"])
ast_struct["ast_type"] = "BundleDecl"

else:
ast_struct["ast_type"] = "VariableDecl"

Expand Down Expand Up @@ -907,6 +913,7 @@ def validate(self):
raise InvalidLiteral("Cannot have an empty tuple", self)


# bool (in python ast, can also be `None`)
class NameConstant(Constant):
__slots__ = ()

Expand Down Expand Up @@ -1268,7 +1275,7 @@ def _op(self, left, right):


class Call(ExprNode):
__slots__ = ("func", "args", "keywords", "keyword")
__slots__ = ("func", "args", "keywords")


class keyword(VyperNode):
Expand Down Expand Up @@ -1445,17 +1452,13 @@ class ImplementsDecl(Stmt):
"""
An `implements` declaration.
Excludes `simple` and `value` attributes from Python `AnnAssign` node.
Attributes
----------
target : Name
Name node for the `implements` keyword
annotation : Name
annotation: Name
Name node for the interface to be implemented
"""

__slots__ = ("target", "annotation")
__slots__ = ("annotation",)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -1468,17 +1471,29 @@ class ExportsDecl(Stmt):
"""
An `exports` declaration.
Excludes `simple` and `value` attributes from Python `AnnAssign` node.
Attributes
----------
annotation: Attribute | Tuple
Attribute | Tuple of functions to be exported
"""

__slots__ = ("annotation",)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class BundleDecl(Stmt):
"""
A `bundle` declaration.
Attributes
----------
target : Name
Name node for the `implements` keyword
annotation : Name
Name node for the interface to be implemented
Name of the bundle
"""

__slots__ = ("target", "annotation")
__slots__ = ("annotation",)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
5 changes: 4 additions & 1 deletion vyper/ast/nodes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ class ImplementsDecl(VyperNode):

class ExportsDecl(VyperNode):
target: Name = ...
annotation: Name = ...
annotation: Attribute = ...

class BundleDecl(VyperNode):
target: Name = ...

class If(VyperNode):
body: list = ...
Expand Down
54 changes: 45 additions & 9 deletions vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

import vyper.builtins.interfaces
from vyper import ast as vy_ast
from vyper.ast.identifiers import validate_identifier
from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, InputBundle
from vyper.evm.opcodes import version_check
from vyper.exceptions import (
CallViolation,
CompilerPanic,
DuplicateImport,
ExceptionList,
InvalidLiteral,
Expand All @@ -31,7 +33,7 @@
)
from vyper.semantics.data_locations import DataLocation
from vyper.semantics.namespace import Namespace, get_namespace, override_global_namespace
from vyper.semantics.types import EnumT, EventT, InterfaceT, StructT
from vyper.semantics.types import BundleT, EnumT, EventT, InterfaceT, StructT
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 @@ -227,18 +229,49 @@ def visit_ExportsDecl(self, node):

funcs = []
for export in exports:
func_t = get_exact_type_from_node(export)
export_t = get_exact_type_from_node(export)

# export a single function
if isinstance(export_t, ContractFunctionT):
if not export_t.is_external:
raise StructureException("{export_t.name} must be external!", export)

for bundle_info in export_t.bundles:
if bundle_info.is_bound:
bundle_name = bundle_info.bundle_t.name
raise StructureException(
f"Cannot export {export_t.name} on its own - "
f"it is bound to {bundle_name}!",
export,
)

funcs.append(export_t)

# export a bundle
elif isinstance(export_t, BundleT):
# the bundle already has .functions populated from when
# the module it came from was analyzed it.
assert len(export_t.functions) > 0 # sanity check
for fn_t in export_t.functions:
assert fn_t.is_external # sanity check
funcs.append(fn_t)

else: # pragma: nocover
raise CompilerPanic("unreachable", export)

if not isinstance(func_t, ContractFunctionT):
raise StructureException("Not a function!", export)
# tag the entire ExportDecl with the exported functions
node._metadata["exported_functions"] = funcs

if not func_t.is_external:
raise StructureException("{func_t.name} must be external!", export)
def visit_BundleDecl(self, node):
if not isinstance(node.annotation, vy_ast.Name):
raise StructureException("Invalid bundle name!", node.annotation)

funcs.append(func_t)
bundle_name = node.annotation.id
validate_identifier(bundle_name)

# tag the entire ExportDecl with the exported functions
node._metadata["exported_functions"] = funcs
node._metadata["bundle_type"] = BundleT(bundle_name)

self.namespace[bundle_name] = node

def visit_VariableDecl(self, node):
name = node.get("target.id")
Expand Down Expand Up @@ -367,6 +400,9 @@ def visit_FunctionDef(self, node):
else:
func_t = ContractFunctionT.from_FunctionDef(node)

for bundle_info in func_t.bundles:
bundle_info.bundle_t.functions.append(func_t)

self.namespace["self"].typ.add_member(func_t.name, func_t)
node._metadata["func_type"] = func_t

Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .module import InterfaceT
from .primitives import AddressT, BoolT, BytesM_T, DecimalT, IntegerT
from .subscriptable import DArrayT, HashMapT, SArrayT, TupleT
from .user import EnumT, EventT, StructT
from .user import BundleT, EnumT, EventT, StructT


def _get_primitive_types():
Expand Down
8 changes: 5 additions & 3 deletions vyper/semantics/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,12 @@ def get_subscripted_type(self, node: vy_ast.Index) -> None:
"""
raise StructureException(f"'{self}' cannot be indexed into", node)

def add_member(self, name: str, type_: "VyperType") -> None:
def add_member(
self, name: str, type_: "VyperType", source: Optional[vy_ast.VyperNode] = None
) -> None:
validate_identifier(name)
if name in self.members:
raise NamespaceCollision(f"Member '{name}' already exists in {self}")
raise NamespaceCollision(f"Member '{name}' already exists in {self}", source)
self.members[name] = type_

def get_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType":
Expand All @@ -308,7 +310,7 @@ def get_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType":
raise UnknownAttribute(f"{self} has no member '{key}'. {suggestions_str}", node)

def __repr__(self):
return self._id
return getattr(self, "_id", str(type(self)))


class KwargSettings:
Expand Down
Loading

0 comments on commit 6d6ab8b

Please sign in to comment.