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: allow functions to be named after builtins and reserved keywords #3307

Merged
merged 23 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
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
18 changes: 18 additions & 0 deletions tests/parser/syntax/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,24 @@ def kick(): payable
kickers: HashMap[address, MyInterface]
""",
"""
interface Foo:
def append(a: uint256): payable

@external
def bar(x: address):
a: Foo = Foo(x)
a.append(1)
""",
"""
interface Foo:
def pop(): payable

@external
def foo(x: address):
a: Foo = Foo(x)
a.pop()
""",
"""
interface ITestInterface:
def foo() -> uint256: view

Expand Down
64 changes: 39 additions & 25 deletions tests/parser/syntax/utils/test_function_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,13 @@
from vyper import compiler
from vyper.exceptions import NamespaceCollision, StructureException

fail_list = [ # noqa: E122
fail_list = [
"""
@external
def ő1qwerty(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def int128(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def decimal(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def wei(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def false(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
]


Expand Down Expand Up @@ -63,6 +39,44 @@ def first1(i: int128) -> int128:
_var123 : int128 = i
return _var123
""",
"""
@external
def int128(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def decimal(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def false(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def wei(i: int128) -> int128:
temp_var : int128 = i
return temp_var
""",
"""
@external
def floor():
pass
""",
"""
@internal
def append():
pass

@external
def foo():
self.append()
""",
]


Expand Down
26 changes: 18 additions & 8 deletions tests/parser/types/test_identifier_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from vyper.builtins.functions import BUILTIN_FUNCTIONS
from vyper.codegen.expr import ENVIRONMENT_VARIABLES
from vyper.exceptions import NamespaceCollision, StructureException, SyntaxException
from vyper.semantics.namespace import RESERVED_KEYWORDS
from vyper.utils import FUNCTION_WHITELIST
from vyper.semantics.namespace import PYTHON_KEYWORDS, RESERVED_KEYWORDS
from vyper.semantics.types.primitives import AddressT

ALL_RESERVED_KEYWORDS = (
set(BUILTIN_CONSTANTS.keys())
Expand Down Expand Up @@ -47,16 +47,26 @@ def test({constant}: int128):
)


RESERVED_KEYWORDS_NOT_WHITELISTED = sorted(ALL_RESERVED_KEYWORDS.difference(FUNCTION_WHITELIST))
SELF_NAMESPACE_MEMBERS = set(AddressT._type_members.keys())
DISALLOWED_FN_NAMES = SELF_NAMESPACE_MEMBERS.union(PYTHON_KEYWORDS)
ALLOWED_FN_NAMES = ALL_RESERVED_KEYWORDS - DISALLOWED_FN_NAMES


@pytest.mark.parametrize("constant", sorted(RESERVED_KEYWORDS_NOT_WHITELISTED))
def test_reserved_keywords_fns(constant, get_contract, assert_compile_failed):
@pytest.mark.parametrize("constant", sorted(ALLOWED_FN_NAMES))
def test_reserved_keywords_fns_pass(constant, get_contract, assert_compile_failed):
code = f"""
@external
def {constant}(var: int128):
pass
"""
assert_compile_failed(
lambda: get_contract(code), (SyntaxException, StructureException, NamespaceCollision)
)
assert get_contract(code) is not None


@pytest.mark.parametrize("constant", sorted(DISALLOWED_FN_NAMES))
def test_reserved_keywords_fns_fail(constant, get_contract, assert_compile_failed):
code = f"""
@external
def {constant}(var: int128):
pass
"""
assert_compile_failed(lambda: get_contract(code), (SyntaxException, NamespaceCollision))
39 changes: 20 additions & 19 deletions vyper/codegen/stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from vyper.codegen.expr import Expr
from vyper.codegen.return_ import make_return_stmt
from vyper.exceptions import CompilerPanic, StructureException, TypeCheckFailure
from vyper.semantics.types import DArrayT
from vyper.semantics.types import DArrayT, MemberFunctionT
from vyper.semantics.types.shortcuts import INT256_T, UINT256_T


Expand Down Expand Up @@ -123,24 +123,25 @@ def parse_Call(self):
"append",
"pop",
):
# TODO: consider moving this to builtins
darray = Expr(self.stmt.func.value, self.context).ir_node
args = [Expr(x, self.context).ir_node for x in self.stmt.args]
if self.stmt.func.attr == "append":
# sanity checks
assert len(args) == 1
arg = args[0]
assert isinstance(darray.typ, DArrayT)
check_assign(
dummy_node_for_type(darray.typ.value_type), dummy_node_for_type(arg.typ)
)

return append_dyn_array(darray, arg)
else:
assert len(args) == 0
return pop_dyn_array(darray, return_popped_item=False)

elif is_self_function:
func_type = self.stmt.func._metadata["type"]
if isinstance(func_type, MemberFunctionT):
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
darray = Expr(self.stmt.func.value, self.context).ir_node
args = [Expr(x, self.context).ir_node for x in self.stmt.args]
if self.stmt.func.attr == "append":
# sanity checks
assert len(args) == 1
arg = args[0]
assert isinstance(darray.typ, DArrayT)
check_assign(
dummy_node_for_type(darray.typ.value_type), dummy_node_for_type(arg.typ)
)

return append_dyn_array(darray, arg)
else:
assert len(args) == 0
return pop_dyn_array(darray, return_popped_item=False)

if is_self_function:
return self_call.ir_for_self_call(self.stmt, self.context)
else:
return external_call.ir_for_external_call(self.stmt, self.context)
Expand Down
1 change: 0 additions & 1 deletion vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ def visit_FunctionDef(self, node):
func = ContractFunctionT.from_FunctionDef(node)

try:
# TODO sketchy elision of namespace validation
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
self.namespace["self"].typ.add_member(func.name, func, skip_namespace_validation=True)
node._metadata["type"] = func
except VyperException as exc:
Expand Down
30 changes: 14 additions & 16 deletions vyper/semantics/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def clear(self):

def validate_assignment(self, attr):
validate_identifier(attr)
validate_namespace_availability(attr)
if attr in self:
obj = super().__getitem__(attr)
raise NamespaceCollision(f"'{attr}' has already been declared as a {obj}")
Expand Down Expand Up @@ -119,16 +120,23 @@ def override_global_namespace(ns):
_namespace = tmp


def validate_identifier(attr):
def validate_namespace_availability(ident):
namespace = get_namespace()
if attr in namespace and attr not in [x for i in namespace._scopes for x in i]:
raise NamespaceCollision(f"Cannot assign to '{attr}', it is a builtin")
if attr.lower() in RESERVED_KEYWORDS or attr.upper() in OPCODES:
raise StructureException(f"'{attr}' is a reserved keyword")
if ident in namespace and ident not in [x for i in namespace._scopes for x in i]:
raise NamespaceCollision(f"Cannot assign to '{ident}', it is a builtin")
if ident.lower() in RESERVED_KEYWORDS or ident.upper() in OPCODES:
raise StructureException(f"'{ident}' is a reserved keyword")


def validate_identifier(attr):
if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
raise StructureException(f"'{attr}' contains invalid character(s)")


# Reserved python keywords
PYTHON_KEYWORDS = {"if", "for", "while", "pass", "def", "assert", "continue", "raise"}


# Cannot be used for variable or member naming
RESERVED_KEYWORDS = {
# decorators
Expand All @@ -145,18 +153,9 @@ def validate_identifier(attr):
"struct",
"event",
"enum",
# control flow
"if",
"for",
"while",
"until",
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
"pass",
"def",
# EVM operations
"send",
"selfdestruct",
"assert",
"raise",
"throw",
"unreachable",
# special functions (no name mangling)
Expand All @@ -178,7 +177,6 @@ def validate_identifier(attr):
"false",
# more control flow and special operations
"this",
"continue",
"range",
# None sentinal value
"none",
Expand Down Expand Up @@ -215,4 +213,4 @@ def validate_identifier(attr):
"min_decimal",
"max_uint256",
"zero_wei",
}
} | PYTHON_KEYWORDS
1 change: 1 addition & 0 deletions vyper/semantics/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import primitives, subscriptable, user
from .base import TYPE_T, KwargSettings, VyperType, is_type_t
from .bytestrings import BytesT, StringT, _BytestringT
from .function import MemberFunctionT
from .primitives import AddressT, BoolT, BytesM_T, DecimalT, IntegerT
from .subscriptable import DArrayT, HashMapT, SArrayT, TupleT
from .user import EnumT, EventT, InterfaceT, StructT
Expand Down
11 changes: 7 additions & 4 deletions vyper/semantics/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
UnknownAttribute,
)
from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
from vyper.semantics.namespace import validate_identifier
from vyper.semantics.namespace import validate_identifier, validate_namespace_availability
Fixed Show fixed Hide fixed


# Some fake type with an overridden `compare_type` which accepts any RHS
Expand Down Expand Up @@ -57,7 +57,9 @@ class VyperType:

size_in_bytes = 32 # default; override for larger types

def __init__(self, members: Optional[Dict] = None) -> None:
def __init__(
self, members: Optional[Dict] = None, skip_namespace_validation: bool = False
) -> None:
self.members: Dict = {}

# add members that are on the class instance.
Expand All @@ -69,7 +71,7 @@ def __init__(self, members: Optional[Dict] = None) -> None:

members = members or {}
for k, v in members.items():
self.add_member(k, v)
self.add_member(k, v, skip_namespace_validation=skip_namespace_validation)

def _get_equality_attrs(self):
return tuple(getattr(self, attr) for attr in self._equality_attrs)
Expand Down Expand Up @@ -283,7 +285,8 @@ def add_member(
# skip_namespace_validation provides a way of bypassing validate_identifier, which
# introduces a dependency cycle with the builtin_functions module
if not skip_namespace_validation:
validate_identifier(name)
validate_namespace_availability(name)
validate_identifier(name)
if name in self.members:
raise NamespaceCollision(f"Member '{name}' already exists in {self}")
self.members[name] = type_
Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class InterfaceT(_UserType):

def __init__(self, _id: str, members: dict, events: dict) -> None:
validate_unique_method_ids(list(members.values())) # explicit list cast for mypy
super().__init__(members)
super().__init__(members, skip_namespace_validation=True)

self._id = _id
self.events = events
Expand Down
3 changes: 0 additions & 3 deletions vyper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,6 @@ class SizeLimits:
MAX_UINT256 = 2**256 - 1


# Otherwise reserved words that are whitelisted for function declarations
FUNCTION_WHITELIST = {"send"}

# List of valid IR macros.
# TODO move this somewhere else, like ir_node.py
VALID_IR_MACROS = {
Expand Down