From 73f3fa81dc376d5f68e39045d5cb9977db454962 Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:55:21 +0800 Subject: [PATCH 1/9] remove code --- vyper/codegen/core.py | 26 -------------------- vyper/codegen/function_definitions/common.py | 5 ---- 2 files changed, 31 deletions(-) diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index e1d3ea12b4..3ecd4db697 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -1044,32 +1044,6 @@ def is_return_from_function(node): return False -# TODO this is almost certainly duplicated with check_terminus_node -# in vyper/semantics/analysis/local.py -def check_single_exit(fn_node): - _check_return_body(fn_node, fn_node.body) - for node in fn_node.get_descendants(vy_ast.If): - _check_return_body(node, node.body) - if node.orelse: - _check_return_body(node, node.orelse) - - -def _check_return_body(node, node_list): - return_count = len([n for n in node_list if is_return_from_function(n)]) - if return_count > 1: - raise StructureException( - "Too too many exit statements (return, raise or selfdestruct).", node - ) - # Check for invalid code after returns. - last_node_pos = len(node_list) - 1 - for idx, n in enumerate(node_list): - if is_return_from_function(n) and idx < last_node_pos: - # is not last statement in body. - raise StructureException( - "Exit statement with succeeding code (that will not execute).", node_list[idx + 1] - ) - - def mzero(dst, nbytes): # calldatacopy from past-the-end gives zero bytes. # cf. YP H.2 (ops section) with CALLDATACOPY spec. diff --git a/vyper/codegen/function_definitions/common.py b/vyper/codegen/function_definitions/common.py index 3fd5ce0b29..7216fc76f2 100644 --- a/vyper/codegen/function_definitions/common.py +++ b/vyper/codegen/function_definitions/common.py @@ -4,7 +4,6 @@ import vyper.ast as vy_ast from vyper.codegen.context import Constancy, Context -from vyper.codegen.core import check_single_exit from vyper.codegen.function_definitions.external_function import generate_ir_for_external_function from vyper.codegen.function_definitions.internal_function import generate_ir_for_internal_function from vyper.codegen.global_context import GlobalContext @@ -101,10 +100,6 @@ def generate_ir_for_function( # generate _FuncIRInfo func_t._ir_info = _FuncIRInfo(func_t) - # Validate return statements. - # XXX: This should really be in semantics pass. - check_single_exit(code) - callees = func_t.called_functions # we start our function frame from the largest callee frame From 2c76c947969d924fb53622c89c041d92289d3f9e Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:01:49 +0800 Subject: [PATCH 2/9] fix lint --- vyper/codegen/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index 3ecd4db697..e16d49972e 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -6,7 +6,7 @@ from vyper.compiler.settings import OptimizationLevel from vyper.evm.address_space import CALLDATA, DATA, IMMUTABLES, MEMORY, STORAGE, TRANSIENT from vyper.evm.opcodes import version_check -from vyper.exceptions import CompilerPanic, StructureException, TypeCheckFailure, TypeMismatch +from vyper.exceptions import CompilerPanic, TypeCheckFailure, TypeMismatch from vyper.semantics.types import ( AddressT, BoolT, From 55c7df39bb0c84f848c4228a819c02e13c8bf870 Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:39:17 +0800 Subject: [PATCH 3/9] handle exceptions; remove dead helper --- vyper/codegen/core.py | 12 ------------ vyper/codegen/stmt.py | 4 ++-- vyper/semantics/analysis/local.py | 23 +++++++++++++++++++++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index e16d49972e..1c9c0b671c 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -1,7 +1,6 @@ import contextlib from typing import Generator -from vyper import ast as vy_ast from vyper.codegen.ir_node import Encoding, IRnode from vyper.compiler.settings import OptimizationLevel from vyper.evm.address_space import CALLDATA, DATA, IMMUTABLES, MEMORY, STORAGE, TRANSIENT @@ -1033,17 +1032,6 @@ def eval_seq(ir_node): return None -def is_return_from_function(node): - if isinstance(node, vy_ast.Expr) and node.get("value.func.id") in ( - "raw_revert", - "selfdestruct", - ): - return True - if isinstance(node, (vy_ast.Return, vy_ast.Raise)): - return True - return False - - def mzero(dst, nbytes): # calldatacopy from past-the-end gives zero bytes. # cf. YP H.2 (ops section) with CALLDATACOPY spec. diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py index 86ea1813ea..e4493ac461 100644 --- a/vyper/codegen/stmt.py +++ b/vyper/codegen/stmt.py @@ -15,7 +15,6 @@ get_dyn_array_count, get_element_ptr, getpos, - is_return_from_function, make_byte_array_copier, make_setter, pop_dyn_array, @@ -25,6 +24,7 @@ from vyper.codegen.return_ import make_return_stmt from vyper.evm.address_space import MEMORY, STORAGE from vyper.exceptions import CompilerPanic, StructureException, TypeCheckFailure +from vyper.semantics.analysis.local import is_terminus_node from vyper.semantics.types import DArrayT, MemberFunctionT from vyper.semantics.types.shortcuts import INT256_T, UINT256_T @@ -425,7 +425,7 @@ def parse_stmt(stmt, context): def _is_terminated(code): last_stmt = code[-1] - if is_return_from_function(last_stmt): + if is_terminus_node(last_stmt): return True if isinstance(last_stmt, vy_ast.If): diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index c0c05325f2..ea17eccf29 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -65,7 +65,7 @@ def validate_functions(vy_module: vy_ast.Module) -> None: err_list.raise_if_not_empty() -def _is_terminus_node(node: vy_ast.VyperNode) -> bool: +def is_terminus_node(node: vy_ast.VyperNode) -> bool: if getattr(node, "_is_terminus", None): return True if isinstance(node, vy_ast.Expr) and isinstance(node.value, vy_ast.Call): @@ -76,8 +76,27 @@ def _is_terminus_node(node: vy_ast.VyperNode) -> bool: def check_for_terminus(node_list: list) -> bool: - if next((i for i in node_list if _is_terminus_node(i)), None): + terminus_nodes = [] + + # Check for invalid code after returns + last_node_pos = len(node_list) - 1 + for idx, n in enumerate(node_list): + if is_terminus_node(n): + terminus_nodes.append(n) + if idx < last_node_pos: + # is not last statement in body. + raise StructureException( + "Exit statement with succeeding code (that will not execute).", + node_list[idx + 1], + ) + + if len(terminus_nodes) > 1: + raise StructureException( + "Too many exit statements (return, raise or selfdestruct).", terminus_nodes[-1] + ) + elif len(terminus_nodes) == 1: return True + for node in [i for i in node_list if isinstance(i, vy_ast.If)][::-1]: if not node.orelse or not check_for_terminus(node.orelse): continue From 5f605133f0737590b0ab69e4247fa17de970849c Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:58:26 +0800 Subject: [PATCH 4/9] move is_terminus_node to analysis utils --- vyper/codegen/stmt.py | 2 +- vyper/semantics/analysis/local.py | 11 +---------- vyper/semantics/analysis/utils.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py index bcad8af4cd..b11fc1203d 100644 --- a/vyper/codegen/stmt.py +++ b/vyper/codegen/stmt.py @@ -24,7 +24,7 @@ from vyper.codegen.return_ import make_return_stmt from vyper.evm.address_space import MEMORY, STORAGE from vyper.exceptions import CompilerPanic, StructureException, TypeCheckFailure -from vyper.semantics.analysis.local import is_terminus_node +from vyper.semantics.analysis.utils import is_terminus_node from vyper.semantics.types import DArrayT, MemberFunctionT from vyper.semantics.types.shortcuts import INT256_T, UINT256_T diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index ba671f9666..18b13ff69e 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -26,6 +26,7 @@ get_exact_type_from_node, get_expr_info, get_possible_types_from_node, + is_terminus_node, validate_expected_type, ) from vyper.semantics.data_locations import DataLocation @@ -65,16 +66,6 @@ def validate_functions(vy_module: vy_ast.Module) -> None: err_list.raise_if_not_empty() -def is_terminus_node(node: vy_ast.VyperNode) -> bool: - if getattr(node, "_is_terminus", None): - return True - if isinstance(node, vy_ast.Expr) and isinstance(node.value, vy_ast.Call): - func = get_exact_type_from_node(node.value.func) - if getattr(func, "_is_terminus", None): - return True - return False - - def check_for_terminus(node_list: list) -> bool: terminus_nodes = [] diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 4f911764e0..a79e6ba5fe 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -498,6 +498,16 @@ def get_common_types(*nodes: vy_ast.VyperNode, filter_fn: Callable = None) -> Li return common_types +def is_terminus_node(node: vy_ast.VyperNode) -> bool: + if getattr(node, "_is_terminus", None): + return True + if isinstance(node, vy_ast.Expr) and isinstance(node.value, vy_ast.Call): + func = get_exact_type_from_node(node.value.func) + if getattr(func, "_is_terminus", None): + return True + return False + + # TODO push this into `ArrayT.validate_literal()` def _validate_literal_array(node, expected): # validate that every item within an array has the same type From 881bbb19d88686dc8377103cf2669b4593e9688f Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:24:11 +0800 Subject: [PATCH 5/9] make is_terminus property of ast node --- vyper/ast/nodes.py | 28 ++++++++++++++++++++++++---- vyper/codegen/stmt.py | 3 +-- vyper/semantics/analysis/local.py | 3 +-- vyper/semantics/analysis/utils.py | 10 ---------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index efab5117d4..a681572d7c 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -229,8 +229,6 @@ class VyperNode: Field names that, if present, must be set to None or a `SyntaxException` is raised. This attribute is used to exclude syntax that is valid in Python but not in Vyper. - _is_terminus : bool, optional - If `True`, indicates that execution halts upon reaching this node. _translated_fields : Dict, optional Field names that are reassigned if encountered. Used to normalize fields across different Python versions. @@ -380,6 +378,13 @@ def is_literal_value(self): """ return False + @property + def is_terminus(self): + """ + Check if execution halts upon reaching this node. + """ + return False + @property def has_folded_value(self): """ @@ -717,7 +722,10 @@ class Stmt(VyperNode): class Return(Stmt): __slots__ = ("value",) - _is_terminus = True + + @property + def is_terminus(self): + return True class Expr(Stmt): @@ -1302,6 +1310,15 @@ def _op(self, left, right): class Call(ExprNode): __slots__ = ("func", "args", "keywords") + @property + def is_terminus(self): + # cursed import cycle! + from vyper.builtins.functions import DISPATCH_TABLE + + func_name = self.func.id + builtin_t = DISPATCH_TABLE[func_name] + return getattr(builtin_t, "_is_terminus", False) + # try checking if this is a builtin, which is foldable def _try_fold(self): if not isinstance(self.func, Name): @@ -1483,7 +1500,10 @@ class AugAssign(Stmt): class Raise(Stmt): __slots__ = ("exc",) _only_empty_fields = ("cause",) - _is_terminus = True + + @property + def is_terminus(self): + return True class Assert(Stmt): diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py index f889068c4a..475ffe3cfc 100644 --- a/vyper/codegen/stmt.py +++ b/vyper/codegen/stmt.py @@ -30,7 +30,6 @@ TypeCheckFailure, tag_exceptions, ) -from vyper.semantics.analysis.utils import is_terminus_node from vyper.semantics.types import DArrayT, MemberFunctionT from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.shortcuts import INT256_T, UINT256_T @@ -406,7 +405,7 @@ def parse_stmt(stmt, context): def _is_terminated(code): last_stmt = code[-1] - if is_terminus_node(last_stmt): + if last_stmt.is_terminus: return True if isinstance(last_stmt, vy_ast.If): diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index e810c0fccb..5b84e70860 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -25,7 +25,6 @@ get_exact_type_from_node, get_expr_info, get_possible_types_from_node, - is_terminus_node, validate_expected_type, ) from vyper.semantics.data_locations import DataLocation @@ -76,7 +75,7 @@ def check_for_terminus(node_list: list) -> bool: # Check for invalid code after returns last_node_pos = len(node_list) - 1 for idx, n in enumerate(node_list): - if is_terminus_node(n): + if n.is_terminus: terminus_nodes.append(n) if idx < last_node_pos: # is not last statement in body. diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 362cb4f4b8..ba1b02b8d6 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -514,16 +514,6 @@ def get_common_types(*nodes: vy_ast.VyperNode, filter_fn: Callable = None) -> Li return common_types -def is_terminus_node(node: vy_ast.VyperNode) -> bool: - if getattr(node, "_is_terminus", None): - return True - if isinstance(node, vy_ast.Expr) and isinstance(node.value, vy_ast.Call): - func = get_exact_type_from_node(node.value.func) - if getattr(func, "_is_terminus", None): - return True - return False - - # TODO push this into `ArrayT.validate_literal()` def _validate_literal_array(node, expected): # validate that every item within an array has the same type From 35bdbb45d20eabb6e99052e04bfe4926a7724fa9 Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:03:38 +0800 Subject: [PATCH 6/9] fix expr and call --- vyper/ast/nodes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index a681572d7c..0bd5e4d23a 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -731,6 +731,10 @@ def is_terminus(self): class Expr(Stmt): __slots__ = ("value",) + @property + def is_terminus(self): + return self.value.is_terminus + class Log(Stmt): __slots__ = ("value",) @@ -1313,10 +1317,10 @@ class Call(ExprNode): @property def is_terminus(self): # cursed import cycle! - from vyper.builtins.functions import DISPATCH_TABLE + from vyper.builtins.functions import get_builtin_functions func_name = self.func.id - builtin_t = DISPATCH_TABLE[func_name] + builtin_t = get_builtin_functions().get(func_name) return getattr(builtin_t, "_is_terminus", False) # try checking if this is a builtin, which is foldable From fec23e7f2872fb9acf9da7f926a35787ad24f05e Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:03:44 +0800 Subject: [PATCH 7/9] remove invalid test --- tests/functional/codegen/features/test_assert.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/functional/codegen/features/test_assert.py b/tests/functional/codegen/features/test_assert.py index af189e6dca..244f820537 100644 --- a/tests/functional/codegen/features/test_assert.py +++ b/tests/functional/codegen/features/test_assert.py @@ -107,14 +107,6 @@ def test(): assert self.ret1() == 1 """, """ -@internal -def valid_address(sender: address) -> bool: - selfdestruct(sender) -@external -def test(): - assert self.valid_address(msg.sender) - """, - """ @external def test(): assert raw_call(msg.sender, b'', max_outsize=1, gas=10, value=1000*1000) == b'' From 25bd14a559d08e0c2e2d4ad43758618f2ada041f Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:28:15 +0800 Subject: [PATCH 8/9] fix call name --- vyper/ast/nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index 0bd5e4d23a..82afcb9217 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -1319,7 +1319,10 @@ def is_terminus(self): # cursed import cycle! from vyper.builtins.functions import get_builtin_functions - func_name = self.func.id + func_name = self.func.get("id") + if not func_name: + return False + builtin_t = get_builtin_functions().get(func_name) return getattr(builtin_t, "_is_terminus", False) From 7b2c74bedde4894583e431d5605ea9cb73e2112d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 14 Jan 2024 12:23:10 -0500 Subject: [PATCH 9/9] rewrite function termination detection fix dead code detection fix detection of returns after valid branching returns add tests --- .../codegen/features/test_conditionals.py | 1 - .../syntax/test_unbalanced_return.py | 43 ++++++++++---- vyper/semantics/analysis/local.py | 57 +++++++++---------- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/tests/functional/codegen/features/test_conditionals.py b/tests/functional/codegen/features/test_conditionals.py index 15ccc40bdf..3b0e57eeca 100644 --- a/tests/functional/codegen/features/test_conditionals.py +++ b/tests/functional/codegen/features/test_conditionals.py @@ -7,7 +7,6 @@ def foo(i: bool) -> int128: else: assert 2 != 0 return 7 - return 11 """ c = get_contract_with_gas_estimation(conditional_return_code) diff --git a/tests/functional/syntax/test_unbalanced_return.py b/tests/functional/syntax/test_unbalanced_return.py index d1d9732777..d5754f0053 100644 --- a/tests/functional/syntax/test_unbalanced_return.py +++ b/tests/functional/syntax/test_unbalanced_return.py @@ -8,7 +8,7 @@ """ @external def foo() -> int128: - pass + pass # missing return """, FunctionDeclarationException, ), @@ -18,6 +18,7 @@ def foo() -> int128: def foo() -> int128: if False: return 123 + # missing return """, FunctionDeclarationException, ), @@ -27,19 +28,19 @@ def foo() -> int128: def test() -> int128: if 1 == 1 : return 1 - if True: + if True: # unreachable return 0 else: assert msg.sender != msg.sender """, - FunctionDeclarationException, + StructureException, ), ( """ @internal def valid_address(sender: address) -> bool: selfdestruct(sender) - return True + return True # unreachable """, StructureException, ), @@ -48,7 +49,7 @@ def valid_address(sender: address) -> bool: @internal def valid_address(sender: address) -> bool: selfdestruct(sender) - a: address = sender + a: address = sender # unreachable """, StructureException, ), @@ -58,7 +59,7 @@ def valid_address(sender: address) -> bool: def valid_address(sender: address) -> bool: if sender == empty(address): selfdestruct(sender) - _sender: address = sender + _sender: address = sender # unreachable else: return False """, @@ -69,7 +70,7 @@ def valid_address(sender: address) -> bool: @internal def foo() -> bool: raw_revert(b"vyper") - return True + return True # unreachable """, StructureException, ), @@ -78,7 +79,7 @@ def foo() -> bool: @internal def foo() -> bool: raw_revert(b"vyper") - x: uint256 = 3 + x: uint256 = 3 # unreachable """, StructureException, ), @@ -88,12 +89,35 @@ def foo() -> bool: def foo(x: uint256) -> bool: if x == 2: raw_revert(b"vyper") - a: uint256 = 3 + a: uint256 = 3 # unreachable else: return False """, StructureException, ), + ( + """ +@internal +def foo(): + return + return # unreachable + """, + StructureException, + ), + ( + """ +@internal +def foo() -> uint256: + if block.number % 2 == 0: + return 5 + elif block.number % 3 == 0: + return 6 + else: + return 10 + return 0 # unreachable + """, + StructureException, + ), ] @@ -154,7 +178,6 @@ def test() -> int128: else: x = keccak256(x) return 1 - return 1 """, """ @external diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index 5b84e70860..00804bfec9 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -69,35 +69,28 @@ def validate_functions(vy_module: vy_ast.Module) -> None: err_list.raise_if_not_empty() -def check_for_terminus(node_list: list) -> bool: - terminus_nodes = [] - - # Check for invalid code after returns - last_node_pos = len(node_list) - 1 - for idx, n in enumerate(node_list): - if n.is_terminus: - terminus_nodes.append(n) - if idx < last_node_pos: - # is not last statement in body. - raise StructureException( - "Exit statement with succeeding code (that will not execute).", - node_list[idx + 1], - ) +# finds the terminus node for a list of nodes. +# raises an exception if any nodes are unreachable +def find_terminating_node(node_list: list) -> Optional[vy_ast.VyperNode]: + ret = None - if len(terminus_nodes) > 1: - raise StructureException( - "Too many exit statements (return, raise or selfdestruct).", terminus_nodes[-1] - ) - elif len(terminus_nodes) == 1: - return True + for node in node_list: + if ret is not None: + raise StructureException("Unreachable code!", node) + if node.is_terminus: + ret = node - for node in [i for i in node_list if isinstance(i, vy_ast.If)][::-1]: - if not node.orelse or not check_for_terminus(node.orelse): - continue - if not check_for_terminus(node.body): - continue - return True - return False + if isinstance(node, vy_ast.If): + body_terminates = find_terminating_node(node.body) + + else_terminates = None + if node.orelse is not None: + else_terminates = find_terminating_node(node.orelse) + + if body_terminates is not None and else_terminates is not None: + ret = else_terminates + + return ret def _check_iterator_modification( @@ -213,11 +206,13 @@ def analyze(self): self.visit(node) if self.func.return_type: - if not check_for_terminus(self.fn_node.body): + if not find_terminating_node(self.fn_node.body): raise FunctionDeclarationException( - f"Missing or unmatched return statements in function '{self.fn_node.name}'", - self.fn_node, + f"Missing return statement in function '{self.fn_node.name}'", self.fn_node ) + else: + # call find_terminator for its unreachable code detection side effect + find_terminating_node(self.fn_node.body) # visit default args assert self.func.n_keyword_args == len(self.fn_node.args.defaults) @@ -519,7 +514,7 @@ def visit_Return(self, node): raise FunctionDeclarationException("Return statement is missing a value", node) return elif self.func.return_type is None: - raise FunctionDeclarationException("Function does not return any values", node) + raise FunctionDeclarationException("Function should not return any values", node) if isinstance(values, vy_ast.Tuple): values = values.elements