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[test]: add hevm to get_contract harness #4499

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
45 changes: 44 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
from contextlib import contextmanager
from random import Random
from typing import Generator
Expand All @@ -15,6 +16,7 @@
from tests.utils import working_directory
from vyper import compiler
from vyper.codegen.ir_node import IRnode
from vyper.compiler import compile_code
from vyper.compiler.input_bundle import FilesystemInputBundle
from vyper.compiler.settings import OptimizationLevel, Settings, set_global_settings
from vyper.exceptions import EvmVersionException
Expand Down Expand Up @@ -123,6 +125,11 @@ def set_hevm(pytestconfig):
tests.hevm.HAS_HEVM = flag_value


@pytest.fixture(scope="session")
def hevm(pytestconfig, set_hevm):
return tests.hevm.HAS_HEVM


@pytest.fixture(scope="session")
def evm_backend(pytestconfig):
backend_str = pytestconfig.getoption("evm_backend")
Expand Down Expand Up @@ -228,13 +235,49 @@ def compiler_settings(optimize, experimental_codegen, evm_version, debug):
return settings


_HEVM_MARKER = None


# request.node.get_closest_marker does something different if fixture is module-scoped,
# workaround with a global variable
@pytest.fixture(autouse=True)
def hevm_marker(request):
global _HEVM_MARKER

_HEVM_MARKER = request.node.get_closest_marker("hevm")


@pytest.fixture(scope="module")
def get_contract(env, optimize, output_formats, compiler_settings):
def get_contract(env, optimize, output_formats, compiler_settings, hevm, request):
def fn(source_code, *args, **kwargs):
if "override_opt_level" in kwargs:
kwargs["compiler_settings"] = Settings(
**dict(compiler_settings.__dict__, optimize=kwargs.pop("override_opt_level"))
)

global _HEVM_MARKER
if hevm and _HEVM_MARKER is not None:
settings1 = copy.copy(compiler_settings)
settings1.experimental_codegen = False
settings1.optimize = OptimizationLevel.NONE
settings2 = copy.copy(compiler_settings)
settings2.experimental_codegen = True
settings2.optimize = OptimizationLevel.NONE

bytecode1 = compile_code(
source_code,
output_formats=("bytecode_runtime",),
settings=settings1,
input_bundle=kwargs.get("input_bundle"),
)["bytecode_runtime"]
bytecode2 = compile_code(
source_code,
output_formats=("bytecode_runtime",),
settings=settings2,
input_bundle=kwargs.get("input_bundle"),
)["bytecode_runtime"]
tests.hevm.hevm_check_bytecode(bytecode1, bytecode2, addl_args=_HEVM_MARKER.args)

return env.deploy_source(source_code, output_formats, *args, **kwargs)

return fn
Expand Down
8 changes: 4 additions & 4 deletions tests/evm_backends/base_env.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import json
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Callable, Optional
from typing import Iterable, Optional

from eth_keys.datatypes import PrivateKey
from eth_utils import to_checksum_address

from tests.evm_backends.abi import abi_decode
from tests.evm_backends.abi_contract import ABIContract, ABIContractFactory, ABIFunction
from vyper.ast.grammar import parse_vyper_source
from vyper.compiler import CompilerData, InputBundle, Settings, compile_code
from vyper.compiler import InputBundle, Settings, compile_code
from vyper.utils import ERC5202_PREFIX, method_id


Expand Down Expand Up @@ -68,7 +68,7 @@ def deploy(self, abi: list[dict], bytecode: bytes, value=0, *args, **kwargs):
def deploy_source(
self,
source_code: str,
output_formats: dict[str, Callable[[CompilerData], str]],
output_formats: Iterable[str],
*args,
compiler_settings: Settings = None,
input_bundle: InputBundle = None,
Expand Down Expand Up @@ -218,7 +218,7 @@ def initcode_size_limit_error(self) -> str:

def _compile(
source_code: str,
output_formats: dict[str, Callable[[CompilerData], str]],
output_formats: Iterable[str],
input_bundle: InputBundle = None,
settings: Settings = None,
) -> tuple[list[dict], bytes]:
Expand Down
21 changes: 21 additions & 0 deletions tests/functional/codegen/calling_convention/test_internal_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from vyper.exceptions import ArgumentException, CallViolation


@pytest.mark.hevm
def test_selfcall_code(get_contract):
selfcall_code = """
def _foo() -> int128:
Expand All @@ -25,6 +26,7 @@ def bar() -> int128:
print("Passed no-argument self-call test")


@pytest.mark.hevm
def test_selfcall_code_2(get_contract, keccak):
selfcall_code_2 = """
def _double(x: int128) -> int128:
Expand All @@ -50,6 +52,7 @@ def return_hash_of_rzpadded_cow() -> bytes32:


# test that side-effecting self calls do not get optimized out
@pytest.mark.hevm
def test_selfcall_optimizer(get_contract):
code = """
counter: uint256
Expand All @@ -67,6 +70,7 @@ def foo() -> (uint256, uint256):
assert c.foo() == (0, 1)


@pytest.mark.hevm
def test_selfcall_code_3(get_contract, keccak):
selfcall_code_3 = """
@internal
Expand All @@ -93,6 +97,7 @@ def returnten() -> uint256:
print("Passed single variable-size argument self-call test")


@pytest.mark.hevm
def test_selfcall_code_4(get_contract):
selfcall_code_4 = """
@internal
Expand Down Expand Up @@ -137,6 +142,7 @@ def return_goose2() -> Bytes[10]:
print("Passed multi-argument self-call test")


@pytest.mark.hevm("--max-iterations", "10")
def test_selfcall_code_5(get_contract):
selfcall_code_5 = """
counter: int128
Expand All @@ -157,6 +163,7 @@ def returnten() -> int128:
print("Passed self-call statement test")


@pytest.mark.hevm
def test_selfcall_code_6(get_contract):
selfcall_code_6 = """
excls: Bytes[32]
Expand Down Expand Up @@ -185,6 +192,7 @@ def return_mongoose_revolution_32_excls() -> Bytes[201]:
print("Passed composite self-call test")


@pytest.mark.hevm
def test_list_call(get_contract):
code = """
@internal
Expand Down Expand Up @@ -223,6 +231,7 @@ def bar3() -> int128:
assert c.bar3() == 66


@pytest.mark.hevm
def test_list_storage_call(get_contract):
code = """
y: int128[2]
Expand Down Expand Up @@ -254,6 +263,7 @@ def bar1() -> int128:
assert c.bar1() == 99


@pytest.mark.hevm
def test_multi_arg_list_call(get_contract):
code = """
@internal
Expand Down Expand Up @@ -326,6 +336,7 @@ def bar6() -> int128:
assert c.bar5() == 88


@pytest.mark.hevm
def test_multi_mixed_arg_list_call(get_contract):
code = """
@internal
Expand All @@ -349,6 +360,7 @@ def bar() -> (int128, decimal):
assert c.bar() == (66, decimal_to_int("66.77"))


@pytest.mark.hevm
def test_internal_function_multiple_lists_as_args(get_contract):
code = """
@internal
Expand All @@ -373,6 +385,7 @@ def bar2() -> int128:
assert c.bar2() == 1


@pytest.mark.hevm
def test_multi_mixed_arg_list_bytes_call(get_contract):
code = """
@internal
Expand Down Expand Up @@ -478,6 +491,7 @@ def foo(x: uint256, y: uint256):
]


# TODO: maybe these belong in tests/functional/syntax/
@pytest.mark.parametrize("failing_contract_code", FAILING_CONTRACTS_ARGUMENT_EXCEPTION)
def test_selfcall_wrong_arg_count(failing_contract_code, assert_compile_failed):
assert_compile_failed(lambda: compile_code(failing_contract_code), ArgumentException)
Expand Down Expand Up @@ -535,6 +549,7 @@ def test_selfcall_kwarg_raises(failing_contract_code, decorator, assert_compile_
_ = compile_code(failing_contract_code.format(decorator))


@pytest.mark.hevm
@pytest.mark.parametrize("i,ln,s,", [(100, 6, "abcde"), (41, 40, "a" * 34), (57, 70, "z" * 68)])
def test_struct_return_1(get_contract, i, ln, s):
contract = f"""
Expand All @@ -558,6 +573,7 @@ def test() -> (int128, String[{ln}], Bytes[{ln}]):
assert c.test() == (i, s, bytes(s, "utf-8"))


@pytest.mark.hevm
def test_dynamically_sized_struct_as_arg(get_contract):
contract = """
struct X:
Expand All @@ -579,6 +595,7 @@ def bar() -> Bytes[6]:
assert c.bar() == b"hello"


@pytest.mark.hevm
def test_dynamically_sized_struct_as_arg_2(get_contract):
contract = """
struct X:
Expand All @@ -600,6 +617,7 @@ def bar() -> String[6]:
assert c.bar() == "hello"


@pytest.mark.hevm
def test_dynamically_sized_struct_member_as_arg(get_contract):
contract = """
struct X:
Expand All @@ -621,6 +639,7 @@ def bar() -> Bytes[6]:
assert c.bar() == b"hello"


@pytest.mark.hevm
def test_make_setter_internal_call(get_contract):
# cf. GH #3503
code = """
Expand All @@ -642,6 +661,7 @@ def bar(i: uint256) -> uint256:
assert c.foo() == [2, 1]


@pytest.mark.hevm
def test_make_setter_internal_call2(get_contract):
# cf. GH #3503
code = """
Expand All @@ -664,6 +684,7 @@ def boo() -> uint256:
assert c.foo() == [1, 2, 3, 4]


@pytest.mark.hevm
def test_dynamically_sized_struct_member_as_arg_2(get_contract):
contract = """
struct X:
Expand Down
19 changes: 14 additions & 5 deletions tests/hevm.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,25 @@ def hevm_check_venom(pre, post, verbose=False):
hevm_check_bytecode(bytecode1, bytecode2, verbose=verbose)


def hevm_check_bytecode(bytecode1, bytecode2, verbose=False):
# use hevm to check equality between two bytecodes (hex)
def hevm_check_bytecode(bytecode1, bytecode2, verbose=False, addl_args: list = None):
# debug:
if verbose:
print("RUN HEVM:")
print(bytecode1)
print(bytecode2)

subp_args = ["hevm", "equivalence", "--code-a", bytecode1, "--code-b", bytecode2]

subp_args.extend(["--num-solvers", "1"])
if addl_args:
subp_args.extend([*addl_args])

res = subprocess.run(
subp_args, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
assert not res.stderr, res.stderr # hevm does not print to stdout
# TODO: get hevm team to provide a way to promote warnings to errors
assert "WARNING" not in res.stdout, res.stdout
assert "issues" not in res.stdout
if verbose:
subprocess.check_call(subp_args)
else:
subprocess.check_output(subp_args)
print(res.stdout)
4 changes: 3 additions & 1 deletion vyper/codegen/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,13 @@ def generate_ir_for_module(module_t: ModuleT) -> tuple[IRnode, IRnode]:

internal_functions_ir: list[IRnode] = []

# module_tinternal functions first so we have the function info
# module_t internal functions first so we have the function info
for func_ast in internal_functions:
func_ir = _ir_for_internal_function(func_ast, module_t, False)
internal_functions_ir.append(IRnode.from_list(func_ir))

# TODO: add option to specifically force linear selector section,
# useful for testing and downstream tooling.
if core._opt_none():
selector_section = _selector_section_linear(external_functions, module_t)
# dense vs sparse global overhead is amortized after about 4 methods.
Expand Down