Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
22 changes: 11 additions & 11 deletions tests/functional/builtins/codegen/test_create_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,10 +1145,13 @@ def deploy_from_calldata(s: Bytes[1024], arg: uint256, salt: bytes32) -> address
assert env.get_code(res) == runtime


# evaluation of the value kwarg changes the value of the salt kwarg
# value kwarg comes after the salt kwarg in the source code
@pytest.mark.xfail(raises=AssertionError, reason="salt kwarg is evaluated after value kwarg")
def test_raw_create_order_of_eval_of_kwargs(get_contract, env, create2_address_of, keccak):
# value is evaluated after salt because that is the source order.
Comment thread
banteg marked this conversation as resolved.
def test_raw_create_order_of_eval_of_kwargs(
get_contract, env, create2_address_of, keccak, experimental_codegen
):
if not experimental_codegen:
pytest.xfail("legacy codegen does not preserve this source order")

to_deploy_code = """
foo: public(uint256)

Expand All @@ -1162,8 +1165,6 @@ def __init__(arg: uint256):
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

# the implementation of `raw_create` firstly caches
# `value` and then `salt`, here the source order is `salt` then `value`
deployer_code = """
c: Bytes[1024]
salt: bytes32
Expand Down Expand Up @@ -1194,11 +1195,10 @@ def deploy_from_calldata(s: Bytes[1024], arg: uint256, salt: bytes32, value_: ui


# test vararg and kwarg order of evaluation
# test fails because `value` gets evaluated
# before the 1st vararg which doesn't follow
# source code order
@pytest.mark.xfail(raises=AssertionError)
def test_raw_create_eval_order(get_contract):
def test_raw_create_eval_order(get_contract, experimental_codegen):
if not experimental_codegen:
pytest.xfail("legacy codegen does not preserve this source order")

code = """
a: public(uint256)

Expand Down
47 changes: 47 additions & 0 deletions tests/functional/builtins/codegen/test_raw_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,53 @@ def foo() -> String[32]:
assert c.foo() == "goo"


def test_raw_call_runtime_kwargs_source_order(get_contract, experimental_codegen):
if not experimental_codegen:
pytest.xfail("legacy codegen does not preserve this source order")

code = """
canary: public(uint256)

@internal
def set_gas(v: uint256) -> uint256:
self.canary = v
return 50000

@internal
def set_value(v: uint256) -> uint256:
self.canary = v
return 0

@external
def value_then_gas() -> uint256:
success: bool = raw_call(
0x0000000000000000000000000000000000000004,
b"",
max_outsize=0,
value=self.set_value(1),
gas=self.set_gas(2),
revert_on_failure=False,
)
return self.canary

@external
def gas_then_value() -> uint256:
success: bool = raw_call(
0x0000000000000000000000000000000000000004,
b"",
max_outsize=0,
gas=self.set_gas(1),
value=self.set_value(2),
revert_on_failure=False,
)
return self.canary
"""
c = get_contract(code)

assert c.value_then_gas() == 2
assert c.gas_then_value() == 2


def test_raw_call_clean_mem_kwargs_value(get_contract, env):
# test msize uses clean memory and does not get overwritten by
# any raw_call() kwargs
Expand Down
93 changes: 84 additions & 9 deletions tests/unit/compiler/venom/test_builtin_kwargs.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,101 @@
import pytest

from vyper import ast as vy_ast
from vyper.codegen_venom.builtins.abi import _get_bool_kwarg as get_abi_bool_kwarg
from vyper.codegen_venom.builtins.misc import _get_bool_kwarg as get_misc_bool_kwarg
from vyper.codegen_venom.builtins._kwargs import (
get_bool_kwarg,
get_kwarg_ast_constants,
get_kwarg_values,
get_literal_kwarg,
kwarg_is_provided,
validate_kwargs,
)
from vyper.exceptions import CompilerPanic


def _call_node(source):
return vy_ast.parse_to_ast(source).body[0].value


@pytest.mark.parametrize("get_bool_kwarg", [get_abi_bool_kwarg, get_misc_bool_kwarg])
def test_bool_kwarg_uses_reduced_value(get_bool_kwarg):
def test_kwarg_ast_constants_return_folded_nodes():
call_node = _call_node("foo(flag=FLAG)")
call_node.keywords[0].value._set_folded_value(vy_ast.NameConstant(value=False))

assert get_bool_kwarg(call_node, "flag", True) is False
constants = get_kwarg_ast_constants(call_node, ("flag",))

assert constants["flag"].value is False

@pytest.mark.parametrize("get_bool_kwarg", [get_abi_bool_kwarg, get_misc_bool_kwarg])
def test_bool_kwarg_rejects_unreduced_value(get_bool_kwarg):

def test_kwarg_ast_constants_reject_unfolded_values():
call_node = _call_node("foo(flag=FLAG)")

with pytest.raises(CompilerPanic, match="unfoldable constant kwarg: flag"):
get_kwarg_ast_constants(call_node, ("flag",))


def test_kwarg_helpers_reject_unexpected_kwargs():
call_node = _call_node("foo(flag=False)")

with pytest.raises(CompilerPanic, match="unexpected kwarg: flag"):
validate_kwargs(call_node, ("other",))


def test_kwarg_helpers_reject_duplicate_kwargs():
call_node = _call_node("foo(flag=False, flag=True)")

with pytest.raises(CompilerPanic, match="duplicate kwarg: flag"):
validate_kwargs(call_node, ("flag",))


def test_kwarg_is_provided():
call_node = _call_node("foo(flag=False)")

assert kwarg_is_provided(call_node, "flag") is True
assert kwarg_is_provided(call_node, "other") is False


def test_bool_kwarg_uses_reduced_value():
call_node = _call_node("foo(flag=FLAG)")
call_node.keywords[0].value._set_folded_value(vy_ast.NameConstant(value=False))
constants = get_kwarg_ast_constants(call_node, ("flag",))

assert get_bool_kwarg(constants, "flag", True) is False


def test_kwarg_constants_fill_defaults():
call_node = _call_node("foo(flag=False)")

constants = get_kwarg_ast_constants(call_node, {"flag": True, "limit": 3, "salt": None})

assert get_bool_kwarg(constants, "flag") is False
assert get_literal_kwarg(constants, "limit") == 3
assert get_literal_kwarg(constants, "salt") is None


def test_kwarg_values_fill_late_defaults():
call_node = _call_node("foo()")

values = get_kwarg_values(call_node, object(), {"gas": lambda: "gas-left", "value": 0})

assert values == {"gas": "gas-left", "value": 0}


def test_bool_kwarg_rejects_unreduced_value():
call_node = _call_node("foo(flag=FLAG)")

with pytest.raises(CompilerPanic, match="unfoldable boolean kwarg: flag"):
get_bool_kwarg(call_node, "flag", True)
with pytest.raises(CompilerPanic, match="unfoldable constant kwarg: flag"):
get_kwarg_ast_constants(call_node, ("flag",))


def test_literal_kwarg_uses_reduced_value():
call_node = _call_node("foo(revert_on_failure=REVERT)")
call_node.keywords[0].value._set_folded_value(vy_ast.NameConstant(value=False))
constants = get_kwarg_ast_constants(call_node, ("revert_on_failure",))

assert get_literal_kwarg(constants, "revert_on_failure", True) is False


def test_literal_kwarg_rejects_unreduced_value():
call_node = _call_node("foo(revert_on_failure=REVERT)")

with pytest.raises(CompilerPanic, match="unfoldable constant kwarg: revert_on_failure"):
get_kwarg_ast_constants(call_node, ("revert_on_failure",))
5 changes: 3 additions & 2 deletions vyper/codegen_venom/builtins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Built-in function lowering for Venom IR.

Each submodule exports a HANDLERS dict mapping builtin_id -> handler function.
Handler signature: (node: vy_ast.Call, ctx: VenomCodegenContext) -> IROperand | VyperValue
Handler signature: (call: BuiltinCall) -> IROperand | VyperValue

Builtins that return memory-located data (abi_decode, concat, slice, etc.)
should return VyperValue.from_ptr() to preserve location info. Builtins that return
Expand All @@ -21,6 +21,7 @@

from vyper.codegen_venom.value import VyperValue

from ._kwargs import BuiltinCall
from .abi import HANDLERS as ABI_HANDLERS
from .bytes import HANDLERS as BYTES_HANDLERS
from .convert import HANDLERS as CONVERT_HANDLERS
Expand Down Expand Up @@ -64,4 +65,4 @@ def lower_builtin(builtin_id: str, node, ctx) -> Union[IROperand, VyperValue]:
handler = BUILTIN_HANDLERS.get(builtin_id)
if handler is None: # pragma: nocover
raise CompilerPanic(f"Built-in '{builtin_id}' not yet implemented in venom codegen")
return handler(node, ctx)
return handler(BuiltinCall(node, ctx))
156 changes: 156 additions & 0 deletions vyper/codegen_venom/builtins/_kwargs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from __future__ import annotations

from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from typing import Any

from vyper import ast as vy_ast
from vyper.exceptions import CompilerPanic

_UNSET = object()


@dataclass(frozen=True)
class BuiltinCall:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

represents a builtin callsite with additional metadata

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a BuiltinCall docstring describing it as the builtin callsite plus the codegen context used to lower it.

node: vy_ast.Call
ctx: Any

@property
def args(self) -> list[vy_ast.VyperNode]:
return self.node.args

@property
def keywords(self) -> list[vy_ast.keyword]:
return self.node.keywords

def validate_kwargs(self, allowed_kwarg_names: Iterable[str]) -> None:
validate_kwargs(self.node, allowed_kwarg_names)

def get_kwarg_ast_constants(
self,
kwarg_defaults: Mapping[str, Any] | Iterable[str],
error_prefix: str = "unfoldable constant kwarg",
) -> dict[str, Any]:
return get_kwarg_ast_constants(self.node, kwarg_defaults, error_prefix)

def get_kwarg_values(self, kwarg_defaults: Mapping[str, Any] | Iterable[str]):
return get_kwarg_values(self.node, self.ctx, kwarg_defaults)

def lower_pos_args(self, arg_nodes: Iterable[vy_ast.VyperNode] | None = None) -> list[Any]:
from vyper.codegen_venom.expr import Expr

arg_nodes = self.node.args if arg_nodes is None else arg_nodes
return [Expr(arg, self.ctx).lower() for arg in arg_nodes]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lower() or lower_value() should be called exactly once for every single arg/kwarg, in order, and it's not clear by inspection that (or why) this invariant holds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified the invariant at the helper boundary: positional args are lowered in AST/source order, and explicit runtime kwargs are lowered once by iterating the collected keyword nodes in the user's keyword order rather than by allowed-name order.


def lower_pos_arg_values(
self, arg_nodes: Iterable[vy_ast.VyperNode] | None = None
) -> list[Any]:
from vyper.codegen_venom.expr import Expr

arg_nodes = self.node.args if arg_nodes is None else arg_nodes
return [Expr(arg, self.ctx).lower_value() for arg in arg_nodes]


def _kwarg_names_and_defaults(
kwarg_defaults: Mapping[str, Any] | Iterable[str],
) -> tuple[set[str], Mapping[str, Any]]:
if isinstance(kwarg_defaults, Mapping):
return set(kwarg_defaults), kwarg_defaults
return set(kwarg_defaults), {}


def _default_value(default):
if callable(default):
return default()
return default


def validate_kwargs(node: vy_ast.Call, allowed_kwarg_names: Iterable[str]) -> None:
seen = set()
for kw in node.keywords:
if kw.arg in seen: # pragma: nocover
raise CompilerPanic(f"duplicate kwarg: {kw.arg}", kw)
seen.add(kw.arg)

allowed_kwarg_names = set(allowed_kwarg_names)
Comment thread
banteg marked this conversation as resolved.
Outdated
for kw in node.keywords:
Comment thread
banteg marked this conversation as resolved.
if kw.arg not in allowed_kwarg_names: # pragma: nocover
raise CompilerPanic(f"unexpected kwarg: {kw.arg}", kw)


def kwarg_is_provided(node: vy_ast.Call, kwarg_name: str) -> bool:
Comment thread
banteg marked this conversation as resolved.
Outdated
return any(kw.arg == kwarg_name for kw in node.keywords)


def get_kwarg_ast_constants(
node: vy_ast.Call,
kwarg_defaults: Mapping[str, Any] | Iterable[str],
Comment thread
banteg marked this conversation as resolved.
error_prefix: str = "unfoldable constant kwarg",
) -> dict[str, Any]:
kwarg_names, defaults = _kwarg_names_and_defaults(kwarg_defaults)
ret = {}
for kw in node.keywords:
if kw.arg not in kwarg_names:
Comment thread
banteg marked this conversation as resolved.
Outdated
continue

kw_node = kw.value.reduced()
if not isinstance(kw_node, vy_ast.Constant): # pragma: nocover
raise CompilerPanic(f"{error_prefix}: {kw.arg}", kw_node)
ret[kw.arg] = kw_node
Comment thread
banteg marked this conversation as resolved.
Outdated

for name, default in defaults.items():
ret.setdefault(name, default)

return ret


def get_kwarg_values(node: vy_ast.Call, ctx, kwarg_defaults: Mapping[str, Any] | Iterable[str]):
from vyper.codegen_venom.expr import Expr

kwarg_names, defaults = _kwarg_names_and_defaults(kwarg_defaults)
ret = {}
for kw in node.keywords:
if kw.arg in kwarg_names:
ret[kw.arg] = Expr(kw.value.reduced(), ctx).lower_value()
for name, default in defaults.items():
ret.setdefault(name, _default_value(default))
return ret


def _literal_value(node: vy_ast.VyperNode) -> Any:
if isinstance(node, vy_ast.Int):
return node.value
if isinstance(node, vy_ast.NameConstant):
return node.value
return _UNSET


def get_bool_kwarg(kwarg_constants: dict[str, Any], kwarg_name: str, default: Any = _UNSET) -> bool:
kw_node = kwarg_constants.get(kwarg_name, _UNSET)
if kw_node is _UNSET:
if default is _UNSET: # pragma: nocover
raise CompilerPanic(f"missing boolean kwarg default: {kwarg_name}")
return default
if isinstance(kw_node, bool):
return kw_node
if isinstance(kw_node, vy_ast.NameConstant):
return kw_node.value
if isinstance(kw_node, vy_ast.Int):
return bool(kw_node.value)
raise CompilerPanic(f"unfoldable boolean kwarg: {kwarg_name}", kw_node)


def get_literal_kwarg(kwarg_constants: dict[str, Any], kwarg_name: str, default: Any = _UNSET):
kw_node = kwarg_constants.get(kwarg_name, _UNSET)
if kw_node is _UNSET:
if default is _UNSET: # pragma: nocover
raise CompilerPanic(f"missing literal kwarg default: {kwarg_name}")
return default
if kw_node is None or isinstance(kw_node, (bool, int)):
return kw_node

value = _literal_value(kw_node)
if value is not _UNSET:
return value

raise CompilerPanic(f"unfoldable literal kwarg: {kwarg_name}", kw_node)
Loading